設定和配置
獲取和建立專案
基本快照
分支與合併
共享和更新專案
檢查和比較
打補丁
除錯
電子郵件
外部系統
伺服器管理
指南
管理
底層命令
- 2.43.2 → 2.50.1 無更改
-
2.43.1
2024-02-09
- 2.25.2 → 2.43.0 無更改
-
2.25.1
2020-02-17
- 2.23.1 → 2.25.0 無更改
-
2.23.0
2019-08-16
- 2.16.6 → 2.22.5 無更改
-
2.15.4
2019-12-06
- 2.14.6 無更改
-
2.13.7
2018-05-22
-
2.12.5
2017-09-22
-
2.11.4
2017-09-22
- 2.10.5 無更改
-
2.9.5
2017-07-30
-
2.8.6
2017-07-30
- 2.5.6 → 2.7.6 無變更
-
2.4.12
2017-05-05
- 2.3.10 無更改
-
2.2.3
2015-09-04
- 2.1.4 無更改
-
2.0.5
2014-12-17
描述
本教程解釋瞭如何使用 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
子目錄將包含另外兩個子目錄,分別名為 heads
和 tags
。它們的作用正如其名所示:它們包含對任意數量的不同開發 頭部(亦稱 分支)的引用,以及對您在倉庫中建立的用於命名特定版本的任何 標籤 的引用。
請注意:特殊的 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 實際上總是使用索引進行比較,因此說它將一個樹與工作樹進行比較並不完全準確。特別是,要比較的檔案列表(“元資料”) 始終 來自索引檔案,無論是否使用 這不難理解,只要您意識到 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
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
將您當前的狀態與該標籤進行比較,此時顯然會是一個空差異,但如果您繼續開發和提交內容,您可以使用您的標籤作為“錨點”來檢視自您標記以來發生了什麼變化。
“附註標籤”實際上是一個真實的 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 倉庫,無論是 scp、rsync 還是 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 status 和 git 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
位置建立一個新分支,並切換到該分支。
注意
|
如果您決定在新分支的建立點選擇歷史中不同於當前 $ git switch -c mybranch earlier-commit 它將在較早的提交處建立新分支 |
您可以透過執行以下操作隨時跳回您的原始 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
hello
和 git
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 分支的這些提交。提交日誌訊息前面括號中的字串是您可以用來命名提交的短名稱。在上面的示例中,master 和 mybranch 是分支頭。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
登入遠端機器的許可權。它透過交換雙方擁有的頭部提交來找出對方缺少的物件集合,並傳輸(接近)最小的物件集合。這是迄今為止在倉庫之間交換 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。
示例。
-
git
pull
linus
-
git
pull
linus
tag
v0.99.1
以上等同於
-
git
pull
http://www.kernel.org/pub/scm/git/git.git/
HEAD
-
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-*.pack 和 pack-*.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
push
和 git
pull
同步時,源倉庫中打包的物件通常在目標倉庫中以未打包形式儲存。雖然這允許您在兩端使用不同的打包策略,但這也意味著您可能需要不時地重新打包兩個倉庫。
與他人協作
儘管 Git 是一個真正的分散式系統,但通常以非正式的開發者層級結構組織專案會很方便。Linux 核心開發就是以這種方式執行的。在 Randy Dunlap 的簡報 中有一個很好的插圖(第 17 頁,“合併到主線”)。
應該強調的是,這種層級結構純粹是 非正式的。Git 中沒有根本性的東西來強制執行這種層級結構所暗示的“補丁流鏈”。您不必只從一個遠端倉庫拉取。
“專案負責人”的推薦工作流程如下
-
在您的本地機器上準備您的主倉庫。您的工作在那裡完成。
-
準備一個可供他人訪問的公共倉庫。
如果其他人透過啞傳輸協議(HTTP)從您的倉庫拉取,您需要保持此倉庫 對啞傳輸友好。在
git
init
之後,從標準模板複製的$GIT_DIR/hooks/post-update.sample
將包含對 git update-server-info 的呼叫,但您需要手動使用mv
post-update.sample
post-update
啟用鉤子。這確保 git update-server-info 保持必要的檔案最新。 -
從您的主倉庫推送到公共倉庫。
-
對公共倉庫進行 git repack。這會建立一個大包,其中包含初始物件集作為基線,如果用於從您的倉庫拉取的傳輸支援打包倉庫,則可能進行 git prune。
-
繼續在您的主倉庫中工作。您的更改包括您自己的修改、透過電子郵件收到的補丁,以及從“子系統維護者”的“公共”倉庫拉取而產生的合併。
您可以隨時重新打包此私有倉庫。
-
將您的更改推送到公共倉庫,並向公眾宣佈。
-
每隔一段時間,對公共倉庫進行 git repack。回到步驟 5 並繼續工作。
一個“子系統維護者”在專案上工作並擁有自己的“公共倉庫”的推薦工作週期如下
-
透過對“專案負責人”(如果您在一個子系統上工作,則為“子系統維護者”)的公共倉庫執行 git clone 來準備您的工作倉庫。用於初始克隆的 URL 儲存在 remote.origin.url 配置變數中。
-
準備一個可供他人訪問的公共倉庫,就像“專案負責人”所做的那樣。
-
將“專案負責人”公共倉庫中的打包檔案複製到您的公共倉庫,除非“專案負責人”倉庫與您的倉庫位於同一臺機器上。在後一種情況下,您可以使用
objects/info/alternates
檔案指向您借用物件的倉庫。 -
從您的主倉庫推送到公共倉庫。執行 git repack,如果用於從您的倉庫拉取的傳輸支援打包倉庫,則可能執行 git prune。
-
繼續在您的主倉庫中工作。您的更改包括您自己的修改、透過電子郵件收到的補丁,以及從您的“專案負責人”以及可能從您的“子子系統維護者”的“公共”倉庫拉取而產生的合併。
您可以隨時重新打包此私有倉庫。
-
將您的更改推送到您的公共倉庫,並要求您的“專案負責人”以及可能您的“子子系統維護者”從那裡拉取。
-
每隔一段時間,對公共倉庫進行 git repack。回到步驟 5 並繼續工作。
對於沒有“公共”倉庫的“個人開發者”來說,推薦的工作週期有所不同。它如下所示
-
透過 git clone “專案負責人”(如果您在一個子系統上工作,則為“子系統維護者”)的公共倉庫來準備您的工作倉庫。用於初始克隆的 URL 儲存在 remote.origin.url 配置變數中。
-
在您的倉庫的 master 分支上進行工作。
-
不時地從上游的公共倉庫執行
git
fetch
origin
。這隻執行git
pull
的前半部分,但不進行合併。公共倉庫的 HEAD 儲存在.git/refs/remotes/origin/master
中。 -
使用
git
cherry
origin
檢視您的哪些補丁被接受,和/或使用git
rebase
origin
將您未合併的更改向前移植到更新後的上游。 -
使用
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.
請注意,不要僅僅因為可以就進行章魚式合併。章魚式合併是一種有效的做法,如果你同時合併兩個以上的獨立變更,它通常能讓提交歷史更容易檢視。然而,如果你在合併任何分支時遇到合併衝突,並且需要手動解決,這表明這些分支中的開發畢竟不是獨立的,你應該一次合併兩個,並記錄你如何解決衝突以及為什麼你偏好其中一方的更改。否則,這將使專案歷史更難跟蹤,而不是更容易。