Git 如何保存文件
其它版本管理系統通常會保存所有文件及其歷次提交的差異(diff / revision),通過 merge 原始文件與各階段的差異就能獲取任何版本的狀態
而 Git 保存的是每一次提交時所有文件的快照(snapshot),對於發生改變(modified)的文件會生成新的快照,而對於未發生改變的文件,其新版本快照為上一個版本的快照的索引(圖中虛線框所示),這樣可以減小版本庫的體積
這里比較費解的是:快照究竟是什么?
簡單的理解:快照就是壓縮文件,只不過 git 會將文件內容壓縮為 blob 格式,例如僅含一段 hello world 的 txt 文件壓縮后的內容為:
7801 4bca c94f 5230 3462 c848 cdc9 c957
28cf 2fca 49e1 0200 4411 0689
所有文件快照都會被儲存在 .git 倉庫文件夾下的 objects 目錄中
經測試,一份 200k 的未經壓縮的代碼文件,其文件快照大小約 65k
文件名 eef...542 是根據內容生成的 40 位哈希字符串,文件名 + 文件本身就構成了一組鍵值對。所有文件都以這種形式保存,而 objects 目錄就是一個以鍵值對形式保存文件的數據庫
可以想象,隨着版本不斷迭代,.git 倉庫目錄的體積往往會超過工作區所有文件的體積之和,因為哪怕只做了一丁點的改變,git 都會重新生成快照。如下圖所示,我僅僅刪掉了 vue.runtime.js 的一行注釋,然后執行 `git add -A`,.git 中就重新生成了一份快照
一個長期維護的代碼庫,其代碼總量可能只有幾 MB,但 .git 完全可能大到以 G 計
比起其它版本管理系統僅僅記錄差異,git 的這種做法不是顯得更浪費空間嗎?git 之所這么設計,是出於“空間換時間”的考慮。用過 SVN 的人都知道要從一個幾百 MB 的項目庫開出一個分支是多么費時,而使用 git 開分支,無論體積有多大,都是一瞬間的事情,原因就是二者的“分支”的原理完全不同
Git 如何保存文件版本
理解了 git 保存文件的方式,就很容易理解其保存版本的方式:采用一個樹對象來表示目錄結構與文件
root: {
sub1: {
hash
hash
...
}
sub2: {
hash
hash
...
}
}
根據文件索引就可以直接從數據庫中取出文件,然后再按樹對象表征的目錄結構進行組合排列,就很容易恢復出一套文件版本
每次 commit 除了保存樹對象以外,還會記錄提交的作者、批注、上一次提交的索引等信息,每個 commit 都會根據內容生成一個 hash 作為其唯一的索引
而每次 commit 就是一個版本記錄
可以看到,所有的 commit 形成了一個鏈表,而這個鏈表有一個形象的名稱:分支
但我們最好不要把分支這個概念看成是一條“鏈”,而應該看成是某個版本(commit)的索引,我們說合並兩個分支,合並的不是兩條鏈,而是兩個版本(commit)
Git 開分支的原理
git 分支的本質,就是指向某個特定 commit 的指針,假設當前只有一個分支,默認就叫做 master,當前已經是第三個提交了:
{
master: commit-3
}
那么開一個分支,無非就是新創建一個指針:
{
master: commit-3
dev: commit-3
}
當前用戶處於哪個分支,需要用另一個指針來表示:
{
HEAD: master
}
執行 `git checkout dev` 切換分支后:
{
HEAD: dev
}
在 dev 分支提交一次 commit 后:
{
master: commit-3
dev: commit-4
}
切回 master,執行 `git branch -d dev` 刪除分支:
{
master: commit-3
}
master 分支其實並沒有什么特殊之處,它和其它分支本質是一樣的,只不過它是初始化項目時的默認分支,同時在項目開發中約定作為主分支
Git 合並分支的策略
兩個分支的合並只有兩種情況:無分叉、有分叉
無分叉的情形最簡單,合並分支就把 master 指向的 commit 更換為最新的 commit
{
master: commit-3
dev: commit-4
}
merge:
{
master: commit-4
dev: commit-4
}
這種策略被稱為 fast forward
有分叉的情況稍微麻煩一些,git 會將兩個分支的分叉點和頭部的 commit 做一次三方合並,然后形成一個新的 commit:
顯然第一種方式最簡便,那有沒有辦法在分叉的情況下仍然采用 fast forward 的策略呢,有
在 experiment 分支上執行 `git rebase master`,首先會計算出分叉點與 experiment 分支頭部的兩個 commit 的差異,然后以 C3 為新的基礎,整合之前計算出的差異,得到一個新的 commit
var patch = C4 - C2
var C4` = C3 + patch
C4`.parent = C3
rebase 就是改變基礎的意思。這下回到 master 分支執行 merge 操作,就可以實現 fast forward 了