git-merge完全解析
Git的git-merge是在Git中頻繁使用的一個命令,很多人都覺得git合並是一個非常麻煩的事情,一不小心就會遇到丟失代碼的問題,從而對git望而卻步。本文基於Git 2.8.2對git-merge命令進行完整詳細的介紹,特別是關於交叉合並所帶來的代碼遺失問題,在文末給出自己的建議,希望能夠幫助到git的使用者。本文所介紹的內容基於Git 2.8.2
git-merge命令是用於將兩個或兩個以上的開發歷史合並在一起的操作,通常也可寫作:git merge。
1.git-merge相關的選項參數
1.1摘要
在git-merge命令中,有以下三種使用參數:
git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]] [--[no-]rerere-autoupdate] [-m <msg>] [<commit>...]
git merge <msg> HEAD <commit>...
git merge --abort
1.2git-merge簡介
git-merge命令是用於從指定的commit(s)合並到當前分支的操作。
注:這里的指定commit(s)是指從這些歷史commit節點開始,一直到當前分開的時候。
git-merge命令有以下兩種用途:
- 用於git-pull中,來整合另一代碼倉庫中的變化(即:git pull = git fetch + git merge)
- 用於從一個分支到另一個分支的合並
假設下面的歷史節點存在,並且當前所在的分支為“master”:

那么
git merge topic
命令將會把在master分支上二者共同的節點(E節點)之后分離的節點(即topic分支的A B C節點)重現在master分支上,直到topic分支當前的commit節點(C節點),並位於master分支的頂部。並且沿着master分支和topic分支創建一個記錄合並結果的新節點,該節點帶有用戶描述合並變化的信息。
即下圖中的H節點,C節點和G節點都是H節點的父節點。

1.3git merge <msg> HEAD <commit>...
命令
該命令的存在是由於歷史原因,在新版本中不應該使用它,應該使用git merge -m <msg> <commit>....
進行替代
1.4git merge --abort
命令
該命令僅僅在合並后導致沖突時才使用。git merge --abort
將會拋棄合並過程並且嘗試重建合並前的狀態。但是,當合並開始時如果存在未commit的文件,git merge --abort
在某些情況下將無法重現合並前的狀態。(特別是這些未commit的文件在合並的過程中將會被修改時)
警告:運行
git-merge
時含有大量的未commit文件很容易讓你陷入困境,這將使你在沖突中難以回退。因此非常不鼓勵在使用git-merge
時存在未commit的文件,建議使用git-stash
命令將這些未commit文件暫存起來,並在解決沖突以后使用git stash pop
把這些未commit文件還原出來。
2.參數
本部分用於介紹git-merge
命令中使用的參數
2.1--commit
和--no-commit
--commit
參數使得合並后產生一個合並結果的commit節點。該參數可以覆蓋--no-commit
。
--no-commit
參數使得合並后,為了防止合並失敗並不自動提交,能夠給使用者一個機會在提交前審視和修改合並結果。
2.2--edit
和-e
以及--no-edit
--edit
和-e
用於在成功合並、提交前調用編輯器來進一步編輯自動生成的合並信息。因此使用者能夠進一步解釋和判斷合並的結果。
--no-edit
參數能夠用於接受自動合並的信息(通常情況下並不鼓勵這樣做)。
如果你在合並時已經給定了
-m
參數(下文介紹),使用--edit
(或-e
)依然是有用的,這將在編輯器中進一步編輯-m
所含的內容。
舊版本的節點可能並不允許用戶去編輯合並日志信息。
2.3--ff
命令
--ff
是指fast-forward命令。當使用fast-forward模式進行合並時,將不會創造一個新的commit節點。默認情況下,git-merge
采用fast-forward模式。
關於fast-forward模式的詳細解釋,請看我的另一篇文章:一個成功的Git分支模型的“關於fast forward”一節。
2.4--no-ff
命令
即使可以使用fast-forward模式,也要創建一個新的合並節點。這是當git merge
在合並一個tag時的默認行為。
2.5--ff-only
命令
除非當前HEAD節點已經up-to-date(更新指向到最新節點)或者能夠使用fast-forward模式進行合並,否則的話將拒絕合並,並返回一個失敗狀態。
2.5 --log[=<n>]
和 --no-log
--log[=<n>]
將在合並提交時,除了含有分支名以外,還將含有最多n個被合並commit節點的日志信息。
--no-log
並不會列出該信息。
2.6 --stat
, -n
, --no-stat
命令
--stat
參數將會在合並結果的末端顯示文件差異的狀態。文件差異的狀態也可以在git配置文件中的merge.stat配置。
相反,-n
, --no-stat
參數將不會顯示該信息。
2.7--squash
和--no-squash
--squash
當一個合並發生時,從當前分支和對方分支的共同祖先節點之后的對方分支節點,一直到對方分支的頂部節點將會壓縮在一起,使用者可以經過審視后進行提交,產生一個新的節點。
注意1:該參數和
--no-ff
沖突
注意2:該參數使用后的結果類似於在當前分支提交一個新節點。在某些情況下這個參數非常有用,例如使用Git Flow時(關於Git Flow,請參考:一個成功的Git分支模型),功能分支在進行一個功能需求的研發時,開發者可能在本地提交了大量且無意義的節點,當需要合並到develop分支時,可能僅僅需要用一個新的節點來表示這一長串節點的修改內容,這時
--squash
命令將會發揮作用。此外,如果功能分支的多次提交並不是瑣碎而都是有意義的,使用--no-ff
命令更為合適。
--no-squash
的作用正好相反。
2.8 -s <strategy>
和 --strategy=<strategy>
-s <strategy>
和 --strategy=<strategy>
用於指定合並的策略。默認情況如果沒有指定該參數,git將按照下列情況采用默認的合並策略:
- 合並節點只含有單個父節點時(如采用fast-forward模式時),采用recursive策略(下文介紹)。
- 合並節點含有多個父節點時(如采用no-fast-forward模式時),采用octopus策略(下文介紹)。
2.9 -X <option>
和 --strategy-option=<option>
在-s <strategy>
時指定該策略的具體參數(下文介紹)。
2.10 --verify-signatures
, --no-verify-signatures
用於驗證被合並的節點是否帶有GPG簽名,並在合並中忽略那些不帶有GPG簽名驗證的節點。
(以下引用摘自一篇轉載的文章,由於我沒有找到原作者,因此無法提供原作者信息和原文鏈接,如果有所侵權請私信或者評論告知,我將刪除以下引用內容。)
GPG是加密軟件,可以使用GPG生成的公鑰在網上安全的傳播你的文件、代碼。
為什么說安全的?以Google所開發的repo為例,repo即采用GPG驗證的方式,每個里程碑tag都帶有GPG加密驗證,假如在里程碑v1.12.3處你想要做修改,修改完后將這個tag刪除,然后又創建同名tag指向你的修改點,這必然是可以的。但是,在你再次clone你修改后的項目時,你會發現,你對此里程碑tag的改變不被認可,驗證失敗,導致你的修改在這里無法正常實現。這就是GPG驗證的作用,這樣就能夠保證項目作者(私鑰持有者)所制定的里程碑別人將無法修改。那么,就可以說,作者的代碼是安全傳播的。
為什么會有這種需求?一個項目從開發到發布,再到后期的更新迭代,一定會存在若干的穩定版本與開發版本(存在不穩定因素)。作為項目發起者、持有者,有權定義他(們)所認可的穩定版本,這個穩定版本,將不允許其他開發者進行改動。還以Google的repo項目為例,項目所有者定義項目開發過程中的點A為穩定版v1.12.3,那么用戶在下載v1.12.3版本后,使用的肯定是A點所生成的項目、產品,就算其他開發者能夠在本地對v1.12.3進行重新指定,指定到他們修改后的B點,但是最終修改后的版本給用戶用的時候,會出現GPG簽名驗證不通過的問題,也就是說這樣的修改是不生效的。
2.11 —summary
,--no-summary
和--stat
與 --no-stat
相似,並將在未來版本移除。
2.12 -q
和 --quiet
靜默操作,不顯示合並進度信息。
2.13 -v
和 --verbose
顯示詳細的合並結果信息。
2.14 --progress
和 --no-progress
切換是否顯示合並的進度信息。如果二者都沒有指定,那么在標准錯誤發生時,將在連接的終端顯示信息。請注意,並不是所有的合並策略都支持進度報告。
2.15-S[<keyid>]
和 --gpg-sign[=<keyid>]
GPG簽名。
2.16-m <msg>
設置用於創建合並節點時的提交信息。
如果指定了--log
參數,那么commit節點的短日志將會附加在提交信息里。
2.17--[no-]rerere-autoupdate
rerere即reuse recorded resolution,重復使用已經記錄的解決方案。它允許你讓 Git 記住解決一個塊沖突的方法,這樣在下一次看到相同沖突時,Git 可以為你自動地解決它。
2.18--abort
拋棄當前合並沖突的處理過程並嘗試重建合並前的狀態。
3.關於合並的其他概念
3.1合並前的檢測
在合並外部分支時,你應當保持自己分支的整潔,否則的話當存在合並沖突時將會帶來很多麻煩。
為了避免在合並提交時記錄不相關的文件,如果有任何在index所指向的HEAD節點中登記的未提交文件,git-pull和git-merge命令將會停止。
3.2fast-forward合並
通常情況下分支合並都會產生一個合並節點,但是在某些特殊情況下例外。例如調用git pull命令更新遠端代碼時,如果本地的分支沒有任何的提交,那么沒有必要產生一個合並節點。這種情況下將不會產生一個合並節點,HEAD直接指向更新后的頂端代碼,這種合並的策略就是fast-forward合並。
3.3合並細節
除了上文所提到的fast-forward合並模式以外,被合並的分支將會通過一個合並節點和當前分支綁在一起,該合並節點同時擁有合並前的當前分支頂部節點和對方分支頂部節點,共同作為父節點。
一個合並了的版本將會使所有相關分支的變化一致,包括提交節點,HEAD節點和index指針以及節點樹都會被更新。只要這些節點中的文件沒有重疊的地方,那么這些文件的變化都會在節點樹中改動並更新保存。
如果無法明顯地合並這些變化,將會發生以下的情況:
- HEAD指針所指向的節點保持不變
-
MERGE_HEAD
指針被置於其他分支的頂部 - 已經合並干凈的路徑在index文件和節點樹中同時更新
- 對於沖突路徑,index文件記錄了三個版本:版本1記錄了二者共同的祖先節點,版本2記錄了當前分支的頂部,即HEAD,版本3記錄了
MERGE_HEAD
。節點樹中的文件包含了合並程序運行后的結果。例如三路合並算法會產生沖突。 - 其他方面沒有任何變化。特別地,你之前進行的本地修改將繼續保持原樣。
如果你嘗試了一個導致非常復雜沖突的合並,並想重新開始,那么可以使用git merge --abort
關於三路合並算法:
三路合並算法是用於解決沖突的一種方式,當產生沖突時,三路合並算法會獲取三個節點:本地沖突的B節點,對方分支的C節點,B,C節點的共同最近祖先節點A。三路合並算法會根據這三個節點進行合並。具體過程是,B,C節點和A節點進行比較,如果B,C節點的某個文件和A節點中的相同,那么不產生沖突;如果B或C只有一個和A節點相比發生變化,那么該文件將會采用該變化了的版本;如果B和C和A相比都發生了變化,且變化不相同,那么則需要手動去合並;如果B,C都發生了變化,且變化相同,那么並不產生沖突,會自動采用該變化的版本。最終合並后會產生D節點,D節點有兩個父節點,分別為B和C。
3.4合並tag
當合並一個tag時,Git總是創建一個合並的提交,即使這時能夠使用fast-forward模式。該提交信息的模板預設為該tag的信息。額外地,如果該tag被簽名,那么簽名的檢測信息將會附加在提交信息模板中。
3.5沖突是如何表示的
當產生合並沖突時,該部分會以<<<<<<<
, =======
和 >>>>>>>
表示。在=======
之前的部分是當前分支這邊的情況,在=======
之后的部分是對方分支的情況。
3.6如何解決沖突
在看到沖突以后,你可以選擇以下兩種方式:
- 決定不合並。這時,唯一要做的就是重置index到HEAD節點。
git merge --abort
用於這種情況。 - 解決沖突。Git會標記沖突的地方,解決完沖突的地方后使用
git add
加入到index中,然后使用git commit
產生合並節點。
你可以用以下工具來解決沖突: - 使用合並工具。
git mergetool
將會調用一個可視化的合並工具來處理沖突合並。 - 查看差異。
git diff
將會顯示三路差異(三路合並中所采用的三路比較算法)。 - 查看每個分支的差異。
git log --merge -p <path>
將會顯示HEAD
版本和MERGE_HEAD
版本的差異。 - 查看合並前的版本。
git show :1:文件名
顯示共同祖先的版本,git show :2:文件名
顯示當前分支的HEAD版本,git show :3:文件名
顯示對方分支的MERGE_HEAD
版本。
4.合並策略
Git可以通過添加-s參數來指定合並的策略。一些合並策略甚至含有自己的參數選項,通過-X<option>
設置這些合並策略的參數選項。(不要忘記,合並可以在git merge和git pull命令中發生,因此該合並策略同樣適用於git pull)。
4.1resolve
僅僅使用三路合並算法合並兩個分支的頂部節點(例如當前分支和你拉取下來的另一個分支)。這種合並策略遵循三路合並算法,由兩個分支的HEAD節點以及共同子節點進行三路合並。
當然,真正會困擾我們的其實是交叉合並(criss-cross merge)這種情況。所謂的交叉合並,是指共同祖先節點有多個的情況,例如在兩個分支合並時,很有可能出現共同祖先節點有兩個的情況發生,這時候無法按照三路合並算法進行合並(因為共同祖先節點不唯一)。resolve策略在解決交叉合並問題時是這樣處理的,這里參考《Version Control with Git》:
In criss-cross merge situations, where there is more than one possible merge basis, the resolve strategy works like this: pick one of the possible merge bases, and hope for the best. This is actually not as bad as it sounds. It often turns out that the users have been working on different parts of the code. In that case, Git detects that it's remerging some changes that are already in place and skips the duplicate changes, avoiding the conflict. Or, if these are slight changes that do cause conflict, at least the conflict should be easy for the developer to handle
這里簡單翻譯一下:在交叉合並的情況時有一個以上的合並基准點(共同祖先節點),resolve策略是這樣工作的:選擇其中一個可能的合並基准點並期望這是合並最好的結果。實際上這並沒有聽起來的那么糟糕。通常情況下用戶修改不同部分的代碼,在這種情況下,很多的合並沖突其實是多余和重復的。而使用resolve進行合並時,產生的沖突也較易於處理,真正會遺失代碼的情況很少。
4.2recursive
僅僅使用三路合並算法合並兩個分支。和resolve不同的是,在交叉合並的情況時,這種合並方式是遞歸調用的,從共同祖先節點之后兩個分支的不同節點開始遞歸調用三路合並算法進行合並,如果產生沖突,那么該文件不再繼續合並,直接拋出沖突;其他未產生沖突的文件將一直執行到頂部節點。額外地,這種方式也能夠檢測並處理涉及修改文件名的操作。這是git合並和拉取代碼的默認合並操作。
recursive合並策略有以下參數:
4.2.1 ours
該參數將強迫沖突發生時,自動使用當前分支的版本。這種合並方式不會產生任何困擾情況,甚至git都不會去檢查其他分支版本所包含的沖突內容這種方式會拋棄對方分支任何沖突內容。
4.2.2 theirs
正好和ours相反。
theirs和ours參數都適用於合並二進制文件沖突的情況。
4.2.2 patience
在這種參數下,git merge-recursive
花費一些額外的時間來避免錯過合並一些不重要的行(如函數的括號)。如果當前分支和對方分支的版本分支分離非常大時,建議采用這種合並方式。
4.2.3diff-algorithm=[patience|minimal|histogram|myers]
告知git merge-recursive
使用不同的比較算法。
4.2.4 ignore-space-change
, ignore-all-space
, ignore-space-at-eol
根據指定的參數來對待空格沖突。
- 如果對方的版本僅僅添加了空格的變化,那么沖突合並時采用我們自己的版本
- 如果我們的版本含有空格,但是對方的版本包含大量的變化,那么沖突合並時采用對方的版本
- 采用正常的處理過程
4.2.5 no-renames
關閉重命名檢測。
4.2.6subtree[=<path>]
該選項是subtree合並策略的高級形式,將會猜測兩顆節點樹在合並的過程中如何移動。不同的是,指定的路徑將在合並開始時除去,以使得其他路徑能夠在尋找子樹的時候進行匹配。(關於subtree合並策略詳見下文)
4.3octopus
這種合並方式用於兩個以上的分支,但是在遇到沖突需要手動合並時會拒絕合並。這種合並方式更適合於將多個分支捆綁在一起的情況,也是多分支合並的默認合並策略。
4.4ours
這種方式可以合並任意數量的分支,但是節點樹的合並結果總是當前分支所沖突的部分。這種方式能夠在替代舊版本時具有很高的效率。請注意,這種方式和recursive策略下的ours參數是不同的。
4.5subtree
subtree是修改版的recursive策略。當合並樹A和樹B時,如果B是A的子樹,B首先調整至匹配A的樹結構,而不是讀取相同的節點。
4.5總結
在使用三路合並的策略時(指默認的recursive策略),如果一個文件(或一行代碼)在當前分支和對方分支都產生變化,但是稍后又在其中一個分支回退,那么這種回退的變化將會在結果中體現。這一點可能會使一些人感到困惑。這是由於在合並的過程中,git僅僅關注共同祖先節點以及兩個分支的HEAD節點,而不是兩個分支的所有節點。因此,合並算法將會把被回退的部分認為成沒有變化,這樣,合並后的結果就會變為另一個分支中變化的部分。
5.關於Git使用的一些個人看法
本人一直認為Git是一款非常優秀的版本控制工具,但是在公司中很多人覺得Git很難使用。這種情況很大一部分原因是之前使用subversion時帶來的使用慣性對接受新技術造成了影響;另一方面,很多人僅僅通過GUI客戶端去使用Git。很久以來,大部分人認為使用GUI是一種較為便捷的入門方式,其實這是值得商榷的。依我個人的經驗來說,使用GUI會形成惰性,往往點擊幾個按鈕就能完成操作,使得很多人認為學習Git的命令是一種浪費時間和精力的行為。但是事實上,在沒有理解清楚Git命令和思想的情況下,使用那些簡單的按鈕其實會帶來很大的困擾:很多人根本不知道點擊按鈕后會發生什么,GUI的過於智能讓同一個按鈕的點擊事件可能對應着不同參數的命令。最后真正受到傷害的是可憐的使用者們,因為他們根本不知道問題出在哪里。
綜合全文的內容,這里總結一些個人使用Git時所遵守的約定。所謂約定,即非強迫性的,自願的行為。不遵守這些約定並不會帶來什么缺陷,但是遵守這些約定可能會減輕在使用Git時帶來的困難,提高效率。
- 多提交,少推送。多人協作時,推送會頻繁地帶來合並沖突的問題,影響效率。因此,盡量多使用提交命令,減少合並的使用,這樣會節省很多時間。
- 使用Git流(Git Flow),詳見我的另一篇文章:一個成功的Git分支模型
- 使用分支,保持主分支的整潔。這是我強烈推薦的一點,在分支進行提交,然后切到主分支更新(git pull —rebase),再合並分支、推送。這樣的流程會避免交叉合並的情況出現(不會出現共同祖先節點為多個的情況)。事實上,git合並操作讓很多人感到不知所措的原因就是各種原因所產生的交叉合並問題,從而造成在合並的過程中丟失某些代碼。保持主分支的整潔能夠避免交叉合並的情況出現。
- 禁用fast-forward模式。在拉取代碼的時候使用rebase參數(前提是保持主分支的整潔)、合並的時候使用—no-ff參數禁用fast-forward模式,這樣做既能保證節點的清晰,又避免了交叉合並的情況出現。
作者:Chuckiefan
鏈接:https://www.jianshu.com/p/58a166f24c81
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。