在開發專案中,Git是一個天賜良機。然而,如果舞池裡有很多舞者,總有一兩個會互相踩到對方的腳趾。對於您的專案來說,這意味著兩個開發者可能會在同一套程式碼上工作,並且都有可能提交。在這種情況下,您需要採取一些 Git 合併策略來解決衝突。
雖然 Git 合併可以很簡單,但很多時候您還需要更高階的方法。比如遞迴合併、三方合併等等。您甚至可能需要在某些時候撤銷 Git 合併。
本教程將討論一些複雜的 Git 合併技術。事實上,我們可以直接進入正題!
Git 合併策略介紹
合併的核心概念很簡單:把兩個分支合併起來,把多個提交變成一個提交。然而,為了確保提交和合並的程式碼是正確的,您可以使用一些技巧。
下面我們將介紹一些您需要了解的重要策略。它們並不按順序排列,在你的開發生涯中,你可能會用到它們。此外,您還需要對指標、分支和提交等Git基本概念有紮實的理解。
雙向合併和三向合併的區別
瞭解雙向合併和三向合併的區別很有幫助。我們接下來介紹的大多數合併策略都是針對三向合併的。事實上,三向合併更簡單明瞭。請看下面這個例子:
- 您有一個有若干提交的主分支和一個也有提交的特性分支。
- 然而,如果主分支現在進一步提交,兩個分支就會出現分歧。
- 通俗地說,主分支和特性分支都有提交,而另一個沒有。如果用雙向方法合併,就會丟失一個提交(很可能是主分支上的。)
- 相反,Git 會從當前的主分支和特性分支中建立一個新的合併提交。
一言以蔽之,Git 會檢視三個不同的快照來合併變更:主分支的頭,特性分支的頭,以及共同祖先。這將是主分支和特性分支共同的最終提交。
在實踐中,你不需要擔心某個合併策略是雙向的還是三向的。很多時候,你必須使用某種策略。無論如何,瞭解 Git 在合併分支和版本庫時是如何 “思考” 的,是很有幫助的。
快速合併
第一個策略可能不需要執行任何操作。快進合併會將指標移到主分支的最新提交上,而不會產生額外的提交(這可能會造成混亂)。這是一種簡潔的方法,許多開發者會將其作為標準。
該技術從一個可能有提交也可能沒有提交的主分支開始。在這種情況下,您開啟一個新的分支,編寫程式碼並提交。此時,您還需要將這些變更合併回主分支。快進合併有一個要求:
- 您需要確保在新分支上工作時,主分支上沒有其他提交。
這並不總是可能的,尤其是當你在一個大團隊中工作時。即便如此,如果您選擇將您的提交與當前的、沒有自己提交的主分支合併,這將執行一次快進合併。有幾種不同的方法:
git merge <branch> git merge --ff-only
很多時候,你不需要指定要執行快進合併。這種型別的合併發生在單人專案或小團隊專案中。在快節奏的環境中,這種合併很少見。因此,其他型別的合併更為常見。
遞迴合併
遞迴合併通常是預設的,因為它比其他型別的合併更常見。遞迴合併是指在一個分支上進行提交,但在主分支上也進行提交。
當需要合併時,Git 會對分支進行遞迴,以完成最終提交。這意味著,一旦完成合並,就會有兩個父提交。
和快進合併一樣,通常不需要指定遞迴合併。不過,你可以用下面的命令和標誌確保 Git 不會選擇類似快進合併的方式:
git merge --no-ff git merge -s recursive <branch1> <branch2>
第二行使用 -s
策略選項和顯式命名來執行合併。與快進合併不同,遞迴合併會建立一個專門的合併提交。對於雙向合併,遞迴策略是可靠的,並且執行良好。
我們的和他們的
開發過程中常見的一種情況是,你在專案中建立了一個新特性,但最終沒有獲得綠燈。在很多情況下,你會有很多程式碼需要合併,而這些程式碼又是相互依賴的。我們的合併是解決這些衝突的最佳方式。
這種型別的合併可以根據需要處理多個分支,並忽略其他分支上的所有變更。如果你想清除舊特性或不需要的開發,它是個不錯的選擇。下面是你需要的命令:
git merge -s ours <branch1> <branch2>
我們的合併本質上意味著當前分支包含了法律上的程式碼。這與 “他們的” 合併有關,後者將其他分支視為正確的。不過,您需要在這裡傳遞另一個策略選項:
git merge -X theirs <branch2>
使用 “我們的” 和 “他們的” 合併可能會引起混淆,但一般來說,堅持典型的使用情況(即保留當前分支中的所有內容並丟棄其餘部分)是安全的。
Octopus
處理多頭合併–即把多個分支合併到另一個分支–對於 git 合併來說是個棘手的問題。可以說您需要兩隻以上的手來解決衝突。這對於octopus合併來說再合適不過了。
octopus合併與我們和他們的合併截然相反。典型的用例是你想把多個類似功能的提交合併成一個。下面是如何傳遞的:
git merge -s octopus <branch1> <branch2>
但如果需要手動解決,Git會拒絕octopus合併。對於自動決議,如果需要將多個分支合併為一個,則預設使用octopus合併。
解決
決議合併是最安全的合併提交方式之一,如果遇到交叉合併的情況,決議合併是個不錯的選擇。這也是一種快速的解決方法。您可能還想用它來處理更復雜的合併歷史–但僅限於雙頭合併。
git merge -s resolve <branch1> <branch2>
由於解析合併使用一種三向演算法,同時處理您當前的分支和您從其中提取的分支,因此它可能不如其他合併方法靈活。不過,就您需要它完成的工作而言,解析合併近乎完美。
子樹
遞迴合併的同伴可能會讓你感到困惑。我們試著用一個清晰的例子來解釋:
- 首先,考慮兩個不同的樹–X和Y。
- 你想把這兩個樹合併成一個。
- 如果Y樹與X中的一個子樹相對應,那麼Y樹就會被修改以匹配X的結構。
這意味著,如果您想將多個版本合併為一篇明確的文章,子樹合併就非常有用。它還會對兩個分支的共同 “ancestor” 樹進行必要的修改。
git merge -s subtree <branch1> <branch2>
簡而言之,如果您需要合併兩個版本庫,子樹合併就是您想要的。事實上,你可能很難理解哪種合併策略適合你。稍後,我們將討論一些可以提供幫助的工具。
在此之前,你必須知道如何解決一些高階的合併衝突。
如何處理更復雜的 Git 合併衝突
在 Git 中合併分支更像是管理和解決衝突。團隊和專案規模越大,發生衝突的機率就越大。有些衝突可能很複雜,很難解決。
考慮到衝突會侵蝕時間、金錢和資源,您需要想辦法把它們快速消滅在萌芽狀態。在大多數情況下,兩個開發人員會在同一套程式碼上工作,並且兩人都會決定提交。
這可能意味著你可能會因為待處理的變更而根本無法開始合併,或者在合併過程中出現故障,需要人工干預。一旦工作目錄 “乾淨” 了,就可以開始了。很多時候,Git 會在你開始合併後通知你有衝突:
終端視窗顯示Git中的合併衝突。
不過,您可以執行 git status
檢視詳細資訊:
顯示 git status 命令結果的終端視窗。
從這裡,您可以開始處理導致衝突的各種檔案。我們接下來討論的一些工具和技術會有所幫助。
中止和重置合併
有時,您需要完全停止合併,從一片淨土開始。事實上,我們提到的兩個命令都適用於你還不知道如何處理衝突的情況。
你可以用下面的命令中止或重置正在進行的合併:
git merge --abort git reset
這兩個命令類似,但在不同的情況下使用。例如,中止合併會簡單地將分支恢復到合併前的狀態。在某些情況下,這並不起作用。例如,如果你的工作目錄中包含了未提交和未合併的修改,你就無法中止合併。
然而,重置合併意味著將檔案恢復到已知的 “良好” 狀態。如果 Git 啟動合併失敗,可以考慮後者。需要注意的是,這條命令會刪除任何你沒有提交的改動,這意味著這是一個需要小心謹慎的行為。
檢查衝突
大多數合併衝突都可以直接確定和解決。然而,在某些情況下,你可能需要深入挖掘,以找出衝突發生的原因,以及如何開始修復它。
您可以在 git merge
後使用 checkout 獲取更多資訊:
git checkout --conflict=diff3 <filename>
這將使用檢出所提供的典型導航,並建立兩個檔案之間的比較,以顯示合併衝突:
檢查特定專案檔案中的衝突。
從技術意義上講,這將再次檢查檔案並替換衝突標記。在整個解決過程中,您可能會多次這樣做。在這裡,如果您通過 diff3
引數,它將給出基本版本和 “我們的” 和 “他們的” 版本的替代版本。
注意預設的引數選項是 merge
,除非你改變了合併衝突的樣式,否則不必指定。
忽略Negative Space
負空格(Negative space)及其用法是一個常見的討論點。一些程式語言會使用不同型別的間距,甚至個別開發人員也會使用不同的格式。
空格與製表符是我們不會加入的戰場。不過,如果您遇到格式根據檔案和編碼實踐而變化的情況,您可能會遇到這個 Git 合併問題。
您會發現合併失敗的原因,因為當您檢視衝突時,會發現有一些行被移除或新增:
在編輯器中顯示衝突差異的檔案。
這是因為 Git 會檢視這些行,並將負空格視為修改。
不過,您可以在 git merge
命令中新增特定引數,這樣就可以忽略相關檔案中的負空格:
git merge -Xignore-all-space git merge -Xignore-space-change
雖然這兩個引數看似相似,但它們有一個獨特的區別。如果您選擇忽略所有負空格,Git 會這樣做。這是一種一刀切的做法,但相比之下, -Xignore-space-change
只將一個或多個負空格字元的序列視為等價字元。因此,它會忽略行尾的單空格。
為了更安全起見,你也可以使用 --no-commit
命令檢視合併結果,以檢查你是否以正確的方式忽略和計算了負空格。
合併日誌
日誌對於幾乎所有傳遞資料的軟體都至關重要。對於 Git 來說,您可以使用日誌來確定合併衝突的更多細節。您可以使用 git log
訪問這些資訊:
在終端執行並檢視 Git 日誌。
它本質上是一個文字檔案轉儲站,記錄 repo 中的每個操作。不過,您可以新增更多引數來細化檢視,只檢視您希望看到的提交:
git log --oneline --left-right <branch1>...<branch2>
它使用 “Triple Dot” 來提供合併過程中兩個分支所涉及的提交列表。它將過濾兩個分支共享的所有提交,留下一部分提交供進一步調查。
您也可以使用 git log --oneline --left-right --merge
來只顯示合併時兩邊 “觸及 “衝突檔案的提交。 -p
選項將顯示特定 “diff” 的確切改動,但注意這隻針對非合併提交。有一個解決方法,我們接下來介紹。
使用組合差分格式調查 Git 合併衝突
您可以利用 git log
的檢視進一步調查合併衝突。在通常情況下,Git 會合並程式碼,並將合併成功的程式碼分期。這將只留下有衝突的行,您可以使用 git diff
命令檢視它們:
在終端執行 git diff 命令。
這種 “合併差異” 格式增加了兩列額外的資訊。第一列告訴您某行在您的分支(”我們的”)和工作拷貝之間是否不同;第二列給您 “他們的” 分支的相同資訊。
對於符號,加號表示該行是否是工作副本中的新增行,但不在合併的那一側;減號表示該行是否被移除。
在 Git 日誌中也可以看到這種組合的差異格式:
git show git log --cc -p
第一個命令用於檢視合併提交的歷史。第二個命令使用 -p
的功能來顯示對非合併提交的修改,同時顯示合併的 diff 格式。
如何撤銷 Git 合併
錯誤可能會發生,你可能會進行一些需要收回的合併。在某些情況下,您可以使用 git commit --amend
簡單地修改最近的提交。這會開啟編輯器,讓您修改最近的提交資訊。
雖然您可以逆轉更復雜的合併衝突和由此產生的改動,但這可能很困難,因為提交通常是永久性的。
因此,你需要遵循很多步驟:
- 首先,您需要檢視提交,找到您需要的合併的引用。
- 其次,檢查分支,檢視提交歷史。
- 一旦瞭解了所需的分支和提交,就可以根據需要執行特定的Git命令了。
讓我們詳細瞭解一下這些命令,從審查流程開始。接下來,我們將向您展示如何快速撤銷 Git 合併,然後針對更高階的用例介紹具體的命令。
稽覈提交
如果想檢視當前分支的修訂ID和提交資訊, git log --oneline
命令是個不錯的選擇:
在終端執行一行 git diff 命令。
git log --branches=*
命令會顯示同樣的資訊,但針對所有分支。無論如何,您可以在使用 git checkout
的同時使用引用 ID 來建立一個 “分離的 HEAD
“狀態。這意味著從技術角度看,您不會在任何分支上工作,而一旦您切換回已建立的分支,您就成了修改的 “孤兒”。
因此,您幾乎可以把簽出當作一個無風險的沙箱來使用。不過,如果您想保留修改,可以簽出該分支,然後用 git checkout -b <branch-name>
給它起個新名字。這是撤銷 Git 合併的可靠方法,但對於高階用例,還有更細緻的方法。
使用 git 重置
許多合併衝突可能發生在本地倉庫。在這種情況下,您需要使用 git reset
命令。不過,這個命令有更多的引數。下面是該命令的實際使用方法:
git reset --hard <reference>
第一部分 – git reset --hard
– 經過三個步驟:
- 將參考分支移動到合併提交前的位置。
- 硬重置使 “索引”(即下一個提交快照)看起來像參考分支。
- 使工作目錄看起來像索引。
一旦你呼叫了這個命令,提交歷史就會刪除後來的提交,並重置歷史到引用的ID。這是撤銷 Git 合併的簡潔方法,但並非適用於所有情況。
例如,如果你試圖將本地重置的提交推送到包含該提交的遠端倉庫,就會導致錯誤。在這種情況下,您可以使用另一種命令。
使用 git revert
雖然 git reset
和 git revert
看起來很相似,但還是有一些重要的區別。在目前的例子中,撤銷過程涉及移動引用指標和 HEAD 到特定的提交。這類似於洗牌來建立一個新的順序。
相比之下, git revert
會根據回溯的改動建立一個新的提交,然後更新參考指標,使該分支成為新的 “頂端”。這也是為什麼您應該用這條命令來處理遠端倉庫合併衝突的原因。
您可以用 git revert <reference>
來撤銷 Git 合併。注意,您需要指定一個提交引用,否則命令無法執行。你也可以給命令傳遞 HEAD
來恢復到最新的提交。
不過,你可以讓 Git 更清楚地知道你想做什麼:
git revert -m 1 <reference>
當您呼叫合併時,新提交將有兩個 “父分支”。一個是你指定的引用,另一個是你要合併的分支的頂端。在這種情況下, -m 1
會告訴 Git 保留第一個父分支,即指定的引用,作為 “主線”。
git revert
的預設選項是 -e
或 --edit
。這會開啟編輯器,以便在還原之前修改提交資訊。不過,您也可以通過 --no-edit
,它不會開啟編輯器。
也可以通過 -n
或 --no-commit
。這會告訴 git revert 不建立新的提交,而是 “反向” 修改並將其新增到暫存索引和工作目錄中。
Git 合併與重定向的區別
除了使用 git merge
命令,您還可以使用 git rebase
。這也是一種將改動整合到一個目錄中的方法,但有所不同:
- 當您使用
git merge
時,預設情況下是三路合併。它結合了兩個當前分支的快照,然後與兩個分支的共同祖先合併,建立一個新的提交。 - 而重置則是將不同分支中的補丁應用到另一個分支中,而不需要祖先分支。這意味著不會有新的提交。要使用這個命令,先簽出到你想重定向的分支。
在那裡,你可以使用下面的命令:
git rebase -i <reference>
在很多情況下,您的參考分支就是您的主分支。 -i
選項啟動 “互動式重定向”。這使您有機會在提交移動時修改它們。您可以用它來清理提交歷史,這也是使用 git rebase
的一大好處。
執行該命令將在編輯器中顯示潛在的提交列表。這樣您就可以完全改變提交歷史的外觀。如果將 pick
命令改為 fixup
,您還可以合併提交。一旦您儲存了修改,Git 就會執行重置(rebase)。
總的來說,Git 合併可以解決很多衝突。不過,重置也有很多好處。例如,合併簡單易用,可以保留合併歷史的上下文,而重置則可以簡化提交歷史。
儘管如此,在重定向時你必須更加小心,因為出錯的可能性很大。此外,你不應該在公共分支上使用這種技術,因為rebasing只會影響你的repo。要修復由此產生的問題,您需要進行更多合併,並會看到多次提交。
幫助您更好地管理 Git 合併的工具
鑑於 Git 合併衝突的複雜性,你可能需要幫手。有很多工具可以幫助您成功合併,如果您使用的是Intellij IDEA,您可以使用 “Branches” 選單中的內建方法:
在Intellij IDEA中檢視分支。
VSCode在其使用者介面(UI)中也包含了類似的功能。Atom的老使用者會發現微軟在這裡繼承了其出色的Git整合,無需其他擴充套件或外掛即可連線GitHub。
您還可以通過命令面板(Command Palette)獲得更多選項。這甚至適用於基於VSCode開源框架的編輯器,如Onivim2:
在Onivim2的命令面板中使用 “合併分支 “命令。
和本列表中的所有工具一樣,這個命令的好處是你不需要命令列來執行合併。您通常需要從下拉選單中選擇源分支和目標分支,然後讓編輯器執行合併。即便如此,您也不必放手不管。你可以在合併後檢視修改,然後進行你需要的提交。
Sublime Text是一款為Git工作提供獨立圖形使用者介面(GUI)的編輯器。如果你使用的是Sublime Text,那麼Sublime Merge將是你工作流程的理想補充:
Sublime Merge應用程式。
無論你選擇哪種程式碼編輯器,通常都能在不使用命令列的情況下使用Git。Vim和Neovim甚至可以使用Tim Pope的Git Fugitive外掛。
不過,也有一些專門的第三方合併工具專注於這項任務。
專用的Git合併
例如,Mergify是一款企業級的程式碼合併工具,可以整合到持續整合/持續交付(CI/CD)管道和工作流中:
Mergify 網站。
這裡的一些功能可以幫助您在合併前自動更新您的拉取請求,根據優先順序重新排序,以及批處理它們。對於開源解決方案,Meld可能是有價值的:
Meld應用程式介面。
其穩定版本支援Windows和Linux,並在GPL許可下執行。它為您提供了比較分支、編輯合併等基本功能。您甚至可以進行雙向或三向比較,並支援Subversion等其他版本控制系統。
小結
Git是高效協作和管理程式碼變更的重要工具。然而,如果多個開發者在同一程式碼上工作,可能會產生衝突。Git 合併策略可以幫助您解決這些衝突,而且有很多方法。對於更復雜的 Git 合併策略,您需要使用高階策略。
這甚至可以簡單到忽略負空格或搜尋日誌。不過,您也不必總是使用命令列。有很多應用程式可以幫助你,你的程式碼編輯器通常也會使用內建介面。
評論留言