1. 程式人生 > 實用技巧 >Merge,Rebase,Cherry-Pick 一文解惑

Merge,Rebase,Cherry-Pick 一文解惑

程式碼合併在日常開發中是較為常見的場景,採用合適的合併方式,可以起到事半功倍的效果。對應在 Git 中合併的方式主要有三個,Merge,Rebase,Cherry-Pick. 開始部分會首先介紹一下這三個命令,並錄製了一些動畫,用於演示三個命令的不同合併過程。之後會實操這三個命令,演示如何解決衝突。

Git Merge

Merge 會將兩個分支合併到一起,並生成一個新的 commit 記錄。新生成的 commit 節點會有兩個父節點。一般在開發某個新功能時,會選擇新的分支,之後在合併回主線。

在 master 分支上,使用 git merge bugFix 將 bugFix 分支合併到 master 分支。

Git rebase

rebase 也能合併分支,它會取出一系列 commit 記錄,複製它們。然後在目標分支逐個放下去。rebase 的好處是能保持線性的提交歷史,從而使歷史更加清晰。

在 bugFix 分支上,使用 git rebase master 分支,會將 bugFix 分支上的 commit 記錄複製到 master 分支,並且保證了提交歷史是線性的。

rebase 除了可以在合併時使用,還可用於整理 commit 記錄。

使用 rebase 來整理記錄

使用 git rebase -i 命令開啟互動式的 rebase UI. 在開啟的介面中會顯示每個 commit 的雜湊值以及提交說明,可以在其中調整 commit 的順序、刪除不想要的提交、以及合併提交。

通過 git rebase -i HEAD~4 後,我們看到了實際的互動式介面,但實際介面一般由 vim 開啟並進行相應的操作。在這裡,我們刪除 c2,調整了 c5 和 c4 的位置。

這裡需要注意的是:在真實的環境中,開啟後的互動頁面,commit 記錄方向和 log 記錄的 commit 是相反的。

cherry-pick

cherry-pick 而是將一些 commit 複製到當前的分支的 HEAD 上,和 rebase 相比,更加靈活,可以隨意的選擇 commit 進行復制。

通過 git cherry-pick c3 c4 c7 將其他分支上的 3 個 commit 複製到當前的 master.

三種情況下的衝突解決

場景模擬:團隊開發時,不同的開發者需要經常會對公共類進行修改。當修改的位置重合或者刪除公共的程式碼片段後,便會產生衝突。現在模擬這一場景。

首先,建立一個倉庫用於演示,其中包含一個名叫 origin.txt 的檔案,並在其中寫入 "line1",在 master 分支提交。

git 倉庫初始化:

mkdir merge-conflict-things
cd merge-conflict-things
git init
vim origin.txt
echo line1 > origin.txt

建立初始 commit 及三種情況 branch 用於演示:

git add .
git commit -m "add origin.txt"
git branch merge_branch
git branch rebase_branch
git branch cherrypick_branch

檢視初始化提交資訊

git log --oneline --all  -n4 --graph

準備工作:在 master 分支上,建立兩個 commit.

第一個 commit : 對 origin.txt 增加一些內容,相當於增加一個 feature.

origin.txt 中新增 "line2" 後:

line1
line2

第二個 commit 用於對過去提交的內容進行修改,相當於 Bug Fix.

origin.txt 的內容如下:

line1 - fixed
line2

接下來,會在三個分支進行相同的操作,並演示如何解決衝突。

git merge

merge_branch 分支,模擬和 master 分支相同的操作,這裡提交的過程就不演示了,只要和 master 分支提交的內容不同即可。

第一個 commit:用於增加 feature "line 2 in merge branch"。

第二個 commit:同於修改之前的 bug "line 1 fixed in merge branch"。

提交後的內容如下,merge 分支多了兩個 commit :

下面將 merge 和 master 分支進行合併,通常的規範是在次分支去主動合併主分支的程式碼,然後再推送給主分支,這樣做是由於所有分支都是基於主分支,萬一在主分支發生錯誤,影響較為廣泛。

進行 merge 操作

git merge master

由於 master 和 merge 分支修改了相同的內容,這時 git 並不知道哪個是需要的,所以提示 origin.txt 發生衝突,讓我們手動解決。

<<< HEAD ===== 中間的內容是當前所在的分支的內容,下面的是 master 分支上的內容。

這裡你需要注意的是,這裡提示衝突的行是兩個 commit 的內容。這就意味著,**merge 解決衝突時內容是基於兩個分支的所有的 commit **

選擇 merge_branch 的方案進行提交。

git add origin.txt
git commit -m "fixed conflict with master"

可以看到,merge 操作將分開的分支進行合併,並**形成一個新的 merge commit. **

git rebase

rebase_branch 分支上模擬 master 的提交操作。

第一個 commit 添加了新行:“line2 - rebase branch”

第二個 commit 用於修改bug: “line1 - rebase branch”

結果如下:

使用 rebase 合併操作

git rebase master

檢視衝突的檔案:

rebase 的合併過程不同於 merge,在合併時會選取 master 的所有 commit,和當前分支中一個出現衝突的 commit 進行合併。

如上圖中所示,HEAD 指標停留於master 分支上最新的 commit 節點,而 rebase 指標指向新增 feature 的節點。

由於 rebase 和 merge 相比較為特殊,這裡詳細演示下,選取 master 分支內容,選取 rebase 分支內容 ,選擇 master 和 rebase 分支的共同內容的處理方法。

1. 選擇 rebase 分支內容作為合併後的結果

儲存當前的修改:

git add origin.txt
git rebase --continue

修改後的內容直接就提交成功了,因為第二次需要合併的 commit 本就是基於當前內容的 commit,並不會產生衝突。

2. 選擇 master 分支內容作為合併後的結果

儲存當前的修改:

git add origin.txt
# 由於我們選擇了 master 分支內容進行合併,而當前又基於 master,和 master 本身分支上的內容沒有任何區別。
# 所以執行 skip 跳過這次衝突。 也就是意味著放棄當前分支提交的 commit,這個也能理解,既然要 master 的 commit,
# 當前分支的 commit 自然就不需要了,直接忽略掉,同時當前的 commit 也不會出現在 commit 的歷史中。
git rebase --skip

接著會出現第二次衝突:

再次產生的衝突的原因是,之前解決衝突時,選擇了 master 的內容,也就意味著在 rebase_branch 提交的 commit add feature for origin.txt in rebase 被放棄了,而當前的 commit fixed before commit in rebase_branch 是基於丟棄的那個 commit 。

瞭解到產生衝突的原因,任意選擇內容,儲存就可以了,這裡我們選擇了 rebase_merge 分支上的內容。

git add .
git rebase --continue

3. 選擇將 master 和 rebase_branch 的內容合併後的結果

這裡我們選擇將 master 和 rebase_branch 的分支都保留下來:

儲存當前的修改:

git add origin.txt
git rebase --continue

可以看到,第二次衝突產生,原因是 rebase_branch 分支中過去的 commit 出現變更,和 rebase_branch 原有 commit 的記錄不一致。

同樣的,我們也會遇到三種情況:

A 以第一次解決衝突後的內容為準:

git add .
# 這就意味著 rebase_branch 最新節點的新增的內容被捨棄,所以沒有任何改變,可以直接跳過
git rebase --skip

B 以 rebase_branch 分支上的結果為準

C 保留 rebase_branch 和 之前修改後的內容:

B C 處理方法相同:

git add .
# 儲存我們的更改就可以了
git rebase --continue

注意有時 git rebase --continute 操作無法生效時,原因在於 rebase 會逐一應用當前分支的 commit 到目標分支(一般是 master)。像例子中演示的那樣,第一次解決衝突後的內容,和第二次解決衝突後的內容一樣,這時用 git rebase --skip 跳過就好了。

git cheery-pick

cherrypick_branch 分支上模擬 master 的提交操作。

使用 cherry-pick 進行合併操作

git cherry-pick  eb1a8fb

出現了衝突,可以看到和 rebase 操作的合併操作剛好相反,這裡是以 cherrypick_branch 的最新 commit 為準,去合併 master 的 commit.

選擇 master 內容,進行合併:

git add origin.txt
git commit -m "fixed add feature conflict with master"

合併 master 分支上提交的第二個 commit:

git cherry-pick 23e106b

再次出現了衝突:

修改後,提交就可以了:

git add origin.txt
git commit -m "fixed add feature conflict with master"

注意有時 git commit 操作無法生效,提示你是否提交的一個空的 commit. 原因在於修改後的衝突內容和當前分支的原有內容一樣。但這樣有些矛盾,取過來別人的 commit,最後卻不用。解決辦法是,取消 cherry-pick 這個 commit 或者提交一個空的 commit 資訊。

總結

git merge 是基於兩個分支上的最新內容到有衝突的內容之間的所有 commit 進行合併,之後會形成一個新的 commit 記錄,將兩個分支重新關聯起來。

git rebase 正如它的中文名字“變基操作”一樣,會將所在分支新新增的內容,增加到目的分支,並保證了 commit 提交記錄的序列性。簡單來說就是,會以目的分支(一般是 master)為基礎,逐一的將當前分支的 commit 記錄應用。需要注意的是,在應用時並不是直接應用在 master 分支,而是將 master 分支整體拷貝,然後將當前 commit 應用在拷貝後的分支上。

git cherry-pick 和 rebase 操作正好相反,會以當前的分支為基礎,然後將 commit 一個個的拿過來應用。形成的 commit 記錄也是序列的。

最後附上一個 Git 練習網站,網站上設定了各種任務,可以用於檢驗自己的操作水平。