iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Software Development

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

Day 24-深入一點點認識 Git:git rebase 不是直接把一分支上的 commit 搬到另一分支

  • 分享至 

  • xImage
  •  

除了 git merge,另一指令 git rebase 也能把兩分支變成一個分支,單看名稱看起來是「把整段分支基底搬家」,但在 git 內部真的是這樣運作的嗎?讓我們透過實驗觀察。

無衝突的 git rebase

前置準備共包含兩個分支、五個 commit:

  1. main 分支:建立兩個 commit。
  2. feature 分支:從 main 分支的第一個 commit 長出來,共有兩個 commit。

先建立 main 分支的第一個 commit:

echo "Main file 1" > main.txt
git add main.txt
git commit -m "Main first commit"

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

git switch -c feature

feature 分支上,建立第一個 commit:

echo "Feature file 1" > feature.txt
git add feature.txt
git commit -m "Feature first commit"

再做第二個 commit:

echo "Feature file 2" > feature.txt
git add feature.txt
git commit -m "Feature second commit"

接著切回 main 分支:

git switch main

main 分支建立第二個 commit:

echo "Main file 2" > main.txt
git add main.txt
git commit -m "Main second commit"

此時透過 git log --oneline --graph 檢視結構如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513p4LlzA2KoU.png

如過透過觀察 .git/objects 檢視裡面物件的關係:

.git/
├── ...
│
├── objects/
│   ├── 5e/
│   │   └── 6dd6162de52bc17b7e20837a270225e49d8cf3
│   ├── 43/
│   │   └── cea2671514b7a5ea8ad16228e9c6974e2a4cdc
│   ├── 46/
│   │   └── 104debd0c75c4b5e4cb899d993ab6efbb6d838
│   ├── 65/
│   │   └── 39128c8ee3d44d9c946f5726ebd2675d650bf9
│   ├── 93/
│   │   └── e4c61bf80a2edbb68694151e57588ac89b3a0f
│   ├── bf/
│   │   └── 1b2b7c097db735c71b3f7697c223bd0689fbd4
│   ├── c8/
│   │   └── c70c538c9dcdf415e5c2e129f6402cc78e0470
│   ├── ec/
│   │   └── 99b810cadab90d54dae77dd0dba2f04542a801.
│   ├── f1/
│   │   └── 5b8894770fb0a2aaacfa0f0e00918c8d5e4d4c
│   ├── f5/
│   │   ├── 5a28c8d0b0673b9d2bf586190868285d45437e
│   │   └── 7e0404cf93078e485934dd432a57a00c8a1d72
│   ├── fb/
│   │   └── c6bb6631586e946e6fbae7335ffc97f289041a
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 指向43cea2671514b7a5ea8ad16228e9c6974e2a4cdc
│   │   └── main    # 指向f57e0404cf93078e485934dd432a57a00c8a1d72
│   └── tags/
│
├── ...
└── index

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

開始 git rebase

目前 feature 是從 main 分支的第一個 commit 長出來的,如果我們在 feature 分支上,輸入以下指令:

git rebase main

則終端機畫面顯示如下:
https://ithelp.ithome.com.tw/upload/images/20250924/201785132DNZiONsbq.png

這時候如果用 git log --oneline --graph 觀察,則會發現:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513cguvpv0UVh.png

這是什麼意思呢?我們先觀察 .git/ 資料夾有什麼變化?

.git/
├── ...
│
├── objects/
│   ├── 4b/                                          # 新增的tree物件
│   │   └── 3b32745085c22bb2d608456a841c8cdf922675
│   ├── 5e/
│   │   └── 6dd6162de52bc17b7e20837a270225e49d8cf3
│   ├── 43/
│   │   └── cea2671514b7a5ea8ad16228e9c6974e2a4cdc
│   ├── 46/
│   │   └── 104debd0c75c4b5e4cb899d993ab6efbb6d838
│   ├── 65/
│   │   └── 39128c8ee3d44d9c946f5726ebd2675d650bf9
│   ├── 83/                                          # 新增的commit物件
│   │   └── dc248c0cf04a4bc9102ec40653915d4fd03708
│   ├── 90/                                          # 新增的commit物件
│   │   └── 95c9ae890783b921cb7b6faa12e1ffa75580b4
│   ├── 93/
│   │   └── e4c61bf80a2edbb68694151e57588ac89b3a0f
│   ├── bf/
│   │   └── 1b2b7c097db735c71b3f7697c223bd0689fbd4
│   ├── c5/                                          # 新增的tree物件
│   │   └── ee8c5f5dcaae3892f7584ad85439cf0164f6e7
│   ├── c8/
│   │   └── c70c538c9dcdf415e5c2e129f6402cc78e0470
│   ├── ec/
│   │   └── 99b810cadab90d54dae77dd0dba2f04542a801
│   ├── f1/
│   │   └── 5b8894770fb0a2aaacfa0f0e00918c8d5e4d4c
│   ├── f5/
│   │   ├── 5a28c8d0b0673b9d2bf586190868285d45437e
│   │   └── 7e0404cf93078e485934dd432a57a00c8a1d72
│   ├── fb/
│   │   └── c6bb6631586e946e6fbae7335ffc97f289041a
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 指向9095c9ae890783b921cb7b6faa12e1ffa75580b4
│   │   └── main    # 指向f57e0404cf93078e485934dd432a57a00c8a1d72
│   └── tags/
│
├── ...
└── index

多了四個物件、feature 參考指向的 commit 也變成新 commit 9095c9a...,圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513luDASEdlOM.png

main 分支的 commit 不變,但原本在 feature 分支的 commit 都多了新的,且從 main 分支最新的 commit f57e040... 長出來。

feature 分支舊有 commit 相比,新 commit 物件共有以下兩大不同:

  1. commit 時間:為下 git rebase 的時間,比同內容在原 feature 分支建立 commit 的時間晚。
  2. 指向的 tree 物件:是新的,指向兩個 blob 物件,這兩個 blob 物件是在 git rebase 前, main 最新 commit 指向的 tree 所指的 blob 物件,以及 feature 分支原對應 commit 指向的 tree 所指之 blob 物件。

有衝突的 git rebase

現在我們改建立以下結構:

  1. main 分支:共兩個 commit。
  2. feature 分支:從 main 分支的第一個 commit 長出來,共一個 commit。

首先建立 main 分支的第一個 commit:

echo "Original line" > conflict.txt
git add conflict.txt
git commit -m "Base commit"

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

git switch -c feature

feature 分支上建立 commit:

echo "Feature change" > conflict.txt
git add conflict.txt
git commit -m "Feature changes the line"

切回 main 分支:

git switch main

建立 main 分支的第二個 commit:

echo "Main change" > conflict.txt
git add conflict.txt
git commit -m "Main changes the line"

這時候透過 git log --oneline --graph 觀察如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513yUFNpm5IoY.png

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

.git/
├── ...
│
├── objects/
│   ├── 00/
│   │   └── c3c587aea7032edac6c281aded0bf6d1594590
│   ├── 1c/
│   │   └── 3f7cb068aea61861c0e14493f40b0e540a78c1
│   ├── 4d/
│   │   └── 425736e2d7fae050b8ba037ccf2d7cdb269d8d
│   ├── 54/
│   │   └── c3adb4e4011ec9618357a9250bf03500866464
│   ├── 59/
│   │   └── 51ce18fc8c2b28c2ab2ce4580a5f0da72e412a
│   ├── 87/
│   │   └── d242b9844a068fa031abc39544fb6071be48bc
│   ├── 88/
│   │   └── c31fe377f67ab4ea955d208f1b111aecc96680
│   ├── be/
│   │   └── 8db260ba955563834b52cea94755538119866f
│   ├── e6/
│   │   └── f44e94cddafe2ff23f891f03a07f98213f76fc
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   ├── feature # 指向f44e94cddafe2ff23f891f03a07f98213f76fc
│   │   └── main    # 指向1c3f7cb068aea61861c0e14493f40b0e540a78c1
│   └── tags/
│
├── ...
└── index

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250924/201785139OyrXPUTIG.png

在同為 conflict.txt 的檔案中,我們在兩分支上做出不同修改。

這時如果在 feature 分支上,同樣做 git rebase main 會發生什麼事呢?
https://ithelp.ithome.com.tw/upload/images/20250924/20178513aUfCIpZ7kq.png

發生衝突了!現在打開編輯器看看:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513ZbsltG1xMu.png

上面的 Current changemain 分支版本、下面的 Incoming changefeature 分支版本,而在衝突的過程中,.git/ 資料夾結構如下:

.git/
├── ...
│
├── objects/
│   ├── 00/
│   │   └── c3c587aea7032edac6c281aded0bf6d1594590
│   ├── 1c/
│   │   └── 3f7cb068aea61861c0e14493f40b0e540a78c1
│   ├── 4d/
│   │   └── 425736e2d7fae050b8ba037ccf2d7cdb269d8d
│   ├── 54/
│   │   └── c3adb4e4011ec9618357a9250bf03500866464
│   ├── 59/
│   │   └── 51ce18fc8c2b28c2ab2ce4580a5f0da72e412a
│   ├── 73/                                  # 新增的tree物件
│   │   └── 577ca98f7a6d9fb78438a3305b96ceba039e9d
│   ├── 75/                                  # 新增的blob物件
│   │   └── 656995eafcac31fcf89044dd821dc0eefac74a
│   ├── 87/
│   │   └── d242b9844a068fa031abc39544fb6071be48bc
│   ├── 88/
│   │   └── c31fe377f67ab4ea955d208f1b111aecc96680
│   ├── be/
│   │   └── 8db260ba955563834b52cea94755538119866f
│   ├── e6/
│   │   └── f44e94cddafe2ff23f891f03a07f98213f76fc
│   ├── info/
│   └── pack/
│
├── rebase-merge/    # 新增的資料夾
│   ├── author-script
│   ├── done
│   ├── ...
│   └── tags/
│
├── refs/
│   ├── heads/
│   │   ├── feature  # 改為指向00c3c587aea7032edac6c281aded0bf6d1594590
│   │   └── main     # 依舊指向1c3f7cb068aea61861c0e14493f40b0e540a78c1
│   └── tags/
│
├── AUTO-MERGE      # 指向73577ca98f7a6d9fb78438a3305b96ceba039e9d
├── COMMIT_EDITMSG  # 仍為Main changes the line
├── config
├── description
├── HEAD            # 1c3f7cb068aea61861c0e14493f40b0e540a78c1
├── index           # 包含100644 4d425736e2d7fae050b8ba037ccf2d7cdb269d8d 1 conflict.txt
│                   # 與100644 e6f44e94cddafe2ff23f891f03a07f98213f76fc 2 conflict.txt
│                   # 以及100644 be8db260ba955563834b52cea94755538119866f 3 conflict.txt
├── MERGE_MSG       # Feature changes the line
│                   # # Conflicts:
│                   # #       conflict.txt
├── ORIG_HEAD       # 00c3c587aea7032edac6c281aded0bf6d1594590
└── REBASE_HEAD     # 00c3c587aea7032edac6c281aded0bf6d1594590

多了一個 rebase-merge/ 資料夾,包含本次 git rebase 相關資訊,內有:

├── rebase-merge/
│   ├── author-script # GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE
│   ├── done          # pick 00c3c587aea7032edac6c281aded0bf6d1594590 Feature changes the line
│   ├── drop_redundant_commits
│   ├── end
│   ├── git-rebase-todo
│   ├── git-rebase-todo.backup
│   ├── head-name   # refs/heads/feature
│   ├── interactive
│   ├── message    # Feature changes the line
│   ├── msgnum
│   ├── no-reschedule-failed-exec
│   ├── onto      # 搬到1c3f7cb068aea61861c0e14493f40b0e540a78c1這個commit
│   ├── orig-head # 從00c3c587aea7032edac6c281aded0bf6d1594590這個commit
│   ├── patch     # diff --git a/conflict.txt b/conflict.txt相關訊息
│   └── stopped-sha  # 00c3c587aea7032edac6c281aded0bf6d1594590
│

而 .git/ 資料夾結構的圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513tJqmIxN5KQ.png

主要變化包含:

  1. 新的 AUTO_MERGE 參考指向新的 tree 物件,這個 tree 物件指向新的 blob 物件,blob 物件包含編輯器上 conflict.txt 的內容。
  2. REBASE_HEAD 指向要搬走的分支 feature 的最新 commit。
  3. HEAD 指向要被搬到的分支 main 上最新的 commit。

這時我們在編輯器上選 Accept Both Changes,這時 main 分支的內容在上、feature 分支的內容在下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513iiLGwhwDqL.png

再回到終端機,輸入以下指令:

git add conflict.txt

再輸入以下指令:

git rebase --continue

這時會跳出編輯器,要我們輸入本次 rebase 的 commit 訊息如下:

Feature changes the line

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 1c3f7cb
# Last command done (1 command done):
#    pick 00c3c58 Feature changes the line
# No commands remaining.
# You are currently rebasing branch 'feature' on '1c3f7cb'.
#
# Changes to be committed:
# modified:   conflict.txt
#

假設我把 commit 訊息從 Feature changes the line 改成 Rebase feature onto main,再把編輯器關掉,即完成 git rebase
https://ithelp.ithome.com.tw/upload/images/20250924/20178513LZKBeiyhgk.png

這時 .git/ 資料夾結構如下:

.git/
├── ...
│
├── objects/
│   ├── 00/
│   │   └── c3c587aea7032edac6c281aded0bf6d1594590
│   ├── 1c/
│   │   └── 3f7cb068aea61861c0e14493f40b0e540a78c1
│   ├── 3a/                               # 新增的commit物件
│   │   └── d34150719faa29471b6a4198010edd654d7fa8
│   ├── 4d/
│   │   └── 425736e2d7fae050b8ba037ccf2d7cdb269d8d
│   ├── 20/                               # 新增的blob物件
│   │   └── 062a99ded06d0cf8fb98101692435d6aaf98b3
│   ├── 54/
│   │   └── c3adb4e4011ec9618357a9250bf03500866464
│   ├── 58/                               # 新增的tree物件
│   │   └── faf20772daf92b61373db32d9c4e034b3ef645
│   ├── 59/
│   │   └── 51ce18fc8c2b28c2ab2ce4580a5f0da72e412a
│   ├── 73/
│   │   └── 577ca98f7a6d9fb78438a3305b96ceba039e9d
│   ├── 75/
│   │   └── 656995eafcac31fcf89044dd821dc0eefac74a
│   ├── 87/
│   │   └── d242b9844a068fa031abc39544fb6071be48bc
│   ├── 88/
│   │   └── c31fe377f67ab4ea955d208f1b111aecc96680
│   ├── be/
│   │   └── 8db260ba955563834b52cea94755538119866f
│   ├── e6/
│   │   └── f44e94cddafe2ff23f891f03a07f98213f76fc
│   ├── info/
│   └── pack/
│
├── rebase-merge/     # 資料夾消失
│   ├── author-script
│   ├── done
│   ├── ...
│   └── tags/
│
├── refs/
│   ├── heads/
│   │   ├── feature   # 改為指向3ad34150719faa29471b6a4198010edd654d7fa8
│   │   └── main      # 依舊指向1c3f7cb068aea61861c0e14493f40b0e540a78c1
│   └── tags/
│
├── AUTO-MERGE        # 消失
├── COMMIT_EDITMSG    # 與剛剛編輯器要我們輸入commit訊息頁面之文字
├── config
├── description
├── HEAD              # 指向ref: refs/heads/feature
├── index             # 僅有100644 20062a99ded06d0cf8fb98101692435d6aaf98b3 0       conflict.txt
├── MERGE_MSG         # 消失
├── ORIG_HEAD         # 依然為00c3c587aea7032edac6c281aded0bf6d1594590
└── REBASE_HEAD       # 依然為00c3c587aea7032edac6c281aded0bf6d1594590

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

透過圖示,主要可以發現兩大變化:

  1. 多了新 commit 3ad3415...,代表 git rebase 後的樣子,由 feature 指著,上代為 main 分支最新 commit 1c3f7cb...
  2. feature 指向的 commit 00c3c58... 變成懸置物件。

那如果選的是 Accept Current ChangeAccept Incoming Change 呢?

  • Accept Current Change
    Accept Current Change 表示保留原 main 分支的版本,feature 分支參考便跑到原 main 分支指向的參考,不會做出新的 commit:
    https://ithelp.ithome.com.tw/upload/images/20250924/20178513K9nGxPx0Se.png

  • Accept Incoming Change
    Accept Incoming Change 表示保留原 feature 分支的版本,一樣會做出新 commit,新 commit 上代為 main 分支上最新的 commit,但指向的 tree 物件為舊 feature 分支最新 commit 指向的 tree 87d242b...
    https://ithelp.ithome.com.tw/upload/images/20250924/20178513TEWeOKov6V.png

小結

進行 git rebase 時,git 內部運作如下:

  • 無衝突:形成暫時的 tree 物件,指向暫時的 blob 物件,blob 物件為 conflict.txt 上顯示衝突的內容。rebase 完成後,會再形成新的 commit 物件指向新的 tree 物件,但因內容未改變,tree 物件指向原本就存在的 blob 物件。
  • 有衝突:一樣形成一個暫時的 tree 物件,指向暫時的 blob 物件,blob 物件內容為 conflict.txt 顯示衝突的內容。

在有衝突時,可能用以下三種方案解衝突:

  1. Accept Current Change:不形成新 commit,被搬運的分支參考直接指向目的地分支上的最新 commit。
  2. Accept Incoming Change:被搬運的分支參考指向新的 commit 物件,但這個新 commit 物件指向被搬運分支上舊有的 tree 物件。
  3. Accept Both Changes:被搬運的分支參考指向新 commit 物件,這個新 commit 物件指的 tree 物件、以及該 tree 物件指向的 blob 物件都是新的。

參考資料

  1. git-rebase
  2. [Git] rebase 使用場景

上一篇
Day 23-深入一點點認識 Git:合併與合併衝突時,Git 內部是怎麼運作的?
下一篇
Day 25-深入一點點認識 Git:對 commit 的任何更動其實都是在做新 commit
系列文
深入一點點認識 Git27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言