Git
英文 ▾ 主題 ▾ 最新版本 ▾ gitcore-tutorial 最後更新於 2.43.1

名稱

gitcore-tutorial - 給開發者的 Git 核心教學

概要

git *

說明

本教學說明如何使用「核心」Git 命令來設定和使用 Git 儲存庫。

如果您只需要將 Git 作為修訂控制系統使用,您可能更喜歡從「Git 入門教學」(gittutorial[7])或 Git 使用者手冊 開始。

不過,如果您想了解 Git 的內部機制,了解這些底層工具會很有幫助。

核心 Git 通常稱為「管線」,而其上較漂亮的用戶介面則稱為「瓷器」。您可能不會經常直接使用管線,但了解瓷器未正常工作時管線的作用會很有幫助。

當初撰寫本文時,許多瓷器命令都是 shell 腳本。為了簡化起見,本文仍然使用它們作為範例來說明如何將管線組合在一起以形成瓷器命令。原始碼樹在 contrib/examples/ 中包含了一些這些腳本以供參考。儘管這些腳本不再以 shell 腳本形式實作,但對管線層命令作用的說明仍然有效。

注意
較深入的技術細節通常標記為「注意」,您可以在第一次閱讀時跳過它們。

建立 Git 儲存庫

建立新的 Git 儲存庫非常容易:所有 Git 儲存庫一開始都是空的,您唯一需要做的就是找到要用作工作樹的子目錄 - 一個全新的專案的空子目錄,或者您想要匯入到 Git 中的現有工作樹。

在我們的第一個範例中,我們將從頭開始建立一個全新的儲存庫,沒有任何預先存在的文件,我們將其命名為 git-tutorial。若要啟動,請為其建立一個子目錄,變更到該子目錄中,並使用 git init 初始化 Git 基礎結構

$ mkdir git-tutorial
$ cd git-tutorial
$ git init

Git 會回覆

Initialized empty Git repository in .git/

這只是 Git 表示您沒有做任何奇怪的事情,並且它會為您的新專案建立一個本地 .git 目錄設定。您現在會有一個 .git 目錄,您可以使用 ls 進行檢查。對於您新的空專案,它應該會顯示三個條目,其中包括

  • 一個名為 HEAD 的檔案,其中包含 ref: refs/heads/master。這類似於符號連結,並指向相對於 HEAD 檔案的 refs/heads/master

    不要擔心 HEAD 連結指向的檔案甚至還不存在的事實 - 您尚未建立將啟動 HEAD 開發分支的提交。

  • 一個名為 objects 的子目錄,其中將包含專案的所有物件。您永遠不應該有任何實際理由直接查看物件,但您可能想知道這些物件包含儲存庫中的所有實際資料

  • 一個名為 refs 的子目錄,其中包含對物件的參考。

特別是,refs 子目錄將包含兩個其他子目錄,分別命名為 headstags。它們的作用正如其名稱所示:它們包含對任何數量的不同開發(又稱為分支)的參考,以及您為了命名儲存庫中特定版本而建立的任何標籤的參考。

一個注意事項:特殊的 master 頭是預設分支,這就是為什麼建立 .git/HEAD 檔案時會指向它,即使它尚不存在。基本上,HEAD 連結應該始終指向您目前正在工作的分支,並且您總是希望從 master 分支開始工作。

但是,這只是一種慣例,您可以隨意命名分支,甚至不必擁有 master 分支。不過,許多 Git 工具會假設 .git/HEAD 是有效的。

注意
物件由其 160 位元的 SHA-1 雜湊(又稱為物件名稱)識別,而對物件的參考始終是該 SHA-1 名稱的 40 位元組十六進位表示法。refs 子目錄中的檔案預期會包含這些十六進位參考(通常在結尾處加上 \n),因此當您實際開始填入樹狀結構時,您應該會看到這些 refs 子目錄中有許多包含這些參考的 41 位元組檔案。
注意
進階使用者可能想在完成本教學後查看 gitrepository-layout[5]

您現在已建立您的第一個 Git 儲存庫。當然,由於它是空的,所以沒有什麼用處,因此讓我們開始使用資料來填入它。

填入 Git 儲存庫

我們將保持簡單和直接,因此我們將先填入一些瑣碎的檔案,以便對其有所了解。

首先建立您想要在 Git 儲存庫中維護的任何隨機檔案。我們將從一些不好的範例開始,只是為了了解其運作方式

$ echo "Hello World" >hello
$ echo "Silly example" >example

您現在已在工作樹(又稱為工作目錄)中建立兩個檔案,但要實際檢查您的辛勤工作,您必須經過兩個步驟

  • 使用您的工作樹狀態的相關資訊填入索引檔案(又稱為快取)。

  • 將該索引檔案作為物件提交。

第一步很簡單:當您想要告知 Git 您的工作樹的任何變更時,您可以使用 git update-index 程式。該程式通常只會取得您想要更新的檔案名稱清單,但為了避免瑣碎的錯誤,除非您明確告知它您要使用 --add 旗標加入新條目(或使用 --remove 旗標移除條目),否則它會拒絕將新條目加入索引(或移除現有條目)。

因此,若要使用您剛建立的兩個檔案填入索引,您可以執行

$ git update-index --add hello example

您現在已告知 Git 追蹤這兩個檔案。

事實上,在您這樣做時,如果您現在查看您的物件目錄,您會注意到 Git 會將兩個新物件加入物件資料庫中。如果您完全按照上述步驟操作,您現在應該可以執行

$ ls .git/objects/??/*

並看到兩個檔案

.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238
.git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962

分別對應於名稱為 557db...f24c7... 的物件。

如果需要,您可以使用 git cat-file 來查看這些物件,但您必須使用物件名稱,而不是物件的檔案名稱

$ git cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238

其中 -t 告知 git cat-file 告訴您物件的「類型」。Git 會告知您有一個「blob」物件(即,只是一個普通檔案),您可以使用以下命令查看內容

$ git cat-file blob 557db03

這會印出「Hello World」。物件 557db03 只不過是您檔案 hello 的內容。

注意
別把該物件與檔案 hello 本身搞混。該物件實際上只是檔案中那些特定的內容,而且無論你之後如何更改 hello 檔案中的內容,我們剛才看到的物件永遠不會改變。物件是不可變的。
注意
第二個範例示範了在大多數情況下,你可以將物件名稱縮寫為只使用前幾個十六進制數字。

總之,如同我們之前提到的,你通常不會真的去查看物件本身,而且輸入 40 個字元的長十六進制名稱也不是你通常會想做的事。以上這些題外話只是為了說明 git update-index 做了一些神奇的事,實際上將你的檔案內容儲存到了 Git 物件資料庫中。

更新索引也做了另一件事:它建立了一個 .git/index 檔案。這是描述你目前工作目錄的索引,你應該非常清楚這一點。再次強調,你通常不會去擔心索引檔案本身,但你應該要知道事實上你到目前為止還沒有真的將你的檔案「簽入」Git,你只是告知 Git 關於它們的存在。

然而,由於 Git 知道它們的存在,你現在可以開始使用一些最基本的 Git 指令來操作這些檔案或查看它們的狀態。

特別是,我們甚至先不要將這兩個檔案簽入 Git,我們先在 hello 中加入另一行。

$ echo "It's a new day for git" >>hello

由於你已經告知 Git 關於 hello 的先前狀態,你現在可以使用 git diff-files 指令,詢問 Git 工作目錄相較於舊索引有什麼變化

$ git diff-files

喔。那並不是很容易閱讀。它只是吐出它自己內部的 diff 版本,但該內部版本實際上只是告訴你,它注意到「hello」已被修改,並且它擁有的舊物件內容已被其他內容取代。

為了使其可讀,我們可以告訴 git diff-files 使用 -p 標誌,將差異輸出為 patch 格式

$ git diff-files -p
diff --git a/hello b/hello
index 557db03..263414f 100644
--- a/hello
+++ b/hello
@@ -1 +1,2 @@
 Hello World
+It's a new day for git

也就是說,我們透過在 hello 中加入另一行所造成的變更差異。

換句話說,git diff-files 總是顯示我們索引中記錄的內容與目前工作目錄中的內容之間的差異。這非常有用。

git diff-files -p 的常見簡寫方式是直接寫 git diff,這會做同樣的事情。

$ git diff
diff --git a/hello b/hello
index 557db03..263414f 100644
--- a/hello
+++ b/hello
@@ -1 +1,2 @@
 Hello World
+It's a new day for git

提交 Git 狀態

現在,我們想要進入 Git 的下一個階段,將 Git 在索引中知道的檔案,作為一個真實的樹狀結構提交。我們分兩個階段完成:建立一個 tree 物件,並將該 tree 物件作為 commit 物件提交,同時附上關於該樹狀結構的解釋,以及我們如何到達該狀態的資訊。

建立樹狀結構物件很簡單,使用 git write-tree 完成。沒有選項或其他輸入:git write-tree 會取得目前的索引狀態,並寫入一個描述整個索引的物件。換句話說,我們現在將所有不同的檔案名稱與它們的內容(以及它們的權限)結合在一起,並且我們正在建立相當於 Git 的「目錄」物件。

$ git write-tree

這只會輸出結果樹狀結構的名稱,在這個例子中(如果你完全按照我描述的方式做了),它應該是

8988da15d077d4829fc51d8544c097def6644dbb

這是另一個難以理解的物件名稱。再次強調,如果你想的話,你可以使用 git cat-file -t 8988d... 來查看這次的物件不是 "blob" 物件,而是 "tree" 物件(你也可以使用 git cat-file 來實際輸出原始物件內容,但你會看到主要的二進制亂碼,所以那比較沒意思)。

然而,通常你永遠不會單獨使用 git write-tree,因為通常你總是使用 git commit-tree 指令將樹狀結構提交到提交物件中。事實上,最好不要單獨使用 git write-tree,而是將其結果作為 git commit-tree 的參數傳遞。

git commit-tree 通常會接收多個參數,它需要知道提交的 parent 是什麼,但由於這是這個新儲存庫中的第一次提交,並且沒有父級,我們只需要傳遞樹狀結構的物件名稱。然而,git commit-tree 也希望在標準輸入上取得提交訊息,它會將提交的結果物件名稱寫入到其標準輸出。

而這就是我們建立 .git/refs/heads/master 檔案的地方,該檔案由 HEAD 指向。這個檔案應該包含指向 master 分支樹狀結構頂端的參考,由於這正是 git commit-tree 輸出的內容,我們可以透過一系列簡單的 shell 指令來完成這一切

$ tree=$(git write-tree)
$ commit=$(echo 'Initial commit' | git commit-tree $tree)
$ git update-ref HEAD $commit

在這個案例中,這會建立一個與任何其他內容無關的全新提交。通常你只會對一個專案執行一次,而所有後續的提交都會以較早的提交為基礎。

再次強調,通常你永遠不會手動執行此操作。有一個有用的腳本叫做 git commit,它會為你完成這一切。所以你原本可以只寫 git commit,它就會為你完成以上那些神奇的腳本編寫。

做出變更

還記得我們如何對檔案 hello 執行 git update-index,然後我們更改了 hello,並能將 hello 的新狀態與我們在索引檔案中儲存的狀態進行比較嗎?

此外,還記得我說過 git write-tree 會將索引檔案的內容寫入樹狀結構,因此我們剛提交的實際上是檔案 hello原始內容,而不是新的內容。我們故意這樣做,是為了顯示索引狀態與工作目錄中的狀態之間的差異,以及它們不一定要匹配,即使我們提交了內容。

和以前一樣,如果我們在 git-tutorial 專案中執行 git diff-files -p,我們仍然會看到上次看到的相同差異:索引檔案並沒有因為提交任何內容而改變。然而,現在我們已經提交了一些東西,我們也可以學會使用一個新的指令:git diff-index

與顯示索引檔案與工作目錄之間差異的 git diff-files 不同,git diff-index 會顯示已提交的樹狀結構與索引檔案或工作目錄之間的差異。換句話說,git diff-index 需要一個樹狀結構進行比較,而我們在提交之前無法做到這一點,因為我們沒有任何可以比較的對象。

但現在我們可以執行

$ git diff-index -p HEAD

(其中 -p 的意義與 git diff-files 中的相同),它會向我們顯示相同的差異,但原因完全不同。現在我們不是將工作目錄與索引檔案進行比較,而是與我們剛寫入的樹狀結構進行比較。碰巧的是,這兩者顯然是相同的,所以我們得到了相同的結果。

再次強調,由於這是一個常見的操作,你也可以簡單地縮寫為

$ git diff HEAD

這會為你執行上述操作。

換句話說,git diff-index 通常會將樹狀結構與工作目錄進行比較,但是當給定 --cached 標誌時,它會被告知改為僅與索引快取內容進行比較,並完全忽略目前的工作目錄狀態。由於我們剛將索引檔案寫入 HEAD,因此執行 git diff-index --cached -p HEAD 應該會返回一組空的差異,而這正是它所做的。

注意

git diff-index 實際上始終使用索引進行比較,因此說它將樹狀結構與工作目錄進行比較並不完全準確。特別是,要比較的檔案清單(「中繼資料」)始終來自索引檔案,無論是否使用 --cached 標誌。--cached 標誌實際上只決定要比較的檔案內容是來自工作目錄還是來自索引快取。

這並不難理解,只要你意識到 Git 根本不知道(或不關心)它沒有被明確告知的檔案。Git 永遠不會去尋找要比較的檔案,它希望你告訴它檔案是什麼,這就是索引的作用。

然而,我們的下一步是提交我們所做的變更,並且再次強調,為了理解正在發生的事情,請記住「工作目錄內容」、「索引檔案」和「已提交的樹狀結構」之間的差異。我們在工作目錄中有想要提交的變更,而且我們始終必須透過索引檔案進行操作,所以我們需要做的第一件事是更新索引快取

$ git update-index hello

(請注意,這次我們不需要 --add 標誌,因為 Git 已經知道該檔案了)。

請注意這裡不同的 git diff-* 版本會發生什麼事。在我們更新索引中的 hello 之後,git diff-files -p 現在顯示沒有差異,但 git diff-index -p HEAD 仍然顯示目前狀態與我們提交的狀態不同。事實上,現在無論我們是否使用 --cached 標誌,git diff-index 都會顯示相同的差異,因為現在索引與工作目錄一致。

現在,由於我們已經在索引中更新了 hello,我們可以提交新版本。我們可以再次手動寫入樹狀結構並提交樹狀結構(這次我們必須使用 -p HEAD 標誌來告訴 commit HEAD 是新提交的父級,並且這不再是初始提交),但你已經做過一次了,所以這次讓我們使用有用的腳本

$ git commit

這會啟動一個編輯器讓你編寫提交訊息,並告訴你一些你所做的事情。

寫下你想要的任何訊息,所有以 # 開頭的行都會被刪除,其餘部分將作為變更的提交訊息。如果你決定現在根本不想提交任何內容(你可以繼續編輯內容並更新索引),你可以只留下一個空的訊息。否則,git commit 將會為你提交變更。

你現在已經做了你的第一個真正的 Git 提交。如果你有興趣了解 git commit 實際上做了什麼,請隨意研究:它是一些非常簡單的 shell 腳本,用於產生有用的 (?) 提交訊息標頭,以及一些實際上執行提交的一行程式碼(git commit)。

檢查變更

建立變更很有用,但如果之後能知道變更了什麼會更有用。這方面最有用的指令是 diff 系列中的另一個指令,也就是 git diff-tree

git diff-tree 可以接收兩個任意的樹狀結構,並告訴你它們之間的差異。不過,更常見的情況是,你可以只給它一個提交物件,它會自行找出該提交的父提交,並直接顯示差異。因此,要獲得我們已經看過幾次的相同差異,現在我們可以執行

$ git diff-tree -p HEAD

(再次強調,-p 表示以人類可讀的修補程式格式顯示差異),它會顯示最後一次提交(在 HEAD 中)實際變更的內容。

注意

以下是 Jon Loeliger 繪製的 ASCII 圖,說明了各種 diff-* 指令如何比較事物。

            diff-tree
             +----+
             |    |
             |    |
             V    V
          +-----------+
          | Object DB |
          |  Backing  |
          |   Store   |
          +-----------+
            ^    ^
            |    |
            |    |  diff-index --cached
            |    |
diff-index  |    V
            |  +-----------+
            |  |   Index   |
            |  |  "cache"  |
            |  +-----------+
            |    ^
            |    |
            |    |  diff-files
            |    |
            V    V
          +-----------+
          |  Working  |
          | Directory |
          +-----------+

更有趣的是,你還可以為 git diff-tree 提供 --pretty 旗標,這會指示它同時顯示提交訊息、作者和提交日期,並且你可以指示它顯示一系列的差異。或者,你可以指示它「靜音」,完全不顯示差異,只顯示實際的提交訊息。

事實上,結合 git rev-list 程式(產生修訂列表),git diff-tree 最終會成為變更的真正來源。你可以用一個簡單的腳本來模擬 git loggit log -p 等,該腳本將 git rev-list 的輸出傳送到 git diff-tree --stdin,這正是早期版本 git log 的實作方式。

為版本加上標籤

在 Git 中,標籤有兩種:一種是「輕量」標籤,另一種是「附註標籤」。

「輕量」標籤在技術上只不過是一個分支,只是我們將它放在 .git/refs/tags/ 子目錄中,而不是將它稱為 head。因此,最簡單的標籤形式只涉及

$ git tag my-first-tag

它只會將目前的 HEAD 寫入 .git/refs/tags/my-first-tag 檔案,之後你就可以使用這個符號名稱來表示特定的狀態。例如,你可以執行

$ git diff my-first-tag

將目前的狀態與該標籤進行比較,此時顯然會是一個空的差異,但如果你繼續開發並提交內容,則可以使用你的標籤作為「錨點」來查看自你加上標籤以來發生的變更。

「附註標籤」實際上是一個真正的 Git 物件,不僅包含指向你要標記的狀態的指標,還包含一個小的標籤名稱和訊息,以及可選的 PGP 簽名,表示是的,你確實建立了該標籤。你可以使用 git tag-a-s 旗標來建立這些附註標籤

$ git tag -s <tagname>

這將簽署目前的 HEAD(但你也可以提供另一個參數來指定要標記的內容,例如,你可以使用 git tag <tagname> mybranch 來標記目前的 mybranch 點)。

你通常只會為主要版本或類似的內容建立簽名標籤,而輕量標籤則適用於任何你想要進行的標記 — 任何時候你決定要記住某個點,只需為它建立一個私有標籤,你就有了一個很好的符號名稱來表示該點的狀態。

複製儲存庫

Git 儲存庫通常是完全自給自足且可重新定位的。例如,與 CVS 不同,沒有「儲存庫」和「工作樹」的單獨概念。Git 儲存庫通常就是工作樹,本機 Git 資訊隱藏在 .git 子目錄中。沒有其他東西。你看到的就是你得到的。

注意
你可以指示 Git 將 Git 內部資訊與它追蹤的目錄分開,但我們現在先忽略它:這不是正常專案的運作方式,它實際上只適用於特殊用途。因此,「Git 資訊始終與它描述的工作樹直接相關聯」的心理模型可能在技術上不是 100% 準確的,但它是一個適用於所有正常用途的良好模型。

這有兩個含義

  • 如果你對你建立的教學儲存庫感到厭煩(或者你犯了錯誤並且想要重新開始),你可以直接執行簡單的

    $ rm -rf git-tutorial

    它就會消失。沒有外部儲存庫,也沒有你建立的專案之外的歷史記錄。

  • 如果你想要移動或複製 Git 儲存庫,你可以這樣做。有 git clone 指令,但如果你只想建立儲存庫的副本(包括它隨附的所有完整歷史記錄),你可以使用普通的 cp -a git-tutorial new-git-tutorial 來進行。

    請注意,當你移動或複製 Git 儲存庫時,你的 Git 索引檔(它快取各種資訊,尤其是所涉及檔案的一些「stat」資訊)可能需要重新整理。因此,在你執行 cp -a 來建立新副本之後,你需要在新的儲存庫中執行

    $ git update-index --refresh

    以確保索引檔是最新的。

請注意,第二點即使在不同機器之間也是成立的。你可以使用任何常規複製機制(無論是 scprsyncwget)來複製遠端 Git 儲存庫。

複製遠端儲存庫時,你至少需要在執行此操作時更新索引快取,尤其是對於其他人的儲存庫,你通常希望確保索引快取處於某個已知狀態(你不知道他們已經做了什麼並且尚未簽入),因此通常你會在 git update-index 之前加上

$ git read-tree --reset HEAD
$ git update-index --refresh

這將強制從 HEAD 指向的樹狀結構完全重建索引。它會將索引內容重設為 HEAD,然後 git update-index 會確保將所有索引項目與已簽出的檔案相匹配。如果原始儲存庫在其工作樹中有未提交的變更,則 git update-index --refresh 會注意到它們並告訴你需要更新它們。

以上也可以簡單地寫成

$ git reset

事實上,許多常見的 Git 指令組合都可以使用 git xyz 介面進行編寫。你可以透過查看各種 git 腳本的運作方式來學習東西。例如,git reset 過去就是上述在 git reset 中實作的兩行程式碼,但 git statusgit commit 之類的東西是基於基本 Git 指令的稍微複雜的腳本。

許多(大多數?)公用遠端儲存庫不會包含任何已簽出的檔案,甚至不會包含索引檔,並且只會包含實際的核心 Git 檔案。這樣的儲存庫通常甚至沒有 .git 子目錄,而是將所有 Git 檔案直接放在儲存庫中。

若要建立自己的此類「原始」Git 儲存庫的本機即時副本,你首先需要為專案建立自己的子目錄,然後將原始儲存庫內容複製到 .git 目錄中。例如,若要建立自己的 Git 儲存庫副本,你需要執行下列操作

$ mkdir my-git
$ cd my-git
$ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git

然後執行

$ git read-tree HEAD

來填入索引。但是,現在你已經填入了索引,並且擁有所有 Git 內部檔案,但你會注意到你實際上沒有任何要處理的工作樹檔案。若要取得這些檔案,你需要使用

$ git checkout-index -u -a

其中 -u 旗標表示你要讓簽出保持索引的最新狀態(以便你之後不必重新整理它),而 -a 旗標表示「簽出所有檔案」(如果你有過時的副本或較舊版本的已簽出樹狀結構,你可能還需要先新增 -f 旗標,以指示 git checkout-index 強制覆寫任何舊檔案)。

同樣,這一切都可以簡化為

$ git clone git://git.kernel.org/pub/scm/git/git.git/ my-git
$ cd my-git
$ git checkout

這最終會為你執行以上所有操作。

你現在已經成功複製了其他人的(我的)遠端儲存庫,並將其簽出。

建立新分支

Git 中的分支實際上只不過是從 .git/refs/ 子目錄內部指向 Git 物件資料庫的指標,正如我們已經討論過的,HEAD 分支只不過是指向其中一個物件指標的符號連結。

你可以隨時在專案歷史記錄中選擇任意點來建立新分支,只需將該物件的 SHA-1 名稱寫入 .git/refs/heads/ 下的檔案即可。你可以使用任何你想要的檔案名稱(以及子目錄),但慣例是將「正常」分支稱為 master。不過,這只是一個慣例,沒有任何東西會強制執行它。

為了舉例說明,讓我們回到我們之前使用的 git-tutorial 儲存庫,並在其中建立一個分支。你只需說你要簽出一個新分支即可完成此操作

$ git switch -c mybranch

這將在目前的 HEAD 位置建立一個新分支,並切換到它。

注意

如果你決定在新分支的起點選擇與目前 HEAD 不同的歷史記錄中的某個點,你可以透過告知 git switch 簽出的基礎是什麼來完成此操作。換句話說,如果你有較早的標籤或分支,你只需執行

$ git switch -c mybranch earlier-commit

它就會在較早的提交中建立新分支 mybranch,並簽出當時的狀態。

你可以隨時使用以下指令跳回原始 master 分支

$ git switch master

(或者任何其他分支名稱),如果你忘記了你目前位於哪個分支,則簡單的

$ cat .git/HEAD

會告訴你它指向的位置。若要取得你擁有的分支列表,你可以說

$ git branch

這過去只不過是一個圍繞 ls .git/refs/heads 的簡單腳本。你目前所在的分支前面會有一個星號。

有時你可能希望建立新分支,而無需實際簽出並切換到它。如果是這樣,只需使用指令

$ git branch <branchname> [startingpoint]

這只會建立分支,而不會執行任何其他操作。然後,你可以稍後 — 一旦你決定要實際在該分支上開發 — 使用常規的 git switch 和分支名稱作為參數來切換到該分支。

合併兩個分支

擁有分支的一個想法是,你在其中完成一些(可能是實驗性的)工作,並最終將其合併回主分支。因此,假設你建立了上述 mybranch,其起點與原始 master 分支相同,讓我們確保我們位於該分支中,並在那裡執行一些工作。

$ git switch mybranch
$ echo "Work, work, work" >>hello
$ git commit -m "Some work." -i hello

在這裡,我們只是在 hello 中新增了一行,並且我們使用簡寫方式來執行 git update-index hellogit commit,只需將檔案名稱直接提供給 git commit,並加上 -i 旗標(這會告知 Git 除了你目前對索引檔所做的操作之外,還在提交時包含該檔案)。-m 旗標是從命令列提供提交日誌訊息。

現在,為了讓它更有趣一點,讓我們假設其他人在原始分支中執行了一些工作,並透過回到 master 分支,並在那裡以不同的方式編輯相同檔案來模擬它

$ git switch master

在這裡,花點時間查看 hello 的內容,並注意它們如何不包含我們剛剛在 mybranch 中完成的工作 — 因為該工作根本沒有在 master 分支中發生。然後執行

$ echo "Play, play, play" >>hello
$ echo "Lots of fun" >>example
$ git commit -m "Some fun." -i hello example

因為 master 分支顯然心情好多了。

現在,你有了兩個分支,並且你決定要合併完成的工作。在我們執行此操作之前,讓我們介紹一個很酷的圖形工具,可以幫助你查看正在發生的事情

$ gitk --all

會以圖形方式顯示您的所有分支(這是 --all 的含義:通常它只會顯示您目前的 HEAD)及其歷史記錄。您還可以確切看到它們是如何從共同來源產生的。

無論如何,讓我們退出 gitk^Q 或「檔案」選單),並決定將我們在 mybranch 分支上所做的工作合併到 master 分支(目前也是我們的 HEAD)。若要執行此操作,有一個名為 git merge 的好用指令稿,它會想知道您要解決哪些分支,以及合併的全部內容

$ git merge -m "Merge work in mybranch" mybranch

其中第一個引數將在合併可以自動解決時用作提交訊息。

現在,在本例中,我們有意建立一個需要手動修復合併的情況,因此 Git 會盡可能自動執行(在本例中,只是合併 example 檔案,該檔案在 mybranch 分支中沒有任何差異),然後說

	Auto-merging hello
	CONFLICT (content): Merge conflict in hello
	Automatic merge failed; fix conflicts and then commit the result.

它告訴您它執行了「自動合併」,但由於 hello 中存在衝突而失敗。

別擔心。它將 hello 中的(微不足道的)衝突以您如果使用過 CVS 就應該已經很熟悉的相同形式留下,所以讓我們在我們的編輯器(無論是什麼)中開啟 hello,並以某種方式修復它。我建議將 hello 設為包含所有四行

Hello World
It's a new day for git
Play, play, play
Work, work, work

一旦您對手動合併感到滿意,只需執行

$ git commit -i hello

這會大聲警告您現在正在提交合併(這是正確的,所以沒關係),您可以寫一小段關於您在 git merge 世界中冒險的合併訊息。

完成後,啟動 gitk --all 以圖形方式查看歷史記錄的外觀。請注意,mybranch 仍然存在,您可以切換到它,並在需要時繼續使用它。mybranch 分支不會包含合併,但下次您從 master 分支合併它時,Git 會知道您是如何合併的,因此您不必再次執行合併。

另一個有用的工具,尤其是在您不總是在 X-Window 環境中工作時,是 git show-branch

$ git show-branch --topo-order --more=1 master mybranch
* [master] Merge work in mybranch
 ! [mybranch] Some work.
--
-  [master] Merge work in mybranch
*+ [mybranch] Some work.
*  [master^] Some fun.

前兩行表示它正在顯示兩個分支及其樹頂提交的標題,您目前位於 master 分支(請注意星號 * 字元),並且後續輸出行的第一列用於顯示 master 分支中包含的提交,第二列用於 mybranch 分支。顯示了三個提交及其標題。它們的第一列中都有非空白字元(* 表示目前分支上的普通提交,- 是合併提交),這表示它們現在是 master 分支的一部分。只有「Some work」提交在第二列中有加號 + 字元,因為 mybranch 尚未合併以納入來自 master 分支的這些提交。提交日誌訊息前的方括號內的字串是您可以使用的提交簡短名稱。在上述範例中,mastermybranch 是分支頭。master^master 分支頭的第一個父代。如果您想查看更複雜的情況,請參閱 gitrevisions[7]

注意
如果沒有 --more=1 選項,git show-branch 將不會輸出 [master^] 提交,因為 [mybranch] 提交是 mastermybranch 頂端的共同祖先。請參閱 git-show-branch[1] 以取得詳細資料。
注意
如果在合併之後 master 分支上有更多提交,則依預設,git show-branch 不會顯示合併提交本身。您需要提供 --sparse 選項才能讓合併提交在這種情況下可見。

現在,讓我們假設您是在 mybranch 中完成所有工作的人,而您辛苦工作的成果終於合併到 master 分支。讓我們回到 mybranch,並執行 git merge 將「上游變更」返回您的分支。

$ git switch mybranch
$ git merge -m "Merge upstream changes." master

這會輸出類似這樣的內容(實際的提交物件名稱會有所不同)

Updating from ae3a2da... to a80b4aa....
Fast-forward (no commit created; -m option ignored)
 example | 1 +
 hello   | 1 +
 2 files changed, 2 insertions(+)

由於您的分支沒有包含任何超出已合併到 master 分支中的內容,因此合併操作實際上並未執行合併。相反,它只是將分支的樹頂更新為 master 分支的樹頂。這通常稱為快轉合併。

您可以再次執行 gitk --all 來查看提交祖先的外觀,或執行 show-branch,它會告訴您這個。

$ git show-branch master mybranch
! [master] Merge work in mybranch
 * [mybranch] Merge work in mybranch
--
-- [master] Merge work in mybranch

合併外部工作

您通常更常與其他人合併,而不是與自己的分支合併,因此值得指出的是,Git 也讓這變得非常容易,事實上,它與執行 git merge 沒有太大區別。實際上,遠端合併最終只不過是「將遠端儲存庫中的工作提取到臨時標籤」,然後執行 git merge

從遠端儲存庫提取是透過執行 git fetch 來完成的,這並不令人意外

$ git fetch <remote-repository>

可以使用下列傳輸方式之一來命名要從中下載的儲存庫

SSH

remote.machine:/path/to/repo.git/

ssh://remote.machine/path/to/repo.git/

此傳輸可用於上傳和下載,並要求您擁有透過 ssh 登入遠端機器的權限。它透過交換兩端都具有的頭部提交來找出另一端缺少的一組物件,並傳輸(接近)最少的物件集。這是儲存庫之間交換 Git 物件的最有效方式。

本機目錄

/path/to/repo.git/

此傳輸與 SSH 傳輸相同,但使用 sh 在本機上執行兩端,而不是透過 ssh 在遠端機器上執行另一端。

Git 原生

git://remote.machine/path/to/repo.git/

此傳輸專為匿名下載而設計。與 SSH 傳輸一樣,它會找出下游端缺少的一組物件,並傳輸(接近)最少的物件集。

HTTP(S)

http://remote.machine/path/to/repo.git/

從 http 和 https URL 下載程式首先透過查看 repo.git/refs/ 目錄下的指定 refname,從遠端網站取得最上層提交物件名稱,然後嘗試透過使用該提交物件的物件名稱從 repo.git/objects/xx/xxx... 下載來取得提交物件。然後它會讀取提交物件以找出其父提交和相關聯的樹狀物件;它會重複此程序,直到取得所有必要的物件。由於此行為,它們有時也稱為提交追蹤器

提交追蹤器有時也稱為不聰明的傳輸,因為它們不需要任何像 Git 原生傳輸一樣具有 Git 感知能力的智慧型伺服器。任何甚至不支援目錄索引的現成 HTTP 伺服器都足夠了。但您必須使用 git update-server-info 準備您的儲存庫,以協助不聰明的傳輸下載程式。

從遠端儲存庫提取之後,您可以將其與目前的分支合併

然而,提取然後立即合併是很常見的事情,因此它被稱為 git pull,您可以簡單地執行

$ git pull <remote-repository>

並可選擇性地將遠端分支名稱作為第二個引數。

注意
您可以在不使用任何分支的情況下執行操作,方法是保留與您想要擁有分支一樣多的本機儲存庫,並使用 git pull 在它們之間合併,就像在分支之間合併一樣。這種方法的優點是它允許您為每個已簽出的 branch 保留一組檔案,並且如果您同時處理多個開發線,您可能會發現更容易來回切換。當然,您將付出更多磁碟使用量的代價來容納多個工作樹,但現在磁碟空間很便宜。

您很可能不時會從相同的遠端儲存庫提取。作為簡短的捷徑,您可以將遠端儲存庫 URL 儲存在本機儲存庫的組態檔案中,如下所示

$ git config remote.linus.url https://git.kernel.org/pub/scm/git/git.git/

並使用 "linus" 關鍵字搭配 git pull,而不是完整的 URL。

範例。

  1. git pull linus

  2. git pull linus tag v0.99.1

以上等同於

  1. git pull http://www.kernel.org/pub/scm/git/git.git/ HEAD

  2. git pull http://www.kernel.org/pub/scm/git/git.git/ tag v0.99.1

合併如何運作?

我們說本教學課程會說明管道系統如何協助您處理無法清除的瓷器,但到目前為止,我們尚未討論合併的真正運作方式。如果您是第一次遵循本教學課程,我建議跳到「發佈您的工作」章節,稍後再回到這裡。

好,還在嗎?為了提供一個範例來查看,讓我們回到具有「hello」和「example」檔案的早期儲存庫,並讓自己回到合併前的狀態

$ git show-branch --more=2 master mybranch
! [master] Merge work in mybranch
 * [mybranch] Merge work in mybranch
--
-- [master] Merge work in mybranch
+* [master^2] Some work.
+* [master^] Some fun.

請記住,在執行 git merge 之前,我們的 master 頭部位於「Some fun.」提交,而我們的 mybranch 頭部位於「Some work.」提交。

$ git switch -C mybranch master^2
$ git switch master
$ git reset --hard master^

還原之後,提交結構應如下所示

$ git show-branch
* [master] Some fun.
 ! [mybranch] Some work.
--
*  [master] Some fun.
 + [mybranch] Some work.
*+ [master^] Initial commit

現在,我們已準備好手動試驗合併。

git merge 命令在合併兩個分支時,會使用三向合併演算法。首先,它會找出它們之間的共同祖先。它使用的命令是 git merge-base

$ mb=$(git merge-base HEAD mybranch)

該命令會將共同祖先的提交物件名稱寫入標準輸出,因此我們將其輸出擷取到變數中,因為我們將在下一步中使用它。順便說一下,在此情況下,共同祖先提交是「初始提交」。您可以透過以下方式得知

$ git name-rev --name-only --tags $mb
my-first-tag

在找出共同祖先提交之後,第二個步驟如下

$ git read-tree -m -u $mb HEAD mybranch

這是我們已經看過的相同 git read-tree 命令,但它採用三個樹狀結構,與先前的範例不同。這會將每個樹狀結構的內容讀取到索引檔案中的不同階段(第一個樹狀結構移至階段 1,第二個移至階段 2,依此類推)。將三個樹狀結構讀取到三個階段之後,所有三個階段中相同的路徑會摺疊到階段 0。此外,在三個階段中的兩個階段中相同的路徑也會摺疊到階段 0,並採用階段 2 或階段 3 中的 SHA-1,以與階段 1 不同者(即,只有一側從共同祖先變更)。

摺疊操作之後,三個樹狀結構中不同的路徑會留在非零階段。此時,您可以使用此命令檢查索引檔案

$ git ls-files --stage
100644 7f8b141b65fdcee47321e399a2598a235a032422 0	example
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

在我們只有兩個檔案的範例中,我們沒有未變更的檔案,因此只有 example 導致摺疊。但在實際的大型專案中,當一個提交中只有少數檔案變更時,這種摺疊往往會相當快速地將大多數路徑輕易地合併,只留下少數非零階段的實際變更。

若要僅查看非零階段,請使用 --unmerged 旗標

$ git ls-files --unmerged
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

合併的下一步是使用三向合併來合併這三個檔案版本。這是透過將 git merge-one-file 命令作為 git merge-index 命令的其中一個引數來完成的

$ git merge-index git-merge-one-file hello
Auto-merging hello
ERROR: Merge conflict in hello
fatal: merge program failed

git merge-one-file 指令稿會使用參數來呼叫,以描述這三個版本,並負責將合併結果留在工作樹中。這是一個相當簡單的 Shell 指令稿,最終會從 RCS 套件呼叫 merge 程式,以執行檔案層級的三向合併。在這種情況下,merge 會偵測到衝突,並將具有衝突標記的合併結果留在工作樹中。如果您此時再次執行 ls-files --stage,則可以看到這一點

$ git ls-files --stage
100644 7f8b141b65fdcee47321e399a2598a235a032422 0	example
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1	hello
100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2	hello
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello

這是 `git merge` 返回控制權給您後,索引檔案和工作檔案的狀態,留下衝突的合併讓您解決。請注意,路徑 `hello` 仍然是未合併的,而且此時使用 `git diff` 看到的差異是自 stage 2(即您的版本)以來的差異。

發佈您的工作

所以,我們可以從遠端儲存庫使用別人的工作,但是您要如何準備一個儲存庫讓其他人從中拉取呢?

您在您的工作樹中完成實際工作,該工作樹的主儲存庫以 `.git` 子目錄的形式存在。您**可以**讓該儲存庫可遠端存取,並要求其他人從中拉取,但在實務上,這不是通常的做法。建議的方式是擁有一個公開儲存庫,讓其他人可以存取,當您在主要工作樹中所做的變更狀態良好時,從中更新公開儲存庫。這通常稱為「推送」。

注意
這個公開儲存庫可以進一步鏡像,這就是 `kernel.org` 的 Git 儲存庫的管理方式。

將變更從您的本地(私有)儲存庫發佈到您的遠端(公開)儲存庫需要在遠端機器上具有寫入權限。您需要在該處有一個 SSH 帳戶來執行單一命令 `git-receive-pack`。

首先,您需要在遠端機器上建立一個空的儲存庫,用來存放您的公開儲存庫。這個空的儲存庫將會被填入內容,並在稍後透過推送到該處來保持更新。顯然地,這個儲存庫建立只需要執行一次。

注意
`git push` 使用一對命令,您本地機器上的 `git send-pack` 和遠端機器上的 `git-receive-pack`。兩者之間透過網路的通訊在內部使用 SSH 連線。

您私有儲存庫的 Git 目錄通常是 `.git`,但您的公開儲存庫通常以專案名稱命名,例如 `<專案>.git`。讓我們為專案 `my-git` 建立一個這樣的公開儲存庫。登入遠端機器後,建立一個空的目錄

$ mkdir my-git.git

然後,執行 `git init` 將該目錄變成一個 Git 儲存庫,但是這次,由於其名稱不是常見的 `.git`,我們做的事情稍微不同

$ GIT_DIR=my-git.git git init

請確保這個目錄可以透過您選擇的傳輸方式讓您想要拉取變更的其他人存取。此外,您需要確保 `git-receive-pack` 程式在 `$PATH` 中。

注意
許多 sshd 安裝在您直接執行程式時,不會將您的 shell 作為登入 shell 叫用;這意味著,如果您的登入 shell 是 *bash*,則只會讀取 `.bashrc` 而不會讀取 `.bash_profile`。作為一種變通方法,請確保 `.bashrc` 設定 `$PATH`,以便您可以執行 `git-receive-pack` 程式。
注意
如果您計劃發佈此儲存庫以透過 http 存取,您應該在此時執行 `mv my-git.git/hooks/post-update.sample my-git.git/hooks/post-update`。這可確保每次您推送到此儲存庫時,都會執行 `git update-server-info`。

您的「公開儲存庫」現在已準備好接受您的變更。回到您擁有私有儲存庫的機器。從那裡,執行此命令

$ git push <public-host>:/path/to/my-git.git master

這會同步您的公開儲存庫,以符合指定的branch head(在本例中為 `master`)和您目前儲存庫中可從它們存取的物件。

作為一個實際的範例,這是我如何更新我的公開 Git 儲存庫的方式。Kernel.org 鏡像網路負責將其傳播到其他公開可見的機器

$ git push master.kernel.org:/pub/scm/git/git.git/

打包您的儲存庫

先前,我們看到在 `.git/objects/??/` 目錄下,每個您建立的 Git 物件都儲存一個檔案。這種表示方式可以有效率地以原子方式安全地建立,但不太方便透過網路傳輸。由於 Git 物件在建立後是不可變的,因此有一種方法可以透過「將它們打包在一起」來最佳化儲存。指令

$ git repack

會為您執行此操作。如果您遵循本教學範例,您現在應該在 `.git/objects/??/` 目錄中累積大約 17 個物件。`git repack` 會告訴您它打包了多少個物件,並將打包的檔案儲存在 `.git/objects/pack` 目錄中。

注意
您將在 `.git/objects/pack` 目錄中看到兩個檔案 `pack-*.pack` 和 `pack-*.idx`。它們彼此密切相關,如果您因任何原因手動將它們複製到不同的儲存庫,則應確保將它們一起複製。前者保存 pack 中所有物件的資料,後者保存隨機存取的索引。

如果您很偏執,執行 `git verify-pack` 命令會偵測您是否有損毀的 pack,但不要太擔心。我們的程式始終是完美的 ;-)。

一旦您打包了物件,您就不再需要保留 pack 檔案中包含的未打包物件。

$ git prune-packed

會為您移除它們。

如果您感到好奇,可以在執行 `git prune-packed` 之前和之後嘗試執行 `find .git/objects -type f`。此外,`git count-objects` 會告訴您儲存庫中有多少個未打包的物件,以及它們佔用了多少空間。

注意
對於 HTTP 傳輸而言,`git pull` 有點麻煩,因為打包的儲存庫可能在相對較大的 pack 中包含相對較少的物件。如果您預期從您的公開儲存庫中進行許多 HTTP 拉取,您可能需要經常重新打包並修剪,或者永遠不要。

如果您此時再次執行 `git repack`,它會說「沒有新的東西可以打包」。一旦您繼續開發並累積變更,再次執行 `git repack` 將會建立一個新的 pack,其中包含自上次打包儲存庫以來建立的物件。我們建議您在初始匯入後不久打包您的專案(除非您從頭開始建立專案),然後每隔一段時間執行 `git repack`,具體取決於您的專案有多活躍。

當透過 `git push` 和 `git pull` 同步儲存庫時,來源儲存庫中打包的物件通常會在目的地中以未打包的方式儲存。雖然這允許您在兩端使用不同的打包策略,但也表示您可能需要每隔一段時間重新打包兩個儲存庫。

與他人協作

雖然 Git 是一個真正的分散式系統,但通常方便使用非正式的開發人員層級來組織您的專案。Linux 核心開發就是以這種方式運作的。在 Randy Dunlap 的簡報中有一個很好的說明(第 17 頁,「合併到主線」)。

應該強調的是,這種層級純粹是**非正式的**。Git 中沒有任何根本性的東西會強制執行此層級所暗示的「修補程式流程鏈」。您不必僅從一個遠端儲存庫拉取。

「專案負責人」的建議工作流程如下

  1. 在您的本地機器上準備您的主要儲存庫。您的工作在那裡完成。

  2. 準備一個其他人可以存取的公開儲存庫。

    如果其他人透過啞傳輸協定 (HTTP) 從您的儲存庫拉取,您需要保持此儲存庫 *與啞傳輸相容*。在 `git init` 之後,從標準範本複製的 `$GIT_DIR/hooks/post-update.sample` 將包含對 *git update-server-info* 的呼叫,但您需要使用 `mv post-update.sample post-update` 手動啟用 hook。這可確保 *git update-server-info* 使必要的檔案保持最新。

  3. 從您的主要儲存庫推送到公開儲存庫。

  4. *git repack* 公開儲存庫。這會建立一個大型 pack,其中包含初始物件集作為基準,並且如果用於從您的儲存庫拉取的傳輸支援打包的儲存庫,則可能會 *git prune*。

  5. 繼續在您的主要儲存庫中工作。您的變更包括您自己的修改、您透過電子郵件收到的修補程式,以及從拉取「子系統維護者」的「公開」儲存庫所產生的合併。

    您可以隨時重新打包此私有儲存庫。

  6. 將您的變更推送到公開儲存庫,並向大眾宣佈。

  7. 每隔一段時間,*git repack* 公開儲存庫。回到步驟 5,然後繼續工作。

「子系統維護者」在該專案上工作並擁有自己的「公開儲存庫」的建議工作週期如下

  1. 透過在「專案負責人」的公開儲存庫上執行 *git clone* 來準備您的工作儲存庫。用於初始複製的 URL 儲存在 remote.origin.url 設定變數中。

  2. 準備一個其他人可以存取的公開儲存庫,就像「專案負責人」所做的那樣。

  3. 將打包的檔案從「專案負責人」的公開儲存庫複製到您的公開儲存庫,除非「專案負責人」的儲存庫與您的儲存庫位於同一機器上。在後一種情況下,您可以使用 `objects/info/alternates` 檔案指向您正在借用的儲存庫。

  4. 從您的主要儲存庫推送到公開儲存庫。執行 *git repack*,如果用於從您的儲存庫拉取的傳輸支援打包的儲存庫,則可能會執行 *git prune*。

  5. 繼續在您的主要儲存庫中工作。您的變更包括您自己的修改、您透過電子郵件收到的修補程式,以及從拉取「專案負責人」和您可能的「子子系統維護者」的「公開」儲存庫所產生的合併。

    您可以隨時重新打包此私有儲存庫。

  6. 將您的變更推送到您的公開儲存庫,並要求您的「專案負責人」以及您可能的「子子系統維護者」從中拉取。

  7. 每隔一段時間,*git repack* 公開儲存庫。回到步驟 5,然後繼續工作。

沒有「公開」儲存庫的「個別開發人員」的建議工作週期有些不同。如下所示

  1. 透過 *git clone*「專案負責人」(或者如果您在子系統上工作,則為「子系統維護者」)的公開儲存庫來準備您的工作儲存庫。用於初始複製的 URL 儲存在 remote.origin.url 設定變數中。

  2. 在您的儲存庫中的 *master* branch 上完成您的工作。

  3. 每隔一段時間從您上游的公開儲存庫執行 `git fetch origin`。這只執行 `git pull` 的前半部分,但不執行合併。公開儲存庫的 head 儲存在 `.git/refs/remotes/origin/master` 中。

  4. 使用 `git cherry origin` 來查看您的哪些修補程式被接受,和/或使用 `git rebase origin` 將您未合併的變更向前移植到更新的上游。

  5. 使用 `git format-patch origin` 來準備用於提交到您的上游的電子郵件修補程式並將其發送出去。回到步驟 2 並繼續。

與他人協作,共用儲存庫樣式

如果您有 CVS 背景,則上一節中建議的協作樣式對您來說可能是新的。您不必擔心。Git 也支援您可能更熟悉的「共用公開儲存庫」協作樣式。

有關詳細資訊,請參閱 gitcvs-migration[7]

將您的工作捆綁在一起

您很可能需要同時處理多項任務。使用 Git 的分支可以輕鬆管理這些或多或少獨立的任務。

我們之前已經看過分支如何運作,使用「樂趣和工作」的範例來說明兩個分支。如果有兩個以上的分支,概念也是相同的。假設您從「master」頭開始,並且在「master」分支中有一些新的程式碼,以及在「commit-fix」和「diff-fix」分支中有兩個獨立的修復。

$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Release candidate #1
---
 +  [diff-fix] Fix rename detection.
 +  [diff-fix~1] Better common substring algorithm.
+   [commit-fix] Fix commit message normalization.
  * [master] Release candidate #1
++* [diff-fix~2] Pretty-print messages.

兩個修復都經過充分測試,此時您想要將它們合併。您可以先合併 diff-fix,然後再合併 commit-fix,如下所示

$ git merge -m "Merge fix in diff-fix" diff-fix
$ git merge -m "Merge fix in commit-fix" commit-fix

結果會是

$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Merge fix in commit-fix
---
  - [master] Merge fix in commit-fix
+ * [commit-fix] Fix commit message normalization.
  - [master~1] Merge fix in diff-fix
 +* [diff-fix] Fix rename detection.
 +* [diff-fix~1] Better common substring algorithm.
  * [master~2] Release candidate #1
++* [master~3] Pretty-print messages.

然而,當您擁有一組真正獨立的變更時(如果順序很重要,那麼它們在定義上就不是獨立的),沒有任何理由必須先合併某個分支,再合併另一個分支。您可以一次將這兩個分支合併到目前的分支中。首先,讓我們還原剛才的操作並重新開始。我們會希望透過將其重置為 master~2 來取得這兩個合併之前的 master 分支

$ git reset --hard master~2

您可以確保 git show-branch 符合您剛才執行這兩個 git merge 之前的狀態。然後,您可以合併這兩個分支頭,而不是連續執行兩個 git merge 命令(這被稱為「建立八爪魚」)

$ git merge commit-fix diff-fix
$ git show-branch
! [commit-fix] Fix commit message normalization.
 ! [diff-fix] Fix rename detection.
  * [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
---
  - [master] Octopus merge of branches 'diff-fix' and 'commit-fix'
+ * [commit-fix] Fix commit message normalization.
 +* [diff-fix] Fix rename detection.
 +* [diff-fix~1] Better common substring algorithm.
  * [master~1] Release candidate #1
++* [master~2] Pretty-print messages.

請注意,您不應該只是因為可以就執行八爪魚合併。八爪魚合併是有效的操作,如果同時合併兩個以上的獨立變更,通常可以更容易地檢視提交歷史記錄。但是,如果您在合併的任何分支中發生合併衝突並且需要手動解決,這表示那些分支中的開發並非完全獨立,您應該一次合併兩個分支,記錄您如何解決衝突,以及您偏好其中一方變更的原因。否則,這將使專案歷史記錄更難追蹤,而不是更容易。

GIT

屬於 git[1] 套件的一部分

scroll-to-top