Git 重寫(xiě)歷史

2018-09-27 15:50 更新

很多時(shí)候,在 Git 上工作的時(shí)候,你也許會(huì)由于某種原因想要修訂你的提交歷史。Git 的一個(gè)卓越之處就是它允許你在最后可能的時(shí)刻再作決定。你可以在你即將提交暫存區(qū)時(shí)決定什么文件歸入哪一次提交,你可以使用 stash 命令來(lái)決定你暫時(shí)擱置的工作,你可以重寫(xiě)已經(jīng)發(fā)生的提交以使它們看起來(lái)是另外一種樣子。這個(gè)包括改變提交的次序、改變說(shuō)明或者修改提交中包含的文件,將提交歸并、拆分或者完全刪除——這一切在你尚未開(kāi)始將你的工作和別人共享前都是可以的。

在這一節(jié)中,你會(huì)學(xué)到如何完成這些很有用的任務(wù)以使你的提交歷史在你將其共享給別人之前變成你想要的樣子。

改變最近一次提交

改變最近一次提交也許是最常見(jiàn)的重寫(xiě)歷史的行為。對(duì)于你的最近一次提交,你經(jīng)常想做兩件基本事情:改變提交說(shuō)明,或者改變你剛剛通過(guò)增加,改變,刪除而記錄的快照。

如果你只想修改最近一次提交說(shuō)明,這非常簡(jiǎn)單:

$ git commit --amend
這會(huì)把你帶入文本編輯器,里面包含了你最近一次提交說(shuō)明,供你修改。當(dāng)你保存并退出編輯器,這個(gè)編輯器會(huì)寫(xiě)入一個(gè)新的提交,里面包含了那個(gè)說(shuō)明,并且讓它成為你的新的最近一次提交。

如果你完成提交后又想修改被提交的快照,增加或者修改其中的文件,可能因?yàn)槟阕畛跆峤粫r(shí),忘了添加一個(gè)新建的文件,這個(gè)過(guò)程基本上一樣。你通過(guò)修改文件然后對(duì)其運(yùn)行g(shù)it add或?qū)σ粋€(gè)已被記錄的文件運(yùn)行g(shù)it rm,隨后的git commit --amend會(huì)獲取你當(dāng)前的暫存區(qū)并將它作為新提交對(duì)應(yīng)的快照。

使用這項(xiàng)技術(shù)的時(shí)候你必須小心,因?yàn)樾拚龝?huì)改變提交的SHA-1值。這個(gè)很像是一次非常小的rebase——不要在你最近一次提交被推送后還去修正它。

修改多個(gè)提交說(shuō)明

要修改歷史中更早的提交,你必須采用更復(fù)雜的工具。Git沒(méi)有一個(gè)修改歷史的工具,但是你可以使用rebase工具來(lái)衍合一系列的提交到它們?cè)瓉?lái)所在的HEAD上而不是移到新的上。依靠這個(gè)交互式的rebase工具,你就可以停留在每一次提交后,如果你想修改或改變說(shuō)明、增加文件或任何其他事情。你可以通過(guò)給git rebase增加-i選項(xiàng)來(lái)以交互方式地運(yùn)行rebase。你必須通過(guò)告訴命令衍合到哪次提交,來(lái)指明你需要重寫(xiě)的提交的回溯深度。

例如,你想修改最近三次的提交說(shuō)明,或者其中任意一次,你必須給git rebase -i提供一個(gè)參數(shù),指明你想要修改的提交的父提交,例如HEAD~2或者HEAD~3??赡苡涀3更加容易,因?yàn)槟阆胄薷淖罱翁峤?;但是?qǐng)記住你事實(shí)上所指的是四次提交之前,即你想修改的提交的父提交。

再次提醒這是一個(gè)衍合命令——HEAD~3..HEAD范圍內(nèi)的每一次提交都會(huì)被重寫(xiě),無(wú)論你是否修改說(shuō)明。不要涵蓋你已經(jīng)推送到中心服務(wù)器的提交——這么做會(huì)使其他開(kāi)發(fā)者產(chǎn)生混亂,因?yàn)槟闾峁┝送瑯幼兏牟煌姹尽?/p>

運(yùn)行這個(gè)命令會(huì)為你的文本編輯器提供一個(gè)提交列表,看起來(lái)像下面這樣

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

很重要的一點(diǎn)是你得注意這些提交的順序與你通常通過(guò)log命令看到的是相反的。如果你運(yùn)行l(wèi)og,你會(huì)看到下面這樣的結(jié)果:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

請(qǐng)注意這里的倒序。交互式的rebase給了你一個(gè)即將運(yùn)行的腳本。它會(huì)從你在命令行上指明的提交開(kāi)始(HEAD~3)然后自上至下重播每次提交里引入的變更。它將最早的列在頂上而不是最近的,因?yàn)檫@是第一個(gè)需要重播的。

你需要修改這個(gè)腳本來(lái)讓它停留在你想修改的變更上。要做到這一點(diǎn),你只要將你想修改的每一次提交前面的pick改為edit。例如,只想修改第三次提交說(shuō)明的話,你就像下面這樣修改文件:

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

當(dāng)你保存并退出編輯器,Git會(huì)倒回至列表中的最后一次提交,然后把你送到命令行中,同時(shí)顯示以下信息:

$ git rebase -i HEAD~3
Stopped at 7482e0d... updated the gemspec to hopefully work better
You can amend the commit now, with

       git commit --amend

Once you’re satisfied with your changes, run

       git rebase --continue

這些指示很明確地告訴了你該干什么。輸入


$ git commit --amend

修改提交說(shuō)明,退出編輯器。然后,運(yùn)行

$ git rebase --continue

這個(gè)命令會(huì)自動(dòng)應(yīng)用其他兩次提交,你就完成任務(wù)了。如果你將更多行的 pick 改為 edit ,你就能對(duì)你想修改的提交重復(fù)這些步驟。Git每次都會(huì)停下,讓你修正提交,完成后繼續(xù)運(yùn)行。

重排提交

你也可以使用交互式的衍合來(lái)徹底重排或刪除提交。如果你想刪除"added cat-file"這個(gè)提交并且修改其他兩次提交引入的順序,你將rebase腳本從這個(gè)

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

改為這個(gè):

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

當(dāng)你保存并退出編輯器,Git 將分支倒回至這些提交的父提交,應(yīng)用310154e,然后f7f3f6d,接著停止。你有效地修改了這些提交的順序并且徹底刪除了"added cat-file"這次提交。

壓制(Squashing)提交

交互式的衍合工具還可以將一系列提交壓制為單一提交。腳本在 rebase 的信息里放了一些有用的指示:

Commands:
 p, pick = use commit
  e, edit = use commit, but stop for amending
  s, squash = use commit, but meld into previous commit

 If you remove a line here THAT COMMIT WILL BE LOST.
 However, if you remove everything, the rebase will be aborted.

如果不用"pick"或者"edit",而是指定"squash",Git 會(huì)同時(shí)應(yīng)用那個(gè)變更和它之前的變更并將提交說(shuō)明歸并。因此,如果你想將這三個(gè)提交合并為單一提交,你可以將腳本修改成這樣:

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

當(dāng)你保存并退出編輯器,Git 會(huì)應(yīng)用全部三次變更然后將你送回編輯器來(lái)歸并三次提交說(shuō)明。

This is a combination of 3 commits.
 The first commit's message is:
changed my name a bit

 This is the 2nd commit message:

updated README formatting and added blame

 This is the 3rd commit message:

added cat-file

當(dāng)你保存之后,你就擁有了一個(gè)包含前三次提交的全部變更的單一提交。

拆分提交

拆分提交就是撤銷(xiāo)一次提交,然后多次部分地暫存或提交直到結(jié)束。例如,假設(shè)你想將三次提交中的中間一次拆分。將"updated README formatting and added blame"拆分成兩次提交:第一次為"updated README formatting",第二次為"added blame"。你可以在rebase -i腳本中修改你想拆分的提交前的指令為"edit":

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

然后,這個(gè)腳本就將你帶入命令行,你重置那次提交,提取被重置的變更,從中創(chuàng)建多次提交。當(dāng)你保存并退出編輯器,Git 倒回到列表中第一次提交的父提交,應(yīng)用第一次提交(f7f3f6d),應(yīng)用第二次提交(310154e),然后將你帶到控制臺(tái)。那里你可以用git reset HEAD^對(duì)那次提交進(jìn)行一次混合的重置,這將撤銷(xiāo)那次提交并且將修改的文件撤回。此時(shí)你可以暫存并提交文件,直到你擁有多次提交,結(jié)束后,運(yùn)行g(shù)it rebase --continue。

$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

Git在腳本中應(yīng)用了最后一次提交(a5f4a0d),你的歷史看起來(lái)就像這樣了:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

再次提醒,這會(huì)修改你列表中的提交的 SHA 值,所以請(qǐng)確保這個(gè)列表里不包含你已經(jīng)推送到共享倉(cāng)庫(kù)的提交。

核彈級(jí)選項(xiàng): filter-branch

如果你想用腳本的方式修改大量的提交,還有一個(gè)重寫(xiě)歷史的選項(xiàng)可以用——例如,全局性地修改電子郵件地址或者將一個(gè)文件從所有提交中刪除。這個(gè)命令是filter-branch,這個(gè)會(huì)大面積地修改你的歷史,所以你很有可能不該去用它,除非你的項(xiàng)目尚未公開(kāi),沒(méi)有其他人在你準(zhǔn)備修改的提交的基礎(chǔ)上工作。盡管如此,這個(gè)可以非常有用。你會(huì)學(xué)習(xí)一些常見(jiàn)用法,借此對(duì)它的能力有所認(rèn)識(shí)。

從所有提交中刪除一個(gè)文件

這個(gè)經(jīng)常發(fā)生。有些人不經(jīng)思考使用git add .,意外地提交了一個(gè)巨大的二進(jìn)制文件,你想將它從所有地方刪除。也許你不小心提交了一個(gè)包含密碼的文件,而你想讓你的項(xiàng)目開(kāi)源。filter-branch大概會(huì)是你用來(lái)清理整個(gè)歷史的工具。要從整個(gè)歷史中刪除一個(gè)名叫password.txt的文件,你可以在filter-branch上使用--tree-filter選項(xiàng):

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

--tree-filter選項(xiàng)會(huì)在每次檢出項(xiàng)目時(shí)先執(zhí)行指定的命令然后重新提交結(jié)果。在這個(gè)例子中,你會(huì)在所有快照中刪除一個(gè)名叫 password.txt 的文件,無(wú)論它是否存在。如果你想刪除所有不小心提交上去的編輯器備份文件,你可以運(yùn)行類(lèi)似git filter-branch --tree-filter "find * -type f -name '*~' -delete" HEAD的命令。

你可以觀察到 Git 重寫(xiě)目錄樹(shù)并且提交,然后將分支指針移到末尾。一個(gè)比較好的辦法是在一個(gè)測(cè)試分支上做這些然后在你確定產(chǎn)物真的是你所要的之后,再 hard-reset 你的主分支。要在你所有的分支上運(yùn)行filter-branch的話,你可以傳遞一個(gè)--all給命令。

將一個(gè)子目錄設(shè)置為新的根目錄

假設(shè)你完成了從另外一個(gè)代碼控制系統(tǒng)的導(dǎo)入工作,得到了一些沒(méi)有意義的子目錄(trunk, tags等等)。如果你想讓trunk子目錄成為每一次提交的新的項(xiàng)目根目錄,filter-branch也可以幫你做到:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

現(xiàn)在你的項(xiàng)目根目錄就是trunk子目錄了。Git 會(huì)自動(dòng)地刪除不對(duì)這個(gè)子目錄產(chǎn)生影響的提交。

全局性地更換電子郵件地址

另一個(gè)常見(jiàn)的案例是你在開(kāi)始時(shí)忘了運(yùn)行g(shù)it config來(lái)設(shè)置你的姓名和電子郵件地址,也許你想開(kāi)源一個(gè)項(xiàng)目,把你所有的工作電子郵件地址修改為個(gè)人地址。無(wú)論哪種情況你都可以用filter-branch來(lái)更換多次提交里的電子郵件地址。你必須小心一些,只改變屬于你的電子郵件地址,所以你使用--commit-filter:

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="schacon@example.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

這個(gè)會(huì)遍歷并重寫(xiě)所有提交使之擁有你的新地址。因?yàn)樘峤焕锇怂鼈兊母柑峤坏腟HA-1值,這個(gè)命令會(huì)修改你的歷史中的所有提交,而不僅僅是包含了匹配的電子郵件地址的那些。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)