Git
章節 ▾ 第二版

7.1 Git 工具 - 版本選擇

到目前為止,您已經學習了大多數日常所需的命令和工作流程,來管理或維護您原始碼控制的 Git 儲存庫。您已經完成了追蹤和提交檔案的基本任務,並且善用了暫存區以及輕量級的主題分支和合併的功能。

現在,您將探索 Git 可以做的一些非常強大的事情,這些事情您可能不一定每天都會使用,但您可能在某些時候會需要它們。

版本選擇

Git 允許您以多種方式參照單個提交、一組提交或一系列提交。它們不一定顯而易見,但知道它們很有幫助。

單個修訂版本

您當然可以透過完整的 40 個字元的 SHA-1 雜湊值來參照任何單個提交,但也有更人性化的方式來參照提交。本節概述您可以參照任何提交的各種方式。

簡短的 SHA-1

如果您提供 SHA-1 雜湊值的前幾個字元,Git 就足以判斷您要參照哪個提交,只要該部分雜湊值至少有四個字元長且沒有歧義即可;也就是說,物件資料庫中沒有其他物件的雜湊值以相同的前綴開頭。

例如,若要檢查您知道已加入特定功能的特定提交,您可能會先執行 git log 命令來找到該提交

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

    Fix refs handling, add gc auto, update 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

    Add 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 Change the version number
085bb3b Remove unnecessary test code
a11bef0 Initial commit

一般而言,八到十個字元足以在專案內保持唯一性。例如,截至 2019 年 2 月,Linux 核心(這是一個相當大的專案)在其物件資料庫中有超過 875,000 個提交和將近 700 萬個物件,其中沒有兩個物件的 SHA-1 值在前 12 個字元中是相同的。

注意
關於 SHA-1 的簡短注意事項

很多人有時會擔心,他們可能會因為隨機的巧合,在儲存庫中有兩個不同的物件雜湊到相同的 SHA-1 值。那會發生什麼事?

如果您碰巧提交了一個與儲存庫中先前 *不同* 的物件雜湊到相同 SHA-1 值的物件,Git 會看到先前物件已在 Git 資料庫中,假設它已經寫入,並直接重複使用它。如果您在稍後的某個時間點嘗試再次檢出該物件,您將始終獲得第一個物件的資料。

但是,您應該知道這種情況發生的可能性極低。SHA-1 摘要是 20 個位元組或 160 個位元。需要隨機雜湊物件的數量,才能確保發生單次碰撞的機率為 50%,大約是 280(用於確定碰撞機率的公式為 p = (n(n-1)/2) * (1/2^160))。280 是 1.2 x 1024 或 100 萬億億。這是地球上沙粒數量的 1,200 倍。

這裡有一個範例,讓您了解產生 SHA-1 碰撞需要付出多少代價。假設地球上所有 65 億人口都在編寫程式,而且每秒鐘,每個人都產生相當於整個 Linux 核心歷史的程式碼(650 萬個 Git 物件),並將其推送到一個巨大的 Git 儲存庫中,大約需要 2 年的時間,該儲存庫才會有足夠的物件,達到單一 SHA-1 物件碰撞的 50% 可能性。因此,發生自然 SHA-1 碰撞的可能性,比您程式設計團隊的每個成員在同一晚因無關事件而被狼攻擊並殺死的可能性還低。

如果您投入數千美元的運算能力,就有可能合成兩個具有相同雜湊值的文件,如同 2017 年 2 月在 https://shattered.io/ 上證明的那樣。Git 正朝向使用 SHA256 作為預設的雜湊演算法,這種演算法對碰撞攻擊的抵抗力更強,並且內建程式碼來幫助減輕這種攻擊(儘管無法完全消除)。

分支參照

一種直接參照特定提交的方式是,如果該提交是分支頂端的提交;在這種情況下,您可以在任何需要參照提交的 Git 命令中直接使用分支名稱。例如,如果您想檢查分支上的最後一個提交物件,以下命令是等效的,假設 topic1 分支指向提交 ca82a6d…​

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

如果您想查看分支指向的特定 SHA-1 值,或者如果您想查看這些範例在 SHA-1 方面的結果,您可以使用一個名為 rev-parse 的 Git 底層工具。您可以查看Git 內部原理以獲取有關底層工具的更多資訊;基本上,rev-parse 存在用於較底層的操作,不適用於日常操作。然而,當您需要查看實際發生的情況時,它有時會很有幫助。您可以在您的分支上執行 rev-parse

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

RefLog 簡稱

當您在工作時,Git 在後台執行的一件事是保留一個「reflog」 — 一個記錄您的 HEAD 和分支參照在過去幾個月內的位置的日誌。

您可以使用 git reflog 來查看您的 reflog

$ git reflog
734713b HEAD@{0}: commit: Fix refs handling, add gc auto, update tests
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: Add 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 都會將該資訊儲存在這個臨時歷史記錄中。您可以使用您的 reflog 資料來參照較舊的提交。例如,如果您想查看您儲存庫 HEAD 的第五個先前值,您可以使用在 reflog 輸出中看到的 @{5} 參照。

$ git show HEAD@{5}

您也可以使用此語法來查看分支在特定時間點的位置。例如,要查看您的 master 分支昨天在哪裡,您可以輸入

$ git show master@{yesterday}

這會顯示您的 master 分支昨天所在的頂端位置。此技術僅適用於仍然在您的 reflog 中的資料,因此您無法使用它來尋找幾個月前的提交。

要查看格式化為類似 git log 輸出的 reflog 資訊,您可以執行 git log -g

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

    Fix refs handling, add gc auto, update 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'

重要的是要注意,reflog 資訊是嚴格的本地資訊 — 它僅記錄了您在您的儲存庫中所做的操作。這些參照在其他人的儲存庫副本上不會相同;此外,在您最初複製儲存庫之後,您會擁有一個空的 reflog,因為您的儲存庫中尚未發生任何活動。只有在您至少兩個月前複製了該專案時,執行 git show HEAD@{2.months.ago} 才會顯示符合的提交 — 如果您複製它的時間比這更晚,您將只會看到您的第一個本地提交。

提示
將 reflog 視為 Git 版本的 Shell 歷史記錄

如果您有 UNIX 或 Linux 背景,您可以將 reflog 視為 Git 版本的 shell 歷史記錄,這強調了那裡的內容顯然只與您和您的「會話」相關,與可能在同一台機器上工作的其他人無關。

注意
在 PowerShell 中跳脫大括號

當使用 PowerShell 時,大括號如 {} 是特殊字元,必須進行跳脫。您可以使用反引號 ` 來跳脫它們,或將提交參照放在引號中

$ git show HEAD@{0}     # will NOT work
$ git show HEAD@`{0`}   # OK
$ git show "HEAD@{0}"   # OK

祖先參照

指定提交的另一種主要方式是透過其祖先。如果您在參照的末尾放置一個 ^ (脫字符號),Git 會將其解析為表示該提交的父項。假設您查看專案的歷史記錄

$ git log --pretty=format:'%h %s' --graph
* 734713b Fix refs handling, add gc auto, update tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd Add 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'
注意
在 Windows 上跳脫脫字符號

在 Windows 的 cmd.exe 中,^ 是一個特殊字元,需要以不同的方式處理。您可以將其加倍或將提交參照放在引號中

$ git show HEAD^     # will NOT work on Windows
$ git show HEAD^^    # OK
$ git show "HEAD^"   # OK

您也可以在 ^ 後面指定一個數字,以識別您想要的哪個父項;例如,d921970^2 表示「d921970 的第二個父項」。此語法僅適用於合併提交,這些提交具有多個父項 — 合併提交的第一個父項來自您合併時所在的分支(通常是 master),而合併提交的第二個父項來自被合併的分支(例如 topic

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

    Add 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 解析從一個提交可到達但從另一個提交不可到達的提交範圍。例如,假設您的提交歷史記錄看起來像 範圍選取的範例歷史記錄

Example history for range selection
圖 136. 範圍選取的範例歷史記錄

假設您想查看您的 experiment 分支中有哪些尚未合併到您的 master 分支中。您可以使用 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

這很好,因為使用此語法,您可以在查詢中指定兩個以上的參照,而雙點語法無法做到。例如,如果您想查看從 refArefB 可到達但從 refC 不可到達的所有提交,您可以使用以下任何一種

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

這形成了一個非常強大的修訂查詢系統,應該可以幫助您找出您的分支中的內容。

三點

最後一個主要的範圍選擇語法是三點語法,它指定了兩個參照任何一個可到達但兩者都不可到達的所有提交。回顧 範圍選取的範例歷史記錄 中的範例提交歷史記錄。如果您想查看 masterexperiment 中有哪些內容,但沒有任何常見的參照,您可以執行

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

同樣,這會給您正常的 log 輸出,但只會顯示這四個提交的提交資訊,按照傳統的提交日期順序顯示。

在這種情況下與 log 命令一起使用的常見開關是 --left-right,它會顯示每個提交在哪個範圍側。這有助於使輸出更有用

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

有了這些工具,您可以更輕鬆地讓 Git 知道您要檢查哪個或哪些提交。

scroll-to-top