【轉】Git詳解之六 Git工具

Git 工具

如今,你已經學習了管理或者維護 Git 倉庫,實現代碼控制所需的大多很多天常命令和工做流程。你已經完成了跟蹤和提交文件的基本任務,而且發揮了暫存區和輕量級的特性分支及合併的威力。javascript

接下來你將領略到一些 Git 能夠實現的很是強大的功能,這些功能你可能並不會在平常操做中使用,但在某些時候你也許會須要。html

 

6.1  修訂版本(Revision)選擇

Git 容許你經過幾種方法來指明特定的或者必定範圍內的提交。瞭解它們並非必需的,可是瞭解一下總沒壞處。java

單個修訂版本

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

簡短的SHA

Git 很聰明,它可以經過你提供的前幾個字符來識別你想要的那次提交,只要你提供的那部分 SHA-1 不短於四個字符,而且沒有歧義——也就是說,當前倉庫中只有一個對象以這段 SHA-1 開頭。git

例如,想要查看一次指定的提交,假設你運行 git log 命令並找到你增長了功能的那次提交:github

$ 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

假設是 1c002dd.... 。若是你想 git show 此次提交,下面的命令是等價的(假設簡短的版本沒有歧義):web

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

Git 能夠爲你的 SHA-1 值生成出簡短且惟一的縮寫。若是你傳遞 --abbrev-commit 給 git log 命令,輸出結果裏就會使用簡短且惟一的值;它默認使用七個字符來表示,不過必要時爲了不 SHA-1 的歧義,會增長字符數:shell

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

一般在一個項目中,使用八到十個字符來避免 SHA-1 歧義已經足夠了。最大的 Git 項目之一,Linux 內核,目前也只須要最長 40 個字符中的 12 個字符來保持惟一性。數據庫

關於 SHA-1 的簡短說明

許多人可能會擔憂一個問題:在隨機的偶然狀況下,在他們的倉庫裏會出現兩個具備相同 SHA-1 值的對象。那會怎麼樣呢?編程

若是你真的向倉庫裏提交了一個跟以前的某個對象具備相同 SHA-1 值的對象,Git 將會發現以前的那個對象已經存在在 Git 數據庫中,並認爲它已經被寫入了。若是何時你想再次檢出那個對象時,你會老是獲得先前的那個對象的數據。

不過,你應該瞭解到,這種狀況發生的機率是多麼微小。SHA-1 摘要長度是 20 字節,也就是 160 位。爲了保證有 50% 的機率出現一次衝突,須要 2^80 個隨機哈希的對象(計算衝突機率的公式是p = (n(n-1)/2) * (1/2^160))。2^80 是 1.2 x 10^24,也就是一億億億,那是地球上沙粒總數的 1200 倍。

如今舉例說一下怎樣才能產生一次 SHA-1 衝突。若是地球上 65 億的人類都在編程,每人每秒都在產生等價於整個 Linux 內核歷史(一百萬個 Git 對象)的代碼,並將之提交到一個巨大的 Git 倉庫裏面,那將花費 5 年的時間纔會產生足夠的對象,使其擁有 50% 的機率產生一次 SHA-1 對象衝突。這要比你編程團隊的成員同一個晚上在互不相干的意外中被狼襲擊並殺死的機率還要小。

分支引用

指明一次提交的最直接的方法要求有一個指向它的分支引用。這樣,你就能夠在任何須要一個提交對象或者 SHA-1 值的 Git 命令中使用該分支名稱了。若是你想要顯示一個分支的最後一次提交的對象,例如假設topic1 分支指向 ca82a6d,那麼下面的命令是等價的:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

若是你想知道某個分支指向哪一個特定的 SHA,或者想看任何一個例子中被簡寫的 SHA-1,你可使用一個叫作 rev-parse的 Git 探測工具。在第 9 章你能夠看到關於探測工具的更多信息;簡單來講,rev-parse 是爲了底層操做而不是平常操做設計的。不過,有時你想看 Git 如今到底處於什麼狀態時,它可能會頗有用。這裏你能夠對你的分支運執行rev-parse

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

引用日誌裏的簡稱

在你工做的同時,Git 在後臺的工做之一就是保存一份引用日誌——一份記錄最近幾個月你的 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

每次你的分支頂端由於某些緣由被修改時,Git 就會爲你將信息保存在這個臨時歷史記錄裏面。你也可使用這份數據來指明更早的分支。若是你想查看倉庫中 HEAD 在五次前的值,你可使用引用日誌的輸出中的@{n} 引用:

$ git show HEAD@{5}

你也可使用這個語法來查看必定時間前分支指向哪裏。例如,想看你的 master 分支昨天在哪,你能夠輸入

$ git show master@{yesterday}

它就會顯示昨天分支的頂端在哪。這項技術只對還在你引用日誌裏的數據有用,因此不能用來查看比幾個月前還早的提交。

想要看相似於 git log 輸出格式的引用日誌信息,你能夠運行 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'

須要注意的是,日誌引用信息只存在於本地——這是一個你在倉庫裏作過什麼的日誌。其餘人的倉庫拷貝里的引用和你的相同;而你新克隆一個倉庫的時候,引用日誌是空的,由於你在倉庫裏尚未操做。只有你克隆了一個項目至少兩個月,git show HEAD@{2.months.ago} 纔會有用——若是你是五分鐘前克隆的倉庫,將不會有結果返回。

祖先引用

另外一種指明某次提交的經常使用方法是經過它的祖先。若是你在引用最後加上一個 ^,Git 將其理解爲這次提交的父提交。 假設你的工程歷史是這樣的:

$ 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'

你也能夠在 ^ 後添加一個數字——例如,d921970^2 意思是「d921970 的第二父提交」。這種語法只在合併提交時有用,由於合併提交可能有多個父提交。第一父提交是你合併時所在分支,而第二父提交是你所合併的分支:

$ 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

另一個指明祖先提交的方法是 ~。這也是指向第一父提交,因此 HEAD~ 和 HEAD^ 是等價的。當你指定數字的時候就明顯不同了。HEAD~2 是指「第一父提交的第一父提交」,也就是「祖父提交」——它會根據你指定的次數檢索第一父提交。例如,在上面列出的歷史記錄裏面,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 指明先前引用的第二父提交(假設它是一個合併提交)。

提交範圍

如今你已經能夠指明單次的提交,讓咱們來看看怎樣指明必定範圍的提交。這在你管理分支的時候尤顯重要——若是你有不少分支,你能夠指明範圍來圈定一些問題的答案,好比:「這個分支上我有哪些工做還沒合併到主分支的?」

雙點

最經常使用的指明範圍的方法是雙點的語法。這種語法主要是讓 Git 區分出可從一個分支中得到而不能從另外一個分支中得到的提交。例如,假設你有相似於圖 6-1 的提交歷史。


圖 6-1. 範圍選擇的提交歷史實例

你想要查看你的試驗分支上哪些沒有被提交到主分支,那麼你就可使用 master..experiment 來讓 Git 顯示這些提交的日誌——這句話的意思是「全部可從experiment分支中得到而不能從master分支中得到的提交」。爲了使例子簡單明瞭,我使用了圖標中提交對象的字母來代替真實日誌的輸出,因此會顯示:

$ git log master..experiment
D
C

另外一方面,若是你想看相反的——全部在 master 而不在 experiment 中的分支——你能夠交換分支的名字。experiment..master 顯示全部可在master 得到而在 experiment 中不能的提交:

$ git log experiment..master
F
E

這在你想保持 experiment 分支最新和預覽你將合併的提交的時候特別有用。這個語法的另外一種常見用途是查看你將把什麼推送到遠程:

$ git log origin/master..HEAD

這條命令顯示任何在你當前分支上而不在遠程origin 上的提交。若是你運行 git push 而且的你的當前分支正在跟蹤origin/master,被git log origin/master..HEAD 列出的提交就是將被傳輸到服務器上的提交。 你也能夠留空語法中的一邊來讓 Git 來假定它是 HEAD。例如,輸入git log origin/master.. 將獲得和上面的例子同樣的結果—— Git 使用 HEAD 來代替不存在的一邊。

多點

雙點語法就像速記同樣有用;可是你也許會想針對兩個以上的分支來指明修訂版本,好比查看哪些提交被包含在某些分支中的一個,可是不在你當前的分支上。Git容許你在引用前使用^字符或者--not指明你不但願提交被包含其中的分支。所以下面三個命令是等同的:

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

這樣很好,由於它容許你在查詢中指定多於兩個的引用,而這是雙點語法所作不到的。例如,若是你想查找全部從refArefB包含的可是不被refC包含的提交,你能夠輸入下面中的一個

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

這創建了一個很是強大的修訂版本查詢系統,應該能夠幫助你解決分支裏包含了什麼這個問題。

三點

最後一種主要的範圍選擇語法是三點語法,這個能夠指定被兩個引用中的一個包含但又不被二者同時包含的分支。回過頭來看一下圖6-1裏所列的提交歷史的例子。 若是你想查看master或者experiment中包含的但不是二者共有的引用,你能夠運行

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

這個再次給出你普通的log輸出可是隻顯示那四次提交的信息,按照傳統的提交日期排列。

這種情形下,log命令的一個經常使用參數是--left-right,它會顯示每一個提交到底處於哪一側的分支。這使得數據更加有用。

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

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

 

6.2  交互式暫存

Git提供了不少腳原本輔助某些命令行任務。這裏,你將看到一些交互式命令,它們幫助你方便地構建只包含特定組合和部分文件的提交。在你修改了一大批文件而後決定將這些變動分佈在幾個各有側重的提交而不是單個又大又亂的提交時,這些工具很是有用。用這種方法,你能夠確保你的提交在邏輯上劃分爲相應的變動集,以便於供和你一塊兒工做的開發者審閱。若是你運行git add時加上-i或者--interactive選項,Git就進入了一個交互式的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>

你會看到這個命令以一個徹底不一樣的視圖顯示了你的暫存區——主要是你經過git status獲得的那些信息可是稍微簡潔但信息更加豐富一些。它在左側列出了你暫存的變動,在右側列出了未被暫存的變動。

在這以後是一個命令區。這裏你能夠作不少事情,包括暫存文件,撤回文件,暫存部分文件,加入未被追蹤的文件,查看暫存文件的差異。

暫存和撤回文件

若是你在What now>的提示後輸入2或者u,這個腳本會提示你那些文件你想要暫存:

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,你能夠輸入相應的編號:

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>>

每一個文件旁邊的*表示選中的文件將被暫存。若是你在update>>提示後直接敲入回車,Git會替你把全部選中的內容暫存:

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

如今你能夠看到TODO和index.html文件被暫存了同時simplegit.rb文件仍然未被暫存。若是這時你想要撤回TODO文件,就使用3或者r(表明revert,恢復)選項:

*** 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的狀態,你會看到你已經撤回了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

要查看你暫存內容的差別,你可使用6或者d(表示diff)命令。它會顯示你暫存文件的列表,你能夠選擇其中的幾個,顯示其被暫存的差別。這跟你在命令行下指定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">

經過這些基本命令,你可使用交互式增長模式更加方便地處理暫存區。

暫存補丁

只讓Git暫存文件的某些部分而忽略其餘也是有可能的。例如,你對simplegit.rb文件做了兩處修改可是隻想暫存其中一個而忽略另外一個,在Git中實現這一點很是容易。在交互式的提示符下,輸入5或者p(表示patch,補丁)。Git會詢問哪些文件你但願部分暫存;而後對於被選中文件的每一節,他會逐個顯示文件的差別區塊並詢問你是否但願暫存他們:

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

若是你想暫存各個區塊,一般你會輸入y或者n,可是暫存特定文件裏的所有區塊或者暫時跳過對一個區塊的處理一樣也頗有用。若是你暫存了文件的一個部分而保留另一個部分不被暫存,你的狀態輸出看起來會是這樣:

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的狀態很是有意思。它顯示有幾行被暫存了,有幾行沒有。你部分地暫存了這個文件。在這時,你能夠退出交互式腳本而後運行git commit來提交部分暫存的文件。

最後你也能夠不經過交互式增長的模式來實現部分文件暫存——你能夠在命令行下經過git add -p或者git add --patch來啓動一樣的腳本。

 

6.3  儲藏(Stashing)

常常有這樣的事情發生,當你正在進行項目中某一部分的工做,裏面的東西處於一個比較雜亂的狀態,而你想轉到其餘分支上進行一些工做。問題是,你不想提交進行了一半的工做,不然之後你沒法回到這個工做點。解決這個問題的辦法就是git stash命令。

「‘儲藏」「能夠獲取你工做目錄的中間狀態——也就是你修改過的被追蹤的文件和暫存的變動——並將它保存到一個未完結變動的堆棧中,隨時能夠從新應用。

儲藏你的工做

爲了演示這一功能,你能夠進入你的項目,在一些文件上進行工做,有可能還暫存其中一個變動。若是你運行 git status,你能夠看到你的中間狀態:

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

如今你想切換分支,可是你還不想提交你正在進行中的工做;因此你儲藏這些變動。爲了往堆棧推送一個新的儲藏,只要運行 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)

這時,你能夠方便地切換到其餘分支工做;你的變動都保存在棧上。要查看現有的儲藏,你可使用 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

在這個案例中,以前已經進行了兩次儲藏,因此你能夠訪問到三個不一樣的儲藏。你能夠從新應用你剛剛實施的儲藏,所採用的命令就是以前在原始的 stash 命令的幫助輸出裏提示的:git stash apply。若是你想應用更早的儲藏,你能夠經過名字指定它,像這樣:git stash apply stash@{2}。若是你不指明,Git 默認使用最近的儲藏並嘗試應用它:

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

你能夠看到 Git 從新修改了你所儲藏的那些當時還沒有提交的文件。在這個案例裏,你嘗試應用儲藏的工做目錄是乾淨的,而且屬於同一分支;可是一個乾淨的工做目錄和應用到相同的分支上並非應用儲藏的必要條件。你能夠在其中一個分支上保留一份儲藏,隨後切換到另一個分支,再從新應用這些變動。在工做目錄裏包含已修改和未提交的文件時,你也能夠應用儲藏——Git 會給出歸併衝突若是有任何變動沒法乾淨地被應用。

對文件的變動被從新應用,可是被暫存的文件沒有從新被暫存。想那樣的話,你必須在運行 git stash apply 命令時帶上一個 --index 的選項來告訴命令從新應用被暫存的變動。若是你是這麼作的,你應該已經回到你原來的位置:

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

apply 選項只嘗試應用儲藏的工做——儲藏的內容仍然在棧上。要移除它,你能夠運行 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)

你也能夠運行 git stash pop 來從新應用儲藏,同時馬上將其從堆棧中移走。

Un-applying a Stash

In some use case scenarios you might want to apply stashed changes, do some work, but then un-apply those changes that originally came form the stash. Git does not provide such astash unapply command, but it is possible to achieve the effect by simply retrieving the patch associated with a stash and applying it in reverse:

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

Again, if you don’t specify a stash, Git assumes the most recent stash:

$ git stash show -p | git apply -R

You may want to create an alias and effectively add a stash-unapply command to your git. For example:

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

從儲藏中建立分支

若是你儲藏了一些工做,暫時不去理會,而後繼續在你儲藏工做的分支上工做,你在從新應用工做時可能會碰到一些問題。若是嘗試應用的變動是針對一個你那以後修改過的文件,你會碰到一個歸併衝突而且必須去化解它。若是你想用更方便的方法來從新檢驗你儲藏的變動,你能夠運行git stash branch,這會建立一個新的分支,檢出你儲藏工做時的所處的提交,從新應用你的工做,若是成功,將會丟棄儲藏。

$ 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
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)

這是一個很棒的捷徑來恢復儲藏的工做而後在新的分支上繼續當時的工做。

 

6.4  重寫歷史

不少時候,在 Git 上工做的時候,你也許會因爲某種緣由想要修訂你的提交歷史。Git 的一個卓越之處就是它容許你在最後可能的時刻再做決定。你能夠在你即將提交暫存區時決定什麼文件納入哪一次提交,你可使用 stash 命令來決定你暫時擱置的工做,你能夠重寫已經發生的提交以使它們看起來是另一種樣子。這個包括改變提交的次序、改變說明或者修改提交中包含的文件,將提交歸併、拆分或者徹底刪除——這一切在你還沒有開始將你的工做和別人共享前都是能夠的。

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

改變最近一次提交

改變最近一次提交也許是最多見的重寫歷史的行爲。對於你的最近一次提交,你常常想作兩件基本事情:改變提交說明,或者改變你剛剛經過增長,改變,刪除而記錄的快照。

若是你只想修改最近一次提交說明,這很是簡單:

$ git commit --amend

這會把你帶入文本編輯器,裏面包含了你最近一次提交說明,供你修改。當你保存並退出編輯器,這個編輯器會寫入一個新的提交,裏面包含了那個說明,而且讓它成爲你的新的最近一次提交。

若是你完成提交後又想修改被提交的快照,增長或者修改其中的文件,可能由於你最初提交時,忘了添加一個新建的文件,這個過程基本上同樣。你經過修改文件而後對其運行git add或對一個已被記錄的文件運行git rm,隨後的git commit --amend會獲取你當前的暫存區並將它做爲新提交對應的快照。

使用這項技術的時候你必須當心,由於修正會改變提交的SHA-1值。這個很像是一次很是小的rebase——不要在你最近一次提交被推送後還去修正它。

修改多個提交說明

要修改歷史中更早的提交,你必須採用更復雜的工具。Git沒有一個修改歷史的工具,可是你可使用rebase工具來衍合一系列的提交到它們原來所在的HEAD上而不是移到新的上。依靠這個交互式的rebase工具,你就能夠停留在每一次提交後,若是你想修改或改變說明、增長文件或任何其餘事情。你能夠經過給git rebase增長-i選項來以交互方式地運行rebase。你必須經過告訴命令衍合到哪次提交,來指明你須要重寫的提交的回溯深度。

例如,你想修改最近三次的提交說明,或者其中任意一次,你必須給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
#  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.
#

很重要的一點是你得注意這些提交的順序與你一般經過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

請注意這裏的倒序。交互式的rebase給了你一個即將運行的腳本。它會從你在命令行上指明的提交開始(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 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

修改提交說明,退出編輯器。而後,運行

$ git rebase --continue

這個命令會自動應用其餘兩次提交,你就完成任務了。若是你將更多行的 pick 改成 edit ,你就能對你想修改的提交重複這些步驟。Git每次都會停下,讓你修正提交,完成後繼續運行。

重排提交

你也可使用交互式的衍合來完全重排或刪除提交。若是你想刪除」added cat-file」這個提交而且修改其餘兩次提交引入的順序,你將rebase腳本從這個

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」此次提交。

壓制(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 會同時應用那個變動和它以前的變動並將提交說明歸併。所以,若是你想將這三個提交合併爲單一提交,你能夠將腳本修改爲這樣:

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

當你保存以後,你就擁有了一個包含前三次提交的所有變動的單一提交。

拆分提交

拆分提交就是撤銷一次提交,而後屢次部分地暫存或提交直到結束。例如,假設你想將三次提交中的中間一次拆分。將」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

而後,這個腳本就將你帶入命令行,你重置那次提交,提取被重置的變動,從中建立屢次提交。當你保存並退出編輯器,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 值,因此請確保這個列表裏不包含你已經推送到共享倉庫的提交。

核彈級選項: filter-branch

若是你想用腳本的方式修改大量的提交,還有一個重寫歷史的選項能夠用——例如,全局性地修改電子郵件地址或者將一個文件從全部提交中刪除。這個命令是filter-branch,這個會大面積地修改你的歷史,因此你頗有可能不應去用它,除非你的項目還沒有公開,沒有其餘人在你準備修改的提交的基礎上工做。儘管如此,這個能夠很是有用。你會學習一些常見用法,藉此對它的能力有所認識。

從全部提交中刪除一個文件

這個常常發生。有些人不經思考使用git add .,意外地提交了一個巨大的二進制文件,你想將它從全部地方刪除。也許你不當心提交了一個包含密碼的文件,而你想讓你的項目開源。filter-branch大概會是你用來清理整個歷史的工具。要從整個歷史中刪除一個名叫password.txt的文件,你能夠在filter-branch上使用--tree-filter選項:

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

--tree-filter選項會在每次檢出項目時先執行指定的命令而後從新提交結果。在這個例子中,你會在全部快照中刪除一個名叫 password.txt 的文件,不管它是否存在。若是你想刪除全部不當心提交上去的編輯器備份文件,你能夠運行相似git filter-branch --tree-filter 'rm -f *~' HEAD的命令。

你能夠觀察到 Git 重寫目錄樹而且提交,而後將分支指針移到末尾。一個比較好的辦法是在一個測試分支上作這些而後在你肯定產物真的是你所要的以後,再 hard-reset 你的主分支。要在你全部的分支上運行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值,這個命令會修改你的歷史中的全部提交,而不只僅是包含了匹配的電子郵件地址的那些。

 

6.5  使用 Git 調試

Git 一樣提供了一些工具來幫助你調試項目中遇到的問題。因爲 Git 被設計爲可應用於幾乎任何類型的項目,這些工具是通用型,可是在遇到問題時能夠常常幫助你查找缺陷所在。

文件標註

若是你在追蹤代碼中的缺陷想知道這是何時爲何被引進來的,文件標註會是你的最佳工具。它會顯示文件中對每一行進行修改的最近一次提交。所以,若是你發現本身代碼中的一個方法存在缺陷,你能夠用git blame來標註文件,查看那個方法的每一行分別是由誰在哪一天修改的。下面這個例子使用了-L選項來限制輸出範圍在第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

請注意第一個域裏是最後一次修改該行的那次提交的 SHA-1 值。接下去的兩個域是從那次提交中抽取的值——做者姓名和日期——因此你能夠方便地獲知誰在何時修改了這一行。在這後面是行號和文件的內容。請注意^4832fe2提交的那些行,這些指的是文件最初提交的那些行。那個提交是文件第一次被加入這個項目時存在的,自那之後未被修改過。這會帶來小小的困惑,由於你已經至少看到了Git使用^來修飾一個提交的SHA值的三種不一樣的意義,但這裏確實就是這個意思。

另外一件很酷的事情是在 Git 中你不須要顯式地記錄文件的重命名。它會記錄快照而後根據現實嘗試找出隱式的重命名動做。這其中有一個頗有意思的特性就是你可讓它找出全部的代碼移動。若是你在git blame後加上-C,Git會分析你在標註的文件而後嘗試找出其中代碼片斷的原始出處,若是它是從其餘地方拷貝過來的話。最近,我在將一個名叫GITServerHandler.m的文件分解到多個文件中,其中一個是GITPackUpload.m。經過對GITPackUpload.m執行帶-C參數的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)

這真的很是有用。一般,你會把你拷貝代碼的那次提交做爲原始提交,由於這是你在這個文件中第一次接觸到那幾行。Git能夠告訴你編寫那些行的原始提交,即使是在另外一個文件裏。

二分查找

標註文件在你知道問題是哪裏引入的時候會有幫助。若是你不知道,而且自上次代碼可用的狀態已經經歷了上百次的提交,你可能就要求助於bisect命令了。bisect會在你的提交歷史中進行二分查找來儘快地肯定哪一次提交引入了錯誤。

例如你剛剛推送了一個代碼發佈版本到產品環境中,對代碼爲何會表現成那樣百思不得其解。你回到你的代碼中,還好你能夠重現那個問題,可是找不到在哪裏。你能夠對代碼執行bisect來尋找。首先你運行git bisect start啓動,而後你用git bisect bad來告訴系統當前的提交已經有問題了。而後你必須告訴bisect已知的最後一次正常狀態是哪次提交,使用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 發如今你標記爲正常的提交(v1.0)和當前的錯誤版本之間有大約12次提交,因而它檢出中間的一個。在這裏,你能夠運行測試來檢查問題是否存在於此次提交。若是是,那麼它是在這個中間提交以前的某一次引入的;若是否,那麼問題是在中間提交以後引入的。假設這裏是沒有錯誤的,那麼你就經過git bisect good來告訴 Git 而後繼續你的旅程:

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

如今你在另一個提交上了,在你剛剛測試經過的和一個錯誤提交的中點處。你再次運行測試而後發現此次提交是錯誤的,所以你經過git bisect bad來告訴Git:

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

此次提交是好的,那麼 Git 就得到了肯定問題引入位置所需的全部信息。它告訴你第一個錯誤提交的 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

當你完成以後,你應該運行git bisect reset來重設你的HEAD到你開始前的地方,不然你會處於一個詭異的地方:

$ git bisect reset

這是個強大的工具,能夠幫助你檢查上百的提交,在幾分鐘內找出缺陷引入的位置。事實上,若是你有一個腳本會在工程正常時返回0,錯誤時返回非0的話,你能夠徹底自動地執行git bisect。首先你須要提供已知的錯誤和正確提交來告訴它二分查找的範圍。你能夠經過bisect start命令來列出它們,先列出已知的錯誤提交再列出已知的正確提交:

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

這樣會自動地在每個檢出的提交裏運行test-error.sh直到Git找出第一個破損的提交。你也能夠運行像make或者make tests或者任何你所擁有的來爲你執行自動化的測試。

 

6.6  子模塊

常常有這樣的事情,當你在一個項目上工做時,你須要在其中使用另一個項目。也許它是一個第三方開發的庫或者是你獨立開發和並在多個父項目中使用的。這個場景下一個常見的問題產生了:你想將兩個項目單獨處理可是又須要在其中一箇中使用另一個。

這裏有一個例子。假設你在開發一個網站,爲之建立Atom源。你不想編寫一個本身的Atom生成代碼,而是決定使用一個庫。你可能不得不像CPAN install或者Ruby gem同樣包含來自共享庫的代碼,或者將代碼拷貝到你的項目樹中。若是採用包含庫的辦法,那麼無論用什麼辦法都很難去定製這個庫,部署它就更加困難了,由於你必須確保每一個客戶都擁有那個庫。把代碼包含到你本身的項目中帶來的問題是,當上遊被修改時,任何你進行的定製化的修改都很難歸併。

Git 經過子模塊處理這個問題。子模塊容許你將一個 Git 倉庫看成另一個Git倉庫的子目錄。這容許你克隆另一個倉庫到你的項目中而且保持你的提交相對獨立。

子模塊初步

假設你想把 Rack 庫(一個 Ruby 的 web 服務器網關接口)加入到你的項目中,可能既要保持你本身的變動,又要延續上游的變動。首先你要把外部的倉庫克隆到你的子目錄中。你經過git submodule add將外部項目加爲子模塊:

$ git submodule add git://github.com/chneukirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 422 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.

如今你就在項目裏的rack子目錄下有了一個 Rack 項目。你能夠進入那個子目錄,進行變動,加入你本身的遠程可寫倉庫來推送你的變動,從原始倉庫拉取和歸併等等。若是你在加入子模塊後馬上運行git status,你會看到下面兩項:

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      new file:   .gitmodules
#      new file:   rack
#

首先你注意到有一個.gitmodules文件。這是一個配置文件,保存了項目 URL 和你拉取到的本地子目錄

$ cat .gitmodules 
[submodule "rack"]
      path = rack
      url = git://github.com/chneukirchen/rack.git

若是你有多個子模塊,這個文件裏會有多個條目。很重要的一點是這個文件跟其餘文件同樣也是處於版本控制之下的,就像你的.gitignore文件同樣。它跟項目裏的其餘文件同樣能夠被推送和拉取。這是其餘克隆此項目的人獲知子模塊項目來源的途徑。

git status的輸出裏所列的另外一項目是 rack 。若是你運行在那上面運行git diff,會發現一些有趣的東西:

$ git diff --cached rack
diff --git a/rack b/rack
new file mode 160000
index 0000000..08d709f
--- /dev/null
+++ b/rack
@@ -0,0 +1 @@
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

儘管rack是你工做目錄裏的子目錄,但 Git 把它視做一個子模塊,當你不在那個目錄裏時並不記錄它的內容。取而代之的是,Git 將它記錄成來自那個倉庫的一個特殊的提交。當你在那個子目錄裏修改並提交時,子項目會通知那裏的 HEAD 已經發生變動並記錄你當前正在工做的那個提交;經過那樣的方法,當其餘人克隆此項目,他們能夠從新建立一致的環境。

這是關於子模塊的重要一點:你記錄他們當前確切所處的提交。你不能記錄一個子模塊的master或者其餘的符號引用。

當你提交時,會看到相似下面的:

$ git commit -m 'first commit with submodule rack'
[master 0550271] first commit with submodule rack
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack

注意 rack 條目的 160000 模式。這在Git中是一個特殊模式,基本意思是你將一個提交記錄爲一個目錄項而不是子目錄或者文件。

你能夠將rack目錄看成一個獨立的項目,保持一個指向子目錄的最新提交的指針而後反覆地更新上層項目。全部的Git命令都在兩個子目錄裏獨立工做:

$ git log -1
commit 0550271328a0038865aad6331e620cd7238601bb
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:03:56 2009 -0700

    first commit with submodule rack
$ cd rack/
$ git log -1
commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433
Author: Christian Neukirchen <chneukirchen@gmail.com>
Date:   Wed Mar 25 14:49:04 2009 +0100

    Document version change

克隆一個帶子模塊的項目

這裏你將克隆一個帶子模塊的項目。當你接收到這樣一個項目,你將獲得了包含子項目的目錄,但裏面沒有文件:

$ git clone git://github.com/schacon/myproject.git
Initialized empty Git repository in /opt/myproject/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
$ cd myproject
$ ls -l
total 8
-rw-r--r--  1 schacon  admin   3 Apr  9 09:11 README
drwxr-xr-x  2 schacon  admin  68 Apr  9 09:11 rack
$ ls rack/
$

rack目錄存在了,可是是空的。你必須運行兩個命令:git submodule init來初始化你的本地配置文件,git submodule update來從那個項目拉取全部數據並檢出你上層項目裏所列的合適的提交:

$ git submodule init
Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack'
$ git submodule update
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 173 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.
Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433'

如今你的rack子目錄就處於你先前提交的確切狀態了。若是另一個開發者變動了 rack 的代碼並提交,你拉取那個引用而後歸併之,將獲得稍有點怪異的東西:

$ git merge origin/master
Updating 0550271..85a3eee
Fast forward
 rack |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
[master*]$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#      modified:   rack
#

你歸併來的僅僅上是一個指向你的子模塊的指針;可是它並不更新你子模塊目錄裏的代碼,因此看起來你的工做目錄處於一個臨時狀態:

$ git diff
diff --git a/rack b/rack
index 6c5e70b..08d709f 160000
--- a/rack
+++ b/rack
@@ -1 +1 @@
-Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

事情就是這樣,由於你所擁有的子模塊的指針並對應於子模塊目錄的真實狀態。爲了修復這一點,你必須再次運行git submodule update

$ git submodule update
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0)
Unpacking objects: 100% (3/3), done.
From git@github.com:schacon/rack
   08d709f..6c5e70b  master     -> origin/master
Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0'

每次你從主項目中拉取一個子模塊的變動都必須這樣作。看起來很怪可是管用。

一個常見問題是當開發者對子模塊作了一個本地的變動可是並無推送到公共服務器。而後他們提交了一個指向那個非公開狀態的指針而後推送上層項目。當其餘開發者試圖運行git submodule update,那個子模塊系統會找不到所引用的提交,由於它只存在於第一個開發者的系統中。若是發生那種狀況,你會看到相似這樣的錯誤:

$ git submodule update
fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack'

你不得不去查看誰最後變動了子模塊

$ git log -1 rack
commit 85a3eee996800fcfa91e2119372dd4172bf76678
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:19:14 2009 -0700

    added a submodule reference I will never make public. hahahahaha!

而後,你給那個傢伙發電子郵件說他一通。

上層項目

有時候,開發者想按照他們的分組獲取一個大項目的子目錄的子集。若是你是從 CVS 或者 Subversion 遷移過來的話這個很常見,在那些系統中你已經定義了一個模塊或者子目錄的集合,而你想延續這種類型的工做流程。

在 Git 中實現這個的一個好辦法是你將每個子目錄都作成獨立的 Git 倉庫,而後建立一個上層項目的 Git 倉庫包含多個子模塊。這個辦法的一個優點是你能夠在上層項目中經過標籤和分支更爲明確地定義項目之間的關係。

子模塊的問題

使用子模塊並不是沒有任何缺點。首先,你在子模塊目錄中工做時必須相對當心。當你運行git submodule update,它會檢出項目的指定版本,可是不在分支內。這叫作得到一個分離的頭——這意味着 HEAD 文件直接指向一次提交,而不是一個符號引用。問題在於你一般並不想在一個分離的頭的環境下工做,由於太容易丟失變動了。若是你先執行了一次submodule update,而後在那個子模塊目錄裏不建立分支就進行提交,而後再次從上層項目裏運行git submodule update同時不進行提交,Git會毫無提示地覆蓋你的變動。技術上講你不會丟失工做,可是你將失去指向它的分支,所以會很難取到。

爲了不這個問題,當你在子模塊目錄裏工做時應使用git checkout -b work建立一個分支。當你再次在子模塊裏更新的時候,它仍然會覆蓋你的工做,可是至少你擁有一個能夠回溯的指針。

切換帶有子模塊的分支一樣也頗有技巧。若是你建立一個新的分支,增長了一個子模塊,而後切換回不帶該子模塊的分支,你仍然會擁有一個未被追蹤的子模塊的目錄

$ git checkout -b rack
Switched to a new branch "rack"
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/myproj/rack/.git/
...
Receiving objects: 100% (3184/3184), 677.42 KiB | 34 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
$ git commit -am 'added rack submodule'
[rack cc49a69] added rack submodule
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack
$ git checkout master
Switched to branch "master"
$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#      rack/

你將不得不將它移走或者刪除,這樣的話當你切換回去的時候必須從新克隆它——你可能會丟失你未推送的本地的變動或分支。

最後一個須要引發注意的是關於從子目錄切換到子模塊的。若是你已經跟蹤了你項目中的一些文件可是想把它們移到子模塊去,你必須很是當心,不然Git會生你的氣。假設你的項目中有一個子目錄裏放了 rack 的文件,而後你想將它轉換爲子模塊。若是你刪除子目錄而後運行submodule add,Git會向你大吼:

$ rm -Rf rack/
$ git submodule add git@github.com:schacon/rack.git rack
'rack' already exists in the index

你必須先將rack目錄撤回。而後你才能加入子模塊:

$ git rm -r rack
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/testsub/rack/.git/
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 88 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.

如今假設你在一個分支裏那樣作了。若是你嘗試切換回一個仍然在目錄裏保留那些文件而不是子模塊的分支時——你會獲得下面的錯誤:

$ git checkout master
error: Untracked working tree file 'rack/AUTHORS' would be overwritten by merge.

你必須先移除rack子模塊的目錄才能切換到不包含它的分支:

$ mv rack /tmp/
$ git checkout master
Switched to branch "master"
$ ls
README	rack

而後,當你切換回來,你會獲得一個空的rack目錄。你能夠運行git submodule update從新克隆,也能夠將/tmp/rack目錄從新移回空目錄。

 

6.7  子樹合併

如今你已經看到了子模塊系統的麻煩之處,讓咱們來看一下解決相同問題的另外一途徑。當 Git 歸併時,它會檢查須要歸併的內容而後選擇一個合適的歸併策略。若是你歸併的分支是兩個,Git使用一個_遞歸_策略。若是你歸併的分支超過兩個,Git採用_章魚_策略。這些策略是自動選擇的,由於遞歸策略能夠處理複雜的三路歸併狀況——好比多於一個共同祖先的——可是它只能處理兩個分支的歸併。章魚歸併能夠處理多個分支可是但必須更加當心以免衝突帶來的麻煩,所以它被選中做爲歸併兩個以上分支的默認策略。

實際上,你也能夠選擇其餘策略。其中的一個就是_子樹_歸併,你能夠用它來處理子項目問題。這裏你會看到如何換用子樹歸併的方法來實現前一節裏所作的 rack 的嵌入。

子樹歸併的思想是你擁有兩個工程,其中一個項目映射到另一個項目的子目錄中,反過來也同樣。當你指定一個子樹歸併,Git能夠聰明地探知其中一個是另一個的子樹從而實現正確的歸併——這至關神奇。

首先你將 Rack 應用加入到項目中。你將 Rack 項目看成你項目中的一個遠程引用,而後將它檢出到它自身的分支:

$ git remote add rack_remote git@github.com:schacon/rack.git
$ git fetch rack_remote
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From git@github.com:schacon/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

如今在你的rack_branch分支中就有了Rack項目的根目錄,而你本身的項目在master分支中。若是你先檢出其中一個而後另一個,你會看到它們有不一樣的項目根目錄:

$ ls
AUTHORS	       KNOWN-ISSUES   Rakefile      contrib	       lib
COPYING	       README         bin           example	       test
$ git checkout master
Switched to branch "master"
$ ls
README

要將 Rack 項目看成子目錄拉取到你的master項目中。你能夠在 Git 中用git read-tree來實現。你會在第9章學到更多與read-tree和它的朋友相關的東西,當前你會知道它讀取一個分支的根目錄樹到當前的暫存區和工做目錄。你只要切換回你的master分支,而後拉取rack分支到你主項目的master分支的rack子目錄:

$ git read-tree --prefix=rack/ -u rack_branch

當你提交的時候,看起來就像你在那個子目錄下擁有Rack的文件——就像你從一個tarball裏拷貝的同樣。有意思的是你能夠比較容易地歸併其中一個分支的變動到另一個。所以,若是 Rack 項目更新了,你能夠經過切換到那個分支並執行拉取來得到上游的變動:

$ git checkout rack_branch
$ git pull

而後,你能夠將那些變動歸併回你的 master 分支。你可使用git merge -s subtree,它會工做的很好;可是 Git 同時會把歷史歸併到一塊兒,這可能不是你想要的。爲了拉取變動並預置提交說明,須要在-s subtree策略選項的同時使用--squash--no-commit選項。

$ git checkout master
$ git merge --squash -s subtree --no-commit rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

全部 Rack 項目的變動都被歸併能夠進行本地提交。你也能夠作相反的事情——在你主分支的rack目錄裏進行變動而後歸併回rack_branch分支,而後將它們提交給維護者或者推送到上游。

爲了獲得rack子目錄和你rack_branch分支的區別——以決定你是否須要歸併它們——你不能使用通常的diff命令。而是對你想比較的分支運行git diff-tree

$ git diff-tree -p rack_branch

或者,爲了比較你的rack子目錄和服務器上你拉取時的master分支,你能夠運行

$ git diff-tree -p rack_remote/master


6.8  總結

你已經看到了不少高級的工具,容許你更加精確地操控你的提交和暫存區。當你碰到問題時,你應該能夠很容易找出是哪一個分支何時由誰引入了它們。若是你想在項目中使用子項目,你也已經學會了一些方法來知足這些需求。到此,你應該可以完成平常裏你須要用命令行在 Git 下作的大部分事情,而且感到比較順手。

相關文章
相關標籤/搜索