iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Software Development

深入一點點認識 Git系列 第 23

Day 23-深入一點點認識 Git:合併與合併衝突時,Git 內部是怎麼運作的?

  • 分享至 

  • xImage
  •  

要展現 git 的協作功能,則不能不提其開分支、合併分支的機制。之前我們在 Day 9 提過,分支的本質是「參考(ref)」,那合併時,這些參考是怎麼跑的?新的 commit 物件內容是什麼?commit 又指著哪些 tree 跟 blob 物件呢?如果發生衝突,內部又是怎麼變化的呢?

這篇文章將在「無衝突」與「有衝突」兩種情境下,細細檢視合併的每個過程中,.git/ 資料夾發生的變化。

無衝突的合併

首先在 main 分支上,建立第一個 commit:

touch file.txt
git add file.txt
git commit -m "Initial commit"

接著建立並切換到新分支 feature

git switch -c feature

在這裡建立一個新的 commit:

echo "Hello, world" > hello_world.txt
git add hello_world.txt
git commit -m "Add file on feature"

再切回 main 分支:

git switch main

main 分支建立 commit:

echo "Main content" > file.txt
git add file.txt
git commit -m "Revise content in file.txt"

目前 .git/ 資料夾結構如下:

.git/
├── ...
│
├── objects/
│   ├── 22/                             # Initial commit物件
│   │   └── 044b2153b50be5e131579d1a25008737c4b7d4
│   ├── 28/                             # 指向ccc05fb...的tree物件
│   │   └── 96f069a49567bc3b9a15b2a946754d63b2c06d
│   ├── a5/                             # 內容為Hello, world的blob物件
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── bd/                             # 指向e69de29...的tree物件
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── c9/                             # main分支的第二個commit物件
│   │   └── 30e9edfb1c6372e197b1260544ea213b93d7e7
│   ├── cc/                             # 內容為Main content的blob物件
│   │   └── c05fb42ee018ccbd119d1b9e8f47372f530632
│   ├── e6/                             # 空的blob物件
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── e9/                             # feature分支的commit物件
│   │   └── 9a7c7249b6729a6a122ae306187cc793d43d72
│   ├── fc/                             # 指向e69de29...與a5c1966...的tree物件
│   │   └── 153946417df4388babc50d03074899baf7eae7
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 指向e99a7c7249b6729a6a122ae306187cc793d43d72
│   │   └── main    # 指向c930e9edfb1c6372e197b1260544ea213b93d7e7
│   └── tags/
│
├── ...
└── index

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513gA6cul90tD.png

如果這時要把 feature 分支合併進 main 分支,就在 main 分支上輸入以下指令:

git merge feature

這時會跳出一個編輯器,內容為:

Merge branch 'feature'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

上面的 "Merge branch 'feature'" 為預設的 commit 訊息,而若觀察 .git/ 資料夾,會發現變這樣:

.git/
├── ...
│
├── objects/
│   ├── 22/
│   │   └── 044b2153b50be5e131579d1a25008737c4b7d4
│   ├── 28/
│   │   └── 96f069a49567bc3b9a15b2a946754d63b2c06d
│   ├── 31/             # 新增的tree物件,指向ccc05fb...與a5c1966...
│   │   └── 47c1ebd37d3dbf87ee4d67cc95657dfffeed34
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── bd/
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── c9/
│   │   └── 30e9edfb1c6372e197b1260544ea213b93d7e7
│   ├── cc/
│   │   └── c05fb42ee018ccbd119d1b9e8f47372f530632
│   ├── e6/
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── e9/
│   │   └── 9a7c7249b6729a6a122ae306187cc793d43d72
│   ├── fc/
│   │   └── 153946417df4388babc50d03074899baf7eae7
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 仍指向e99a7c7249b6729a6a122ae306187cc793d43d72
│   │   └── main    # 仍指向c930e9edfb1c6372e197b1260544ea213b93d7e7
│   └── tags/
│
├── AUTO_MERGE     # 新增檔案,內容為3147c1ebd37d3dbf87ee4d67cc95657dfffeed34
├── COMMIT_EDITMSG # 內容仍為Revise content in file.txt
├── config         # 舊有檔案
├── description    # 舊有檔案
├── HEAD           # 內容仍為ref: refs/heads/main
├── index # 內容為100644 ccc05fb42ee018ccbd119d1b9e8f47372f530632 0 file.txt
│         # 以及100644 a5c19667710254f835085b99726e523457150e03 0 hello_world.txt
├── MERGE_HEAD     # 內容為e99a7c7249b6729a6a122ae306187cc793d43d72
├── MERGE_MODE     # 內容為空
├── MERGE_MSG      # 內含上述編輯器內容
└── ORIG_HEAD      # 內容為c930e9edfb1c6372e197b1260544ea213b93d7e7

這時主要有兩大變化:

  1. 新的 tree 物件 3147c1e... 已經被做出來,指向兩個要被合併分支指向的 blob 物件,同時被 AUTO_MERGE 參考指著。
  2. 多一個 MERGE_HEAD 參考,指向要被合併進來的 commit。

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250923/201785138qAHZ6Jn2C.png

待編輯完合併的 commit 訊息,再關閉編輯器後,終端機畫面如下:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513C107JIltJ0.png

接著再來觀察 .git/ 資料夾:

.git/
├── ...
│
├── objects/
│   ├── 22/
│   │   └── 044b2153b50be5e131579d1a25008737c4b7d4
│   ├── 28/
│   │   └── 96f069a49567bc3b9a15b2a946754d63b2c06d
│   ├── 31/      # 剛剛新增的tree物件,指向ccc05fb...與a5c1966...
│   │   └── 47c1ebd37d3dbf87ee4d67cc95657dfffeed34
│   ├── a1/      # 合併後新增的commit物件
│   │   └── 9a784cc3b6915bd33d1e0f768c8c79c69bf001
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── bd/
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── c9/
│   │   └── 30e9edfb1c6372e197b1260544ea213b93d7e7
│   ├── cc/
│   │   └── c05fb42ee018ccbd119d1b9e8f47372f530632
│   ├── e6/
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── e9/
│   │   └── 9a7c7249b6729a6a122ae306187cc793d43d72
│   ├── fc/
│   │   └── 153946417df4388babc50d03074899baf7eae7
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 仍指向e99a7c7249b6729a6a122ae306187cc793d43d72
│   │   └── main    # 改指向a19a784cc3b6915bd33d1e0f768c8c79c69bf001
│   └── tags/
│
├── AUTO_MERGE     # 檔案消失
├── COMMIT_EDITMSG # 內容仍為Revise content in file.txt
├── config         # 舊有檔案
├── description    # 舊有檔案
├── HEAD           # 內容仍為ref: refs/heads/main
├── index # 內容仍為100644 ccc05fb42ee018ccbd119d1b9e8f47372f530632 0 file.txt
│         # 以及100644 a5c19667710254f835085b99726e523457150e03 0 hello_world.txt
├── MERGE_HEAD     # 檔案消失
├── MERGE_MODE     # 檔案消失
├── MERGE_MSG      # 檔案消失
└── ORIG_HEAD      # 內容仍為c930e9edfb1c6372e197b1260544ea213b93d7e7

合併完後,出現兩大變化:

  1. 出現新的 commit a19a784...,main 分支指著它、它指向剛剛新做出來的 3147c1e... 物件。
  2. 合併過程中出現的檔案 AUTO_MERGEMERGE_HEADMERGE_MODEMERGE_MSG 全部消失。

此時 git 物件結構如下:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513UsGNkR4hSv.png

有衝突的合併

在一個空的資料夾中,先在 main 分支建立第一個 commit:

touch file.txt
git add file.txt
git commit -m "Initial commit"

接著建立並切換到 feature 分支:

git switch -c feature

feature 分支也建立一個 commit:

echo "Feature version" > file.txt
git add file.txt
git commit -m "Add feature version"

接著切回 main 分支:

git switch main

main 分支上,建立一份檔名同為 file.txt 的檔案,但內容與 feature 分支不同:

echo "Main version" > file.txt
git add file.txt
git commit -m "Add main version"

這時候 .git/ 資料夾長什麼模樣呢?

.git/
├── ...
│
├── objects/
│   ├── 3c/                                         # feature分支的commit物件
│   │   └── 42ca7eee01372a11e05c88db7198ab7be57df0
│   ├── 8f/                                         # 指向dbc69068...的tree物件
│   │   └── 3e35b1bef33d7af8f47a872d0c938ac2409932
│   ├── 26/                                         # main分支的第二個commit物件
│   │   └── 6bff847229de5ca95fd459d364f95611f96779
│   ├── 50/                                         # 內容為Main version的blob物件
│   │   └── 55b9198f7069db185ec0f12534e44bfd5c9623
│   ├── bd/                                         # 指向e69de29...的blob物件
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── db/                                         # 內容為Feature version的blob物件
│   │   └── c69068b98a226af5471ad0543d5ad3316b4cb7
│   ├── e3/                                         # main分支第一個commit物件
│   │   └── 27f3b729ac07053557bb5e98de113ec3fbdd3e
│   ├── e6/                                         # 內容為空的blob物件
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── f8/                                         # 指向5055b91的tree物件
│   │   └── f64737b18c5aff3c7a0a488d5d1d2d8f36ad2f
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 指向3c42ca7eee01372a11e05c88db7198ab7be57df0
│   │   └── main    # 指向266bff847229de5ca95fd459d364f95611f96779
│   └── tags/
│
├── ...
└── index

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513kJwUghBydS.png

如果我們一樣在 main 分支輸入以下指令:

git merge feature

從終端機可以看出有衝突:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513BHU0uh2Cin.png

在編輯器中,也可以看出要我們解衝突:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513Kpkkm1FYY2.png

那此時 .git/ 資料夾長怎樣呢?

.git/
├── ...
│
├── objects/
│   ├── 3b/            # 新增指向a3a09d6...的tree物件
│   │   └── 655319debc05c2433eba45fa9dfc39791759af
│   ├── 3c/
│   │   └── 42ca7eee01372a11e05c88db7198ab7be57df0
│   ├── 8f/
│   │   └── 3e35b1bef33d7af8f47a872d0c938ac2409932
│   ├── 26/
│   │   └── 6bff847229de5ca95fd459d364f95611f96779
│   ├── 50/
│   │   └── 55b9198f7069db185ec0f12534e44bfd5c9623
│   ├── a3/             # 編輯器顯示衝突內容,為blob物件
│   │   └── a09d68cc1023eccc8cd0cad04a3ef8fbd483cb
│   ├── bd/
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── db/
│   │   └── c69068b98a226af5471ad0543d5ad3316b4cb7
│   ├── e3/
│   │   └── 27f3b729ac07053557bb5e98de113ec3fbdd3e
│   ├── e6/
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── f8/
│   │   └── f64737b18c5aff3c7a0a488d5d1d2d8f36ad2f
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 仍指向3c42ca7eee01372a11e05c88db7198ab7be57df0
│   │   └── main    # 仍指向266bff847229de5ca95fd459d364f95611f96779
│   └── tags/
│
├── AUTO_MERGE      # 內容為3b655319debc05c2433eba45fa9dfc39791759af
├── COMMIT_EDITMSG  # 內容仍為Add main version
├── config          # 舊有檔案
├── description     # 舊有檔案
├── HEAD            # 內容仍為ref: refs/heads/main
├── index  # 內容為100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 file.txt
│          # 與100644 5055b9198f7069db185ec0f12534e44bfd5c9623 2 file.txt
│          # 以及100644 dbc69068b98a226af5471ad0543d5ad3316b4cb7 3 file.txt
├── MERGE_HEAD      # 內容為3c42ca7eee01372a11e05c88db7198ab7be57df0
├── MERGE_MODE      # 內容為空
├── MERGE_MSG       # 內含為 Merge branch 'feature'
│                   #       Conflicts:
│                   #        file.txt
└── ORIG_HEAD       # 內容為266bff847229de5ca95fd459d364f95611f96779

圖示如下,可發現跟沒有衝突時相比,右上角多了一個 blob 物件,內容即為兩種版本;預存區 index 則包含要合併的兩分支所有 blob 物件:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513vdIwtsekTL.png

假設我們選 Accept Both Changes,並把檔案加進預存區:

git add file.txt

這時 .git/ 資料夾檔案變成:

.git/
├── ...
│
├── objects/
│   ├── 2b/          # 新增的blob物件,包含處理衝突完後的檔案內容
│   │   └── 557f90890804e02656db31b7f27863fb81d10a
│   ├── 3b/
│   │   └── 655319debc05c2433eba45fa9dfc39791759af
│   ├── 3c/
│   │   └── 42ca7eee01372a11e05c88db7198ab7be57df0
│   ├── 8f/
│   │   └── 3e35b1bef33d7af8f47a872d0c938ac2409932
│   ├── 26/
│   │   └── 6bff847229de5ca95fd459d364f95611f96779
│   ├── 50/
│   │   └── 55b9198f7069db185ec0f12534e44bfd5c9623
│   ├── a3/
│   │   └── a09d68cc1023eccc8cd0cad04a3ef8fbd483cb
│   ├── bd/
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── db/
│   │   └── c69068b98a226af5471ad0543d5ad3316b4cb7
│   ├── e3/
│   │   └── 27f3b729ac07053557bb5e98de113ec3fbdd3e
│   ├── e6/
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── f8/
│   │   └── f64737b18c5aff3c7a0a488d5d1d2d8f36ad2f
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 仍指向3c42ca7eee01372a11e05c88db7198ab7be57df0
│   │   └── main    # 仍指向266bff847229de5ca95fd459d364f95611f96779
│   └── tags/
│
├── AUTO_MERGE
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── index  # 內容變成100644 2b557f90890804e02656db31b7f27863fb81d10a 0 file.txt
├── MERGE_HEAD
├── MERGE_MODE
├── MERGE_MSG
└── ORIG_HEAD

git add 指令多做出一個 blob 物件,為合併後的內容,此物件已被加到預存區,但還沒有 tree 物件指向它:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513H2l2WjFvzj.png

接著再下指令完成合併、形成 commit:

git commit -m "Merge feature into main"

合併完後,回頭觀察 .git/ 資料夾:

.git/
├── ...
│
├── objects/
│   ├── 2b/
│   │   └── 557f90890804e02656db31b7f27863fb81d10a
│   ├── 3b/
│   │   └── 655319debc05c2433eba45fa9dfc39791759af
│   ├── 3c/
│   │   └── 42ca7eee01372a11e05c88db7198ab7be57df0
│   ├── 6f/                           # 合併的commit物件
│   │   └── 4c29c1822ba090b23e384aa05ff25ecdf26063
│   ├── 8f/
│   │   └── 3e35b1bef33d7af8f47a872d0c938ac2409932
│   ├── 26/
│   │   └── 6bff847229de5ca95fd459d364f95611f96779
│   ├── 50/
│   │   └── 55b9198f7069db185ec0f12534e44bfd5c9623
│   ├── a3/
│   │   └── a09d68cc1023eccc8cd0cad04a3ef8fbd483cb
│   ├── bd/
│   │   └── d68b0120ca91384c1606468b4ca81b8f67c728
│   ├── db/
│   │   └── c69068b98a226af5471ad0543d5ad3316b4cb7
│   ├── e3/
│   │   └── 27f3b729ac07053557bb5e98de113ec3fbdd3e
│   ├── e6/
│   │   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
│   ├── ef/                           # 指向2b557f9...的tree物件
│   │   └── 210594c4614c494768ea3327ec4f2bab0137a4
│   ├── f8/
│   │   └── f64737b18c5aff3c7a0a488d5d1d2d8f36ad2f
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 仍指向3c42ca7eee01372a11e05c88db7198ab7be57df0
│   │   └── main    # 改指向6f4c29c1822ba090b23e384aa05ff25ecdf26063
│   └── tags/
│
├── COMMIT_EDITMSG  # 最新的commit訊息Merge feature into main
├── config
├── description
├── HEAD           # 仍為ref: refs/heads/main
├── index          # 100644 2b557f90890804e02656db31b7f27863fb81d10a 0 file.txt
└── ORIG_HEAD      # 266bff847229de5ca95fd459d364f95611f96779

圖示如下,可發現在隨著合併的 commit 建立,對應的 tree 與 blob 物件也都跟著出現;至於在 git add 階段出現的 blob 與 tree 物件,就沒有 commit 指向它:
https://ithelp.ithome.com.tw/upload/images/20250923/20178513HfJNkXyOVM.png

此圖示也可以搭配 git log --onelinegit log --oneline --graph 結果觀察、理解:
https://ithelp.ithome.com.tw/upload/images/20250923/2017851307bTMv9hgQ.png

  • 關於 git log --oneline --graph 小記
    我之前在看 git log --oneline --graph 時常常看不懂分支與 commit 間的關係,以下說明:
# 每顆星星代表其右邊對應的commit
# 左邊直線(終端機是先綠再紅線)是main分支
# 右邊拉出來(終端機是全綠線)再收回去是feature分支

*   6f4c29c (HEAD -> main) Merge feature into main   # HEAD指向main分支,而main分支最新的commit是這個
|\                                                   # feature分支合併到main分支
| * 3c42ca7 (feature) Add feature version            # feature分支最新的commit是這個
* | 266bff8 Add main version                         # 此commit對應的星星在左邊那條線,而左邊那條線是main分支,表示此commit在main分支上
|/                                                   # 從main分支開出一條feature分支
* e327f3b Initial commit                             # 兩分支共同祖先commit

是不是相當於圖示中只保留 commit 物件的樣子?(不過我把紅色的 main 分支畫到右邊、綠色的 feature 分支畫到左邊)
https://ithelp.ithome.com.tw/upload/images/20250923/20178513oQMbrkJy84.png

  • 補充:選擇 Accept Current ChangeAccept Incoming Change
    如果在合併衝突時選其中一分支的版本,則:
  1. git add 後,也會出現過渡的 tree 與 blob 物件。
  2. git commit 後,出現的新 commit 指向既有的 tree 物件。

例如若選 Accept Current Change 表示選「現在所在分支 main」的版本,合併完後的新 commit 指向原 main 分支最新 commit 指向的 tree(如下圖綠色箭頭);若選 Accept Incoming Change 表示選「合併進來的分支 feature 」版本,合併完後的新 commit 指向原 feature 分支最新 commit 指向的 tree(如下圖藍色箭頭)。

https://ithelp.ithome.com.tw/upload/images/20250923/20178513jjCDoeU5E4.png

小結

合併分支時,git 內部運作如下:

  • 無衝突:形成一個暫時的 tree 物件,tree 物件指向既有要被合併相關的 blob 物件;待合併完成後,新的合併 commit 會指向該新 tree 物件。
  • 有衝突:形成一個暫時的 tree 物件,指向暫時的 blob 物件,blob 物件內容為編輯器中顯示衝突的文字。

在有衝突時,若選擇 Accept Current ChangeAccept Incoming Change,則新 commit 會直接指向指定版本的既有 tree 物件;若選擇 Accept Both Changes,則新 commit 會指向新的 tree、新的 tree 再指向新的 blob 物件,這個新 blob 物件內容即為兩版本合併後的檔案內容。

參考資料

  1. git-merge

上一篇
Day 22-深入一點點認識 Git:為什麼我不再使用 git checkout
系列文
深入一點點認識 Git23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言