Git 實用操作:重寫 Commit 歷史
阿新 • • 發佈:2020-09-09
當我們修改完程式碼,提交了一個 commit,然後發現改錯了,怎麼修正?下面分兩種情況來討論:修正最近一次提交,和修正歷史多個提交。
## 修正最近一次提交
如果發現剛剛提交的內容有錯誤,當場再修改一下再提交一個新 commit 不就可以麼?可以是可以,不過還有一個更加優雅和簡單的解決方法:
```shell
git commit --amend
```
"amend" 是“修正”的意思。在提交時,如果加上 `--amend` 引數,Git 不會在當前 commit 上增加 commit,而是會把當前 commit 的內容和暫存區(stageing area)裡的最近一次 commit 的內容合併起來後建立一個新的 commit,用這個新的 commit 把之前最新的 commit 替換掉。所以 `commit --amend` 做的事就是它的字面意思:對最新一條 commit 進行修正。
![](https://img2020.cnblogs.com/blog/191097/202009/191097-20200909095321506-957655146.gif)
具體地,比如你發現剛剛的提交中 foo.txt 檔案有錯別字,你就可以把檔案中的錯別字修改好之後,輸入以下命令:
```shell
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` 命令了:
```shell
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 上:
![](https://img2020.cnblogs.com/blog/191097/202009/191097-20200909095334675-959757826.gif)
如果沒有 `-i` 引數的話,這種原地 rebase 相當於空操作,會直接結束。而在加了 `-i` 後,就會跳到一個新的介面:
```bash
pick 310154e 第 N-2 次提交
pick a5f4a0d 第 N-1 次提交
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# 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
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop = remove commit
...
```
這個編輯介面的最頂部,列出了將要被 rebase 的所有 commits,也就是倒數第二個 commit “第 N-2 次提交”和最近的 commit “第 N-1 次提交”。需要注意,這個排列是正序的,舊的 commit 會排在上面,新的排在下面。
這兩行指示了兩個資訊:需要處理哪些 commit 和如何處理它們。
你需要修改這兩行的內容來指定你需要的操作。每個 commit 預設的操作都是 `pick`,表示直接應用這個 commit。所以如果你現在直接退出編輯介面,那麼結果仍然是空操作。
但你的目標是修改倒數第二個 commit,也就是上面的那個“第 N-2 次提交”,所以你需要把它的操作指令從 `pick` 改成 `edit` 。 `edit` 的意思是應用這個 commit,然後停下來等待繼續修正。其他的操作指令,在這個介面裡都已經列舉了出來(下面的 "Commands…" 部分文字),你可以自己研究一下。
```bash
edit 310154e 第 N-2 次提交
pick a5f4a0d 第 N-1 次提交
...
```
把 `pick` 修改成 `edit` 後,就可以退出編輯介面了,並輸出以下資訊:
```bash
$ 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:
```shell
git add foo.txt
git commit --amend
```
這樣你的倒數第二個錯誤的 commit 就被修正了。在此過程中,把原來倒數第二個 commit 的內容和當前修改的內容合併在一起建立了一個新的 commit,並用此 commit 原地替換了原來的原來倒數第二個 commit:
![](https://img2020.cnblogs.com/blog/191097/202009/191097-20200909095350914-219145909.gif)
然後,你可以用 `rebase --continue` 來繼續上述 rebase 過程。
```shell
git rebase --continue
```
所有需要重寫的 commit 都修改完成後,這次互動式 rebase 的過程就完美結束了。
## 總結
只修正最近的錯誤提交,使用簡單的 `commit --amend` 即可。若修改歷史多個提交用互動式變基 `rebase -i`,它可以在 rebase 開始之前指定一些額外操作。通過互動式變基還可以實現其它歷史重寫操作,如“重新排序提交”、“壓縮提交”、“拆分提交”等,這些歷史重寫操作不常用,我個人從來沒用過,所以就不講了,你可以在實際有需要的時候自己再去研究