Skip to main content

Git Rebase Onto 全中文最完整教學

本文延續 使用變基 Rebase 合併分支 但是專注說明 onto 用法,也可以視為 rebase 的完整解析版本,完全遵照文檔說明沒有模糊地帶。本文會說明為何網路上的說法為何錯誤,同時包含實際範例展示,並且和上一篇相同,筆者保證本文絕對正確。

資訊

我做了一個迷你repo以便在真實操作前測試結果是否符合預期。

TL;DR

git rebase --onto A B C 的作用是

  1. 切換分支:首先 git switch 切換分支到 C
  2. 解析範圍:解析 B 與 C 的共同祖先,找到從共同祖先到 C 之間,並且忽略存在於 B 的提交1,最後暫存這些提交
  3. 重演提交:以 A 為基底將剛才暫存的提交重演2在 A 之後

或是用一句話記起來:

口訣

比較 B C 後找到共同祖先,找出從共同祖先到 C 之間的提交,將其重演在新基底 A 之後。

B 預設是當前分支的上游分支,C 預設為 HEAD,三個參數可以是 commit 不強迫是分支。

怎麼不像是能輕鬆背誦的口訣,因為他就是這麼運作的,強烈建議如果使用 --onto 參數就一次把 B C 參數也附上,因為這個指令太複雜了,網路上說明 rebase --onto 用法的文章 99% 都只適用於他那個情況,換了一個情境他的說明就不適用了。

附帶一提,網路上有說法是「把 B~C 之間的提交放在 A 之後」,這個說法是最接近正確的說法,但是不會永遠成立,本文範例中的修改提交所屬分支展示了這個說法是錯誤的,因此特別闢謠。

你知道 rebase 加上 onto 參數之後總共有幾種輸入方式嗎?有六種!

git rebase --onto 各種排列組合

  1. git rebase --onto x
  2. git rebase --onto x y
  3. git rebase --onto x y z
  4. git rebase x --onto y
  5. git rebase x --onto y z
  6. git rebase x y --onto z

想搞懂他勢必得讀懂文檔,然而看文檔又昏了,因為這是 POSIX 用語,版本管理都不會用看 POSIX 我哪讀的懂(似懂非懂不算懂),開始閱讀前請務必確保自己有能力看的懂文檔,或者閱讀我寫的看懂 Git 文檔

git rebase --onto 用法

參數解釋

看懂參數怎麼解析後,接下來解釋三個參數的意思。

git rebase --onto <newbase> [<upstream> [<branch>]]
  • upstream: 要和目前分支比較,用於尋找共同祖先的分支或提交。如果沒有設定,預設為 branch.<name>.remotebranch.<name>.merge,因此文檔將這個變數命名為 upstream
  • branch: 設定 branch 後,在 rebase 操作前都會提前執行 git switch <branch>,預設為 HEAD,也就是在當前分支進行 rebase
  • newbase: 使用 --onto 時必須設定 newbase,用來指定 rebase 時生成的 commit 起點,如果不指定 --onto,等效於將起點設定在 <upstream>

全部組合起來就是開頭說的:

  1. 比較 <upstream><branch> 後找到共同祖先
  2. 找出從共同祖先到 <branch> 之間的提交,跳過屬於 <upstream> 的部分,並且暫存他們
  3. 將其重演在新基底 <newbase> 之後

附帶一提,把 <newbase> 去掉後,這個說法是原有口訣的超集 (superset) 而不是推翻原有解釋,網路文章很糟糕的一點是他們的解釋每種情境都是全新的用法。

用一個變數

這個用法不會用到

把這個段落放進來的原因是沒寫感覺好像是作者忘記一樣,所以用折疊形式放上來。

我想不到什麼情況需要只用 git rebase --onto <newbase> 而不帶其他參數,將另外兩個變數的預設值套上後,指令目的變成「把 HEAD rebase 到 <newbase>」,基本上不會用到這個功能,在我的 範例 repo 中可以用這個指令測試:

git rebase --onto feat
git rebase --onto origin/main

第一個指令的作用會是把目前分支的 HEAD 直接跳到指定位置,然而回來就沒辦法了,因為這時候等同於在別的分支進行 rebase 並且設定根基是 origin/main,與其搞混自己不如直接忘了這個用法。

用兩個變數

一般情況下使用 --onto 功能至少都會帶兩個參數,也就是說設定 <newbase><upstream>

# 兩者等效
git rebase --onto A B
git rebase B --onto A

複習一下前面說的,這時候 A 代表 newbase,而 B 是 upstream,這代表將「目前分支」和「B的分支」進行比較,找到共同祖先後,以 A commit 作為新的基底,將共同祖先到目前分支的所有提交重演到 A 的後面。讀者可以 clone 我的範例 repo 測試指令行為,使用以下指令:

git switch fix
git rebase --onto feat fix~2

把指令翻譯成人話就是「將 fix 和 fix~2 比較,找到共同祖先後,以 feat 作為基底開始重演」。這裡不貼執行結果,因為截圖看了反而頭痛,自己真正使用過一次就很清楚他在做什麼了,會把 fix~1fix 移動到 feat 後面。

用三個變數

三個參數的用法非常簡單,因為第三個變數只是提前變換分支的縮寫,這除了讓你少打一次 switch 以外,最重要的功能是可以在任何位置執行 rebase 都有同樣效果,否則 rebase 會因為目前所在位置不同而有不同結果。由於一個參數的指令沒機會用、三個參數的指令只是提前切換,所以可以整理出 onto 其實只有一種用法。總結得很簡單,但是沒真正研究永遠都搞不清楚 --onto 究竟在做什麼,我講的就是網路上那些亂教一通的文章。

有一點需要注意,網路文章說 git rebase --onto A B C 這個指令是將「B~C 之間的 commit 重演在 A 之上」有誤,依照文檔比較類似的說法,之間應該改成 git log <upstream>..HEAD 的提交。

提示

筆者建議只要使用 --onto 選項,無論何時都使用三個變數的語法避免混亂。

提醒

為了避免有人搞混特別強調這篇是在講 onto,如果不使用 onto 又只用兩個參數,這兩個參數會變成 upstream 和 branch。關於變數順序的問題也請注意:

危險

我不確定像裡面說的這樣排列組合修改變數順序是官方的 feature 還是僅僅只是 behavior,我找不到任何相關討論,建議照順序打比較穩妥。

實用指令整理

自己寫完都能感覺到「OK 現在我指令看懂了但是不會用」,git rebase --onto 確實比較複雜,於是把文檔中不錯的用法搬過來讓讀者知道這可以拿來做什麼。

以下情境題可以用我的 範例 repo 進行測試,分支結構完全相同。

刪除中間一段提交

git rebase --onto main~5 main~1 main

如果要刪除提交應該使用 git rebase -i 更方便,但是這是一個理解 onto 用法的好例子

比較 main~1 main 後找到共同祖先 (main~1),把共同祖先到 main 之間的提交重演在新基底 main~5 之後。

    E---F---G---H---I---J---K  main
\
o---o---o---o feat
\
o---o---o fix

變成

    E---F---K'  main
\
o---o---o---o feat
\
o---o---o fix

將某一段提交移動到主分支

git rebase --onto main feat fix

比較 feat fix 後找到共同祖先 (feat^),把共同祖先到 fix 之間的提交重演在新基底 main 之後。

    o---o---o---o---o---o  main
\
o---o---o---o feat
\
o---o---o fix

變成

    o---o---o---o---o---o  main
| \
| o'--o'--o' fix
\
o---o---o---o feat

把子分支的提交改為主分支的提交

git rebase --onto feat~2 feat main

比較 feat main 後找到共同祖先 (B),把共同祖先到 main 之間的提交重演在新基底 feat~2 之後。

    o---B---m1---m2---m3  main
\
f1---f2---f3---f4 feat
\
o---o---o fix

變成

    o---B---f1---f2---m1'---m2'---m3'  main
\
f3---f4 feat
\
o---o---o fix

這個指令演示了「找到 B~C 之間的提交貼在 A 之後」這個說法是錯的,主要是祖先這塊沒有解釋到。

進階題

這幾個指令留給讀者猜是什麼用途,可以自行 clone 範例 repo 驗證和想的一不一樣。

git rebase --onto main~3 main fix
git rebase --onto feat main fix

# 猜猜看為何這兩個指令沒有任何修改
git rebase --onto feat~2 main fix
git rebase --onto main~4 main fix

參考

cursing

rebase onto 用法這麼複雜的指令,網路上教學文章只說一句「改接」「嫁接」「任意改接」有講跟沒講一樣,因為所有 rebase 都是改接,讀者看完之後不明所以要嘛死記要嘛乾脆不敢用,而所有講解 git rebase --onto 卻又不講指令如何解析的文章都在亂教,連輸入的指令都不知道對應到哪個變數,我們怎麼敢用這個指令?更糟糕是自創名詞的文章,指令用法只在他說的那種情況下適用,這種文章非常差勁。

這個文檔從我還不熟 Git 的時候開始寫,開宗明義就說使用官方翻譯而不是自己造詞,現在回頭看我最初講的話是對的。在蒐集 rebase onto 資料時發現很多文章寫了「onto 之後的參數是 <new base-commit> <current base-commit>」,這就是明顯的亂造詞問題,後來發現所有的錯誤用法都來自於同一篇文章,也可以算是一種 error propagation 吧。

我就問寫第一篇文章的人,如果 onto 使用三個參數,你這參數說明是不是要改成 <new base-commit> <start-commit> <end-commit>?還沒完,再照他文章中自己的範例指令 git rebase 分支 --onto 哈希值 用法,是不是又要多一種解釋?官方這樣設定參數名稱自然有他的道理,沒想清楚就自己亂改又放在網路上,結果就是讓所有讀過文章的人都搞混。開頭說的還是不會永遠成立,這傢伙的解釋方式更慘,只在那時會成立。

甚至還使用 git checkout main; git rebase sub-branch,rebase 難學有很大一部分就是拜這傢伙所賜,最誇張的是竟然把 rebase onto 排版在 rebase interactive 前面,把使用頻率低又難理解的指令放在前面到底什麼意思,初學者就是最沒有判斷能力的時期,然後網路搜尋最前面的幾個教學都錯,我不知道要怪教學、怪 Google 還是怪自己。

順帶一提 rebase 對象連 gitbook.tw 都寫錯,我真的是無言。

Footnotes

  1. 忽略條件除了 B 的一部分之外,如果檔案相同只有提交訊息和時間不同也會被跳過 (Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).)。

  2. 重演 (replay),用於表示不只是簡單的將提交複製貼上,而是會重新生成 commit hash。