除了 git merge
,另一指令 git rebase
也能把兩分支變成一個分支,單看名稱看起來是「把整段分支基底搬家」,但在 git 內部真的是這樣運作的嗎?讓我們透過實驗觀察。
前置準備共包含兩個分支、五個 commit:
main
分支:建立兩個 commit。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
檢視結構如下:
如過透過觀察 .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
圖示如下:
目前 feature
是從 main
分支的第一個 commit 長出來的,如果我們在 feature
分支上,輸入以下指令:
git rebase main
則終端機畫面顯示如下:
這時候如果用 git log --oneline --graph
觀察,則會發現:
這是什麼意思呢?我們先觀察 .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...
,圖示如下:
main
分支的 commit 不變,但原本在 feature
分支的 commit 都多了新的,且從 main
分支最新的 commit f57e040...
長出來。
跟 feature
分支舊有 commit 相比,新 commit 物件共有以下兩大不同:
git rebase
的時間,比同內容在原 feature
分支建立 commit 的時間晚。git rebase
前, main
最新 commit 指向的 tree 所指的 blob 物件,以及 feature
分支原對應 commit 指向的 tree 所指之 blob 物件。現在我們改建立以下結構:
main
分支:共兩個 commit。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
觀察如下:
檢視目前 .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
圖示如下:
在同為 conflict.txt
的檔案中,我們在兩分支上做出不同修改。
這時如果在 feature
分支上,同樣做 git rebase main
會發生什麼事呢?
發生衝突了!現在打開編輯器看看:
上面的 Current change
是 main
分支版本、下面的 Incoming change
是 feature
分支版本,而在衝突的過程中,.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/
資料夾結構的圖示如下:
主要變化包含:
AUTO_MERGE
參考指向新的 tree 物件,這個 tree 物件指向新的 blob 物件,blob 物件包含編輯器上 conflict.txt
的內容。REBASE_HEAD
指向要搬走的分支 feature
的最新 commit。HEAD
指向要被搬到的分支 main
上最新的 commit。這時我們在編輯器上選 Accept Both Changes
,這時 main
分支的內容在上、feature
分支的內容在下:
再回到終端機,輸入以下指令:
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
:
這時 .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
圖示如下:
透過圖示,主要可以發現兩大變化:
3ad3415...
,代表 git rebase
後的樣子,由 feature
指著,上代為 main
分支最新 commit 1c3f7cb...
。feature
指向的 commit 00c3c58...
變成懸置物件。那如果選的是 Accept Current Change
或 Accept Incoming Change
呢?
Accept Current Change
Accept Current Change
表示保留原 main
分支的版本,feature
分支參考便跑到原 main
分支指向的參考,不會做出新的 commit:
Accept Incoming Change
Accept Incoming Change
表示保留原 feature
分支的版本,一樣會做出新 commit,新 commit 上代為 main
分支上最新的 commit,但指向的 tree 物件為舊 feature
分支最新 commit 指向的 tree 87d242b...
。
進行 git rebase
時,git 內部運作如下:
conflict.txt
上顯示衝突的內容。rebase 完成後,會再形成新的 commit 物件指向新的 tree 物件,但因內容未改變,tree 物件指向原本就存在的 blob 物件。conflict.txt
顯示衝突的內容。在有衝突時,可能用以下三種方案解衝突:
Accept Current Change
:不形成新 commit,被搬運的分支參考直接指向目的地分支上的最新 commit。Accept Incoming Change
:被搬運的分支參考指向新的 commit 物件,但這個新 commit 物件指向被搬運分支上舊有的 tree 物件。Accept Both Changes
:被搬運的分支參考指向新 commit 物件,這個新 commit 物件指的 tree 物件、以及該 tree 物件指向的 blob 物件都是新的。