Git存儲機制
在理解Git分支的運行機制之前,要了解一下Git是如何存儲數據的。
當使用 git commit
新建一個提交對象前,Git 會先計算每一個子目錄的校驗和,然後在 Git 倉庫中將這些目錄保存爲樹(tree)對象。之後 Git 創建的提交對象,除了包含相關提交信息以外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就可以在將來需要的時候,重現此次快照的內容了。
單個提交對象在倉庫中的數據結構如下圖所示:
上圖中,Git 倉庫中有五個對象:三個表示文件快照內容的 blob 對象;一個記錄着目錄樹內容及其中各個文件對應 blob 對象索引的 tree 對象;以及一個包含指向 tree 對象(根目錄)的索引和其他提交信息元數據的 commit 對象。
作些修改後再次提交,那麼這次的提交對象會包含一個指向上次提交對象的指針,即下圖中的 parent 對象。兩次提交後,倉庫歷史會變成下圖的樣子:
上圖展現了多個提交對象之間的鏈接關係。
什麼是Git分支
Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 作爲分支的默認名字。在若干次提交後,你其實已經有了一個指向最後一次提交對象的 master 分支,它在每次提交的時候都會自動向前移動。
如下圖所示:
Git 創建一個新的分支就等同於創建一個新的分支指針。
比如我們要創建一個名爲testing的分支,我們可以使用git branch
命令:
$ git branch testing
這會在當前 commit 對象上新建一個分支指針,如下圖所示:
Git會保存着一個名爲 HEAD 的特別指針,用來記錄當前當前你正在哪個分支上工作。
運行 git branch
命令,僅僅是建立了一個新的分支,但不會自動切換到這個分支中去,所以在上個例子中,我們依然還在 master 分支裏工作,如圖:
要切換到其他分支,可以執行 git checkout
命令。我們現在轉換到新建的 testing 分支:
$ git checkout testing
這樣 HEAD 就指向了 testing 分支:
此時,如果我們再提交一次:
$ vim test.rb
$ git commit -a -m 'made a change'
提交後的結果示意圖如下:
我們會發現,每次提交後 HEAD 隨着當前所在分支一起向前移動,testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout 時所在的 commit 對象。
現在我們回到 master 分支看看:
$ git checkout master
這條命令做了兩件事。它把 HEAD 指針移回到 master 分支,並把工作目錄中的文件換成了 master 分支所指向的快照內容。也就是說,現在開始所做的改動,將始於本項目中一個較老的版本。它的主要作用是將 testing 分支裏作出的修改暫時取消,這樣你就可以向另一個方向進行開發。
我們作些修改後再次提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
結果如下圖所示:
現在我們的項目提交歷史產生了分叉,因爲剛纔我們創建了一個分支,轉換到其中進行了一些工作,然後又回到原來的主分支進行了另外一些工作。這些改變分別孤立在不同的分支裏:我們可以在不同分支裏反覆切換,並在時機成熟時把它們合併到一起。而所有這些工作,僅僅需要 branch
和 checkout
這兩條命令就可以完成。
分支的新建與合併
我們假設一個這樣的情景,假設你正在項目中愉快地工作,並且已經提交了幾次更新,如下圖:
此時你突然接到一個電話說有個很嚴重的問題需要緊急修補,利用Git分支,一個很好的做法是:
- 返回到原先已經發布到生產服務器上的分支。
- 爲這次緊急修補建立一個新分支,並在其中修復問題。
- 通過測試後,回到生產服務器所在的分支,將修補分支合並進來,然後再推送到生產服務器上。
- 切換到之前實現新需求的分支,繼續工作。
因此,比如你要修補問題追蹤系統上的 #53 問題,你建立了一個名爲 iss53的分支。
要新建並切換到該分支,運行 git checkout
並加上 -b 參數:
$ git checkout -b iss53
Switched to a new branch "iss53"
這相當於執行下面這兩條命令:
$ git branch iss53
$ git checkout iss53
此時,狀態如下:
接着你開始嘗試修復問題,在提交了若干次更新後,iss53 分支的指針也會隨着向前推進,因爲它就是當前分支(換句話說,當前的 HEAD 指針正指向 iss53)如下圖:
然而此時,你又接到一個緊急電話,原先發布的系統需要需要馬上修補另一個問題。
有了 Git ,我們就不需要同時發佈這個補丁和 iss53 裏作出的修改,也不需要在創建和發佈該補丁到服務器之前花費大力氣來複原這些修改。唯一需要的僅僅是切換回 master 分支。
$ git checkout master
Switched to branch "master"
此時工作目錄中的內容和你在解決問題 #53 之前一模一樣,你可以集中精力進行緊急修補。這一點值得牢記:Git 會把工作目錄的內容恢復爲檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時完全一樣。
接下來,你得進行緊急修補。我們創建一個緊急修補分支 hotfix 來開展工作,直到搞定,如下圖所示:
$ git checkout -b 'hotfix'
Switched to a new branch "hotfix"
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix]: created 3a0874c: "fixed the broken email address"
1 files changed, 0 insertions(+), 1 deletions(-)
修補完成後,我們只需要回到 master
分支並把它合並進來,然後發佈到生產服務器。
分支的合併
用 git merge
命令來進行合併:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast forward
README | 1 -
1 files changed, 0 insertions(+), 1 deletions(-)
合併時出現了“Fast forward”
的提示。由於當前 master
分支所在的提交對象是要併入的 hotfix 分支的直接上游,Git 只需把 master
分支指針直接右移。換句話說,如果順着一個分支走下去可以到達另一個分支的話,那麼 Git 在合併兩者時,只會簡單地把指針右移,因爲這種單線的歷史分支不存在任何需要解決的分歧,所以這種合併過程可以稱爲快進(Fast forward)。
現在最新的修改已經在當前 master
分支所指向的提交對象中了,可以部署到生產服務器上去了。合併之後,master 分支和 hotfix 分支指向同一位置。如下圖所示:
在那個超級重要的修補發佈以後,你想要回到被打擾之前的工作。由於當前 hotfix
分支和 master
都指向相同的提交對象,所以 hotfix
已經完成了歷史使命,可以刪掉了。
分支的刪除
使用 git branch
的 -d
選項執行刪除操作:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
現在回到之前未完成的 #53 問題修復分支上繼續工作:
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53]: created ad82d7a: "finished the new footer [issue 53]"
1 files changed, 1 insertions(+), 0 deletions(-)
在問題 #53 相關的工作完成之後,可以合併回 master
分支。實際操作同前面合併 hotfix
分支差不多,只需回到 master
分支,運行 git merge
命令指定要合並進來的分支:
$ git checkout master
$ git merge iss53
Merge made by recursive.
README | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
這次合併操作的底層實現,並不同於之前 hotfix
的併入方式。因爲這次你的開發歷史是從更早的地方開始分叉的。由於當前 master
分支所指向的提交對象(C4)並不是 iss53
分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。Git 爲分支合併自動識別出最佳的同源合併點。下圖用紅框標出了 Git 用於合併的三個提交對象:
這次,Git 沒有簡單地把分支指針右移,而是對三方合併後的結果重新做一個新的快照,並自動創建一個指向它的提交對象:
遇到衝突時的分支合併
如果在不同的分支中都修改了同一個文件的同一部分,Git 就無法乾淨地把兩者合到一起(邏輯上說,這種問題只能由人來裁決)。如果你在解決問題 #53 的過程中修改了 hotfix 中修改的部分,將得到類似下面的結果:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git 作了合併,但沒有提交,它會停下來等你解決衝突。要看看哪些文件在合併時發生衝突,可以用 git status
查閱:
[master*]$ git status
index.html: needs merge
# On branch master
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# unmerged: index.html
#
任何包含未解決衝突的文件都會以未合併(unmerged)的狀態列出。Git 會在有衝突的文件里加入標準的衝突解決標記,可以通過它們來手工定位並解決這些衝突。
當我們人爲的將不同的地方修改成一樣後,再次運行分支合併,並用 git commit
來完成這次合併提交。
分支的管理
git branch
命令不僅僅能創建和刪除分支,如果不加任何參數,它會給出當前所有分支的清單:
$ git branch
iss53
* master
testing
master
分支前的 *
字符:它表示當前所在的分支。也就是說,如果現在提交更新,master
分支將隨着開發進度前移。
若要查看各個分支最後一個提交對象的信息,運行 git branch -v
:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
要從該清單中篩選出你已經(或尚未)與當前分支合併的分支,可以用 --merge
和 --no-merged
選項。
比如用 git branch --merge
查看哪些分支已被併入當前分支。
$ git branch --merged
iss53
* master
之前我們已經合併了 iss53,所以在這裏會看到它。一般來說,列表中沒有 *
的分支通常都可以用 git branch -d
來刪掉。原因很簡單,既然已經把它們所包含的工作整合到了其他分支,刪掉也不會損失什麼。
用 git branch --no-merged
查看尚未合併的工作:
$ git branch --no-merged
testing
遠程分支
我們經常使用的Git的多數情況,是使用的遠程Git服務器,而不是本地使用Git。
所以我們經常遇到的分支情況,更多的是遠程分支。
假設你們團隊有個地址爲 git.ourcompany.com
的 Git 服務器。如果你從這裏克隆,Git 會自動爲你將此遠程倉庫命名爲 origin
,並下載其中所有的數據,建立一個指向它的 master
分支的指針,在本地命名爲 origin/master
,但你無法在本地更改其數據。接着,Git 建立一個屬於你自己的本地 master
分支,始於 origin
上 master
分支相同的位置,你可以就此開始工作。
如下圖所示:
一次 Git 克隆會建立你自己的本地分支 master
和遠程分支 origin/master
,並且將它們都指向 origin
上的 master
分支。
如果你在本地 master
分支做了些改動,與此同時,其他人向 git.ourcompany.com
推送了他們的更新,那麼服務器上的 master
分支就會向前推進,而於此同時,你在本地的提交歷史正朝向不同方向發展。不過只要你不和服務器通訊,你的 origin/master
指針仍然保持原位不會移動:
可以運行 git fetch origin
來同步遠程服務器上的數據到本地。該命令首先找到 origin
是哪個服務器(本例爲 git.ourcompany.com
),從上面獲取你尚未擁有的數據,更新你本地的數據庫,然後把 origin/master
的指針移到它最新的位置上,如下圖所示:
推送本地分支
想和其他人分享某個本地分支,你需要把它推送到一個你擁有寫權限的遠程倉庫。你創建的本地分支不會因爲你的寫入操作而被自動同步到你引入的遠程服務器上,你需要明確地執行推送分支的操作。
如果你有個叫 serverfix
的分支需要和他人一起開發,可以運行 git push (遠程倉庫名) (分支名)
:
$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To [email protected]:schacon/simplegit.git
* [new branch] serverfix -> serverfix
它的意思是“上傳我本地的 serverfix
分支到遠程倉庫中去,仍舊稱它爲 serverfix
分支”。通過此語法,你可以把本地分支推送到某個命名不同的遠程分支:若想把遠程分支叫作 awesomebranch
,可以用 git push origin serverfix:awesomebranch
來推送數據。
接下來,當你的協作者再次從服務器上獲取數據時,他們將得到一個新的遠程分支 origin/serverfix
,並指向服務器上 serverfix
所指向的版本。
值得注意的是,在 fetch
操作下載好新的遠程分支之後,你仍然無法在本地編輯該遠程倉庫中的分支。換句話說,在本例中,你不會有一個新的 serverfix 分支,有的只是一個你無法移動的 origin/serverfix 指針。
如果要把該遠程分支的內容合併到當前分支,可以運行 git merge origin/serverfix。如果想要一份自己的 serverfix 來開發,可以在遠程分支的基礎上分化出一個新的分支來:
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
這會切換到新建的 serverfix
本地分支,其內容同遠程分支 origin/serverfix
一致,這樣你就可以在裏面繼續開發了。
跟蹤遠程分支
從遠程分支 checkout 出來的本地分支,稱爲 跟蹤分支 (tracking branch)。跟蹤分支是一種和某個遠程分支有直接聯繫的本地分支。在跟蹤分支裏輸入 git push
,Git 會自行推斷應該向哪個服務器的哪個分支推送數據。同樣,在這些分支裏運行 git pull
會獲取所有遠程索引,並把它們的數據都合併到本地分支中來。
在克隆倉庫時,Git 通常會自動創建一個名爲 master
的分支來跟蹤 origin/master
。這正是 git push
和 git pull
一開始就能正常工作的原因。當然,你可以隨心所欲地設定爲其它跟蹤分支,比如 origin
上除了 master
之外的其它分支。剛纔我們已經看到了這樣的一個例子:git checkout -b [分支名] [遠程名]/[分支名]
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
要爲本地分支設定不同於遠程分支的名字,只需在第一個版本的命令裏換個名字:
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "sf"
現在你的本地分支 sf
會自動將推送和抓取數據的位置定位到 origin/serverfix
了。
刪除遠程分支
如果不再需要某個遠程分支了,可以使用git push [遠程名] :[分支名]
命令刪除遠程分支。如果想在服務器上刪除 serverfix
分支,運行下面的命令:
$ git push origin :serverfix
To [email protected]:schacon/simplegit.git
- [deleted] serverfix