Git 基礎
Git 指令,以後絕大多數時間裡用到的也就是這幾個指令。讀完本章,你就能初始化一個新的代碼倉庫,做一些适當配置;開始或停止跟蹤某些檔案;暫存或送出某些更 新。我們還會展示如何讓 Git 忽略某些檔案,或是名稱符合特定模式的檔案;如何既快且容易地撤消犯下的小錯誤;如何浏覽項目的更新曆史,檢視某兩次更新之間的差異;以及如何從遠端倉庫 拉資料下來或者推資料上去。
2.1 取得項目的 Git 倉庫
有兩種取得 Git 項目倉庫的方法。第一種是在現存的目錄下,通過導入所有檔案來建立新的 Git 倉庫。第二種是從已有的 Git 倉庫克隆出一個新的鏡像倉庫來。
在工作目錄中初始化新倉庫
要對現有的某個項目開始用 Git 管理,隻需到此項目所在的目錄,執行:
1
<code>$ git init</code>
初始化後,在目前目錄下會出現一個名為 .git 的目錄,所有 Git 需要的資料和資源都存放在這個目錄中。不過目前,僅僅是按照既有的結構架構初始化好了裡邊所有的檔案和目錄,但我們還沒有開始跟蹤管理項目中的任何一個檔案。(在第九章我們會詳細說明剛才建立的<code>.git</code> 目錄中究竟有哪些檔案,以及都起些什麼作用。)
如果目前目錄下有幾個檔案想要納入版本控制,需要先用 git add 指令告訴 Git 開始對這些檔案進行跟蹤,然後送出:
2
3
<code>$ git add *.c</code>
<code>$ git add README</code>
<code>$ git commit -m</code><code>'initial project version'</code>
稍後我們再逐一解釋每條指令的意思。不過現在,你已經得到了一個實際維護着若幹檔案的 Git 倉庫。
從現有倉庫克隆
如果想對某個開源項目出一份力,可以先把該項目的 Git 倉庫複制一份出來,這就需要用到 git clone 指令。如果你熟悉其他的 VCS 比如 Subversion,你可能已經注意到這裡使用的是 clone 而不是 checkout。這是個非常重要的差别,Git 收取的是項目曆史的所有資料(每一個檔案的每一個版本),伺服器上有的資料克隆之後本地也都有了。實際上,即便伺服器的磁盤發生故障,用任何一個克隆出來 的用戶端都可以重建伺服器上的倉庫,回到當初克隆時的狀态(雖然可能會丢失某些伺服器端的挂鈎設定,但所有版本的資料仍舊還在,有關細節請參考第四章)。github
克隆倉庫的指令格式為 <code>git clone [url]</code>。比如,要克隆 Ruby 語言的 Git 代碼倉庫 Grit,可以用下面的指令:
<code>$ git clone git:</code><code>//github</code><code>.com</code><code>/schacon/grit</code><code>.git</code>
這會在目前目錄下建立一個名為“grit”的目錄,其中包含一個 <code>.git</code> 的目錄,用于儲存下載下傳下來的所有版本記錄,然後從中取出最新版本的檔案拷貝。如果進入這個建立的<code>grit</code> 目錄,你會看到項目中的所有檔案已經在裡邊了,準備好後續的開發和使用。如果希望在克隆的時候,自己定義要建立的項目目錄名稱,可以在上面的指令末尾指定新的名字:
<code>$ git clone git:</code><code>//github</code><code>.com</code><code>/schacon/grit</code><code>.git mygrit</code>
唯一的差别就是,現在建立的目錄成了 mygrit,其他的都和上邊的一樣。
Git 支援許多資料傳輸協定。之前的例子使用的是 <code>git://</code> 協定,不過你也可以用 <code>http(s)://</code> 或者<code>user@server:/path.git</code>表示的
SSH 傳輸協定。我們會在第四章詳細介紹所有這些協定在伺服器端該如何配置使用,以及各種方式之間的利弊。
2.2 記錄每次更新到倉庫
現在我們手上已經有了一個真實項目的 Git 倉庫,并從這個倉庫中取出了所有檔案的工作拷貝。接下來,對這些檔案作些修改,在完成了一個階段的目标之後,送出本次更新到倉庫。
請記住,工作目錄下面的所有檔案都不外乎這兩種狀态:已跟蹤或未跟蹤。已跟蹤的檔案是指本來就被納入版本控制管理的檔案,在上次快照中有它們的記 錄,工作一段時間後,它們的狀态可能是未更新,已修改或者已放入暫存區。而所有其他檔案都屬于未跟蹤檔案。它們既沒有上次更新時的快照,也不在目前的暫存 區域。初次克隆某個倉庫時,工作目錄中的所有檔案都屬于已跟蹤檔案,且狀态為未修改。
在編輯過某些檔案之後,Git 将這些檔案标為已修改。我們逐漸把這些修改過的檔案放到暫存區域,直到最後一次性送出所有這些暫存起來的檔案,如此重複。是以使用 Git 時的檔案狀态變化周期如圖 2-1 所示。

圖 2-1. 檔案的狀态變化周期
檢查目前檔案狀态
要确定哪些檔案目前處于什麼狀态,可以用 git status 指令。如果在克隆倉庫之後立即執行此指令,會看到類似這樣的輸出:
<code>$ git status</code><code># On branch master nothing to commit (working directory clean)</code>
這說明你現在的工作目錄相當幹淨。換句話說,目前沒有任何跟蹤着的檔案,也沒有任何檔案在上次送出後更改過。此外,上面的資訊還表明,目前目錄下沒 有出現任何處于未跟蹤的新檔案,否則 Git 會在這裡列出來。最後,該指令還顯示了目前所在的分支是 master,這是預設的分支名稱,實際是可以修改的,現在先不用考慮。下一章我們就會詳細讨論分支和引用。
現在讓我們用 vim 編輯一個新檔案 README,儲存退出後運作 <code>git status</code> 會看到該檔案出現在未跟蹤檔案清單中:
4
5
6
7
8
9
10
<code>$ vim README</code>
<code>$ git status</code>
<code># On branch master</code>
<code># Untracked files:</code>
<code># (use "git add</code>
<code> </code><code>..." to include</code><code>in</code><code>what will be committed)</code>
<code>#</code>
<code># README</code>
<code>nothing added to commit but untracked files present (use</code><code>"git add"</code> <code>to track)</code>
就是在“Untracked files”這行下面。Git 不會自動将之納入跟蹤範圍,除非你明明白白地告訴它“我需要跟蹤該檔案”,因而不用擔心把臨時檔案什麼的也歸入版本管理。不過現在的例子中,我們确實想要跟蹤管理 README 這個檔案。
跟蹤新檔案
使用指令 <code>git add</code> 開始跟蹤一個新檔案。是以,要跟蹤 README 檔案,運作:
此時再運作 <code>git status</code> 指令,會看到 README 檔案已被跟蹤,并處于暫存狀态:
<code># Changes to be committed:</code>
<code># (use "git reset HEAD</code>
<code> </code><code>..." to unstage)</code>
<code># new file: README</code>
隻要在 “Changes to be committed” 這行下面的,就說明是已暫存狀态。如果此時送出,那麼該檔案此時此刻的版本将被留存在曆史記錄中。你可能會想起之前我們使用<code>git init</code> 後就運作了 <code>git add</code> 指令,開始跟蹤目前目錄下的檔案。在 <code>git add</code> 後面可以指明要跟蹤的檔案或目錄路徑。如果是目錄的話,就說明要遞歸跟蹤該目錄下的所有檔案。(譯注:其實<code>git add</code> 的潛台詞就是把目标檔案快照放入暫存區域,也就是 add file into staged area,同時未曾跟蹤過的檔案标記為需要跟蹤。這樣就好了解後續 add 操作的實際意義了。)
暫存已修改檔案
現在我們修改下之前已跟蹤過的檔案 <code>benchmarks.rb</code>,然後再次運作 <code>status</code> 指令,會看到這樣的狀态報告:
11
12
13
14
15
16
<code># Changed but not updated:</code>
<code> </code><code>..." to update what will be committed)</code>
<code># modified: benchmarks.rb</code>
檔案 benchmarks.rb 出現在 “Changed but not updated” 這行下面,說明已跟蹤檔案的内容發生了變化,但還沒有放到暫存區。要暫存這次更新,需要運作<code>git add</code> 指令(這是個多功能指令,根據目标檔案的狀态不同,此指令的效果也不同:可以用它開始跟蹤新檔案,或者把已跟蹤的檔案放到暫存區,還能用于合并時把有沖突的檔案标記為已解決狀态等)。現在讓我們運作<code>git add</code> 将 benchmarks.rb 放到暫存區,然後再看看 <code>git status</code> 的輸出:
<code>$ git add benchmarks.rb</code>
現在兩個檔案都已暫存,下次送出時就會一并記錄到倉庫。假設此時,你想要在 benchmarks.rb 裡再加條注釋,重新編輯存盤後,準備好送出。不過且慢,再運作<code>git status</code> 看看:
17
18
<code>$ vim benchmarks.rb</code>
怎麼回事?benchmarks.rb 檔案出現了兩次!一次算未暫存,一次算已暫存,這怎麼可能呢?好吧,實際上 Git 隻不過暫存了你運作 git add 指令時的版本,如果現在送出,那麼送出的是添加注釋前的版本,而非目前工作目錄中的版本。是以,運作了<code>git add</code> 之後又作了修訂的檔案,需要重新運作 <code>git add</code> 把最新版本重新暫存起來:
忽略某些檔案
一般我們總會有些檔案無需納入 Git 的管理,也不希望它們總出現在未跟蹤檔案清單。通常都是些自動生成的檔案,比如日志檔案,或者編譯過程中建立的臨時檔案等。我們可以建立一個名為 .gitignore 的檔案,列出要忽略的檔案模式。來看一個實際的例子:
<code>$</code><code>cat</code><code>.gitignore *.[oa] *~</code>
第一行告訴 Git 忽略所有以 .o 或 .a 結尾的檔案。一般這類對象檔案和存檔檔案都是編譯過程中出現的,我們用不着跟蹤它們的版本。第二行告訴 Git 忽略所有以波浪符(<code>~</code>)結尾的檔案,許多文本編輯軟體(比如 Emacs)都用這樣的檔案名儲存副本。此外,你可能還需要忽略
log,tmp 或者 pid 目錄,以及自動生成的文檔等等。要養成一開始就設定好 .gitignore 檔案的習慣,以免将來誤送出這類無用的檔案。
檔案 .gitignore 的格式規範如下:
● 所有空行或者以注釋符号 # 開頭的行都會被 Git 忽略。
● 可以使用标準的 glob 模式比對。 * 比對模式最後跟反斜杠(<code>/</code>)說明要忽略的是目錄。 * 要忽略指定模式以外的檔案或目錄,可以在模式前加上驚歎号(<code>!</code>)取反。
所謂的 glob 模式是指 shell 所使用的簡化了的正規表達式。星号(<code>*</code>)比對零個或多個任意字元;<code>[abc]</code> 比對任何一個列在方括号中的字元(這個例子要麼比對一個
a,要麼比對一個 b,要麼比對一個 c);問号(<code>?</code>)隻比對一個任意字元;如果在方括号中使用短劃線分隔兩個字元,表示所有在這兩個字元範圍内的都可以比對(比如<code>[0-9]</code> 表示比對所有
0 到 9 的數字)。
我們再看一個 .gitignore 檔案的例子:
<code># 此為注釋 – 将被 Git 忽略</code>
<code>*.a </code><code># 忽略所有 .a 結尾的檔案</code>
<code>!lib.a </code><code># 但 lib.a 除外</code>
<code>/TODO</code> <code># 僅僅忽略項目根目錄下的 TODO 檔案,不包括 subdir/TODO</code>
<code>build/ </code><code># 忽略 build/ 目錄下的所有檔案</code>
<code>doc/*.txt</code><code># 會忽略 doc/notes.txt 但不包括 doc/server/arch.txt</code>
檢視已暫存和未暫存的更新
實際上 <code>git status</code> 的顯示比較簡單,僅僅是列出了修改過的檔案,如果要檢視具體修改了什麼地方,可以用 <code>git diff</code> 指令。稍後我們會詳細介紹<code>git diff</code>,不過現在,它已經能回答我們的兩個問題了:目前做的哪些更新還沒有暫存?有哪些更新已經暫存起來準備好了下次送出? <code>git diff</code> 會使用檔案更新檔的格式顯示具體添加和删除的行。
假如再次修改 README 檔案後暫存,然後編輯 benchmarks.rb 檔案後先别暫存,運作 <code>status</code> 指令,會看到:
要檢視尚未暫存的檔案更新了哪些部分,不加參數直接輸入 <code>git diff</code>:
<code>$ git</code><code>diff</code>
<code>diff</code><code>--git a</code><code>/benchmarks</code><code>.rb b</code><code>/benchmarks</code><code>.rb</code>
<code>index 3cb747f..da65585 100644</code>
<code>--- a</code><code>/benchmarks</code><code>.rb</code>
<code>+++ b</code><code>/benchmarks</code><code>.rb</code>
<code>@@ -36,6 +36,10 @@ def main</code>
<code> </code><code>@commit.parents[0].parents[0].parents[0]</code>
<code> </code><code>end</code>
<code>+ run_code(x,</code><code>'commits 1'</code><code>)</code><code>do</code>
<code>+ git.commits.size</code>
<code>+ end</code>
<code>+</code>
<code> </code><code>run_code(x,</code><code>'commits 2'</code><code>)</code><code>do</code>
<code> </code><code>log = git.commits(</code><code>'master'</code><code>, 15)</code>
<code> </code><code>log.size</code>
此指令比較的是工作目錄中目前檔案和暫存區域快照之間的差異,也就是修改之後還沒有暫存起來的變化内容。
若要看已經暫存起來的檔案和上次送出時的快照之間的差異,可以用 <code>git diff --cached</code> 指令。(Git 1.6.1 及更高版本還允許使用<code>git diff --staged</code>,效果是相同的,但更好記些。)來看看實際的效果:
<code>$ git</code><code>diff</code><code>--cached</code>
<code>diff</code><code>--git a</code><code>/README</code><code>b</code><code>/README</code>
<code>new</code><code>file</code><code>mode 100644</code>
<code>index 0000000..03902a1</code>
<code>---</code><code>/dev/null</code>
<code>+++ b</code><code>/README2</code>
<code>@@ -0,0 +1,5 @@</code>
<code>+grit</code>
<code>+ by Tom Preston-Werner, Chris Wanstrath</code>
<code>+ http:</code><code>//github</code><code>.com</code><code>/mojombo/grit</code>
<code>+Grit is a Ruby library</code><code>for</code><code>extracting information from a Git repository</code>
請注意,單單 <code>git diff</code> 不過是顯示還沒有暫存起來的改動,而不是這次工作和上次送出之間的差異。是以有時候你一下子暫存了所有更新過的檔案後,運作<code>git diff</code> 後卻什麼也沒有,就是這個原因。
像之前說的,暫存 benchmarks.rb 後再編輯,運作 <code>git status</code> 會看到暫存前後的兩個版本:
<code>$</code><code>echo</code><code>'# test line'</code> <code>>> benchmarks.rb</code>
現在運作 <code>git diff</code> 看暫存前後的變化:
<code>index e445e28..86b2f7c 100644</code>
<code>@@ -127,3 +127,4 @@ end</code>
<code> </code><code>main()</code>
<code> </code><code>##pp Grit::GitRuby.cache_client.stats</code>
<code>+</code><code># test line</code>
然後用 <code>git diff --cached</code> 檢視已經暫存起來的變化:
<code>index 3cb747f..e445e28 100644</code>
<code> </code><code>@commit.parents[0].parents[0].parents[0]</code>
<code> </code><code>end</code>
<code> </code><code>run_code(x,</code><code>'commits 2'</code><code>)</code><code>do</code>
<code> </code><code>log = git.commits(</code><code>'master'</code><code>, 15)</code>
<code> </code><code>log.size</code>
送出更新
現在的暫存區域已經準備妥當可以送出了。在此之前,請一定要确認還有什麼修改過的或建立的檔案還沒有 <code>git add</code> 過,否則送出的時候不會記錄這些還沒暫存起來的變化。是以,每次準備送出前,先用<code>git status</code> 看下,是不是都已暫存起來了,然後再運作送出指令 <code>git commit</code>:
<code>$ git commit</code>
這種方式會啟動文本編輯器以便輸入本次送出的說明。(預設會啟用 shell 的環境變量 <code>$EDITOR</code> 所指定的軟體,一般都是 vim 或 emacs。當然也可以按照第一章介紹的方式,使用<code>git config --global core.editor</code> 指令設定你喜歡的編輯軟體。)
編輯器會顯示類似下面的文本資訊(本例選用 Vim 的屏顯方式展示):
<code># Please enter the commit message for your changes. Lines starting</code>
<code># with '#' will be ignored, and an empty message aborts the commit.</code>
<code># new file: README</code>
<code># modified: benchmarks.rb</code>
<code>~</code>
<code>".git/COMMIT_EDITMSG"</code><code>10L, 283C</code>
可以看到,預設的送出消息包含最後一次運作 <code>git status</code> 的輸出,放在注釋行裡,另外開頭還有一空行,供你輸入送出說明。你完全可以去掉這些注釋行,不過留着也沒關系,多少能幫你回想起這次更新的内容有哪些。(如果覺得這還不夠,可以用<code>-v</code> 選項将修改差異的每一行都包含到注釋中來。)退出編輯器時,Git
會丢掉注釋行,将說明内容和本次更新送出到倉庫。
另外也可以用 -m 參數後跟送出說明的方式,在一行指令中送出更新:
<code>$ git commit -m</code><code>"Story 182: Fix benchmarks for speed"</code>
<code>[master]: created 463dc4f:</code><code>"Fix benchmarks for speed"</code>
<code> </code><code>2 files changed, 3 insertions(+), 0 deletions(-)</code>
<code> </code><code>create mode 100644 README</code>
好,現在你已經建立了第一個送出!可以看到,送出後它會告訴你,目前是在哪個分支(master)送出的,本次送出的完整 SHA-1 校驗和是什麼(<code>463dc4f</code>),以及在本次送出中,有多少檔案修訂過,多少行添改和删改過。
記住,送出時記錄的是放在暫存區域的快照,任何還未暫存的仍然保持已修改狀态,可以在下次送出時納入版本管理。每一次運作送出操作,都是對你項目作一次快照,以後可以回到這個狀态,或者進行比較。
跳過使用暫存區域
盡管使用暫存區域的方式可以精心準備要送出的細節,但有時候這麼做略顯繁瑣。Git 提供了一個跳過使用暫存區域的方式,隻要在送出的時候,給 <code>git commit</code> 加上<code>-a</code> 選項,Git
就會自動把所有已經跟蹤過的檔案暫存起來一并送出,進而跳過 <code>git add</code> 步驟:
<code>$ git commit -a -m</code><code>'added new benchmarks'</code>
<code>[master 83e38c7] added new benchmarks</code>
<code> </code><code>1 files changed, 5 insertions(+), 0 deletions(-)</code>
看到了嗎?送出之前不再需要 <code>git add</code> 檔案 benchmarks.rb 了。
移除檔案
要從 Git 中移除某個檔案,就必須要從已跟蹤檔案清單中移除(确切地說,是從暫存區域移除),然後送出。可以用 <code>git rm</code> 指令完成此項工作,并連帶從工作目錄中删除指定的檔案,這樣以後就不會出現在未跟蹤檔案清單中了。
如果隻是簡單地從工作目錄中手工删除檔案,運作 <code>git status</code> 時就會在 “Changed but not updated” 部分(也就是_未暫存_清單)看到:
<code>$</code><code>rm</code><code>grit.gemspec</code>
<code># (use "git add/rm</code>
<code> </code><code>..." to update what will be committed)</code>
<code># deleted: grit.gemspec</code>
然後再運作 <code>git rm</code> 記錄此次移除檔案的操作:
<code>$ git</code><code>rm</code><code>grit.gemspec</code>
<code>rm</code><code>'grit.gemspec'</code>
最後送出的時候,該檔案就不再納入版本管理了。如果删除之前修改過并且已經放到暫存區域的話,則必須要用強制删除選項 <code>-f</code>(譯注:即 force 的首字母),以防誤删除檔案後丢失修改的内容。
另外一種情況是,我們想把檔案從 Git 倉庫中删除(亦即從暫存區域移除),但仍然希望保留在目前工作目錄中。換句話說,僅是從跟蹤清單中删除。比如一些大型日志檔案或者一堆<code>.a</code> 編譯檔案,不小心納入倉庫後,要移除跟蹤但不删除檔案,以便稍後在 <code>.gitignore</code> 檔案中補上,用 <code>--cached</code> 選項即可:
<code>$ git</code><code>rm</code><code>--cached readme.txt</code>
後面可以列出檔案或者目錄的名字,也可以使用 glob 模式。比方說:
<code>$ git</code><code>rm</code><code>log/\*.log</code>
注意到星号 <code>*</code> 之前的反斜杠 <code>\</code>,因為
Git 有它自己的檔案模式擴充比對方式,是以我們不用 shell 來幫忙展開(譯注:實際上不加反斜杠也可以運作,隻不過按照 shell 擴充的話,僅僅删除指定目錄下的檔案而不會遞歸比對。上面的例子本來就指定了目錄,是以效果等同,但下面的例子就會用遞歸方式比對,是以必須加反斜 杠。)。此指令删除所有<code>log/</code> 目錄下擴充名為 <code>.log</code> 的檔案。類似的比如:
<code>$ git</code><code>rm</code><code>\*~</code>
會遞歸删除目前目錄及其子目錄中所有 <code>~</code> 結尾的檔案。
移動檔案
不像其他的 VCS 系統,Git 并不跟蹤檔案移動操作。如果在 Git 中重命名了某個檔案,倉庫中存儲的中繼資料并不會展現出這是一次改名操作。不過 Git 非常聰明,它會推斷出究竟發生了什麼,至于具體是如何做到的,我們稍後再談。
既然如此,當你看到 Git 的 <code>mv</code> 指令時一定會困惑不已。要在 Git 中對檔案改名,可以這麼做:
<code>$ git</code><code>mv</code><code>file_from file_to</code>
它會恰如預期般正常工作。實際上,即便此時檢視狀态資訊,也會明白無誤地看到關于重命名操作的說明:
<code>$ git</code><code>mv</code><code>README.txt README</code>
<code># Your branch is ahead of 'origin/master' by 1 commit.</code>
<code># renamed: README.txt -> README</code>
其實,運作 <code>git mv</code> 就相當于運作了下面三條指令:
<code>$</code><code>mv</code><code>README.txt README $ git</code><code>rm</code><code>README.txt $ git add README</code>
如此分開操作,Git 也會意識到這是一次改名,是以不管何種方式都一樣。當然,直接用 <code>git mv</code> 輕便得多,不過有時候用其他工具批處理改名的話,要記得在送出前删除老的檔案名,再添加新的檔案名。
2.3 檢視送出曆史
在送出了若幹更新之後,又或者克隆了某個項目,想回顧下送出曆史,可以使用 <code>git log</code> 指令檢視。
接下來的例子會用我專門用于示範的 simplegit 項目,運作下面的指令擷取該項目源代碼:
<code>git clone git:</code><code>//github</code><code>.com</code><code>/schacon/simplegit-progit</code><code>.git</code>
然後在此項目中運作 <code>git log</code>,應該會看到下面的輸出:
19
20
21
<code>$ git log</code>
<code>commit ca82a6dff817ec66f44342007202690a93763949</code>
<code>Author: Scott Chacon</code>
<code>Date: Mon Mar 17 21:52:11 2008 -0700</code>
<code> </code><code>changed the version number</code>
<code>commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7</code>
<code>Date: Sat Mar 15 16:40:33 2008 -0700</code>
<code> </code><code>removed unnecessary</code><code>test</code><code>code</code>
<code>commit a11bef06a3f659402fe7563abf99ad00de2209e6</code>
<code>Date: Sat Mar 15 10:31:28 2008 -0700</code>
<code> </code><code>first commit</code>
預設不用任何參數的話,<code>git log</code> 會按送出時間列出所有的更新,最近的更新排在最上面。看到了嗎,每次更新都有一個 SHA-1 校驗和、作者的名字和電子郵件位址、送出時間,最後縮進一個段落顯示送出說明。
<code>git log</code> 有許多選項可以幫助你搜尋感興趣的送出,接下來我們介紹些最常用的。
我們常用 <code>-p</code> 選項展開顯示每次送出的内容差異,用 <code>-2</code> 則僅顯示最近的兩次更新:
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<code>$ git log -p -2</code>
<code>diff</code><code>--git a</code><code>/Rakefile</code><code>b</code><code>/Rakefile</code>
<code>index a874b73..8f94139 100644</code>
<code>--- a</code><code>/Rakefile</code>
<code>+++ b</code><code>/Rakefile</code>
<code>@@ -5,7 +5,7 @@ require</code><code>'rake/gempackagetask'</code>
<code> </code><code>spec = Gem::Specification.new</code><code>do</code><code>|s|</code>
<code>- s.version = </code><code>"0.1.0"</code>
<code>+ s.version = </code><code>"0.1.1"</code>
<code> </code><code>s.author = </code><code>"Scott Chacon"</code>
<code>diff</code><code>--git a</code><code>/lib/simplegit</code><code>.rb b</code><code>/lib/simplegit</code><code>.rb</code>
<code>index a0a60ae..47c6340 100644</code>
<code>--- a</code><code>/lib/simplegit</code><code>.rb</code>
<code>+++ b</code><code>/lib/simplegit</code><code>.rb</code>
<code>@@ -18,8 +18,3 @@ class SimpleGit</code>
<code> </code><code>end</code>
<code> </code><code>end</code>
<code>-</code>
<code>-</code><code>if</code><code>$0 == __FILE__</code>
<code>- git = SimpleGit.new</code>
<code>- puts git.show</code>
<code>-end</code>
<code>\ No newline at end of</code><code>file</code>
在做代碼審查,或者要快速浏覽其他協作者送出的更新都作了哪些改動時,就可以用這個選項。此外,還有許多摘要選項可以用,比如 <code>--stat</code>,僅顯示簡要的增改行數統計:
<code>$ git log --stat</code>
<code> </code><code>Rakefile | 2 +-</code>
<code> </code><code>1 files changed, 1 insertions(+), 1 deletions(-)</code>
<code> </code><code>lib</code><code>/simplegit</code><code>.rb | 5 -----</code>
<code> </code><code>1 files changed, 0 insertions(+), 5 deletions(-)</code>
<code> </code><code>README | 6 ++++++</code>
<code> </code><code>Rakefile | 23 +++++++++++++++++++++++</code>
<code> </code><code>lib</code><code>/simplegit</code><code>.rb | 25 +++++++++++++++++++++++++</code>
<code> </code><code>3 files changed, 54 insertions(+), 0 deletions(-)</code>
每個送出都列出了修改過的檔案,以及其中添加和移除的行數,并在最後列出所有增減行數小計。還有個常用的 <code>--pretty</code> 選項,可以指定使用完全不同于預設格式的方式展示送出曆史。比如用<code>oneline</code> 将每個送出放在一行顯示,這在送出數很大時非常有用。另外還有 <code>short</code>,<code>full</code> 和<code>fuller</code> 可以用,展示的資訊或多或少有些不同,請自己動手實踐一下看看效果如何。
<code>$ git log --pretty=oneline</code>
<code>ca82a6dff817ec66f44342007202690a93763949 changed the version number</code>
<code>085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary</code><code>test</code><code>code</code>
<code>a11bef06a3f659402fe7563abf99ad00de2209e6 first commit</code>
但最有意思的是 <code>format</code>,可以定制要顯示的記錄格式,這樣的輸出便于後期程式設計提取分析,像這樣:
<code>$ git log --pretty=</code><code>format</code><code>:</code><code>"%h - %an, %ar : %s"</code>
<code>ca82a6d - Scott Chacon, 11 months ago : changed the version number</code>
<code>085bb3b - Scott Chacon, 11 months ago : removed unnecessary</code><code>test</code><code>code</code>
<code>a11bef0 - Scott Chacon, 11 months ago : first commit</code>
表 2-1 列出了常用的格式占位符寫法及其代表的意義。
<code>選項 說明</code>
<code>%H 送出對象(commit)的完整哈希字串</code>
<code>%h 送出對象的簡短哈希字串</code>
<code>%T 樹對象(tree)的完整哈希字串</code>
<code>%t 樹對象的簡短哈希字串</code>
<code>%P 父對象(parent)的完整哈希字串</code>
<code>%p 父對象的簡短哈希字串</code>
<code>%an 作者(author)的名字</code>
<code>%ae 作者的電子郵件位址</code>
<code>%ad 作者修訂日期(可以用 -</code><code>date</code><code>= 選項定制格式)</code>
<code>%ar 作者修訂日期,按多久以前的方式顯示</code>
<code>%cn 送出者(committer)的名字</code>
<code>%ce 送出者的電子郵件位址</code>
<code>%</code><code>cd</code><code>送出日期</code>
<code>%cr 送出日期,按多久以前的方式顯示</code>
<code>%s 送出說明</code>
你一定奇怪_作者(author)_和_送出者(committer)_之間究竟有何差别,其實作者指的是實際作出修改的人,送出者指的是最後将此 工作成果送出到倉庫的人。是以,當你為某個項目釋出更新檔,然後某個核心成員将你的更新檔并入項目時,你就是作者,而那個核心成員就是送出者。我們會在第五章 再詳細介紹兩者之間的細微差别。
用 oneline 或 format 時結合 <code>--graph</code> 選項,可以看到開頭多出一些 ASCII 字元串表示的簡單圖形,形象地展示了每個送出所在的分支及其分化衍合情況。在我們之前提到的 Grit 項目倉庫中可以看到:
<code>$ git log --pretty=</code><code>format</code><code>:</code><code>"%h %s"</code> <code>--graph</code>
<code>* 2d3acf9 ignore errors from SIGCHLD on</code><code>trap</code>
<code>* 5e3ee11 Merge branch</code><code>'master'</code><code>of git:</code><code>//github</code><code>.com</code><code>/dustin/grit</code>
<code>|\</code>
<code>| * 420eac9 Added a method</code><code>for</code><code>getting the current branch.</code>
<code>* | 30e367c timeout code and tests</code>
<code>* | 5a09431 add timeout protection to grit</code>
<code>* | e1193f8 support</code><code>for</code><code>heads with slashes</code><code>in</code><code>them</code>
<code>|/</code>
<code>* d6016bc require</code><code>time</code><code>for</code>
<code>xmlschema</code>
<code>* 11d191e Merge branch</code><code>'defunkt'</code><code>into</code><code>local</code>
以上隻是簡單介紹了一些 <code>git log</code> 指令支援的選項。表 2-2 還列出了一些其他常用的選項及其釋義。
<code>選項 說明</code>
<code>-p 按更新檔格式顯示每個更新之間的差異。</code>
<code>--stat 顯示每次更新的檔案修改統計資訊。</code>
<code>--shortstat 隻顯示 --stat 中最後的行數修改添加移除統計。</code>
<code>--name-only 僅在送出資訊後顯示已修改的檔案清單。</code>
<code>--name-status 顯示新增、修改、删除的檔案清單。</code>
<code>--abbrev-commit 僅顯示 SHA-1 的前幾個字元,而非所有的 40 個字元。</code>
<code>--relative-</code><code>date</code><code>使用較短的相對時間顯示(比如,“2 weeks ago”)。</code>
<code>--graph 顯示 ASCII 圖形表示的分支合并曆史。</code>
<code>--pretty 使用其他格式顯示曆史送出資訊。可用的選項包括 oneline,short,full,fuller 和</code><code>format</code><code>(後跟指定格式)。</code>
限制輸出長度
除了定制輸出格式的選項之外,<code>git log</code> 還有許多非常實用的限制輸出長度的選項,也就是隻輸出部分送出資訊。之前我們已經看到過 <code>-2</code> 了,它隻顯示最近的兩條送出,實際上,這是 <code>- </code>選項的寫法,其中的 <code>n</code> 可以是任何自然數,表示僅顯示最近的若幹條送出。不過實踐中我們是不太用這個選項的,Git
在輸出所有送出時會自動調用分頁程式(less),要看更早的更新隻需翻到下頁即可。
另外還有按照時間作限制的選項,比如 <code>--since</code> 和 <code>--until</code>。下面的指令列出所有最近兩周内的送出:
<code>$ git log --since=2.weeks</code>
你可以給出各種時間格式,比如說具體的某一天(“2008-01-15”),或者是多久以前(“2 years 1 day 3 minutes ago”)。
還可以給出若幹搜尋條件,列出符合的送出。用 <code>--author</code> 選項顯示指定作者的送出,用 <code>--grep</code> 選項搜尋送出說明中的關鍵字。(請注意,如果要得到同時滿足這兩個選項搜尋條件的送出,就必須用<code>--all-match</code> 選項。)
如果隻關心某些檔案或者目錄的曆史送出,可以在 <code>git log</code> 選項的最後指定它們的路徑。因為是放在最後位置上的選項,是以用兩個短劃線(<code>--</code>)隔開之前的選項和後面限定的路徑名。
表 2-3 還列出了其他常用的類似選項。
<code>-(n) 僅顯示最近的 n 條送出</code>
<code>--since, --after 僅顯示指定時間之後的送出。</code>
<code>--</code><code>until</code><code>, --before 僅顯示指定時間之前的送出。</code>
<code>--author 僅顯示指定作者相關的送出。</code>
<code>--committer 僅顯示指定送出者相關的送出。</code>
來看一個實際的例子,如果要檢視 Git 倉庫中,2008 年 10 月期間,Junio Hamano 送出的但未合并的測試腳本(位于項目的 t/ 目錄下的檔案),可以用下面的查詢指令:
<code>$ git log --pretty=</code><code>"%h - %s"</code> <code>--author=gitster --since=</code><code>"2008-10-01"</code><code>\</code>
<code> </code><code>--before=</code><code>"2008-11-01"</code><code>--no-merges -- t/</code>
<code>5610e3b - Fix testcase failure when extended attribute</code>
<code>acd3b9e - Enhance hold_lock_file_for_{update,append}()</code>
<code>f563754 - demonstrate breakage of detached checkout wi</code>
<code>d1a43f2 - reset --hard</code><code>/read-tree</code><code>--reset -u: remove un</code>
<code>51a94af - Fix</code><code>"checkout --track -b newbranch"</code> <code>on detac</code>
<code>b0ad11e - pull: allow "git pull origin $something:$cur</code>
Git 項目有 20,000 多條送出,但我們給出搜尋選項後,僅列出了其中滿足條件的 6 條。
有時候圖形化工具更容易展示曆史送出的變化,随 Git 一同釋出的 gitk 就是這樣一種工具。它是用 Tcl/Tk 寫成的,基本上相當于 <code>git log</code> 指令的可視化版本,凡是<code>git log</code> 可以用的選項也都能用在 gitk 上。在項目工作目錄中輸入 gitk 指令後,就會啟動圖 2-2 所示的界面。
圖 2-2. gitk 的圖形界面
上半個視窗顯示的是曆次送出的分支祖先圖譜,下半個視窗顯示目前點選的送出對應的具體差異。
2.4 撤消操作
任何時候,你都有可能需要撤消剛才所做的某些操作。接下來,我們會介紹一些基本的撤消操作相關的指令。請注意,有些操作并不總是可以撤消的,是以請務必謹慎小心,一旦失誤,就有可能丢失部分工作成果。
修改最後一次送出
有時候我們送出完了才發現漏掉了幾個檔案沒有加,或者送出資訊寫錯了。想要撤消剛才的送出操作,可以使用 <code>--amend</code>選項重新送出:
<code>$ git commit --amend</code>
此指令将使用目前的暫存區域快照送出。如果剛才送出完沒有作任何改動,直接運作此指令的話,相當于有機會重新編輯送出說明,但将要送出的檔案快照和之前的一樣。
啟動文本編輯器後,會看到上次送出時的說明,編輯它确認沒問題後儲存退出,就會使用新的送出說明覆寫剛才失誤的送出。
如果剛才送出時忘了暫存某些修改,可以先補上暫存操作,然後再運作 <code>--amend</code> 送出:
<code>$ git commit -m</code><code>'initial commit'</code>
<code>$ git add forgotten_file</code>
上面的三條指令最終隻是産生一個送出,第二個送出指令修正了第一個的送出内容。
取消已經暫存的檔案
接下來的兩個小節将示範如何取消暫存區域中的檔案,以及如何取消工作目錄中已修改的檔案。不用擔心,檢視檔案狀态的時候就提示了該如何撤消,是以不需要死記硬背。來看下面的例子,有兩個修改過的檔案,我們想要分開送出,但不小心用<code>git add .</code> 全加到了暫存區域。該如何撤消暫存其中的一個檔案呢?其實,<code>git status</code> 的指令輸出已經告訴了我們該怎麼做:
<code>$ git add .</code>
<code># modified: README.txt</code>
就在 “Changes to be committed” 下面,括号中有提示,可以使用 <code>git reset HEAD ... </code>的方式取消暫存。好吧,我們來試試取消暫存 benchmarks.rb 檔案:
<code>$ git reset HEAD benchmarks.rb</code>
<code>benchmarks.rb: locally modified</code>
<code># (use "git checkout --</code>
<code> </code><code>..." to discard changes</code><code>in</code><code>working directory)</code>
這條指令看起來有些古怪,先别管,能用就行。現在 benchmarks.rb 檔案又回到了之前已修改未暫存的狀态。
取消對檔案的修改
如果覺得剛才對 benchmarks.rb 的修改完全沒有必要,該如何取消修改,回到之前的狀态(也就是修改之前的版本)呢?<code>git status</code> 同樣提示了具體的撤消方法,接着上面的例子,現在未暫存區域看起來像這樣:
<code> </code><code>..." to discard changes</code><code>in</code><code>working directory)</code>
在第二個括号中,我們看到了抛棄檔案修改的指令(至少在 Git 1.6.1 以及更高版本中會這樣提示,如果你還在用老版本,我們強烈建議你更新,以擷取最佳的使用者體驗),讓我們試試看:
<code>$ git checkout -- benchmarks.rb</code>
可以看到,該檔案已經恢複到修改前的版本。你可能已經意識到了,這條指令有些危險,所有對檔案的修改都沒有了,因為我們剛剛把之前版本的檔案複制過 來重寫了此檔案。是以在用這條指令前,請務必确定真的不再需要保留剛才的修改。如果隻是想回退版本,同時保留剛才的修改以便将來繼續工作,可以用下章介紹 的 stashing 和分支來處理,應該會更好些。
記住,任何已經送出到 Git 的都可以被恢複。即便在已經删除的分支中的送出,或者用 <code>--amend</code> 重新改寫的送出,都可以被恢複(關于資料恢複的内容見第九章)。是以,你可能失去的資料,僅限于沒有送出過的,對 Git 來說它們就像從未存在過一樣。
2.5 遠端倉庫的使用
要參與任何一個 Git 項目的協作,必須要了解該如何管理遠端倉庫。遠端倉庫是指托管在網絡上的項目倉庫,可能會有好多個,其中有些你隻能讀,另外有些可以寫。同他人協作開發某 個項目時,需要管理這些遠端倉庫,以便推送或拉取資料,分享各自的工作進展。管理遠端倉庫的工作,包括添加遠端庫,移除廢棄的遠端庫,管理各式遠端庫分 支,定義是否跟蹤這些分支,等等。本節我們将詳細讨論遠端庫的管理和使用。
檢視目前的遠端庫
要檢視目前配置有哪些遠端倉庫,可以用 <code>git remote</code> 指令,它會列出每個遠端庫的簡短名字。在克隆完某個項目後,至少可以看到一個名為 origin 的遠端庫,Git 預設使用這個名字來辨別你所克隆的原始倉庫:
<code>$ git clone git:</code><code>//github</code><code>.com</code><code>/schacon/ticgit</code><code>.git</code>
<code>Initialized empty Git repository</code><code>in</code><code>/private/tmp/ticgit/</code><code>.git/</code>
<code>remote: Counting objects: 595,</code><code>done</code><code>.</code>
<code>remote: Compressing objects: 100% (269</code><code>/269</code><code>),</code><code>done</code><code>.</code>
<code>remote: Total 595 (delta 255), reused 589 (delta 253)</code>
<code>Receiving objects: 100% (595</code><code>/595</code><code>), 73.31 KiB | 1 KiB</code><code>/s</code><code>,</code><code>done</code><code>.</code>
<code>Resolving deltas: 100% (255</code><code>/255</code><code>),</code><code>done</code><code>.</code>
<code>$</code><code>cd</code><code>ticgit</code>
<code>$ git remote</code>
<code>origin</code>
也可以加上 <code>-v</code> 選項(譯注:此為 <code>--verbose</code> 的簡寫,取首字母),顯示對應的克隆位址:
<code>$ git remote -</code><code>v</code>
<code>origin git:</code><code>//github</code><code>.com</code><code>/schacon/ticgit</code><code>.git</code>
如果有多個遠端倉庫,此指令将全部列出。比如在我的 Grit 項目中,可以看到:
<code>$</code><code>cd</code><code>grit</code>
<code>bakkdoor git:</code><code>//github</code><code>.com</code><code>/bakkdoor/grit</code><code>.git</code>
<code>cho45 git:</code><code>//github</code><code>.com</code><code>/cho45/grit</code><code>.git</code>
<code>defunkt git:</code><code>//github</code><code>.com</code><code>/defunkt/grit</code><code>.git</code>
<code>koke git:</code><code>//github</code><code>.com</code><code>/koke/grit</code><code>.git</code>
<code>origin git@ github.com:mojombo</code><code>/grit</code><code>.git</code>
這樣一來,我就可以非常輕松地從這些使用者的倉庫中,拉取他們的送出到本地。請注意,上面列出的位址隻有 origin 用的是 SSH URL 連結,是以也隻有這個倉庫我能推送資料上去(我們會在第四章解釋原因)。
添加遠端倉庫
要添加一個新的遠端倉庫,可以指定一個簡單的名字,以便将來引用,運作 <code>git remote add [shortname] [url]</code>:
<code>$ git remote add pb git:</code><code>//github</code><code>.com</code><code>/paulboone/ticgit</code><code>.git</code>
<code>pb git:</code><code>//github</code><code>.com</code><code>/paulboone/ticgit</code><code>.git</code>
現在可以用字串 pb 指代對應的倉庫位址了。比如說,要抓取所有 Paul 有的,但本地倉庫沒有的資訊,可以運作 <code>git fetch pb</code>:
<code>$ git fetch pb</code>
<code>remote: Counting objects: 58,</code><code>done</code><code>.</code>
<code>remote: Compressing objects: 100% (41</code><code>/41</code><code>),</code><code>done</code><code>.</code>
<code>remote: Total 44 (delta 24), reused 1 (delta 0)</code>
<code>Unpacking objects: 100% (44</code><code>/44</code><code>),</code><code>done</code><code>.</code>
<code>From git:</code><code>//github</code><code>.com</code><code>/paulboone/ticgit</code>
<code> </code><code>* [new branch] master -> pb</code><code>/master</code>
<code> </code><code>* [new branch] ticgit -> pb</code><code>/ticgit</code>
現在,Paul 的主幹分支(master)已經完全可以在本地通路了,對應的名字是 <code>pb/master</code>,你可以将它合并到自己的某個分支,或者切換到這個分支,看看有些什麼有趣的更新。
從遠端倉庫抓取資料
正如之前所看到的,可以用下面的指令從遠端倉庫抓取資料到本地:
<code>$ git fetch [remote-name]</code>
此指令會到遠端倉庫中拉取所有你本地倉庫中還沒有的資料。運作完成後,你就可以在本地通路該遠端倉庫中的所有分支,将其中某個分支合并到本地,或者隻是取出某個分支,一探究竟。(我們會在第三章詳細讨論關于分支的概念和操作。)
如果是克隆了一個倉庫,此指令會自動将遠端倉庫歸于 origin 名下。是以,<code>git fetch origin</code> 會抓取從你上次克隆以來别人上傳到此遠端倉庫中的所有更新(或是上次 fetch 以來别人送出的更新)。有一點很重要,需要記住,fetch 指令隻是将遠端的資料拉到本地倉庫,并不自動合并到目前工作分支,隻有當你确實準備好了,才能手工合并。
如果設定了某個分支用于跟蹤某個遠端倉庫的分支(參見下節及第三章的内容),可以使用 <code>git pull</code> 指令自動抓取資料下來,然後将遠端分支自動合并到本地倉庫中目前分支。在日常工作中我們經常這麼用,既快且好。實際上,預設情況下<code>git clone</code> 指令本質上就是自動建立了本地的 master 分支用于跟蹤遠端倉庫中的 master 分支(假設遠端倉庫确實有 master 分支)。是以一般我們運作<code>git pull</code>,目的都是要從原始克隆的遠端倉庫中抓取資料後,合并到工作目錄中的目前分支。
推送資料到遠端倉庫
項目進行到一個階段,要同别人分享目前的成果,可以将本地倉庫中的資料推送到遠端倉庫。實作這個任務的指令很簡單: <code>git push [remote-name] [branch-name]</code>。如果要把本地的 master 分支推送到<code>origin</code> 伺服器上(再次說明下,克隆操作會自動使用預設的
master 和 origin 名字),可以運作下面的指令:
<code>$ git push origin master</code>
隻有在所克隆的伺服器上有寫權限,或者同一時刻沒有其他人在推資料,這條指令才會如期完成任務。如果在你推資料前,已經有其他人推送了若幹更新,那 你的推送操作就會被駁回。你必須先把他們的更新抓取到本地,合并到自己的項目中,然後才可以再次推送。有關推送資料到遠端倉庫的詳細内容見第三章。
檢視遠端倉庫資訊
我們可以通過指令 <code>git remote show [remote-name]</code> 檢視某個遠端倉庫的詳細資訊,比如要看所克隆的 <code>origin</code> 倉庫,可以運作:
<code>$ git remote show origin</code>
<code>* remote origin</code>
<code> </code><code>URL: git:</code><code>//github</code><code>.com</code><code>/schacon/ticgit</code><code>.git</code>
<code> </code><code>Remote branch merged with</code><code>'git pull'</code> <code>while</code> <code>on branch master</code>
<code> </code><code>master</code>
<code> </code><code>Tracked remote branches</code>
<code> </code><code>ticgit</code>
除了對應的克隆位址外,它還給出了許多額外的資訊。它友善地告訴你如果是在 master 分支,就可以用 <code>git pull</code> 指令抓取資料合并到本地。另外還列出了所有處于跟蹤狀态中的遠端分支。
上面的例子非常簡單,而随着使用 Git 的深入,<code>git remote show</code> 給出的資訊可能會像這樣:
<code> </code><code>URL: git@ github.com:defunkt</code><code>/github</code><code>.git</code>
<code> </code><code>Remote branch merged with</code><code>'git pull'</code> <code>while</code> <code>on branch issues</code>
<code> </code><code>issues</code>
<code> </code><code>New remote branches (next fetch will store</code><code>in</code><code>remotes</code><code>/origin</code><code>)</code>
<code> </code><code>caching</code>
<code> </code><code>Stale tracking branches (use</code><code>'git remote prune'</code><code>)</code>
<code> </code><code>libwalker</code>
<code> </code><code>walker2</code>
<code> </code><code>acl</code>
<code> </code><code>apiv2</code>
<code> </code><code>dashboard2</code>
<code> </code><code>postgres</code>
<code> </code><code>Local branch pushed with</code><code>'git push'</code>
<code> </code><code>master:master</code>
它告訴我們,運作 <code>git push</code> 時預設推送的分支是什麼(譯注:最後兩行)。它還顯示了有哪些遠端分支還沒有同步到本地(譯注:第六行的<code>caching</code> 分支),哪些已同步到本地的遠端分支在遠端伺服器上已被删除(譯注:<code>Stale tracking branches</code> 下面的兩個分支),以及運作<code>git pull</code> 時将自動合并哪些分支(譯注:前四行中列出的 <code>issues</code> 和 <code>master</code> 分支)。
遠端倉庫的删除和重命名
在新版 Git 中可以用 <code>git remote rename</code> 指令修改某個遠端倉庫在本地的簡短名稱,比如想把 <code>pb</code> 改成<code>paul</code>,可以這麼運作:
<code>$ git remote rename pb paul</code>
<code>paul</code>
注意,對遠端倉庫的重命名,也會使對應的分支名稱發生變化,原來的 <code>pb/master</code> 分支現在成了 <code>paul/master</code>。
碰到遠端倉庫伺服器遷移,或者原來的克隆鏡像不再使用,又或者某個參與者不再貢獻代碼,那麼需要移除對應的遠端倉庫,可以運作 <code>git remote rm</code> 指令:
<code>$ git remote</code><code>rm</code><code>paul</code>
2.6 打标簽
同大多數 VCS 一樣,Git 也可以對某一時間點上的版本打上标簽。人們在釋出某個軟體版本(比如 v1.0 等等)的時候,經常這麼做。本節我們一起來學習如何列出所有可用的标簽,如何建立标簽,以及各種不同類型标簽之間的差别。
列顯已有的标簽
列出現有标簽的指令非常簡單,直接運作 <code>git tag</code> 即可:
<code>$ git tag v0.1 v1.3</code>
顯示的标簽按字母順序排列,是以标簽的先後并不表示重要程度的輕重。
我們可以用特定的搜尋模式列出符合條件的标簽。在 Git 自身項目倉庫中,有着超過 240 個标簽,如果你隻對 1.4.2 系列的版本感興趣,可以運作下面的指令:
<code>$ git tag -l</code><code>'v1.4.2.*'</code><code>v1.4.2.1 v1.4.2.2 v1.4.2.3 v1.4.2.4</code>
建立标簽
Git 使用的标簽有兩種類型:輕量級的(lightweight)和含附注的(annotated)。輕量級标簽就像是個不會變化的分支,實際上它就是個指向特 定送出對象的引用。而含附注标簽,實際上是存儲在倉庫中的一個獨立對象,它有自身的校驗和資訊,包含着标簽的名字,電子郵件位址和日期,以及标簽說明,标 簽本身也允許使用 GNU Privacy Guard (GPG) 來簽署或驗證。一般我們都建議使用含附注型的标簽,以便保留相關資訊;當然,如果隻是臨時性加注标簽,或者不需要旁注額外資訊,用輕量級标簽也沒問題。
含附注的标簽
建立一個含附注類型的标簽非常簡單,用 <code>-a</code> (譯注:取 <code>annotated</code> 的首字母)指定标簽名字即可:
<code>$ git tag -a v1.4 -m</code><code>'my version 1.4'</code>
<code>$ git tag</code>
<code>v0.1</code>
<code>v1.3</code>
<code>v1.4</code>
而 <code>-m</code> 選項則指定了對應的标簽說明,Git 會将此說明一同儲存在标簽對象中。如果沒有給出該選項,Git 會啟動文本編輯軟體供你輸入标簽說明。
可以使用 <code>git show</code> 指令檢視相應标簽的版本資訊,并連同顯示打标簽時的送出對象。
<code>$ git show v1.4</code>
<code>tag v1.4</code>
<code>Tagger: Scott Chacon</code>
<code>Date: Mon Feb 9 14:45:11 2009 -0800</code>
<code>my version 1.4</code>
<code>commit 15027957951b64cf874c3557a0f3547bd83b3ff6</code>
<code>Merge: 4a447f7... a6b4c97...</code>
<code>Date: Sun Feb 8 19:02:46 2009 -0800</code>
<code> </code><code>Merge branch</code><code>'experiment'</code>
我們可以看到在送出對象資訊上面,列出了此标簽的送出者和送出時間,以及相應的标簽說明。
簽署标簽
如果你有自己的私鑰,還可以用 GPG 來簽署标簽,隻需要把之前的 <code>-a</code> 改為 <code>-s</code> (譯注:
取 <code>signed</code> 的首字母)即可:
<code>$ git tag -s v1.5 -m</code><code>'my signed 1.5 tag'</code>
<code>You need a passphrase to unlock the secret key</code><code>for</code>
<code>user: "Scott Chacon</code>
<code> </code><code>"</code>
<code>1024-bit DSA key, ID F721C45A, created 2009-02-09</code>
現在再運作 <code>git show</code> 會看到對應的 GPG 簽名也附在其内:
<code>$ git show v1.5</code>
<code>tag v1.5</code>
<code>Date: Mon Feb 9 15:22:20 2009 -0800</code>
<code>my signed 1.5 tag</code>
<code>-----BEGIN PGP SIGNATURE-----</code>
<code>Version: GnuPG v1.4.8 (Darwin)</code>
<code>iEYEABECAAYFAkmQurIACgkQON3DxfchxFr5cACeIMN+ZxLKggJQf0QYiQBwgySN</code>
<code>Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/</code>
<code>=WryJ</code>
<code>-----END PGP SIGNATURE-----</code>
稍後我們再學習如何驗證已經簽署的标簽。
輕量級标簽
輕量級标簽實際上就是一個儲存着對應送出對象的校驗和資訊的檔案。要建立這樣的标簽,一個 <code>-a</code>,<code>-s</code> 或 <code>-m</code> 選項都不用,直接給出标簽名字即可:
<code>$ git tag v1.4-lw</code>
<code>v1.4-lw</code>
<code>v1.5</code>
現在運作 <code>git show</code> 檢視此标簽資訊,就隻有相應的送出對象摘要:
<code>$ git show v1.4-lw</code>
驗證标簽
可以使用 <code>git tag -v [tag-name]</code> (譯注:取 <code>verify</code> 的首字母)的方式驗證已經簽署的标簽。此指令會調用
GPG 來驗證簽名,是以你需要有簽署者的公鑰,存放在 keyring 中,才能驗證:
<code>$ git tag -</code><code>v</code><code>v1.4.2.1</code>
<code>object 883653babd8ee7ea23e6a5c392bb739348b1eb61</code>
<code>type</code><code>commit</code>
<code>tag v1.4.2.1</code>
<code>tagger Junio C Hamano</code>
<code> </code><code>1158138501 -0700</code>
<code>GIT 1.4.2.1</code>
<code>Minor fixes since 1.4.2, including git-</code><code>mv</code><code>and git-http with alternates.</code>
<code>gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A</code>
<code>gpg: Good signature from "Junio C Hamano</code>
<code> </code><code>"</code>
<code>gpg: aka</code><code>"[jpeg image of size 1513]"</code>
<code>Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A</code>
若是沒有簽署者的公鑰,會報告類似下面這樣的錯誤:
<code>gpg: Can't check signature: public key not found</code>
<code>error: could not verify the tag</code><code>'v1.4.2.1'</code>
後期加注标簽
你甚至可以在後期對早先的某次送出加注标簽。比如在下面展示的送出曆史中:
<code>15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch</code><code>'experiment'</code>
<code>a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support</code>
<code>0d52aaab4479697da7686c15f77a3d64d9165190 one</code><code>more</code><code>thing</code>
<code>6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch</code><code>'experiment'</code>
<code>0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit</code><code>function</code>
<code>4682c3261057305bdd616e23b64b0857d832627b added a todo</code><code>file</code>
<code>166ae0c4d3f420721acbb115cc33848dfcc2121a started write support</code>
<code>9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile</code>
<code>964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo</code>
<code>8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme</code>
我們忘了在送出 “updated rakefile” 後為此項目打上版本号 v1.2,沒關系,現在也能做。隻要在打标簽的時候跟上對應送出對象的校驗和(或前幾位字元)即可:
<code>$ git tag -a v1.2 9fceb02</code>
可以看到我們已經補上了标簽:
<code>v1.2</code>
<code>$ git show v1.2</code>
<code>tag v1.2</code>
<code>Date: Mon Feb 9 15:32:16 2009 -0800</code>
<code>version 1.2</code>
<code>commit 9fceb02d0ae598e95dc970b74767f19372d61af8</code>
<code>Author: Magnus Chacon</code>
<code>Date: Sun Apr 27 20:43:35 2008 -0700</code>
<code> </code><code>updated rakefile</code>
<code>...</code>
分享标簽
預設情況下,<code>git push</code> 并不會把标簽傳送到遠端伺服器上,隻有通過顯式指令才能分享标簽到遠端倉庫。其指令格式如同推送分支,運作<code>git push origin [tagname]</code> 即可:
<code>$ git push origin v1.5</code>
<code>Counting objects: 50,</code><code>done</code><code>.</code>
<code>Compressing objects: 100% (38</code><code>/38</code><code>),</code><code>done</code><code>.</code>
<code>Writing objects: 100% (44</code><code>/44</code><code>), 4.56 KiB,</code><code>done</code><code>.</code>
<code>Total 44 (delta 18), reused 8 (delta 1)</code>
<code>To git@ github.com:schacon</code><code>/simplegit</code><code>.git</code>
<code>* [new tag] v1.5 -> v1.5</code>
如果要一次推送所有本地新增的标簽上去,可以使用 <code>--tags</code> 選項:
<code>$ git push origin --tags</code>
<code> </code><code>* [new tag] v0.1 -> v0.1</code>
<code> </code><code>* [new tag] v1.2 -> v1.2</code>
<code> </code><code>* [new tag] v1.4 -> v1.4</code>
<code> </code><code>* [new tag] v1.4-lw -> v1.4-lw</code>
<code> </code><code>* [new tag] v1.5 -> v1.5</code>
現在,其他人克隆共享倉庫或拉取資料同步後,也會看到這些标簽。
2.7 技巧和竅門
在結束本章之前,我還想和大家分享一些 Git 使用的技巧和竅門。很多使用 Git 的開發者可能根本就沒用過這些技巧,我們也不是說在讀過本書後非得用這些技巧不可,但至少應該有所了解吧。說實話,有了這些小竅門,我們的工作可以變得更簡單,更輕松,更高效。
自動完成
如果你用的是 Bash shell,可以試試看 Git 提供的自動完成腳本。下載下傳 Git 的源代碼,進入 <code>contrib/completion</code> 目錄,會看到一個<code>git-completion.bash</code> 檔案。将此檔案複制到你自己的使用者主目錄中(譯注:按照下面的示例,還應改名加上點:<code>cp git-completion.bash ~/.git-completion.bash</code>),并把下面一行内容添加到你的<code>.bashrc</code> 檔案中:
<code>source</code><code>~/.git-completion.</code><code>bash</code>
也可以為系統上所有使用者都設定預設使用此腳本。Mac 上将此腳本複制到 <code>/opt/local/etc/bash_completion.d</code> 目錄中,Linux 上則複制到<code>/etc/bash_completion.d/</code> 目錄中。這兩處目錄中的腳本,都會在
Bash 啟動時自動加載。
如果在 Windows 上安裝了 msysGit,預設使用的 Git Bash 就已經配好了這個自動完成腳本,可以直接使用。
在輸入 Git 指令的時候可以敲兩次跳格鍵(Tab),就會看到列出所有比對的可用指令建議:
<code>$ git co commit config</code>
此例中,鍵入 git co 然後連按兩次 Tab 鍵,會看到兩個相關的建議(指令) commit 和 config。繼而輸入 <code>m </code>會自動完成<code>git commit</code> 指令的輸入。
指令的選項也可以用這種方式自動完成,其實這種情況更實用些。比如運作 <code>git log</code> 的時候忘了相關選項的名字,可以輸入開頭的幾個字母,然後敲 Tab 鍵看看有哪些比對的:
<code>$ git log --s</code>
<code> </code><code>--shortstat --since= --src-prefix= --stat --summary</code>
這個技巧不錯吧,可以節省很多輸入和查閱文檔的時間。
Git 指令别名
Git 并不會推斷你輸入的幾個字元将會是哪條指令,不過如果想偷懶,少敲幾個指令的字元,可以用 <code>git config</code> 為指令設定别名。來看看下面的例子:
<code>$ git config --global</code><code>alias</code><code>.co checkout</code>
<code>$ git config --global</code><code>alias</code><code>.br branch</code>
<code>$ git config --global</code><code>alias</code><code>.ci commit</code>
<code>$ git config --global</code><code>alias</code><code>.st status</code>
現在,如果要輸入 <code>git commit</code> 隻需鍵入 <code>git ci</code> 即可。而随着 Git 使用的深入,會有很多經常要用到的指令,遇到這種情況,不妨建個别名提高效率。
使用這種技術還可以創造出新的指令,比方說取消暫存檔案時的輸入比較繁瑣,可以自己設定一下:
<code>$ git config --global</code><code>alias</code><code>.unstage</code><code>'reset HEAD --'</code>
這樣一來,下面的兩條指令完全等同:
<code>$ git unstage fileA</code>
<code>$ git reset HEAD fileA</code>
顯然,使用别名的方式看起來更清楚。另外,我們還經常設定 <code>last</code> 指令:
<code>$ git config --global</code><code>alias</code><code>.last</code><code>'log -1 HEAD'</code>
然後要看最後一次的送出資訊,就變得簡單多了:
<code>$ git last</code>
<code>commit 66938dae3329c7aebe598c2246a8e6af90d04646</code>
<code>Author: Josh Goebel</code>
<code>Date: Tue Aug 26 19:48:51 2008 +0800</code>
<code> </code><code>test</code><code>for</code>
<code>current</code><code>head</code>
<code> </code><code>Signed-off-by: Scott Chacon</code>
可以看出,實際上 Git 隻是簡單地在指令中替換了你設定的别名。不過有時候我們希望運作某個外部指令,而非 Git 的附屬工具,這個好辦,隻需要在指令前加上 <code>!</code> 就行。如果你自己寫了些處理 Git 倉庫資訊的腳本的話,就可以用這種技術包裝起來。作為示範,我們可以設定用 <code>git visual</code> 啟動<code>gitk</code>:
<code>$ git config --global</code><code>alias</code><code>.visual</code><code>"!gitk"</code>
2.8 小結
到目前為止,你已經學會了最基本的 Git 操作:建立和克隆倉庫,做出更新,暫存并送出這些更新,以及檢視所有曆史更新記錄。接下來,我們将學習 Git 的必殺技特性:分支模型。