簡體中文 ▾ 主題 ▾ 最新版本 ▾ gitcore-tutorial 最後更新於 2.43.1

名稱

gitcore-tutorial - 面向開發者的 Git 核心教程

概要

git *

描述

本教程解釋瞭如何使用“核心” Git 命令來設定和使用 Git 倉庫。

如果你只需要將 Git 作為修訂控制系統使用,你可能更傾向於從“Git 入門教程” (gittutorial[7]) 或 Git 使用者手冊開始。

然而,如果你想了解 Git 的內部機制,理解這些底層工具會有所幫助。

核心 Git 通常被稱為“plumbing”(管道),而頂部的更美觀的使用者介面稱為“porcelain”(瓷器)。你可能不常直接使用 plumbing,但瞭解 plumbing 的作用,在 porcelain 不好用時會有用。

在本檔案最初編寫時,許多 porcelain 命令是 shell 指令碼。為簡化起見,它仍然將它們作為示例,來說明 plumbing 如何組合形成 porcelain 命令。原始碼樹在 contrib/examples/ 中包含了一些這樣的指令碼作為參考。儘管這些不再以 shell 指令碼實現,但 plumbing 層命令的作用的描述仍然有效。

注意
更深入的技術細節通常標記為“註釋”,你可以在初讀時跳過。

建立 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 開發分支的提交。你還沒有建立開始你的 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 標誌將差異輸出為一個補丁。

$ 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 新增另一行所引起的更改的 diff。

換句話說,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 在索引中知道的檔案,並將它們提交為一個真正的樹。我們分兩個階段完成:建立物件,並將該物件提交為一個提交物件,同時附帶關於該樹的解釋,以及我們如何達到該狀態的資訊。

建立樹物件很簡單,透過 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 通常接受幾個引數——它想知道提交是什麼,但由於這是這個新倉庫中的第一個提交,它沒有父提交,我們只需要傳遞樹的物件名。然而,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 diff-files -p 在我們的 git-tutorial 專案中,我們仍然會看到上次看到的相同差異:索引檔案沒有因為提交任何內容而改變。但是,既然我們已經提交了東西,我們也可以學習使用一個新命令:git diff-index

與顯示索引檔案和工作樹之間差異的 git diff-files 不同,git diff-index 顯示已提交的與索引檔案或工作樹之間的差異。換句話說,git diff-index 需要一個樹來進行 diff,而在我們提交之前,我們無法這樣做,因為我們沒有什麼可以進行 diff。

但現在我們可以這樣做。

$ 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 仍然顯示當前狀態與我們提交的狀態不同。事實上,現在git diff-index 顯示的差異與我們是否使用 --cached 標誌無關,因為現在索引與工作樹是一致的。

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

$ git commit

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

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

你現在已經完成了你的第一個真正的 Git 提交。如果你有興趣看看 git commit 實際上做了什麼,請隨時調查:它是一些非常簡單的 shell 指令碼來生成有用的(?)提交訊息頭,以及一些實際進行提交的單行命令(git commit)。

檢查更改

雖然建立更改很有用,但如果以後能夠知道什麼改變了,那會更有用。最有用的命令是另一個diff系列,即 git diff-tree

git diff-tree 可以給定兩個任意的樹,它會告訴你它們之間的差異。或許更常見的是,你可以只給它一個提交物件,它會自己找出該提交的父提交,並直接顯示差異。因此,為了得到我們已經見過幾次相同的 diff,我們現在可以這樣做。

$ 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 標誌,它告訴它也顯示提交訊息、作者和日期,你可以告訴它顯示一系列 diff。或者,你可以告訴它“靜默”,不顯示 diff,只顯示實際的提交訊息。

事實上,結合 git rev-list 程式(它生成修訂列表),git diff-tree 最終成為一個真正的變化源泉。你可以透過一個簡單的指令碼來模擬 git log, git 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

來 diff 你當前的狀態與那個標籤,此時它明顯是一個空 diff,但如果你繼續開發和提交內容,你可以將你的標籤用作一個“錨點”,以檢視自你打標籤以來發生了什麼變化。

“帶註釋”標籤實際上是一個真正的 Git 物件,它不僅包含指向你想要標記的狀態的指標,還包含一個小的標籤名稱和訊息,以及可選的 PGP 簽名,表明是的,你確實進行了這個標記。你使用 git tag-a-s 標誌來建立這些帶註釋的標籤。

$ git tag -s <tagname>

這將會對當前的 HEAD 進行簽名(但你也可以給它一個指定要標記的物件的引數,例如,你可以透過使用 git tag <tagname> mybranch 來標記當前的 mybranch 點)。

你通常只為主要版本釋出或類似的事情進行簽名標籤,而輕量級標籤對於你想要進行的任何標記都很有用——任何時候你決定要記住某個點,只需為它建立一個私有標籤,你就可以有一個漂亮的符號名稱來表示那個點的狀態。

複製倉庫

Git 倉庫通常是完全自給自足且可移動的。與 CVS 等不同,沒有“倉庫”和“工作樹”的獨立概念。一個 Git 倉庫通常就是工作樹,本地 Git 資訊隱藏在 .git 子目錄中。沒有其他了。你看到的(what you see)就是你得到的(what you got)。

注意
你可以告訴 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

    以確保索引檔案是最新的。

請注意,第二點即使在不同機器之間也成立。你可以使用任何常規的複製機制,無論是 scprsync 還是 wget,來複制一個遠端 Git 倉庫。

複製遠端倉庫時,你至少需要在這樣做時更新索引快取,特別是對於別人的倉庫,你通常想確保索引快取處於某個已知狀態(你不知道他們做了什麼並且還沒有簽入),所以通常你會先執行一個。

$ 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] 提交是 master 和 mybranch 尖端的共同祖先。有關詳細資訊,請參閱 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 登入許可權。它透過交換雙方都擁有的 HEAD 提交來找出對方缺少哪些物件,並傳輸(接近)最小數量的物件。這是儲存庫之間交換 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 準備您的儲存庫,以幫助啞傳輸下載器。

從遠端儲存庫獲取後,您將其 merge 到當前分支。

然而——fetch 然後立即 merge 是一個非常普遍的事情,所以它被稱為 git pull,您只需執行

$ git pull <remote-repository>

並可以選擇將遠端端的提交名稱作為第二個引數。

注意
您可以透過保留任意數量的本地儲存庫作為分支,並使用 git pull 在它們之間進行合併,而無需使用任何分支。這種方法的優點是它可以讓您簽出每個 branch 的一組檔案,並且如果您同時處理多條開發線,可能會發現更容易來回切換。當然,您將為儲存多個工作樹付出更多的磁碟空間,但如今磁碟空間很便宜。

您可能需要不時地從同一個遠端儲存庫拉取。作為一種簡寫,您可以將遠端儲存庫 URL 儲存在本地儲存庫的配置檔案中,如下所示

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

並在 git pull 中使用“linus”關鍵字而不是完整的 URL。

示例。

  1. git pull linus

  2. git pull linus tag v0.99.1

以上相當於

  1. git pull https://kernel.linux.club.tw/pub/scm/git/git.git/ HEAD

  2. git pull https://kernel.linux.club.tw/pub/scm/git/git.git/ tag v0.99.1

合併是如何工作的?

我們說本教程展示了“plumbing”(底層命令)如何幫助您處理“porcelain”(高階命令),但到目前為止我們還沒有談論合併實際上是如何工作的。如果您是第一次遵循本教程,我建議跳到“釋出您的工作”部分,稍後再回來。

好的,還在跟著嗎?為了舉例說明,讓我們回到之前帶有“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 HEAD 位於“Some fun.”提交,而我們的 mybranch HEAD 位於“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)

該命令將共同祖先的提交物件名稱寫入標準輸出,因此我們將其輸出捕獲到一個變數中,因為我們將在下一步中使用它。順便說一句,在這種情況下,共同祖先提交是“Initial commit”提交。您可以透過以下方式辨別

$ 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 看到的內容是從階段 2(即您的版本)開始的差異。

釋出您的工作

因此,我們可以使用來自遠端儲存庫的其他人工作,但**您**如何準備一個儲存庫以供其他人從中拉取?

您在工作樹中進行實際工作,您的主儲存庫作為其 .git 子目錄掛在其下方。您可以使該儲存庫在遠端可訪問,並要求人們從中拉取,但在實踐中,事情通常不是這樣做的。推薦的方法是有一個公共儲存庫,使其可供其他人訪問,並在您在主工作樹中所做的更改準備就緒時,從它更新公共儲存庫。這通常稱為推送

注意
此公共儲存庫可以進一步映象,Git 儲存庫在 kernel.org 上的管理方式就是如此。

將更改從您的本地(私有)儲存庫釋出到您的遠端(公共)儲存庫需要遠端機器上的寫入許可權。您需要有一個 SSH 帳戶才能執行一個命令,git-receive-pack

首先,您需要在遠端機器上建立一個空的儲存庫,該儲存庫將存放您的公共儲存庫。稍後將透過推送到該儲存庫來填充和更新此空儲存庫。顯然,此儲存庫建立只需要執行一次。

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

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

$ mkdir my-git.git

然後,透過執行 git init 將該目錄變成一個 Git 儲存庫,但這次,由於其名稱不是通常的 .git,我們進行的操作略有不同

$ GIT_DIR=my-git.git git init

確保此目錄可供您希望透過您選擇的傳輸方式拉取更改的其他人訪問。您還需要確保 $PATH 中有 git-receive-pack 程式。

注意
許多 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

這會將您的公共儲存庫與命名的分支頭(即此處的 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-*.packpack-*.idx。它們彼此密切相關,如果您出於任何原因手動將它們複製到不同的儲存庫,則應確保將它們一起復制。前者包含打包物件的所有資料,後者包含用於隨機訪問的索引。

如果您偏執,執行 git verify-pack 命令可以檢測到您是否有損壞的包,但不要太擔心。我們的程式總是完美的 ;-)。

打包物件後,您就不必再保留包含在包檔案中的未打包物件了。

$ git prune-packed

將為您刪除它們。

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

注意
git pull 對於 HTTP 傳輸來說有點麻煩,因為打包的儲存庫可能包含相對較少但體積相對較大的包中的物件。如果您期望從您的公共儲存庫進行大量 HTTP 拉取,您可能需要經常重新打包和修剪,或者從不。

如果您再次執行 git repack,它會說“沒有新內容可打包”。一旦您繼續開發並積累更改,再次執行 git repack 將建立一個新包,其中包含自上次打包儲存庫以來建立的物件。我們建議您在初始匯入後不久打包您的專案(除非您從頭開始您的專案),然後不時執行 git repack,具體取決於您的專案活躍度。

當透過 git pushgit pull 同步儲存庫時,源儲存庫中打包的物件通常在目標儲存庫中以未打包的形式儲存。雖然這允許您在兩端使用不同的打包策略,但這也意味著您可能需要不時地重新打包兩個儲存庫。

與他人合作

儘管 Git 是一個真正分散式的系統,但通常方便地以非正式的開發者層次結構來組織您的專案。Linux 核心開發就是這樣進行的。在 Randy Dunlap 的簡報(第 17 頁,“Merges to Mainline”)中有一個很好的插圖。

應該強調的是,這種層次結構是純粹**非正式的**。Git 中沒有任何根本性的東西強制執行這種層次結構所暗示的“補丁流鏈”。您不必只從一個遠端儲存庫拉取。

對“專案負責人”推薦的工作流程如下:

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

  2. 準備一個他人可以訪問的公共儲存庫。

    如果其他人透過啞傳輸協議(HTTP)從您的儲存庫拉取,您需要使該儲存庫相容啞傳輸。在 git init 之後,從標準模板複製的 $GIT_DIR/hooks/post-update.sample 將包含對 git update-server-info 的呼叫,但您需要手動啟用該掛鉤,方法是 mv post-update.sample post-update。這確保 git update-server-info 保持必要的檔案最新。

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

  4. git repack 公共儲存庫。這將建立一個包含初始物件集作為基線的大的包,如果用於從您的儲存庫拉取的傳輸支援打包儲存庫,則可能還會進行 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 分支上進行工作。

  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 的分支可以輕鬆管理這些或多或少獨立任務。

我們已經透過“fun and work”示例使用兩個分支看到了分支是如何工作的。如果有兩個以上的分支,想法是一樣的。假設您從“master”HEAD 開始,在“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 重置到 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] 套件的一部分