Git命令解析 - merge、rebase

2021-01-11 AI撲社

作者 | Video++極鏈科技後端Team

整理 | 包包

Git分支和工作流

分支本質是一個指向提交對象的可變指針。Git 保存的不是文件的變化或者差異,而是一系列不同時刻的文件快照。在進行提交操作時,會保存一個提交對象(commit object),在多次提交後,commit對象形成連續的快照鏈,分支指針自動指向最新一次提交。Git 的默認分支名字是 master。如下圖:

branch命令可以輕鬆創建一個新分支,就像這樣:

$ git branch new_branch

這一命令實際是為當前提交對象添加了一個新的指針。這種分支形式比大多數版本控制系統更為輕量,無論是創建還是切換都幾乎可以在瞬間完成。Git 鼓勵在工作流程中頻繁地使用分支與合併,這完全不會增加倉庫負擔,並且可以基於這一特性創建更自由和更可靠的合作開發流程。

許多使用Git 的開發者都喜歡使用這種方式來工作:僅在master分支上保留完全穩定的代碼,這些代碼通常處於已發布或等待發布的狀態。此外使用一些短期分支,比如用develop分支開發新特性,使用test分支修復bug,測試穩定性,直到代碼質量達到發布要求,再合併到master分支,完成一個版本的開發。

不同的開發者團隊可以自由創造適合自己組織形式的分支策略。社區中也存在許多深受歡迎的流程範例,比如經典的gitflow工作流、PR工作流、集中式工作流等等,它們通常適用於不同的合作方式,並不是某種強制規範。有興趣的讀者可以繼續深入探索,此處不再過多介紹。

merge

假設我們基於master分支創建了feature分支用來開發新功能,經過一段時間開發之後,需要把feature的分支代碼合併回到master,通常執行的操作是先檢出master分支,然後執行git merge feature。

一般來說,在單人開發的情況下,merge通常會產生快進(fast-forward)方式的合併。如果在子分支(feature)被創建之後,父分支(master)未產生新的修改和提交,此時把feature合併回master,Git會在提交鏈上把master指針簡單的前移,使兩個分支進度同步,並形成無分支記錄的提交鏈。執行時在控制臺輸出Fast-forward標識。這種merge方式下不會產生衝突,git log命令會看到如下記錄:

但在團隊合作開發時,通常會多人修改同一遠程分支。其中使用的pull和push命令實際包含了merge操作。這時git使用另外一種方式來進行分支合併。目前只有一方修改的情況下,也可以使用 —no-ff 參數來模擬這種方式。

這裡使用了git最基礎的三路遞歸合併(recursive three-way merge),輸出Merge made by the 'recursive' strategy.標明合併方式。這種合併會形成帶分支歷史的提交鏈:

從圖中可以看出,這種merge方式實際在發起合併的分支生成了一個帶有Merge 標識的新提交。如果合併時存在衝突,解決衝突後的最終內容也會包含在這個新的提交中。

看到這裡,可能有人會有疑問,工作空間中自始至終只出現了兩個分支,為什麼會是三路合併。從git 源碼中可以找到merge執行的入口,它有這樣的方法籤名:

可以看出,除了含義明顯的ours和theirs,還有一個待合併的文件叫做ancestor。根據文檔和源碼注釋,這個版本實際是兩個待合併分支的公共部分。在我們的例子中就是創建新分支的那個提交對象。

大體的流程是這樣的,git merge會找出兩個分支指向的最新commit,找到他們最近的公共祖先,然後對每個待合併的文件調用ll_merge,這個方法會比較各分支和祖先節點的差異。然後把這些差異整合成一個Merge提交,應用到當前分支上,生成最終的合併結果。

如果兩個分支之間有多個公共祖先,git會選出最合適的祖先節點依照同樣規則進行遞歸合併。可以使用git merge-base —all命令列出所有的備選祖先節點。

Git還可以一次性合併多個分支,只需要簡單的把分支名當做merge的參數依次列出:

這種策略被稱為octopus,其中核心邏輯與three-way merge相同,不再詳述,可以通過閱讀github上的源碼和文檔繼續深入了解。

three-way merge機制有一定的隱患。如果其中一個待合併分支,比如ours,和ancestor版本的某一部分代碼相同,但另一個待合併分支theirs中有不同的修改,合併的結果就會採用theirs分支不同的那部分,並不會依照修改的時間順序來決定最終內容。在實際項目中可能會反覆修改同一段代碼來響應需求變更,就有機率發生這種合併結果與預計不符的情況,需要特別留意。

rebase

Git rebase,通常被稱作變基或衍合, 可以理解為另外一種合併的方式,與merge 會保留分支結構和原始提交記錄不同,rebase 是在公共祖先的基礎上,把新的提交鏈截取下來,在目標分支上進行重放,逐個應用選中的提交來完成合併。

為了形象理解rebase的過程,可以看下面例子:

使用 merge 合併後:

下面使用rebase方式達到同樣效果:

除了原本的多分支記錄變為了直線提交鏈,還可以注意到,其中原本在feature分支上的提交,rebase後的SHA編碼發生了變化。rebase消除了真實歷史,重新生成了新的提交。

和merge類似,rebase在遇到衝突時也會暫停,需要手動修復後才可以繼續。但是rebase的處理要相對繁瑣一些,merge 如果發生 conflict,只需要在最終的Merge 提交上解決一次。而 rebase 的 conflict 可能發生在每一次提交的重新應用上,所以需要依次解決。

為了避免這種情況,可以在與另一分支合併之前,提前把所有需要提交合併為一個提交。同樣需要用到rebase命令。

執行這樣一個命令來合併當前最新的3個提交:

這條命令將打開一個編輯頁面,我們可以修改前面的命令來合併或丟棄單個提交。

pick 表示將會應用這個提交。

squash 表示把當前提交合併到前一個提交,它的前面必須至少有一個被pick的提交存在。

把某條提交注釋或刪除表示丟棄這條記錄。

這裡選擇合併第一個和第三個,丟棄第二個提交。

保存退出後進入新的編輯頁面,提示編輯提交信息,這裡選擇不做改動。

再次保存退出後成功合併完成,形成這樣的log:

git還有一個可愛的命令cherry-pick,通常譯作揀選。它的參數是提交對象的SHA編碼,可以視為針對單個提交的rebase操作。示例如下:

總結

merge 和 rebase 的差異在於最終的歷史記錄,可以發現 merge 保持了所有分支的原始修改記錄,可能會包含很多不必要的信息;而 rebase相當於對歷史記錄做出修剪,可以維持一條簡單清晰的提交路線。

通常我們會在基於一個過時的版本進行了本地修改的情況下使用rebase,在實際開發中經常會出現這種情況,當你在本地分支上工作了幾天,突然想起應該push到遠程倉庫時,遠程分支已經被別人更新過了。此時你會得到一個reject信息。

有些人會選擇用pull命令合併遠程和本地的同名分支,但pull實際執行了fetch和merge兩個操作,會生成複雜的分支歷史和一個多餘的merge提交。你也可以選擇用fetch和rebase代替pull,始終生成一個美觀的提交鏈。

rebase的另一個重要應用是合併過多的本地提交。因為防止修改內容丟失,經常commit到本地倉庫是一個很好的開發習慣。但是當需要提交到公共分支時,大量無明確意義的提交信息對歷史記錄造成不必要的幹擾。此時你可以用rebase命令把本地記錄規範化,再進行推送。

使用rebase的時候需要遵循一條重要原則:不要對在你的本地倉庫外有副本的提交記錄進行變基。rebase的實質是丟棄一些現有的提交,然後相應地新建一些內容一樣但實際上不同的提交。 如果其他人已經在這些提交上做出過大量修改、衝突合併等工作,那麼你的rebase將成為他們的惡夢。

對於使用rebase還是merge來合併代碼,實際並沒有什麼固定的模式,取決於開發者如何看待倉庫的歷史記錄。一些人認為歷史記錄應該反映全部真實變更細節,而另一些人認為歷史記錄應該是精心維護的變更目錄。具體如何使用取決於項目合作者的一致共識。無論是merge還是rebase,都應該了解其中原理,避免危險操作,才能享受到Git諸多特性帶來的便利。

相關焦點

  • 【譯文】Git merge 和 Git rebase比較
    在這篇文章中,我們會比較git rebase和類似的git merge命令,找到Git工作流中rebase的所有用法。概述   你要知道的第一件事是,git rebase 和git merge 做的事其實是一樣的。它們都被設計來將一個分支的更改併入另一個分支,只不過他們實現方式有些不同。
  • Git: 聊聊Rebase命令
    先切換會origin分支,通過pull指令將origjn分支上的最新改動拉取下來。然後在用git checkout mywork切換會mywork分支,到這裡就能用git rebase orgin來進行變基了。
  • 我到底應該用git-merge還是git-rebase呢?
    本文一共3165字,將通俗的解釋git中兩種合併策略——merge和rebase的不同、運用方式和使用的場景,並輔以我在工作中的運用來介紹;什麼是git merge?git merge是我們在git操作中頻繁會用到的一個命令,它主要實現的功能便是為我們進行分支代碼的合併,也就是將兩個或兩個以上的開發歷史合併在一起的操作。
  • [笨叔點滴5] git rebase和git merge究竟有啥區別?
    git提供了兩個分支合併的命令,一個是git merge,另外一個是git rebase,他們究竟有啥區別呢?我們假設一個git倉庫裡有一個master分支,另外還有一個dev分支。如下圖所示。上述ABCDEFG這幾個節點(每個節點是一個commit)都是按照時間順序來提交的,如下表所示。
  • 從零開始寫git:理解rebase與merge
    rebase(變基)git-rebase - Reapply commits on top of another base tip——git官方文檔什麼是rebasegit的初學者可能對rebase了解不深,它的使用頻率沒有pull、add、commit、push那麼高,但了解rebase的使用還是有必要的,在一些場景下很可能會用到rebase。
  • 深入git rebase使用
    rebas-rep倉庫,任何時候出現問題搞不定了,只需刪除掉rm -rf rebase-repo/.git目錄並重新運行這些命令就可以初始化整個實驗環境。git pull --rebase如果遠程git伺服器倉庫分支已經有了更新上,通常git pull時候將會默認執行merge合併提交。
  • 原來華為是這樣使用 git rebase的
    原文:https://www.mdeditor.tw/pl/pMHD使用git參與多人之間的合作開發大概有三年的時間,大多數場景下使用的git命令一隻手多一點就能數的過來理論上來說,只要能合理管理項目分支,這幾個命令已經足以應付所有的日常開發工作。
  • 【Git】616- git命令的進階和複習(帶動圖效果)
    小姐姐用動畫圖解Git個人比較推薦第二個Git學習網站猴子都能懂的git,圖文結合,簡單明了,本文將介紹一些常用 Git 指令,作為一個學習總結git rebasegit mergegit resetgit revertgit cherry-pickgit fetchgit
  • git命令的進階和複習(帶動圖效果)
    小姐姐用動畫圖解Git個人比較推薦第二個Git學習網站猴子都能懂的git,圖文結合,簡單明了,本文將介紹一些常用 Git 指令,作為一個學習總結git rebasegit mergegit resetgit revertgit cherry-pickgit fetchgit
  • Git 常用命令清單筆記
    git pull origin next # 遠程分支是與當前分支合併上面一條命令等同於下面兩條命令git fetch origingit merge origin/next如果遠程主機刪除了某個分支,默認情況下,git pull 不會在拉取遠程分支的時候,刪除對應的本地分支。
  • Git命令的用法小結
    也可以通過命令行設置,如$ git config --global user.email 郵箱機制$ git config --global user.name 用戶名幫助(help)有多種方式獲取git的整體幫助,或者某個命令的幫助。
  • Git命令的動畫展示,讓我們學習Git事半功倍
    Git幾乎是每個程式設計師的標配,當然有時候也是噩夢,因為如果不是對他的各種命令非常熟悉的話,各種繞腦的命令會把我們弄暈,因為很多時候我們並不知道這個命令內部到底是怎麼樣的,如果每一個命令都有相對應的動畫,我們是不是理解起來更容易一些呢?
  • 7000+ 字帶你全面搞懂 Git 命令+原理!
    git merge我們在開發分支dev開發、測試完成在發布之前,我們一般需要把開發分支dev代碼合併到master,所以git merge也是程式設計師必備的一個命令。git merge master 在當前分支上合併master分支過來git merge --no-ff origin/dev 在當前分支上合併遠程分支devgit merge --abort 終止本次merge,並回到merge前的狀態比如,你開發完需求後,發版需要把代碼合到主幹master
  • 10段超有用的Git命令行代碼
    在最後提交中更改Export(Export changes done in last commit )這個命令通常會使用定期發送已更改的項目,以方便其他人審查/集成。git archive -o ..忽略追蹤文件中的更改( Ignore Changes in a Tracked File )如果你是在某個團隊中工作,他們都在使用同一個分支,也許你會頻繁使用提取/合併(fetch/merge),但這有時需要重置特定的配置文件,這就意味著在每次合併後你必須去做更改。現在,使用這個命令,你可以要求Git忽略更改特定文件。
  • 一篇文章,教你學會Git
    merge命令把不同的分支合併起來。git fetch [remote]merge之前先拉一下遠程倉庫最新代碼git merge [branch]合併指定分支到當前分支一般在merge之後,會出現conflict,需要針對衝突情況,手動解除衝突。主要是因為兩個用戶修改了同一文件的同一塊區域。如下圖所示,需要手動解除。rebase又稱為衍合,是合併的另外一種選擇。
  • git分支概念和分支相關操作
    分支是git中最容易被誤解的概念之一,雖然git分支並不難理解。使用分支時候是不有點不知所措,"老虎吃天,無處下爪"的感覺?還有那一系列的merge和rebase黑魔法操作,甚至是那些許的衝突都曾讓你頭痛不已?
  • Git命令總結,總結收藏
    等命令自動著色 git config --global color.status auto git config --global color.diff auto git config --global color.branch auto git config --global color.interactive auto git
  • Git分支原理命令圖文解析
    一旦使用了checkout命令,我們還會發現,不僅head指針會指向新的分支,而且當前工作目錄中的文件也會換成了新分支對應的文件了。 此外,我們還可以使用git checkout -b [name]命令,它會新建一個分支,並自動將當前的工作目錄切換到該分支上。
  • 工作流一目了然,看小姐姐用動圖展示10大Git命令
    merge、git rebase、git reset、git revert、git fetch、git pull、git reflog……你知道這些 git 命令執行的究竟是什麼任務嗎? 可將一個分支的修改融入到另一個分支的一種方式是執行 git merge。Git 可執行兩種類型的合併:fast-forward 和 no-fast-forward。現在你可能分不清,但我們馬上就來看看它們的差異所在。
  • 這才是真正的Git——Git實用技巧
    rebase -i 是個很實用且應用廣泛的工具,希望大家都學會它的使用。它還可以用來修改commit信息,拋棄某些commit,對commit進行排序等等。具體命令如下,操作方式跟動圖一致,都是在vim裡面進行編輯。這裡不展開,感興趣的同學可以自己操作一下。