接上節,此時的dev分支與master分支的進度就不一樣了,所以需要將dev分支與master分支同步。這里需要的就是合并分支的操作,大家應該都知道用git merge
或者git rebase
。
git merge
merge,即「合并」。
fast-forward
當出現我們上面圖中的那種情況時,時間線只有一條,dev分支只不過是落后master分支而已。此時我們在dev分支上執行git merge maseter
時,git就僅僅會把dev分支指針移動到master分支所在的位置,假裝合并了,就變成了這樣:
這種merge的方式叫做「fast-forward」,也是git默認的merge方式。
如果情況改變了,舉個例子:我們在開發過程中,一直使用的是master分支,這時出了一個很嚴重的bug,我們就需要建立一個叫topic的分支來處理這個bug,但主要的功能工期又不能拖,所以master分支與topic分支就同時向前推進,此時時間線如圖所示:
(此圖出自git自己的幫助文件,使用命令git help merge
即可看到。想看其他命令的幫助就git help <command>
即可):
這時候,topic上的bug修改完畢,需要合并回master分支,需要的操作為:切換到master分支git checkout master
,合并devgit merge dev
。
注意,這時候的這兩條分支是真正的「分支」了,他們在時間線上岔開了,各自分支都有自己獨有的東西。
因為此時的topic分支的末端并不在master分支的父端,需要把不同的修改同步起來,單純的指針移動不能完成這一步,fast-forward方式也就不可能實現了。
這時,git便會將兩個分支不同的地方取出,合并成一個commit,然后把master指針指向這個新的commit(就是在master上生成了一次commit)。這樣,topic分支上的修改就同步到master分支上了。此時分支情況如圖:
no fast-forward
BTW,能夠進行fast-forward的merge情況下,也可以通過增加--no-ff
命令來強制不使用fast-forward模式。假如還是回到我們master-dev兩個分支的例子,master領先于dev分支:
這時我們不用fast-forward,在落后的dev分支上執行git merge master --no-ff
,git會在dev上強行創建一個commit,把master分支上不同于dev的修改加進去,分支線就會變成這種詭異的樣子:
(本手殘渣畫圖實在是不好看,就直接用GUI工具source tree上的情況截圖了)
底層上,git會把將要合并的兩個分支的各個commit快照進行差異比較,求出它們之間的最長公共子序列,并把公共子序列從中去掉,得出各自存在兩個分支中的不同修改,并將其合并成一個commit放在當前分支的頂端。
這里僅僅說明一點原理,具體實現方式與算法本人也只是懂一點皮毛,只要明白fast-forward與不使用的情況下merge,分支會產生什么樣的情況,用來工作就沒有任何問題了。
squash
除了--no-ff
,merge還有另一種合并的方式:--squash
。這種方法在符合fast-forward的情況下依然會執行fast-forward方式,不會有任何改變。但當遇到如下情況時:
假設我們要將topic合并到master上來,squash方式會集中topic的「A、B、C」三次commit中的修改合并,并添加到暫存區中。
這時master分支與topic分支不會有任何的變動,只不過暫存區中會被添加topic上修改的集合(暫存區=A+B+C)。
這時我們就可以查看暫存區中的內容是不是符合一次提交,之后commit就可以了。git help merge
里是這么描述的:「create a single commit instead of doing a merge」,結合上面的講解就可以理解squash的意思了吧。
git merge 解決沖突
fast-forward中是沒有沖突的(不明白為啥沒沖突的面壁思過)。而在其他情況時,如果兩個分支同時有對同一個文件(行)的修改,就會產生沖突。這時git會在產生沖突的文件里寫一堆這樣的東西:
上面的「<<<<<<<< HEAD」直到「========」的部分,就是當前分支的修改(看到HEAD就知道是指向當前分支的指針了)。而「========」到下面的「>>>>>>> dev」的部分自然就是dev分支合并過來的修改啦。
這時需要你仔細對比沖突,如果跟同事合作的話就要商量好,然后把「<<<<< HEAD ===== >>>>> dev」之類git給你加上的東西和不需要的修改部分刪掉。接下來git status
就會看到下面的提示:
上面綠的的東西自然就在暫存區了,這些代表dev分支上并不沖突的部分。下面的紅色文件就代表你沖突的文件,當你修改之后需要走一遍add -> commit的流程(這個commit可以不指定commit message),也可以直接執行git commit -a
,就完成merge創建新commit的過程了。
涉及操作:git merge <branch>
, git merge <branch> --no-ff
, git merge <branch> --squash
, git checkout <branch>
, git help <command>
git rebase
除了merge,git還有一種分支合并的方式,叫做git rebase。
rebase,就是「re」與「base」結合,官方譯名「變基」(咖喱gaygay? ????)。這個「變基」的含義從字面上確實不是很好理解,先來看一下rebase示例:
回到我們master-dev兩個分支的例子,master領先于dev分支:
這時候我們在dev分支上執行git rebase master
,master便與dev合并了,如圖所示:
此時你內心OS:這不是跟fast-forward模式下的merge一樣么?莫急莫急,我們再看一下出現這樣情況下的分支(作者偷懶拿前面圖糊弄了嘿嘿嘿):
不著急解釋原理,我們先看看在topic上執行git rebase master
的結果(就是將master合并到topic上):
看圖得知:master上的「F」「G」兩次提交,變成了topic分支的父節點,整個分支又重新合成為一條時間線。在本例中,你可以想象「biu」的一下把topic分支拔下來,然后「pu」的一下把它插到了master的頂端(大霧)。
當然,git肯定不是像上面那樣「biu」「pu」地操作分支的。
git help rebase中是這樣描述git rebase
的:
git-rebase - Reapply commits on top of another base tip
翻譯一下,就是「將你的commit們在另一個基準點上重新應用」。注意這里的「reapply」,git并不會直接移動commit本身,而是會為需要rebase的分支上的commit分別創建一個patch「補丁」,然后將patch在基準點上依次應用,重建出一條時間線。
可以參照上面的兩張圖梳理一下流程:
當我們在topic分支上執行git rebase master
時,代表了我們要將我們當前的分支(topic)應用到指定的master分支上。
此時topic與master的共同父節點是「E」,topic的特有commit是「A」「B」「C」,git就會按照時間點,分別創建「A」「B」「C」的patch「A'」「B'」「C'」,然后將topic分支的基準點設置為master分支的頂點「G」(「變基」了!),依次將「A'」「B'」「C'」Apply到「G」上。
現在是不是理解「rebase變基」是什么意思了!
git rebase 解決沖突
rebase產生的沖突與merge其實是相同的。但由于rebase操作會按照patch一個個打補丁上去,每打一個都有可能會產生沖突,跟merge的產生一個commit這種一次性操作不一樣,解決沖突之后也就不是提交commit,而是git add <file>
之后執行git rebase --continue
。
也就是「打一個補丁,解決一次沖突,然后繼續下一個補丁」的過程。
如果你不耐煩了,也可以git rebase --abort
直接不進行rebase了。
涉及操作:git rebase <branch>
, git rebase <branch> --onto <commit id>
, git rebase --continue
, git rebase --abort
關于分支處理策略的選擇
上面講了好多關于分支的東西,可能會讓人困惑:分支涉及到的東西這么多,本身又復雜,多分支處理也復雜,應該怎樣利用分支才好?分支合并的策略選哪一種呢?這里我說一下個人的見解:
首先,git保存的是修改這一點,可以很清楚的讓我們知道代碼發生了哪些改變。在這樣的情況下,我們利用commit時間線就可以明確地區分哪個人在什么時間做了什么事情,也就是給了你「查看歷史」與「修改歷史」的權力,這對一個軟件項目來說是至關重要的。
有關git多個分支的設計,其實是非常巧妙的。多個分支解決了以代碼本身不同版本、不同功能或不同目的的開發方向(比如開發新功能或改bug,又暫時不想修改主要版本)開發時的代碼版本管理問題,能夠很方便地管理工作區的文件內容。
所以,我對多分支系統利用的理解是這樣的:
- 分支是需要充分利用的。首先要確定一個master分支,作為這個項目最終上線的版本,要保證合并到master分支上的代碼都是確定無誤的、測試通過的。
- 在開發過程中,可以使用一個development分支來開發,用其部署測試環境,使這個分支成為可以隨意修改的分支,增加開發的靈活度。
- 在master或者dev分支出現問題,或者要分頭行動時,為每個分支在合適的父節點上創建新的功能分支或bug分支來處理這些問題,可以保證主要的代碼不會出錯,就算是開發出問題,直接刪除該分支便是,基本不用涉及到文件層面的修改。
- 所以,對工作區的修改應該僅限于增加新內容和修復bug之類的操作,其他的都應該交給git去處理,保證版本樹是一條路,復雜的功能刪除也不用一行一行找。
- 當某個分支的某些commit出現問題時,可以先將沒有問題的部分建立分支保存起來,保證那些內容不會出問題。
有關git代碼合并策略的選擇,雖然git提供了非常豐富的方法,但一個team使用的方法應該大體固定成同一個,這樣能避免很多混亂,然后在適當的時機使用不同的策略。在《Pro Git》這本書中總結的就很好,我摘下來總結下:
選擇merge還是rebase取決于你對commit歷史時間線的定義。有兩種觀點:第一種認為,commit歷史應該顯示的是什么時候具體發生了什么事,比如分支的創建與合并過程,有哪些分支,分別合并在了什么地方等等。另一種認為,commit歷史應該顯示的是這個項目經歷過的狀態,而不考慮具體的分支構建過程。
每一個團隊,每一個人都是不同的。git作為一個如此強大的工具提供給了你解決任何問題的思路,你就要考慮清楚你的團隊到底需要什么。
一個兩全其美的方法就是:rebase你本地的修改,push到多人環境中時用merge。
亂糟糟的時間線&&完整的分支結構 vs 清爽的一條線&&舍棄修改過程,看你團隊取舍咯。
文章鏈接
理解git結構與簡單操作(一)git的本質
理解git結構與簡單操作(二)工作區與暫存區
理解git結構與簡單操作(三)認識版本庫與分支
理解git結構與簡單操作(四)合并分支的方法與策略