# Git 工具 # 現在,你已經學習了管理或者維護 Git 倉庫,實現代碼控制所需的大多數日常命令和工作流程。你已經完成了跟蹤和提交檔案的基本任務,並且發揮了暫存區(staging area)和羽量級的特性分支及合併的威力。 接下來你將領略到一些 Git 可以實現的非常強大的功能,這些功能你可能並不會在日常操作中使用,但在某些時候你也許會需要。 ## 選擇修訂版本 ## Git 允許你通過幾種方法來指明特定的或者一定範圍內的提交。瞭解它們並不是必需的,但是瞭解一下總沒壞處。 ### 單個修訂版本 ### 顯然你可以使用給出的 SHA-1 值來指明一次提交,不過也有更加人性化的方法來做同樣的事。本節概述了指明單個提交的諸多方法。 ### 簡短的 SHA ### Git 很聰明,它能夠通過你提供的前幾個字元來識別你想要的那次提交,只要你提供的那部分 SHA-1 不短於四個字元,並且沒有歧義——也就是說,當前倉庫中只有一個物件以這段 SHA-1 開頭。 例如,想要查看一次指定的提交,假設你執行 `git log` 命令並找到你增加了功能的那次提交: $ git log commit 734713bc047d87bf7eac9674765ae793478c50d3 Author: Scott Chacon 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 Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff 假設是 `1c002dd`.... 。如果你想 `git show` 這次提交,下面命令的作用是相同的(假設簡短的版本沒有歧義): $ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d Git 可以為你的 SHA-1 值生成出簡短且唯一的縮寫。如果你傳遞 `--abbrev-commit` 給 `git log` 命令,輸出結果裡就會使用簡短且唯一的值;它預設使用七個字元來表示,不過必要時為了避免 SHA-1 的歧義,會增加字元數: $ 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 物件衝突。這要比你程式設計團隊的成員同一個晚上在互不相干的意外中被狼襲擊並殺死的機率還要小。 ### 分支引用 (Branch References) ### 指明一次提交的最直接的方法是有一個指向它的分支引用。這樣,你就可以在任何需要一個提交物件或者 SHA-1 值的 Git 命令中使用該分支名稱了。如果你想要顯示一個分支的最後一次提交的物件,例如假設 topic1 分支指向 ca82a6d,那麼下面的命令是相等的: $ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1 如果你想知道某個分支指向哪個特定的 SHA,或者想看任何一個例子中被簡寫的 SHA-1,你可以使用一個叫做 `rev-parse` 的 Git plumbing 工具。在第 9 章你可以看到關於 plumbing 工具的更多信息;簡單來說,`rev-parse` 是為了底層操作而不是日常操作設計的。不過,有時你想看 Git 現在到底處於什麼狀態時,它可能會很有用。現在,你可以對你的分支執行 `rev-parse`: $ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949 ### 引用日誌(RefLog)裡的簡稱 ### 在你工作的同時,Git 在後臺的工作之一就是保存一份引用日誌(reflog)——一份記錄最近幾個月你的 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 ) Reflog message: commit: fixed refs handling, added gc auto, updated Author: Scott Chacon Date: Fri Jan 2 18:32:33 2009 -0800 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Reflog: master@{1} (Scott Chacon ) Reflog message: merge phedders/rdocs: Merge made by recursive. Author: Scott Chacon Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' 需要注意的是,日誌引用資訊只存在於本地——這是一個你在倉庫裡做過什麼的日誌。這些引用不會和其他人的倉庫拷貝裡的相同;當你新 clone 一個倉庫的時候,引用日誌是空的,因為你在倉庫裡還沒有操作。只有你克隆了一個專案至少兩個月,`git show HEAD@{2.months.ago}` 才會有用——如果你是五分鐘前克隆的倉庫,將不會有結果回傳。 ### 祖先引用 (Ancestry References) ### 另一種指明某次提交的常用方法是通過它的祖先。如果你在引用最後加上一個 `^`,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 Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' 你也可以在 `^` 後添加一個數字——例如,`d921970^2` 意思是「d921970 的第二父提交」。這種語法只在合併提交時有用,因為合併提交可能有多個父提交。第一父提交是你合併時所在分支,而第二父提交是你所合併進來的分支: $ git show d921970^ commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff $ git show d921970^2 commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548 Author: Paul Hedderly 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 Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem 也可以寫成 HEAD^^^,同樣是第一父提交的第一父提交的第一父提交: $ git show HEAD^^^ commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem 你也可以混合使用這些語法——你可以通過 `HEAD~3^2` 指明先前引用的第二父提交(假設它是一個合併提交),依此類推。 ### 提交範圍 ### 現在你已經可以指明單次的提交,讓我們來看看怎樣指明一定範圍的提交。這在你管理分支的時候尤顯重要——如果你有很多分支,你可以指明範圍來圈定一些問題的答案,比如:「這個分支上我有哪些工作還沒合併到主分支的?」 #### 雙點 #### 最常用的指明範圍的方法是雙點的語法。這種語法主要是讓 Git 區分出可從一個分支中獲得而不能從另一個分支中獲得的提交。例如,假設你有類似於圖 6-1 的提交歷史。 Insert 18333fig0601.png Figure 6-1. 範圍選擇的提交歷史實例 你想要查看你的試驗分支(experiment)上哪些沒有被提交到主分支,那麼你就可以使用 `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` 上的 `master` 分支上的提交。如果你執行 `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 這樣很好,因為它允許你在查詢中指定多於兩個的引用,而這是雙點語法所做不到的。例如,如果你想查找所有從 `refA` 或 `refB` 包含的但是不被 `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 知道你要察看哪些提交就容易得多了。 ## 互動式暫存 ## 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` 得到的那些資訊但是稍微簡潔但資訊更加豐富一些。它在左側列出了你暫存的變更,在右側列出了未被暫存的變更。 在這之後是一個命令區。這裡你可以做很多事情,包括暫存檔案(stage)、撤回檔案(unstage)、暫存部分檔案、加入未被追蹤的文件、查看暫存文件的差別。 ### 暫存和撤回檔案 ### 如果你在 `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>> 提示後直接敲入 Enter,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

...

- +