許多時(shí)候,在使用 Git 時(shí),可能會(huì)因?yàn)槟承┰蛳胍拚峤粴v史。 Git 很棒的一點(diǎn)是它允許你在最后時(shí)刻做決定。 你可以在將暫存區(qū)內(nèi)容提交前決定哪些文件進(jìn)入提交,可以通過 stash 命令來決定不與某些內(nèi)容工作,也可以重寫已經(jīng)發(fā)生的提交就像它們以另一種方式發(fā)生的一樣。 這可能涉及改變提交的順序,改變提交中的信息或修改文件,將提交壓縮或是拆分,或完全地移除提交 - 在將你的工作成果與他人共享之前。
在本節(jié)中,你可以學(xué)到如何完成這些非常有用的工作,這樣在與他人分享你的工作成果時(shí)你的提交歷史將如你所愿地展示出來。
修改你最近一次提交可能是所有修改歷史提交的操作中最常見的一個(gè)。 對(duì)于你的最近一次提交,你往往想做兩件事情:修改提交信息,或者修改你添加、修改和移除的文件的快照。
如果,你只是想修改最近一次提交的提交信息,那么很簡單:
$ git commit --amend
這會(huì)把你帶入文本編輯器,里面包含了你最近一條提交信息,供你修改。 當(dāng)保存并關(guān)閉編輯器后,編輯器將會(huì)用你輸入的內(nèi)容替換最近一條提交信息。
如果你已經(jīng)完成提交,又因?yàn)橹疤峤粫r(shí)忘記添加一個(gè)新創(chuàng)建的文件,想通過添加或修改文件來更改提交的快照,也可以通過類似的操作來完成。 通過修改文件然后運(yùn)行?git add
?或?git rm
?一個(gè)已追蹤的文件,隨后運(yùn)行?git commit --amend
?拿走當(dāng)前的暫存區(qū)域并使其做為新提交的快照。
使用這個(gè)技巧的時(shí)候需要小心,因?yàn)樾拚龝?huì)改變提交的 SHA-1 校驗(yàn)和。 它類似于一個(gè)小的變基 - 如果已經(jīng)推送了最后一次提交就不要修正它。
為了修改在提交歷史中較遠(yuǎn)的提交,必須使用更復(fù)雜的工具。 Git 沒有一個(gè)改變歷史工具,但是可以使用變基工具來變基一系列提交,基于它們原來的 HEAD 而不是將其移動(dòng)到另一個(gè)新的上面。 通過交互式變基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通過給?git rebase
?增加?-i
?選項(xiàng)來交互式地運(yùn)行變基。 必須指定想要重寫多久遠(yuǎn)的歷史,這可以通過告訴命令將要變基到的提交來做到。
例如,如果想要修改最近三次提交信息,或者那組提交中的任意一個(gè)提交信息,將想要修改的最近一次提交的父提交作為參數(shù)傳遞給?git rebase -i
命令,即?HEAD~2^
?或?HEAD~3
。 記住?~3
?可能比較容易,因?yàn)槟阏龂L試修改最后三次提交;但是注意實(shí)際上指定了以前的四次提交,即想要修改提交的父提交:
$ git rebase -i HEAD~3
再次記住這是一個(gè)變基命令 - 在?HEAD~3..HEAD
?范圍內(nèi)的每一個(gè)提交都會(huì)被重寫,無論你是否修改信息。 不要涉及任何已經(jīng)推送到中央服務(wù)器的提交 - 這樣做會(huì)產(chǎn)生一次變更的兩個(gè)版本,因而使他人困惑。
運(yùn)行這個(gè)命令會(huì)在文本編輯器上給你一個(gè)提交的列表,看起來像下面這樣:
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
# 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
需要重點(diǎn)注意的是相對(duì)于正常使用的?log
?命令,這些提交顯示的順序是相反的。 運(yùn)行一次?log?命令,會(huì)看到類似這樣的東西:
$ 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
注意其中的反序顯示。 交互式變基給你一個(gè)它將會(huì)運(yùn)行的腳本。 它將會(huì)從你在命令行中指定的提交(HEAD~3
)開始,從上到下的依次重演每一個(gè)提交引入的修改。 它將最舊的而不是最新的列在上面,因?yàn)槟菚?huì)是第一個(gè)將要重演的。
你需要修改腳本來讓它停留在你想修改的變更上。 要達(dá)到這個(gè)目的,你只要將你想修改的每一次提交前面的 ‘pick’ 改為 ‘edit’。 例如,只想修改第三次提交信息,可以像下面這樣修改文件:
edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
當(dāng)保存并退出編輯器時(shí),Git 將你帶回到列表中的最后一次提交,把你送回命令行并提示以下信息:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed my name a bit
You can amend the commit now, with
git commit --amend
Once you’re satisfied with your changes, run
git rebase --continue
這些指令準(zhǔn)確地告訴你該做什么。 輸入
$ git commit --amend
修改提交信息,然后退出編輯器。 然后,運(yùn)行
$ git rebase --continue
這個(gè)命令將會(huì)自動(dòng)地應(yīng)用另外兩個(gè)提交,然后就完成了。 如果需要將不止一處的 pick 改為 edit,需要在每一個(gè)修改為 edit 的提交上重復(fù)這些步驟。 每一次,Git 將會(huì)停止,讓你修正提交,然后繼續(xù)直到完成。
也可以使用交互式變基來重新排序或完全移除提交。 如果想要移除 “added cat-file” 提交然后修改另外兩個(gè)提交引入的順序,可以將變基腳本從這樣:
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
改為這樣:
pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit
當(dāng)保存并退出編輯器時(shí),Git 將你的分支帶回這些提交的父提交,應(yīng)用?310154e
?然后應(yīng)用f7f3f6d
,最后停止。 事實(shí)修改了那些提交的順序并完全地移除了 “added cat-file” 提交。
通過交互式變基工具,也可以將一連串提交壓縮成一個(gè)單獨(dú)的提交。 在變基信息中腳本給出了有用的指令:
#
# 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
如果,指定 “squash” 而不是 “pick” 或 “edit”,Git 將應(yīng)用兩者的修改并合并提交信息在一起。 所以,如果想要這三次提交變?yōu)橐粋€(gè)提交,可以這樣修改腳本:
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file
當(dāng)保存并退出編輯器時(shí),Git 應(yīng)用所有的三次修改然后將你放到編輯器中來合并三次提交信息:
# 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è)包含前三次提交的全部變更的提交。
拆分一個(gè)提交會(huì)撤消這個(gè)提交,然后多次地部分地暫存與提交直到完成你所需次數(shù)的提交。 例如,假設(shè)想要拆分三次提交的中間那次提交。 想要將它拆分為兩次提交:第一個(gè) “updated README formatting”,第二個(gè) “added blame” 來代替原來的 “updated README formatting and added blame”。 可以通過修改?rebase -i
?的腳本來做到這點(diǎn),將要拆分的提交的指令修改為 “edit”:
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
然后,當(dāng)腳本將你進(jìn)入到命令行時(shí),重置那個(gè)提交,拿到被重置的修改,從中創(chuàng)建幾次提交。 當(dāng)保存并退出編輯器時(shí),Git 帶你到列表中第一個(gè)提交的父提交,應(yīng)用第一個(gè)提交(f7f3f6d
),應(yīng)用第二個(gè)提交(310154e
),然后讓你進(jìn)入命令行。 那里,可以通過?git reset HEAD^
?做一次針對(duì)那個(gè)提交的混合重置,實(shí)際上將會(huì)撤消那次提交并將修改的文件未暫存。 現(xiàn)在可以暫存并提交文件直到有幾個(gè)提交,然后當(dāng)完成時(shí)運(yùn)行?git 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
),歷史記錄看起來像這樣:
$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit
再一次,這些改動(dòng)了所有在列表中的提交的 SHA-1 校驗(yàn)和,所以要確保列表中的提交還沒有推送到共享倉庫中。
有另一個(gè)歷史改寫的選項(xiàng),如果想要通過腳本的方式改寫大量提交的話可以使用它 - 例如,全局修改你的郵箱地址或從每一個(gè)提交中移除一個(gè)文件。 這個(gè)命令是?filter-branch
,它可以改寫歷史中大量的提交,除非你的項(xiàng)目還沒有公開并且其他人沒有基于要改寫的工作的提交做的工作,你不應(yīng)當(dāng)使用它。 然而,它可以很有用。 你將會(huì)學(xué)習(xí)到幾個(gè)常用的用途,這樣就得到了它適合使用地方的想法。
這經(jīng)常發(fā)生。 有人粗心地通過?git add .
?提交了一個(gè)巨大的二進(jìn)制文件,你想要從所有地方刪除它。 可能偶然地提交了一個(gè)包括一個(gè)密碼的文件,然而你想要開源項(xiàng)目。?filter-branch
?是一個(gè)可能會(huì)用來擦洗整個(gè)提交歷史的工具。 為了從整個(gè)提交歷史中移除一個(gè)叫做 passwords.txt 的文件,可以使用?--tree-filter
?選項(xiàng)給?filter-branch
:
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
--tree-filter
?選項(xiàng)在檢出項(xiàng)目的每一個(gè)提交后運(yùn)行指定的命令然后重新提交結(jié)果。 在本例中,你從每一個(gè)快照中移除了一個(gè)叫作 passwords.txt 的文件,無論它是否存在。 如果想要移除所有偶然提交的編輯器備份文件,可以運(yùn)行類似?git filter-branch --tree-filter 'rm -f *~' HEAD
?的命令。
最后將可以看到 Git 重寫樹與提交然后移動(dòng)分支指針。 通常一個(gè)好的想法是在一個(gè)測試分支中做這件事,然后當(dāng)你決定最終結(jié)果是真正想要的,可以硬重置 master 分支。 為了讓?filter-branch
在所有分支上運(yùn)行,可以給命令傳遞?--all
?選項(xiàng)。
假設(shè)已經(jīng)從另一個(gè)源代碼控制系統(tǒng)中導(dǎo)入,并且有幾個(gè)沒意義的子目錄(trunk、tags 等等)。 如果想要讓?trunk
?子目錄作為每一個(gè)提交的新的項(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)移除所有不影響子目錄的提交。
另一個(gè)常見的情形是在你開始工作時(shí)忘記運(yùn)行?git config
?來設(shè)置你的名字與郵箱地址,或者你想要開源一個(gè)項(xiàng)目并且修改所有你的工作郵箱地址為你的個(gè)人郵箱地址。 任何情形下,你也可以通過filter-branch
?來一次性修改多個(gè)提交中的郵箱地址。 需要小心的是只修改你自己的郵箱地址,所以你使用?--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
這會(huì)遍歷并重寫每一個(gè)提交來包含你的新郵箱地址。 因?yàn)樘峤话怂鼈兏柑峤坏?SHA-1 校驗(yàn)和,這個(gè)命令會(huì)修改你的歷史中的每一個(gè)提交的 SHA-1 校驗(yàn)和,而不僅僅只是那些匹配郵箱地址的提交。
更多建議: