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

名稱

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

概要

git *

描述

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

如果您只需要將 Git 用作版本控制系統,您可能更喜歡從“Git 教程簡介” (gittutorial[7]) 或 Git 使用者手冊 開始。

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

Git 的核心通常被稱為“管道”(plumbing),而其上更美觀的使用者介面則被稱為“瓷器”(porcelain)。您可能不常直接使用管道,但當瓷器無法正常工作時,瞭解管道的功能會很有益。

當本文件最初撰寫時,許多瓷器命令都是 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 標誌將差異輸出為補丁

$ 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 在索引中知道的檔案,並將其作為真實的樹提交。我們分兩個階段進行:建立 物件,並將該 物件作為一個 提交 物件進行提交,同時附帶對該樹的解釋以及我們如何達到該狀態的資訊。

建立樹物件很簡單,使用 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-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 標誌來告訴提交,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 內部資訊從它跟蹤的目錄中分離出來,但我們暫時忽略這一點:這不是普通專案的工作方式,它實際上只用於特殊用途。因此,“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

    在新倉庫中,以確保索引檔案是最新的。

請注意,第二點即使跨機器也成立。您可以使用 任何 常規復制機制複製遠端 Git 倉庫,無論是 scprsync 還是 wget

複製遠端倉庫時,您至少需要更新索引快取,尤其是在處理他人倉庫時,您通常希望確保索引快取處於某種已知狀態(您不知道他們做了 什麼 以及尚未檢入什麼),因此通常您會在 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 commit 並帶有 -i 標誌(它告訴 Git 在進行提交時 包含 該檔案以及您迄今為止對索引檔案所做的更改),從而實現了 git update-index hellogit commit 的簡寫。 -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/ 目錄中指定的引用名稱來獲取遠端站點的最頂部提交物件名稱,然後嘗試透過從 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/

並使用“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

合併是如何工作的?

我們說過本教程展示了管道(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 頭部在“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 命令在合併兩個分支時,使用 3 向合併演算法。首先,它找到它們之間的共同祖先。它使用的命令是 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 子目錄掛在其下。您 可以 使該倉庫可遠端訪問並讓人們從中拉取,但實際上通常不是這樣做的。推薦的方法是擁有一個公共倉庫,使其可供其他人訪問,當您在主工作樹中所做的更改狀態良好時,從主倉庫更新公共倉庫。這通常被稱為 推送

注意
這個公共倉庫可以進一步被映象,這就是 kernel.org 上的 Git 倉庫的管理方式。

將更改從您的本地(私有)倉庫釋出到遠端(公共)倉庫需要在遠端機器上擁有寫入許可權。您需要有一個 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 會告訴您倉庫中有多少未打包物件以及它們佔用了多少空間。

注意
對於 HTTP 傳輸,git pull 稍微有些麻煩,因為打包的倉庫中可能在相對較大的包中包含相對較少的物件。如果您預期公共倉庫會有大量 HTTP 拉取,您可能需要經常重新打包和修剪,或者從不這樣做。

如果您此時再次執行 git repack,它會顯示“Nothing new to pack.”(沒有新內容可打包)。一旦您繼續開發並積累更改,再次執行 git repack 將建立一個新的包,其中包含自上次打包倉庫以來建立的物件。我們建議您在首次匯入後(除非您從零開始專案)儘快打包您的專案,然後根據專案的活躍程度,不時執行 git repack

當倉庫透過 git pushgit 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 啟用鉤子。這確保 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 的分支可以輕鬆管理這些或多或少獨立的任務。

我們之前已經透過“趣味與工作”的兩個分支示例瞭解了分支的工作方式。如果有兩個以上的分支,其理念是相同的。假設您從“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