Git 實用操作:重寫 Commit 歷史
當我們修改完程式碼,提交了一個 commit,然後發現改錯了,怎麼修正?下面分兩種情況來討論:修正最近一次提交,和修正歷史多個提交。
修正最近一次提交
如果發現剛剛提交的內容有錯誤,當場再修改一下再提交一個新 commit 不就可以麼?可以是可以,不過還有一個更加優雅和簡單的解決方法:
git commit --amend
"amend" 是“修正”的意思。在提交時,如果加上 --amend
引數,Git 不會在當前 commit 上增加 commit,而是會把當前 commit 的內容和暫存區(stageing area)裡的最近一次 commit 的內容合併起來後建立一個新的 commit,用這個新的 commit 把之前最新的 commit 替換掉。所以 commit --amend
具體地,比如你發現剛剛的提交中 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 -i
是 rebase --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
改成 edit
。 edit
的意思是應用這個 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 開始之前指定一些額外操作。通過互動式變基還可以實現其它歷史重寫操作,如“重新排序提交”、“壓縮提交”、“拆分提交”等,這些歷史重寫操作不常用,我個人從來沒用過,所以就不講了,你可以在實際有需要的時候自己再去研究一下。