鍍金池/ 教程/ Java/ Git 工具
起步
Git 分支
自定義 Git
Git 基礎(chǔ)
Git 工具
Git 與其他系統(tǒng)
服務(wù)器上的 Git
Git 內(nèi)部原理
分布式 Git

Git 工具

現(xiàn)在,你已經(jīng)學(xué)習(xí)了管理或者維護(hù) Git 倉庫,實(shí)現(xiàn)代碼控制所需的大多數(shù)日常命令和工作流程。你已經(jīng)完成了跟蹤和提交文件的基本任務(wù),并且發(fā)揮了暫存區(qū)和輕量級的特性分支及合并的威力。

接下來你將領(lǐng)略到一些 Git 可以實(shí)現(xiàn)的非常強(qiáng)大的功能,這些功能你可能并不會在日常操作中使用,但在某些時(shí)候你也許會需要。

修訂版本(Revision)選擇

Git 允許你通過幾種方法來指明特定的或者一定范圍內(nèi)的提交。了解它們并不是必需的,但是了解一下總沒壞處。

單個(gè)修訂版本

顯然你可以使用給出的 SHA-1 值來指明一次提交,不過也有更加人性化的方法來做同樣的事。本節(jié)概述了指明單個(gè)提交的諸多方法。

簡短的SHA

Git 很聰明,它能夠通過你提供的前幾個(gè)字符來識別你想要的那次提交,只要你提供的那部分 SHA-1 不短于四個(gè)字符,并且沒有歧義——也就是說,當(dāng)前倉庫中只有一個(gè)對象以這段 SHA-1 開頭。

例如,想要查看一次指定的提交,假設(shè)你運(yùn)行 git log 命令并找到你增加了功能的那次提交:

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

假設(shè)是 1c002dd.... 。如果你想 git show 這次提交,下面的命令是等價(jià)的(假設(shè)簡短的版本沒有歧義):

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

Git 可以為你的 SHA-1 值生成出簡短且唯一的縮寫。如果你傳遞 --abbrev-commitgit log 命令,輸出結(jié)果里就會使用簡短且唯一的值;它默認(rèn)使用七個(gè)字符來表示,不過必要時(shí)為了避免 SHA-1 的歧義,會增加字符數(shù):

$ git log --abbrev-commit --pretty=oneline
ca82a6d changed the version number
085bb3b removed unnecessary test code
a11bef0 first commit

通常在一個(gè)項(xiàng)目中,使用八到十個(gè)字符來避免 SHA-1 歧義已經(jīng)足夠了。最大的 Git 項(xiàng)目之一,Linux 內(nèi)核,目前也只需要最長 40 個(gè)字符中的 12 個(gè)字符來保持唯一性。

關(guān)于 SHA-1 的簡短說明

許多人可能會擔(dān)心一個(gè)問題:在隨機(jī)的偶然情況下,在他們的倉庫里會出現(xiàn)兩個(gè)具有相同 SHA-1 值的對象。那會怎么樣呢?

如果你真的向倉庫里提交了一個(gè)跟之前的某個(gè)對象具有相同 SHA-1 值的對象,Git 將會發(fā)現(xiàn)之前的那個(gè)對象已經(jīng)存在在 Git 數(shù)據(jù)庫中,并認(rèn)為它已經(jīng)被寫入了。如果什么時(shí)候你想再次檢出那個(gè)對象時(shí),你會總是得到先前的那個(gè)對象的數(shù)據(jù)。

不過,你應(yīng)該了解到,這種情況發(fā)生的概率是多么微小。SHA-1 摘要長度是 20 字節(jié),也就是 160 位。為了保證有 50% 的概率出現(xiàn)一次沖突,需要 2^80 個(gè)隨機(jī)哈希的對象(計(jì)算沖突機(jī)率的公式是 p = (n(n-1)/2) * (1/2^160))。2^80 是 1.2 x 10^24,也就是一億億億,那是地球上沙??倲?shù)的 1200 倍。

現(xiàn)在舉例說一下怎樣才能產(chǎn)生一次 SHA-1 沖突。如果地球上 65 億的人類都在編程,每人每秒都在產(chǎn)生等價(jià)于整個(gè) Linux 內(nèi)核歷史(一百萬個(gè) Git 對象)的代碼,并將之提交到一個(gè)巨大的 Git 倉庫里面,那將花費(fèi) 5 年的時(shí)間才會產(chǎn)生足夠的對象,使其擁有 50% 的概率產(chǎn)生一次 SHA-1 對象沖突。這要比你編程團(tuán)隊(duì)的成員同一個(gè)晚上在互不相干的意外中被狼襲擊并殺死的機(jī)率還要小。

分支引用

指明一次提交的最直接的方法要求有一個(gè)指向它的分支引用。這樣,你就可以在任何需要一個(gè)提交對象或者 SHA-1 值的 Git 命令中使用該分支名稱了。如果你想要顯示一個(gè)分支的最后一次提交的對象,例如假設(shè) topic1 分支指向 ca82a6d,那么下面的命令是等價(jià)的:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

如果你想知道某個(gè)分支指向哪個(gè)特定的 SHA,或者想看任何一個(gè)例子中被簡寫的 SHA-1,你可以使用一個(gè)叫做 rev-parse 的 Git 探測工具。在第 9 章你可以看到關(guān)于探測工具的更多信息;簡單來說,rev-parse 是為了底層操作而不是日常操作設(shè)計(jì)的。不過,有時(shí)你想看 Git 現(xiàn)在到底處于什么狀態(tài)時(shí),它可能會很有用。這里你可以對你的分支運(yùn)執(zhí)行 rev-parse。

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

引用日志里的簡稱

在你工作的同時(shí),Git 在后臺的工作之一就是保存一份引用日志——一份記錄最近幾個(gè)月你的 HEAD 和分支引用的日志。

你可以使用 git reflog 來查看引用日志:

$ git reflog
734713b HEAD@{0}: commit: fixed refs handling, added gc auto, updated
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by recursive.
1c002dd HEAD@{2}: commit: added some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD

每次你的分支頂端因?yàn)槟承┰虮恍薷臅r(shí),Git 就會為你將信息保存在這個(gè)臨時(shí)歷史記錄里面。你也可以使用這份數(shù)據(jù)來指明更早的分支。如果你想查看倉庫中 HEAD 在五次前的值,你可以使用引用日志的輸出中的 @{n} 引用:

$ git show HEAD@{5}

你也可以使用這個(gè)語法來查看某個(gè)分支在一定時(shí)間前的位置。例如,想看你的 master 分支昨天在哪,你可以輸入

$ git show master@{yesterday}

它就會顯示昨天分支的頂端在哪。這項(xiàng)技術(shù)只對還在你引用日志里的數(shù)據(jù)有用,所以不能用來查看比幾個(gè)月前還早的提交。

想要看類似于 git log 輸出格式的引用日志信息,你可以運(yùn)行 git log -g

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: fixed refs handling, added gc auto, updated
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

需要注意的是,引用日志信息只存在于本地——這是一個(gè)記錄你在你自己的倉庫里做過什么的日志。其他人拷貝的倉庫里的引用日志不會和你的相同;而你新克隆一個(gè)倉庫的時(shí)候,引用日志是空的,因?yàn)槟阍趥}庫里還沒有操作。git show HEAD@{2.months.ago} 這條命令只有在你克隆了一個(gè)項(xiàng)目至少兩個(gè)月時(shí)才會有用——如果你是五分鐘前克隆的倉庫,那么它將不會有結(jié)果返回。

祖先引用

另一種指明某次提交的常用方法是通過它的祖先。如果你在引用最后加上一個(gè) ^,Git 將其理解為此次提交的父提交。 假設(shè)你的工程歷史是這樣的:

$ git log --pretty=format:'%h %s' --graph
* 734713b fixed refs handling, added gc auto, updated tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd added some blame and merge stuff
|/
* 1c36188 ignore *.gem
* 9b29157 add open3_detach to gemspec file list

那么,想看上一次提交,你可以使用 HEAD^,意思是“HEAD 的父提交”:

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

你也可以在 ^ 后添加一個(gè)數(shù)字——例如,d921970^2 意思是“d921970 的第二父提交”。這種語法只在合并提交時(shí)有用,因?yàn)楹喜⑻峤豢赡苡卸鄠€(gè)父提交。第一父提交是你合并時(shí)所在分支,而第二父提交是你所合并的分支:

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

另外一個(gè)指明祖先提交的方法是 ~。這也是指向第一父提交,所以 HEAD~HEAD^ 是等價(jià)的。當(dāng)你指定數(shù)字的時(shí)候就明顯不一樣了。HEAD~2 是指“第一父提交的第一父提交”,也就是“祖父提交”——它會根據(jù)你指定的次數(shù)檢索第一父提交。例如,在上面列出的歷史記錄里面,HEAD~3 會是

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

也可以寫成 HEAD^^^,同樣是第一父提交的第一父提交的第一父提交:

$ git show HEAD^^^
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

你也可以混合使用這些語法——你可以通過 HEAD~3^2 指明先前引用的第二父提交(假設(shè)它是一個(gè)合并提交)。

提交范圍

現(xiàn)在你已經(jīng)可以指明單次的提交,讓我們來看看怎樣指明一定范圍的提交。這在你管理分支的時(shí)候尤顯重要——如果你有很多分支,你可以指明范圍來圈定一些問題的答案,比如:“這個(gè)分支上我有哪些工作還沒合并到主分支的?”

雙點(diǎn)

最常用的指明范圍的方法是雙點(diǎn)的語法。這種語法主要是讓 Git 區(qū)分出可從一個(gè)分支中獲得而不能從另一個(gè)分支中獲得的提交。例如,假設(shè)你有類似于圖 6-1 的提交歷史。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0601-tn.png" alt="" />

圖 6-1. 范圍選擇的提交歷史實(shí)例

你想要查看你的試驗(yàn)分支上哪些沒有被提交到主分支,那么你就可以使用 master..experiment 來讓 Git 顯示這些提交的日志——這句話的意思是“所有可從experiment分支中獲得而不能從master分支中獲得的提交”。為了使例子簡單明了,我使用了圖標(biāo)中提交對象的字母來代替真實(shí)日志的輸出,所以會顯示:

$ git log master..experiment
D
C

另一方面,如果你想看相反的——所有在 master 而不在 experiment 中的分支——你可以交換分支的名字。experiment..master 顯示所有可在 master 獲得而在 experiment 中不能的提交:

$ git log experiment..master
F
E

這在你想保持 experiment 分支最新和預(yù)覽你將合并的提交的時(shí)候特別有用。這個(gè)語法的另一種常見用途是查看你將把什么推送到遠(yuǎn)程:

$ git log origin/master..HEAD

這條命令顯示任何在你當(dāng)前分支上而不在遠(yuǎn)程origin 上的提交。如果你運(yùn)行 git push 并且的你的當(dāng)前分支正在跟蹤 origin/master,被git log origin/master..HEAD 列出的提交就是將被傳輸?shù)椒?wù)器上的提交。 你也可以留空語法中的一邊來讓 Git 來假定它是 HEAD。例如,輸入 git log origin/master.. 將得到和上面的例子一樣的結(jié)果—— Git 使用 HEAD 來代替不存在的一邊。

多點(diǎn)

雙點(diǎn)語法就像速記一樣有用;但是你也許會想針對兩個(gè)以上的分支來指明修訂版本,比如查看哪些提交被包含在某些分支中的一個(gè),但是不在你當(dāng)前的分支上。Git允許你在引用前使用^字符或者--not指明你不希望提交被包含其中的分支。因此下面三個(gè)命令是等同的:

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

這樣很好,因?yàn)樗试S你在查詢中指定多于兩個(gè)的引用,而這是雙點(diǎn)語法所做不到的。例如,如果你想查找所有從refArefB包含的但是不被refC包含的提交,你可以輸入下面中的一個(gè)

$ git log refA refB ^refC
$ git log refA refB --not refC

這建立了一個(gè)非常強(qiáng)大的修訂版本查詢系統(tǒng),應(yīng)該可以幫助你解決分支里包含了什么這個(gè)問題。

三點(diǎn)

最后一種主要的范圍選擇語法是三點(diǎn)語法,這個(gè)可以指定被兩個(gè)引用中的一個(gè)包含但又不被兩者同時(shí)包含的分支?;剡^頭來看一下圖6-1里所列的提交歷史的例子。 如果你想查看master或者experiment中包含的但不是兩者共有的引用,你可以運(yùn)行

$ git log master...experiment
F
E
D
C

這個(gè)再次給出你普通的log輸出但是只顯示那四次提交的信息,按照傳統(tǒng)的提交日期排列。

這種情形下,log命令的一個(gè)常用參數(shù)是--left-right,它會顯示每個(gè)提交到底處于哪一側(cè)的分支。這使得數(shù)據(jù)更加有用。

$ git log --left-right master...experiment
< F
< E
> D
> C

有了以上工具,讓Git知道你要察看哪些提交就容易得多了。

交互式暫存

Git提供了很多腳本來輔助某些命令行任務(wù)。這里,你將看到一些交互式命令,它們幫助你方便地構(gòu)建只包含特定組合和部分文件的提交。在你修改了一大批文件然后決定將這些變更分布在幾個(gè)各有側(cè)重的提交而不是單個(gè)又大又亂的提交時(shí),這些工具非常有用。用這種方法,你可以確保你的提交在邏輯上劃分為相應(yīng)的變更集,以便于供和你一起工作的開發(fā)者審閱。如果你運(yùn)行git add時(shí)加上-i或者--interactive選項(xiàng),Git就進(jìn)入了一個(gè)交互式的shell模式,顯示一些類似于下面的信息:

$ git add -i
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now>

你會看到這個(gè)命令以一個(gè)完全不同的視圖顯示了你的暫存區(qū)——主要是你通過git status得到的那些信息但是稍微簡潔但信息更加豐富一些。它在左側(cè)列出了你暫存的變更,在右側(cè)列出了未被暫存的變更。

在這之后是一個(gè)命令區(qū)。這里你可以做很多事情,包括暫存文件,撤回文件,暫存部分文件,加入未被追蹤的文件,查看暫存文件的差別。

暫存和撤回文件

如果你在What now>的提示后輸入2或者u,這個(gè)腳本會提示你那些文件你想要暫存:

What now> 2
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Update>>

如果想暫存TODO和index.html,你可以輸入相應(yīng)的編號:

Update>> 1,2
           staged     unstaged path
* 1:    unchanged        +0/-1 TODO
* 2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Update>>

每個(gè)文件旁邊的*表示選中的文件將被暫存。如果你在update>>提示后直接敲入回車,Git會替你把所有選中的內(nèi)容暫存:

Update>>
updated 2 paths

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 1
           staged     unstaged path
  1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

現(xiàn)在你可以看到TODO和index.html文件被暫存了同時(shí)simplegit.rb文件仍然未被暫存。如果這時(shí)你想要撤回TODO文件,就使用3或者r(代表revert,恢復(fù))選項(xiàng):

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 3
           staged     unstaged path
  1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Revert>> 1
           staged     unstaged path
* 1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Revert>> [enter]
reverted one path

再次查看Git的狀態(tài),你會看到你已經(jīng)撤回了TODO文件

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 1
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

要查看你暫存內(nèi)容的差異,你可以使用6或者d(表示diff)命令。它會顯示你暫存文件的列表,你可以選擇其中的幾個(gè),顯示其被暫存的差異。這跟你在命令行下指定git diff --cached非常相似:

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 6
           staged     unstaged path
  1:        +1/-1      nothing index.html
Review diff>> 1
diff --git a/index.html b/index.html
index 4d07108..4335f49 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@ Date Finder

 <p id="out">...</p>

-<div id="footer">contact : support@github.com</div>
+<div id="footer">contact : email.support@github.com</div>

 <script type="text/javascript">

通過這些基本命令,你可以使用交互式增加模式更加方便地處理暫存區(qū)。

暫存補(bǔ)丁

只讓Git暫存文件的某些部分而忽略其他也是有可能的。例如,你對simplegit.rb文件作了兩處修改但是只想暫存其中一個(gè)而忽略另一個(gè),在Git中實(shí)現(xiàn)這一點(diǎn)非常容易。在交互式的提示符下,輸入5或者p(表示patch,補(bǔ)?。?。Git會詢問哪些文件你希望部分暫存;然后對于被選中文件的每一節(jié),他會逐個(gè)顯示文件的差異區(qū)塊并詢問你是否希望暫存他們:

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index dd5ecc4..57399e0 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -22,7 +22,7 @@ class SimpleGit
   end

   def log(treeish = 'master')
-    command("git log -n 25 #{treeish}")
+    command("git log -n 30 #{treeish}")
   end

   def blame(path)
Stage this hunk [y,n,a,d,/,j,J,g,e,?]?

此處你有很多選擇。輸入?可以顯示列表:

Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help

如果你想暫存各個(gè)區(qū)塊,通常你會輸入y或者n,但是暫存特定文件里的全部區(qū)塊或者暫時(shí)跳過對一個(gè)區(qū)塊的處理同樣也很有用。如果你暫存了文件的一個(gè)部分而保留另外一個(gè)部分不被暫存,你的狀態(tài)輸出看起來會是這樣:

What now> 1
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:        +1/-1      nothing index.html
  3:        +1/-1        +4/-0 lib/simplegit.rb

simplegit.rb的狀態(tài)非常有意思。它顯示有幾行被暫存了,有幾行沒有。你部分地暫存了這個(gè)文件。在這時(shí),你可以退出交互式腳本然后運(yùn)行git commit來提交部分暫存的文件。

最后你也可以不通過交互式增加的模式來實(shí)現(xiàn)部分文件暫存——你可以在命令行下通過git add -p或者git add --patch來啟動(dòng)同樣的腳本。

儲藏(Stashing)

經(jīng)常有這樣的事情發(fā)生,當(dāng)你正在進(jìn)行項(xiàng)目中某一部分的工作,里面的東西處于一個(gè)比較雜亂的狀態(tài),而你想轉(zhuǎn)到其他分支上進(jìn)行一些工作。問題是,你不想提交進(jìn)行了一半的工作,否則以后你無法回到這個(gè)工作點(diǎn)。解決這個(gè)問題的辦法就是git stash命令。

“‘儲藏”“可以獲取你工作目錄的中間狀態(tài)——也就是你修改過的被追蹤的文件和暫存的變更——并將它保存到一個(gè)未完結(jié)變更的堆棧中,隨時(shí)可以重新應(yīng)用。

儲藏你的工作

為了演示這一功能,你可以進(jìn)入你的項(xiàng)目,在一些文件上進(jìn)行工作,有可能還暫存其中一個(gè)變更。如果你運(yùn)行 git status,你可以看到你的中間狀態(tài):

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#

現(xiàn)在你想切換分支,但是你還不想提交你正在進(jìn)行中的工作;所以你儲藏這些變更。為了往堆棧推送一個(gè)新的儲藏,只要運(yùn)行 git stash

$ git stash
Saved working directory and index state \
  "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")

你的工作目錄就干凈了:

$ git status
# On branch master
nothing to commit, working directory clean

這時(shí),你可以方便地切換到其他分支工作;你的變更都保存在棧上。要查看現(xiàn)有的儲藏,你可以使用 git stash list

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log

在這個(gè)案例中,之前已經(jīng)進(jìn)行了兩次儲藏,所以你可以訪問到三個(gè)不同的儲藏。你可以重新應(yīng)用你剛剛實(shí)施的儲藏,所采用的命令就是之前在原始的 stash 命令的幫助輸出里提示的:git stash apply。如果你想應(yīng)用更早的儲藏,你可以通過名字指定它,像這樣:git stash apply stash@{2}。如果你不指明,Git 默認(rèn)使用最近的儲藏并嘗試應(yīng)用它:

$ git stash apply
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   index.html
#      modified:   lib/simplegit.rb
#

你可以看到 Git 重新修改了你所儲藏的那些當(dāng)時(shí)尚未提交的文件。在這個(gè)案例里,你嘗試應(yīng)用儲藏的工作目錄是干凈的,并且屬于同一分支;但是一個(gè)干凈的工作目錄和應(yīng)用到相同的分支上并不是應(yīng)用儲藏的必要條件。你可以在其中一個(gè)分支上保留一份儲藏,隨后切換到另外一個(gè)分支,再重新應(yīng)用這些變更。在工作目錄里包含已修改和未提交的文件時(shí),你也可以應(yīng)用儲藏——Git 會給出歸并沖突如果有任何變更無法干凈地被應(yīng)用。

對文件的變更被重新應(yīng)用,但是被暫存的文件沒有重新被暫存。想那樣的話,你必須在運(yùn)行 git stash apply 命令時(shí)帶上一個(gè) --index 的選項(xiàng)來告訴命令重新應(yīng)用被暫存的變更。如果你是這么做的,你應(yīng)該已經(jīng)回到你原來的位置:

$ git stash apply --index
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#

apply 選項(xiàng)只嘗試應(yīng)用儲藏的工作——儲藏的內(nèi)容仍然在棧上。要移除它,你可以運(yùn)行 git stash drop,加上你希望移除的儲藏的名字:

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log
$ git stash drop stash@{0}
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)

你也可以運(yùn)行 git stash pop 來重新應(yīng)用儲藏,同時(shí)立刻將其從堆棧中移走。

取消儲藏(Un-applying a Stash)

在某些情況下,你可能想應(yīng)用儲藏的修改,在進(jìn)行了一些其他的修改后,又要取消之前所應(yīng)用儲藏的修改。Git沒有提供類似于 stash unapply 的命令,但是可以通過取消該儲藏的補(bǔ)丁達(dá)到同樣的效果:

$ git stash show -p stash@{0} | git apply -R

同樣的,如果你沒有指定具體的某個(gè)儲藏,Git 會選擇最近的儲藏:

$ git stash show -p | git apply -R

你可能會想要新建一個(gè)別名,在你的 Git 里增加一個(gè) stash-unapply 命令,這樣更有效率。例如:

$ git config --global alias.stash-unapply '!git stash show -p | git apply -R'
$ git stash apply
$ #... work work work
$ git stash-unapply

從儲藏中創(chuàng)建分支

如果你儲藏了一些工作,暫時(shí)不去理會,然后繼續(xù)在你儲藏工作的分支上工作,你在重新應(yīng)用工作時(shí)可能會碰到一些問題。如果嘗試應(yīng)用的變更是針對一個(gè)你那之后修改過的文件,你會碰到一個(gè)歸并沖突并且必須去化解它。如果你想用更方便的方法來重新檢驗(yàn)?zāi)銉Σ氐淖兏憧梢赃\(yùn)行 git stash branch,這會創(chuàng)建一個(gè)新的分支,檢出你儲藏工作時(shí)的所處的提交,重新應(yīng)用你的工作,如果成功,將會丟棄儲藏。

$ git stash branch testchanges
Switched to a new branch "testchanges"
# On branch testchanges
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)

這是一個(gè)很棒的捷徑來恢復(fù)儲藏的工作然后在新的分支上繼續(xù)當(dāng)時(shí)的工作。

重寫歷史

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

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

改變最近一次提交

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

如果你只想修改最近一次提交說明,這非常簡單:

$ git commit --amend

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

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

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

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

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

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

$ git rebase -i HEAD~3

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

運(yùn)行這個(gè)命令會為你的文本編輯器提供一個(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
#  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)是你得注意這些提交的順序與你通常通過log命令看到的是相反的。如果你運(yùn)行log,你會看到下面這樣的結(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

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

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

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

當(dāng)你保存并退出編輯器,Git會倒回至列表中的最后一次提交,然后把你送到命令行中,同時(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

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

$ git rebase --continue

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

重排提交

你也可以使用交互式的衍合來徹底重排或刪除提交。如果你想刪除"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 會同時(shí)應(yīng)用那個(gè)變更和它之前的變更并將提交說明歸并。因此,如果你想將這三個(gè)提交合并為單一提交,你可以將腳本修改成這樣:

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

當(dāng)你保存并退出編輯器,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è)包含前三次提交的全部變更的單一提交。

拆分提交

拆分提交就是撤銷一次提交,然后多次部分地暫存或提交直到結(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),然后將你帶到控制臺。那里你可以用git reset HEAD^對那次提交進(jìn)行一次混合的重置,這將撤銷那次提交并且將修改的文件撤回。此時(shí)你可以暫存并提交文件,直到你擁有多次提交,結(jié)束后,運(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

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

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

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

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

這個(gè)經(jīng)常發(fā)生。有些人不經(jīng)思考使用git add .,意外地提交了一個(gè)巨大的二進(jìn)制文件,你想將它從所有地方刪除。也許你不小心提交了一個(gè)包含密碼的文件,而你想讓你的項(xiàng)目開源。filter-branch大概會是你用來清理整個(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)會在每次檢出項(xiàng)目時(shí)先執(zhí)行指定的命令然后重新提交結(jié)果。在這個(gè)例子中,你會在所有快照中刪除一個(gè)名叫 password.txt 的文件,無論它是否存在。如果你想刪除所有不小心提交上去的編輯器備份文件,你可以運(yùn)行類似git filter-branch --tree-filter "find * -type f -name '*~' -delete" HEAD的命令。

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

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

假設(shè)你完成了從另外一個(gè)代碼控制系統(tǒng)的導(dǎo)入工作,得到了一些沒有意義的子目錄(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 會自動(dòng)地刪除不對這個(gè)子目錄產(chǎn)生影響的提交。

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

另一個(gè)常見的案例是你在開始時(shí)忘了運(yùn)行git config來設(shè)置你的姓名和電子郵件地址,也許你想開源一個(gè)項(xiàng)目,把你所有的工作電子郵件地址修改為個(gè)人地址。無論哪種情況你都可以用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

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

使用 Git 調(diào)試

Git 同樣提供了一些工具來幫助你調(diào)試項(xiàng)目中遇到的問題。由于 Git 被設(shè)計(jì)為可應(yīng)用于幾乎任何類型的項(xiàng)目,這些工具是通用型,但是在遇到問題時(shí)可以經(jīng)常幫助你查找缺陷所在。

文件標(biāo)注

如果你在追蹤代碼中的缺陷想知道這是什么時(shí)候?yàn)槭裁幢灰M(jìn)來的,文件標(biāo)注會是你的最佳工具。它會顯示文件中對每一行進(jìn)行修改的最近一次提交。因此,如果你發(fā)現(xiàn)自己代碼中的一個(gè)方法存在缺陷,你可以用git blame來標(biāo)注文件,查看那個(gè)方法的每一行分別是由誰在哪一天修改的。下面這個(gè)例子使用了-L選項(xiàng)來限制輸出范圍在第12至22行:

$ git blame -L 12,22 simplegit.rb
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 12)  def show(tree = 'master')
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 13)   command("git show #{tree}")
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 14)  end
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 15)
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 16)  def log(tree = 'master')
79eaf55d (Scott Chacon  2008-04-06 10:15:08 -0700 17)   command("git log #{tree}")
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 18)  end
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 19)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20)  def blame(path)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21)   command("git blame #{path}")
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22)  end

請注意第一個(gè)域里是最后一次修改該行的那次提交的 SHA-1 值。接下去的兩個(gè)域是從那次提交中抽取的值——作者姓名和日期——所以你可以方便地獲知誰在什么時(shí)候修改了這一行。在這后面是行號和文件的內(nèi)容。請注意^4832fe2提交的那些行,這些指的是文件最初提交的那些行。那個(gè)提交是文件第一次被加入這個(gè)項(xiàng)目時(shí)存在的,自那以后未被修改過。這會帶來小小的困惑,因?yàn)槟阋呀?jīng)至少看到了Git使用^來修飾一個(gè)提交的SHA值的三種不同的意義,但這里確實(shí)就是這個(gè)意思。

另一件很酷的事情是在 Git 中你不需要顯式地記錄文件的重命名。它會記錄快照然后根據(jù)現(xiàn)實(shí)嘗試找出隱式的重命名動(dòng)作。這其中有一個(gè)很有意思的特性就是你可以讓它找出所有的代碼移動(dòng)。如果你在git blame后加上-C,Git會分析你在標(biāo)注的文件然后嘗試找出其中代碼片段的原始出處,如果它是從其他地方拷貝過來的話。最近,我在將一個(gè)名叫GITServerHandler.m的文件分解到多個(gè)文件中,其中一個(gè)是GITPackUpload.m。通過對GITPackUpload.m執(zhí)行帶-C參數(shù)的blame命令,我可以看到代碼塊的原始出處:

$ git blame -C -L 141,153 GITPackUpload.m
f344f58d GITServerHandler.m (Scott 2009-01-04 141)
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 148)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 149)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 150)
56ef2caf GITServerHandler.m (Scott 2009-01-05 151)         if(commit) {
56ef2caf GITServerHandler.m (Scott 2009-01-05 152)                 [refDict setOb
56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

這真的非常有用。通常,你會把你拷貝代碼的那次提交作為原始提交,因?yàn)檫@是你在這個(gè)文件中第一次接觸到那幾行。Git可以告訴你編寫那些行的原始提交,即便是在另一個(gè)文件里。

二分查找

標(biāo)注文件在你知道問題是哪里引入的時(shí)候會有幫助。如果你不知道,并且自上次代碼可用的狀態(tài)已經(jīng)經(jīng)歷了上百次的提交,你可能就要求助于bisect命令了。bisect會在你的提交歷史中進(jìn)行二分查找來盡快地確定哪一次提交引入了錯(cuò)誤。

例如你剛剛推送了一個(gè)代碼發(fā)布版本到產(chǎn)品環(huán)境中,對代碼為什么會表現(xiàn)成那樣百思不得其解。你回到你的代碼中,還好你可以重現(xiàn)那個(gè)問題,但是找不到在哪里。你可以對代碼執(zhí)行bisect來尋找。首先你運(yùn)行git bisect start啟動(dòng),然后你用git bisect bad來告訴系統(tǒng)當(dāng)前的提交已經(jīng)有問題了。然后你必須告訴bisect已知的最后一次正常狀態(tài)是哪次提交,使用git bisect good [good_commit]

$ git bisect start
$ git bisect bad
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this
[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo

Git 發(fā)現(xiàn)在你標(biāo)記為正常的提交(v1.0)和當(dāng)前的錯(cuò)誤版本之間有大約12次提交,于是它檢出中間的一個(gè)。在這里,你可以運(yùn)行測試來檢查問題是否存在于這次提交。如果是,那么它是在這個(gè)中間提交之前的某一次引入的;如果否,那么問題是在中間提交之后引入的。假設(shè)這里是沒有錯(cuò)誤的,那么你就通過git bisect good來告訴 Git 然后繼續(xù)你的旅程:

$ git bisect good
Bisecting: 3 revisions left to test after this
[b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing

現(xiàn)在你在另外一個(gè)提交上了,在你剛剛測試通過的和一個(gè)錯(cuò)誤提交的中點(diǎn)處。你再次運(yùn)行測試然后發(fā)現(xiàn)這次提交是錯(cuò)誤的,因此你通過git bisect bad來告訴Git:

$ git bisect bad
Bisecting: 1 revisions left to test after this
[f71ce38690acf49c1f3c9bea38e09d82a5ce6014] drop exceptions table

這次提交是好的,那么 Git 就獲得了確定問題引入位置所需的所有信息。它告訴你第一個(gè)錯(cuò)誤提交的 SHA-1 值并且顯示一些提交說明以及哪些文件在那次提交里修改過,這樣你可以找出缺陷被引入的根源:

$ git bisect good
b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
Author: PJ Hyett <pjhyett@example.com>
Date:   Tue Jan 27 14:48:32 2009 -0800

    secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

當(dāng)你完成之后,你應(yīng)該運(yùn)行git bisect reset來重設(shè)你的HEAD到你開始前的地方,否則你會處于一個(gè)詭異的地方:

$ git bisect reset

這是個(gè)強(qiáng)大的工具,可以幫助你檢查上百的提交,在幾分鐘內(nèi)找出缺陷引入的位置。事實(shí)上,如果你有一個(gè)腳本會在工程正常時(shí)返回0,錯(cuò)誤時(shí)返回非0的話,你可以完全自動(dòng)地執(zhí)行git bisect。首先你需要提供已知的錯(cuò)誤和正確提交來告訴它二分查找的范圍。你可以通過bisect start命令來列出它們,先列出已知的錯(cuò)誤提交再列出已知的正確提交:

$ git bisect start HEAD v1.0
$ git bisect run test-error.sh

這樣會自動(dòng)地在每一個(gè)檢出的提交里運(yùn)行test-error.sh直到Git找出第一個(gè)破損的提交。你也可以運(yùn)行像make或者make tests或者任何你所擁有的來為你執(zhí)行自動(dòng)化的測試。

子模塊

經(jīng)常有這樣的事情,當(dāng)你在一個(gè)項(xiàng)目上工作時(shí),你需要在其中使用另外一個(gè)項(xiàng)目。也許它是一個(gè)第三方開發(fā)的庫或者是你獨(dú)立開發(fā)和并在多個(gè)父項(xiàng)目中使用的。這個(gè)場景下一個(gè)常見的問題產(chǎn)生了:你想將兩個(gè)項(xiàng)目單獨(dú)處理但是又需要在其中一個(gè)中使用另外一個(gè)。

這里有一個(gè)例子。假設(shè)你在開發(fā)一個(gè)網(wǎng)站,為之創(chuàng)建Atom源。你不想編寫一個(gè)自己的Atom生成代碼,而是決定使用一個(gè)庫。你可能不得不像CPAN install或者Ruby gem一樣包含來自共享庫的代碼,或者將代碼拷貝到你的項(xiàng)目樹中。如果采用包含庫的辦法,那么不管用什么辦法都很難去定制這個(gè)庫,部署它就更加困難了,因?yàn)槟惚仨毚_保每個(gè)客戶都擁有那個(gè)庫。把代碼包含到你自己的項(xiàng)目中帶來的問題是,當(dāng)上游被修改時(shí),任何你進(jìn)行的定制化的修改都很難歸并。

Git 通過子模塊處理這個(gè)問題。子模塊允許你將一個(gè) Git 倉庫當(dāng)作另外一個(gè)Git倉庫的子目錄。這允許你克隆另外一個(gè)倉庫到你的項(xiàng)目中并且保持你的提交相對獨(dú)立。

子模塊初步

假設(shè)你想把 Rack 庫(一個(gè) Ruby 的 web 服務(wù)器網(wǎng)關(guān)接口)加入到你的項(xiàng)目中,可能既要保持你自己的變更,又要延續(xù)上游的變更。首先你要把外部的倉庫克隆到你的子目錄中。你通過git submodule add將外部項(xiàng)目加為子模塊:

$ git submodule add git://github.com/chneukirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Co
下一篇:自定義 Git