Git 實用操作:重寫 Commit 歷史

精緻碼農發表於2020-09-09

當我們修改完程式碼,提交了一個 commit,然後發現改錯了,怎麼修正?下面分兩種情況來討論:修正最近一次提交,和修正歷史多個提交。

修正最近一次提交

如果發現剛剛提交的內容有錯誤,當場再修改一下再提交一個新 commit 不就可以麼?可以是可以,不過還有一個更加優雅和簡單的解決方法:

git commit --amend

"amend" 是“修正”的意思。在提交時,如果加上 --amend 引數,Git 不會在當前 commit 上增加 commit,而是會把當前 commit 的內容和暫存區(stageing area)裡的最近一次 commit 的內容合併起來後建立一個新的 commit,用這個新的 commit 把之前最新的 commit 替換掉。所以 commit --amend 做的事就是它的字面意思:對最新一條 commit 進行修正。

具體地,比如你發現剛剛的提交中 foo.txt 檔案有錯別字,你就可以把檔案中的錯別字修改好之後,輸入以下命令:

git add foo.txt
git commit --amend

此時 Git 會把你帶到提交資訊編輯介面。提交資訊預設是最近那次提交時填的資訊。你可以修改或者保留它,然後儲存退出。然後,你的最新 commit 就被更新了。

需要注意的有一點:commit --amend 並不是直接修改原 commit 的內容,而是如上面動圖所示生成一條新的 commit。

修正歷史多個提交

commit --amend 可以修正最新 commit 的錯誤,但如果是倒數第二個、第三個 commit 寫錯了,怎麼辦?

如果不是最新的 commit 寫錯,就不能用 commit --amend 來修復了,而是要用 rebase。不過需要給 rebase 也加一個引數:-i

rebase -irebase --interactive 的縮寫形式,意為“互動式變基”。

所謂互動式 rebase,就是在 rebase 的操作執行之前,你可以指定要 rebase 的 commit 鏈中的哪一個 commit 是否需要進一步修改。

那麼你就可以利用這個特點,進行一次原地 rebase。

例如你是在寫錯了 commit 之後,又提交了一次才發現之前寫錯了。現在再用 commit --amend 已經晚了,此時就要用 rebase -i 命令了:

git rebase -i HEAD^^

在 Git 中,有兩個「偏移符號」: ^~

^ 的用法:在 commit 的後面加一個或多個 ^ 號,可以把 commit 往回偏移,偏移的數量是 ^ 的數量。例如:master^ 表示 master 指向的 commit 之前的那個 commit; HEAD^^ 表示 HEAD 所指向的 commit 往前數兩個 commit。

~ 的用法:在 commit 的後面加上 ~ 號和一個數,可以把 commit 往回偏移,偏移的數量是 ~ 號後面的數。例如:HEAD~5 表示 HEAD 指向的 commit 往前數 5 個 commit。

上面這行程式碼表示,把當前 commit ( HEAD 所指向的 commit) rebase 到 HEAD 向前兩個的 commit 上:

如果沒有 -i 引數的話,這種原地 rebase 相當於空操作,會直接結束。而在加了 -i 後,就會跳到一個新的介面:

pick 310154e 第 N-2 次提交
pick a5f4a0d 第 N-1 次提交

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
...

這個編輯介面的最頂部,列出了將要被 rebase 的所有 commits,也就是倒數第二個 commit “第 N-2 次提交”和最近的 commit “第 N-1 次提交”。需要注意,這個排列是正序的,舊的 commit 會排在上面,新的排在下面。

這兩行指示了兩個資訊:需要處理哪些 commit 和如何處理它們。

你需要修改這兩行的內容來指定你需要的操作。每個 commit 預設的操作都是 pick,表示直接應用這個 commit。所以如果你現在直接退出編輯介面,那麼結果仍然是空操作。

但你的目標是修改倒數第二個 commit,也就是上面的那個“第 N-2 次提交”,所以你需要把它的操作指令從 pick 改成 editedit 的意思是應用這個 commit,然後停下來等待繼續修正。其他的操作指令,在這個介面裡都已經列舉了出來(下面的 "Commands…" 部分文字),你可以自己研究一下。

edit 310154e 第 N-2 次提交
pick a5f4a0d 第 N-1 次提交

...

pick 修改成 edit 後,就可以退出編輯介面了,並輸出以下資訊:

$ git rebase -i HEAD^^
Stopped at 310154e... 第 N-2 次提交
You can amend the commit now, with

       git commit --amend

Once you're satisfied with your changes, run

       git rebase --continue

上圖的提示資訊說明,rebase 過程已經停在“第 N-2 次提交”的 commit 的位置,那麼現在你就可以去修改你想修改的內容了。

修改完成後,和前文修改最近提交的方法一樣,用 commit --amend 把修改原地應用到一個新的 commit:

git add foo.txt
git commit --amend

這樣你的倒數第二個錯誤的 commit 就被修正了。在此過程中,把原來倒數第二個 commit 的內容和當前修改的內容合併在一起建立了一個新的 commit,並用此 commit 原地替換了原來的原來倒數第二個 commit:

然後,你可以用 rebase --continue 來繼續上述 rebase 過程。

git rebase --continue

所有需要重寫的 commit 都修改完成後,這次互動式 rebase 的過程就完美結束了。

總結

只修正最近的錯誤提交,使用簡單的 commit --amend 即可。若修改歷史多個提交用互動式變基 rebase -i,它可以在 rebase 開始之前指定一些額外操作。通過互動式變基還可以實現其它歷史重寫操作,如“重新排序提交”、“壓縮提交”、“拆分提交”等,這些歷史重寫操作不常用,我個人從來沒用過,所以就不講了,你可以在實際有需要的時候自己再去研究一下。

相關文章