git rebase

本文的讀者需要已經了解 基本的 Git 操作和開發流程

在我們開發完分支後,一般分支上會有很多 commit —— 少不了諸如 “fix typo”, “sth wrong in the previous commit” 之類的 commit。在合併到主幹的時候,往往這類 commit 顯得臃腫多餘。為了方便別人做 code review,我們希望合併一些不必要的 commit 使我們的分支顯得乾淨一目了然,也方便管理。有 3 種方式可以做到。

一、git rebase

$ git rebase -i origin/master

-i 參數表示互動 interactive,這時 git 會使用你設定的編輯器,讓你對 git 歷史記錄做詳細修改。

下面以 Atlassian 的例子來說明

# 開始開發一個新 feature
$ git checkout -b new-feature master
# 改了一些代碼
$ git commit -a -m "Start developing a feature"
# 剛剛的修改有點問題,再改一下
$ git commit -a -m "Fix something from the previous commit"
 
# 緊急修復,直接在 master 分支上改點東西
$ git checkout master
# 改了一些代碼
$ git commit -a -m "Fix security hole"
 
# 開始交互式地 rebase 了
$ git checkout new-feature
$ git rebase -i master

這時 git 會打開編輯器,你會看到 new-feature 分支上的 2 個最新 commit,以及一些指引提示

pick 32618c4 Start developing a feature
pick 62eed47 Fix something from the previous commit
 
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

每個 commit 前有一個操作命令,默認是 pick,表示該行被選中,需要進行 rebase 操作。下面一堆注釋的指引中還有幾個指令,我們常用到的是以下 2 個

  • squash:將這一行的 commit 與上一個 commit 進行合併
  • fixup:與 squash 相同,只是不會保留這行 commit 的提交 message 信息

比如上面的例子,我們在編輯器中修改指令為

pick 32618c4 Start developing a feature
squash 62eed47 Fix something from the previous commit

這樣一改,保存執行後,new-feature 分支就只剩下 1 個 commit 了,這個合併後的 commit 提交的信息包含之前 2 個 commit 的信息

Start developing a feature
Fix something from the previous commit

消除了一個多餘的不那麼重要的 commit,達到了我們的目的。最後,只需 fast-forward merge 到 master

$ git checkout master
$ git merge new-feature

在別人看來,你就是一個天才開發者,沒有出差錯地一次實現了 new-feature,項目的 commit 歷史記錄顯得乾淨而有意義。

如果我們在編輯器中不用 squash 指令,而使用 fixup 指令,這樣改

pick 32618c4 Start developing a feature
fixup 62eed47 Fix something from the previous commit

則結果一樣,還是只剩 1 個 commit,但是合併後的提交消息,就只有之前第一個 commit 的消息,第二個的 commit 消息被注釋掉了

Start developing a feature

這樣也達到了我們的目的,消除了曾經發生過 2 個 commit 的痕迹。在別人看來,這個新功能的分支是按計劃一次性開發好的,中途並未發生 “錯別字” 之類的意外,簡直完美。

二、–fixup & –autosquash

# 自動標記這一次的 commit 為上一個 commit 的 fix
$ git commit --fixup <commit>
# 自動組織合併兩個 commit
$ git rebase -i --autosquash

這種方式和上面的第一種原理是一樣的,只是形式上不是在編輯器中調整歷史記錄,而是直接執行命令,結果等同。

三、撤銷過去的 commit 重建一個新的

$ git reset HEAD~2
$ git add .
$ git commit -am "This is the new feature"
$ git push --force

這種方式就比較暴力了,嗯。


說點題外話

修改上一次 commit 提交的 message

有時我們提交代碼,message 寫的太匆忙有錯字(不是代碼里有錯字,而是 commit message 寫錯),就可以用下面的命令來修正

$ git commit --amend

不過只能修正上一次的 commit。如果很多個 commit 之前就有 message 寫錯,就得用上我們之前說的 git rebase

$ git rebase -i HEAD~4

和之前一樣,會打開編輯器,顯示最近的 4 次提交

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

將 pick 指令改為 reword 指令,就可以修改這一行的 commit message

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
reword 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

– EOF –

參考文檔