## 重寫歷史
許多時候,在使用 Git 時,可能會因為某些原因想要修正提交歷史。 Git 很棒的一點是它允許你在最后時刻做決定。 你可以在將暫存區內容提交前決定哪些文件進入提交,可以通過 stash 命令來決定不與某些內容工作,也可以重寫已經發生的提交就像它們以另一種方式發生的一樣。 這可能涉及改變提交的順序,改變提交中的信息或修改文件,將提交壓縮或是拆分,或完全地移除提交 - 在將你的工作成果與他人共享之前。
在本節中,你可以學到如何完成這些非常有用的工作,這樣在與他人分享你的工作成果時你的提交歷史將如你所愿地展示出來。
### [修改最后一次提交](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#修改最后一次提交)
修改你最近一次提交可能是所有修改歷史提交的操作中最常見的一個。 對于你的最近一次提交,你往往想做兩件事情:修改提交信息,或者修改你添加、修改和移除的文件的快照。
如果,你只是想修改最近一次提交的提交信息,那么很簡單:
~~~
$ git commit --amend
~~~
這會把你帶入文本編輯器,里面包含了你最近一條提交信息,供你修改。 當保存并關閉編輯器后,編輯器將會用你輸入的內容替換最近一條提交信息。
如果你已經完成提交,又因為之前提交時忘記添加一個新創建的文件,想通過添加或修改文件來更改提交的快照,也可以通過類似的操作來完成。 通過修改文件然后運行?`git add`?或?`git rm`?一個已追蹤的文件,隨后運行?`git commit --amend`?拿走當前的暫存區域并使其做為新提交的快照。
使用這個技巧的時候需要小心,因為修正會改變提交的 SHA-1 校驗和。 它類似于一個小的變基 - 如果已經推送了最后一次提交就不要修正它。
### [修改多個提交信息](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#修改多個提交信息)
為了修改在提交歷史中較遠的提交,必須使用更復雜的工具。 Git 沒有一個改變歷史工具,但是可以使用變基工具來變基一系列提交,基于它們原來的 HEAD 而不是將其移動到另一個新的上面。 通過交互式變基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通過給?`git rebase`?增加?`-i`?選項來交互式地運行變基。 必須指定想要重寫多久遠的歷史,這可以通過告訴命令將要變基到的提交來做到。
例如,如果想要修改最近三次提交信息,或者那組提交中的任意一個提交信息,將想要修改的最近一次提交的父提交作為參數傳遞給?`git rebase -i`命令,即?`HEAD~2^`?或?`HEAD~3`。 記住?`~3`?可能比較容易,因為你正嘗試修改最后三次提交;但是注意實際上指定了以前的四次提交,即想要修改提交的父提交:
~~~
$ git rebase -i HEAD~3
~~~
再次記住這是一個變基命令 - 在?`HEAD~3..HEAD`?范圍內的每一個提交都會被重寫,無論你是否修改信息。 不要涉及任何已經推送到中央服務器的提交 - 這樣做會產生一次變更的兩個版本,因而使他人困惑。
運行這個命令會在文本編輯器上給你一個提交的列表,看起來像下面這樣:
~~~
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
~~~
需要重點注意的是相對于正常使用的?`log`?命令,這些提交顯示的順序是相反的。 運行一次?*log*?命令,會看到類似這樣的東西:
~~~
$ 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
~~~
注意其中的反序顯示。 交互式變基給你一個它將會運行的腳本。 它將會從你在命令行中指定的提交(`HEAD~3`)開始,從上到下的依次重演每一個提交引入的修改。 它將最舊的而不是最新的列在上面,因為那會是第一個將要重演的。
你需要修改腳本來讓它停留在你想修改的變更上。 要達到這個目的,你只要將你想修改的每一次提交前面的 ‘pick’ 改為 ‘edit’。 例如,只想修改第三次提交信息,可以像下面這樣修改文件:
~~~
edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
~~~
當保存并退出編輯器時,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
~~~
這些指令準確地告訴你該做什么。 輸入
~~~
$ git commit --amend
~~~
修改提交信息,然后退出編輯器。 然后,運行
~~~
$ git rebase --continue
~~~
這個命令將會自動地應用另外兩個提交,然后就完成了。 如果需要將不止一處的 pick 改為 edit,需要在每一個修改為 edit 的提交上重復這些步驟。 每一次,Git 將會停止,讓你修正提交,然后繼續直到完成。
### [重新排序提交](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#重新排序提交)
也可以使用交互式變基來重新排序或完全移除提交。 如果想要移除 “added cat-file” 提交然后修改另外兩個提交引入的順序,可以將變基腳本從這樣:
~~~
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
~~~
當保存并退出編輯器時,Git 將你的分支帶回這些提交的父提交,應用?`310154e`?然后應用`f7f3f6d`,最后停止。 事實修改了那些提交的順序并完全地移除了 “added cat-file” 提交。
### [壓縮提交](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#壓縮提交)
通過交互式變基工具,也可以將一連串提交壓縮成一個單獨的提交。 在變基信息中腳本給出了有用的指令:
~~~
#
# 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 將應用兩者的修改并合并提交信息在一起。 所以,如果想要這三次提交變為一個提交,可以這樣修改腳本:
~~~
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file
~~~
當保存并退出編輯器時,Git 應用所有的三次修改然后將你放到編輯器中來合并三次提交信息:
~~~
# 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
~~~
當你保存之后,你就擁有了一個包含前三次提交的全部變更的提交。
### [拆分提交](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#拆分提交)
拆分一個提交會撤消這個提交,然后多次地部分地暫存與提交直到完成你所需次數的提交。 例如,假設想要拆分三次提交的中間那次提交。 想要將它拆分為兩次提交:第一個 “updated README formatting”,第二個 “added blame” 來代替原來的 “updated README formatting and 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
~~~
然后,當腳本將你進入到命令行時,重置那個提交,拿到被重置的修改,從中創建幾次提交。 當保存并退出編輯器時,Git 帶你到列表中第一個提交的父提交,應用第一個提交(`f7f3f6d`),應用第二個提交(`310154e`),然后讓你進入命令行。 那里,可以通過?`git reset HEAD^`?做一次針對那個提交的混合重置,實際上將會撤消那次提交并將修改的文件未暫存。 現在可以暫存并提交文件直到有幾個提交,然后當完成時運行?`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 在腳本中應用最后一次提交(`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
~~~
再一次,這些改動了所有在列表中的提交的 SHA-1 校驗和,所以要確保列表中的提交還沒有推送到共享倉庫中。
### [核武器級選項:filter-branch](http://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E5%86%99%E5%8E%86%E5%8F%B2#核武器級選項:filter-branch)
有另一個歷史改寫的選項,如果想要通過腳本的方式改寫大量提交的話可以使用它 - 例如,全局修改你的郵箱地址或從每一個提交中移除一個文件。 這個命令是?`filter-branch`,它可以改寫歷史中大量的提交,除非你的項目還沒有公開并且其他人沒有基于要改寫的工作的提交做的工作,你不應當使用它。 然而,它可以很有用。 你將會學習到幾個常用的用途,這樣就得到了它適合使用地方的想法。
#### 從每一個提交移除一個文件
這經常發生。 有人粗心地通過?`git add .`?提交了一個巨大的二進制文件,你想要從所有地方刪除它。 可能偶然地提交了一個包括一個密碼的文件,然而你想要開源項目。?`filter-branch`?是一個可能會用來擦洗整個提交歷史的工具。 為了從整個提交歷史中移除一個叫做 passwords.txt 的文件,可以使用?`--tree-filter`?選項給?`filter-branch`:
~~~
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
~~~
`--tree-filter`?選項在檢出項目的每一個提交后運行指定的命令然后重新提交結果。 在本例中,你從每一個快照中移除了一個叫作 passwords.txt 的文件,無論它是否存在。 如果想要移除所有偶然提交的編輯器備份文件,可以運行類似?`git filter-branch --tree-filter 'rm -f *~' HEAD`?的命令。
最后將可以看到 Git 重寫樹與提交然后移動分支指針。 通常一個好的想法是在一個測試分支中做這件事,然后當你決定最終結果是真正想要的,可以硬重置 master 分支。 為了讓?`filter-branch`在所有分支上運行,可以給命令傳遞?`--all`?選項。
#### 使一個子目錄做為新的根目錄
假設已經從另一個源代碼控制系統中導入,并且有幾個沒意義的子目錄(trunk、tags 等等)。 如果想要讓?`trunk`?子目錄作為每一個提交的新的項目根目錄,`filter-branch`?也可以幫助你那么做:
~~~
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
~~~
現在新項目根目錄是?`trunk`?子目錄了。 Git 會自動移除所有不影響子目錄的提交。
#### 全局修改郵箱地址
另一個常見的情形是在你開始工作時忘記運行?`git config`?來設置你的名字與郵箱地址,或者你想要開源一個項目并且修改所有你的工作郵箱地址為你的個人郵箱地址。 任何情形下,你也可以通過`filter-branch`?來一次性修改多個提交中的郵箱地址。 需要小心的是只修改你自己的郵箱地址,所以你使用?`--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
~~~
這會遍歷并重寫每一個提交來包含你的新郵箱地址。 因為提交包含了它們父提交的 SHA-1 校驗和,這個命令會修改你的歷史中的每一個提交的 SHA-1 校驗和,而不僅僅只是那些匹配郵箱地址的提交。
- 前言
- Scott Chacon 序
- Ben Straub 序
- 獻辭
- 貢獻者
- 引言
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 命令行
- 1.5 安裝 Git
- 1.6 初次運行 Git 前的配置
- 1.7 獲取幫助
- 1.8 總結
- 2. Git 基礎
- 2.1 獲取 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 Git 別名
- 2.8 總結
- 3. Git 分支
- 3.1 分支簡介
- 3.2 分支的新建與合并
- 3.3 分支管理
- 3.4 分支開發工作流
- 3.5 遠程分支
- 3.6 變基
- 3.7 總結
- 4. 服務器上的 Git
- 4.1 協議
- 4.2 在服務器上搭建 Git
- 4.3 生成 SSH 公鑰
- 4.4 配置服務器
- 4.5 Git 守護進程
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管的選擇
- 4.10 總結
- 5. 分布式 Git
- 5.1 分布式工作流程
- 5.2 向一個項目貢獻
- 5.3 維護項目
- 5.4 總結
- 6. GitHub
- 6.1 賬戶的創建和配置
- 6.2 對項目做出貢獻
- 6.3 維護項目
- 6.4 管理組織
- 6.5 腳本 GitHub
- 6.6 總結
- 7. Git 工具
- 7.1 選擇修訂版本
- 7.2 交互式暫存
- 7.3 儲藏與清理
- 7.4 簽署工作
- 7.5 搜索
- 7.6 重寫歷史
- 7.7 重置揭密
- 7.8 高級合并
- 7.9 Rerere
- 7.10 使用 Git 調試
- 7.11 子模塊
- 7.12 打包
- 7.13 替換
- 7.14 憑證存儲
- 7.15 總結
- 8. 自定義 Git
- 8.1 配置 Git
- 8.2 Git 屬性
- 8.3 Git 鉤子
- 8.4 使用強制策略的一個例子
- 8.5 總結
- 9. Git 與其他系統
- 9.1 作為客戶端的 Git
- 9.2 遷移到 Git
- 9.3 總結
- 10. Git 內部原理
- 10.1 底層命令和高層命令
- 10.2 Git 對象
- 10.3 Git 引用
- 10.4 包文件
- 10.5 引用規格
- 10.6 傳輸協議
- 10.7 維護與數據恢復
- 10.8 環境變量
- 10.9 總結
- A. 其它環境中的 Git
- A1.1 圖形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Eclipse 中的 Git
- A1.4 Bash 中的 Git
- A1.5 Zsh 中的 Git
- A1.6 Powershell 中的 Git
- A1.7 總結
- B. 將 Git 嵌入你的應用
- A2.1 命令行 Git 方式
- A2.2 Libgit2
- A2.3 JGit
- C. Git 命令
- A3.1 設置與配置
- A3.2 獲取與創建項目
- A3.3 快照基礎
- A3.4 分支與合并
- A3.5 項目分享與更新
- A3.6 檢查與比較
- A3.7 調試
- A3.8 補丁
- A3.9 郵件
- A3.10 外部系統
- A3.11 管理
- A3.12 底層命令