# 前言

開始使用分支之後,總是會需要把開發的內容合併回來的一天。

在使用分支之前,操作的指令不太需要在乎目前所在的分支在哪,儘管執行指令操作資料就是了,不過要執行分支合併的動作,代表我們會去處理「兩個分支」的資料,要在哪個分支執行指令就是一件很重要的事情了!

剛接觸分支合併會遇到的幾個疑問不外乎是這些:

指令該在哪個分支執行?
分支合併之後會發生什麼事情?
進行合併分支是不是一定會發生衝突?

種種的疑問將會在這篇文章解答!

直接進入今天的主題:

分支合併與衝突解決!

# 執行合併指令前的注意事項

先來說一個觀念:分支合併的指令,目的是要把「別人的東西」收割回來,這個概念會影響到我們決定「誰」要執行指令。

假如我們有個 master 用來記錄「完成的資料」。
接著我們建立 dev 用來進行「開發項目」。

dev 完成後,是 master 要收割 dev 的內容,所以我們要以 master 的角度,把 dev 的內容拿回來。執行指令時,必須確保自己是位於 master 分支,這樣才能去把 dev 的資料合併回來。

分支合併的第二個觀念:雖然是說把別人的資料「收割」回來,但分支合併的行為並「不會刪掉」任何已經 commit 的資料,無論是否使用「快轉模式」進行合併,已經 commit 的內容都不會發生改變!

關於快轉模式,等等會介紹到,這邊先建立觀念即可!

# 合併分支指令

指令很單純,就這樣而已:

git merge 要被收割的分支

實際來操作一次吧:

  1. 假設已經開了一個分支 ( imall/feature ) ,是用來開發新功能的分支:

  2. 在分支上開發了兩個版本,於是他往前走兩個點點:

  3. master 要把 imall/feature 的資料合併 (收割) 回來,所以要先切換回 master 分支

  4. 執行指令

git merge imall/feature
  1. 完成

# 合併分支的快轉機制 (fast-forward)

相信大家多少看過分支「岔出去」又「合回來」的這種線圖,像是下圖這樣。
devEmiliaRemRam 合併之後,四條線依舊存在著,結果上面的範例可能已經跟一些新手所想的畫面不一樣了:怎麼執行完沒有看到這樣漂亮的分岔線圖,而是只有一條線???

事實上分支合併的行為就像「雙人接力」賽,當 dev 往前走兩步, master 想吃掉他的內容,只要沿著 dev 走過的路再走一遍就好,根本不用浪費時間刻意繞一圈。

而且因為 master 本來就跟 dev 在同一個位置起步,用這種方式追上 dev 的步伐,根本也不存在 「資料衝突」 的問題。

我們稱這種行為叫做「快轉機制」(fast-forward),他是 git merge 的預設機制。

此時可能也有人疑惑了,上面那張圖為什麼沒有出現這種情況,我是不是在唬爛?

我沒唬爛,那圖片是為了讓範例看起來很像一回事,刻意關掉快轉機制。
不信的話,我用預設行為用 dev 合併 Emilia 分支給你看看:

# 合併時關掉快轉機制

如果你堅持每一個合併一定要看到兩個不同的分支線路,那你也可以關掉快轉機制,只要在合併時使用 --no-ff 參數即可:

git merge 分支 --no-ff

--no-ff 就是告訴 Git 不要快轉機制 (no fast forward) 的意思。

用起來會長這樣:

同場加映,把上面 dev 合併 Emilia 分支的行為,用快轉機制跑一次給大家看:

然後不要問我為什麼 Emilia 分支從綠色變成桃紅色,那是 GUI 自動處理的。
愛蜜莉雅 分支應該要是 紫色 才對!!!

# 合併衝突介紹

我們在 Git 分支觀念不清楚的情況去使用分支、合併分支,無論是一個人的使用或是團隊開發,都有機會遇到 合併衝突 的議題。

事實上我們進行分支合併的過程,並沒有辦法確保衝突一定不會發生,既然這是可能會發生的議題,那我們就要知道發生了應該怎麼處理。

知己知彼,百戰不殆,在學習解決衝突之前,先來說為什麼分支合併會發生衝突:
原因只有一個:當兩個分支的 commit 中,存在 同一個檔案同一行 內容不同,只要進行合併,無論是誰合併誰,都會發生衝突。

例如這個操作步驟:

  1. master 分支所在位置建立 dev 分支
  2. master 修改 readme.txt 檔案的 第一行,並且 commit 一版
  3. dev 也修改 readme.txt 檔案的 第一行,然後 commit 一版
  4. master 合併 dev 分支時,就會衝突

由於兩個分支都修改了 同一個檔案同一行文字 ,Git 在合併的過程並不清楚兩個 commit 都改了同行的內容,應該以哪個為主,只好把決定權交給我們,於是就用「分支衝突」的方式叫我們去處理了。

這裡就先模擬上面的行為進行 Git 的操作:
Git 在 merge 指令發生衝突時,會呈現出這樣的資訊,告訴我們 某個檔案 發生了衝突:

$ git merge dev
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

最後一句話最重要:「fix conflicts and then commit the result」,這是 Git 在提示我們 先解決衝突,然後 commit 這個已解決的結果

此時我們用 git status 指令來看一下目前的狀態:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)      
Unmerged paths:
  (use "git add <file>..." to mark resolution)      
        both modified:   readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

這裡可以看到第三行提示一個以前沒看過的訊息:「You have unmerged paths.」,代表著目前正在 尚未合併完成 的狀態

# 解決衝突

一般來說解決合併衝突的方法有兩種

# 1. 放棄合併

直接在終端機執行這個指令放棄合併的行為:

git merge --abort

可能會有人覺得,放棄很可恥,既然合併了就是要成功不是嗎?

事實上,放棄確實可恥,但卻很有用,在操作合併如果發生衝突,選擇放棄也是一個蠻正常的事情。

舉例來說,你是負責 master 的人,同事負責 dev 分支。

今天你在 masterdev 合併回來結果發生衝突,但衝突的內容因為是同事改的,你也不知道怎麼修比較好。

這樣的狀況,選擇放棄合併,等同事回來再跟他討論如何修改,就會是很恰當的做法。

畢竟如果改壞了,同事說不定還會甩鍋給你。

但如果這個同事在場,可以找到人進行討論的話,就可以透過第二種方式進行處理。

# 2. 手動處理合併衝突

回頭看一下剛剛 git status 的內容的第七行的內容, Unmerged paths: ,這裡面會顯示尚未合併成功的路徑檔案:

Unmerged paths:
  (use "git add <file>..." to mark resolution)      
        both modified:   readme.txt  # <= 告訴我們這個檔案衝突了

只要這個區塊列出來的檔案,都要一個一個打開,然後處理這些檔案的合併。

以這個例子來說,發生衝突的檔案是 readme.txt,此時我們把這個檔案打開,會看到原本的內容被 Git 進行修改

<<<<<<< HEAD
這是 master 修改的 readme.txt
=======
這是 dev 修改的 readme.txt
>>>>>>> dev

原本文件內容只有一行,發生合併衝突時,Git 會把 原本的分支要合併的分支 發生衝突的部分全部都列出來,並且會呈現很固定的格式:

  1. 開頭會是七個 < 符號,加上 HEAD ,代表著目前的分支。
  2. 結尾會是七個 > 符號,加上預計合併的分支名稱,代表預計合併的分支。
  3. 兩者之間會有七個 = 符號,進行資料的相隔。

看懂了之後,我們就可以把下面三行內容刪掉,並且撰寫真正要留下來的內容:

  1. <<<<<<< HEAD
  2. =======
  3. >>>>>>> dev

此時你有四個選擇:

  1. 保留 master 的版本
  2. 保留 dev 的版本
  3. 保留 兩者 的版本
  4. 全部刪掉,自己編輯新的內容

無論你選擇哪一種,編輯完檔案然後 存檔 之後,就可以透過 add 指令把 修改完的檔案加到暫存區

git add readme.txt

當執行 git add 成功之後,執行 git status 指令看看:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)
Changes to be committed:
        modified:   readme.txt

All conflicts fixed but you are still merging. ,這是告訴我們衝突已經被解決了,但是我們依然處於正在合併的狀態。

此外,有看到一個很熟悉的 準備提交 (Changes to be committed) 字樣嗎?

沒錯,下一步就是要叫我們執行 git commit 來提交版本。

git commit -m "將 master 跟 dev 進行合併"

這裡的 commit 內容,記得要清楚的寫上 merge 過程發生的事情,好讓後續能追蹤曾經有衝突發生!

到了這邊,我們手動解決衝突的步驟,就正式完成!

# 補充:為什麼合併衝突還要自己 commit?

不知道你有沒有發現一件事情,merge 指令如果關掉快轉機制,他就是一個 commit 行為?

如果你忘記了,我把上面的 demo 搬來讓你看看 (聽話,讓我看看!!)

原本只有「三個」點點,執行 git merge 之後竟然變成 「四個」,不覺得這就是一個 commit 行為嗎!!

既然他是一個 commit 行為,就表示你可以定義 commit 的訊息:

git merge 分支 --no-ff -m "我把 feature 分支的內容吃回來了!!!"

如果合併沒有發生衝突,整個 commit 行為,Git 會幫我們把它處理掉。
但如果發生了衝突,Git 就必須把資料內容的決定權交給我們,讓我們自己 commit 了。

這也是為什麼,合併衝突的 SOP 會是這樣:

  1. 打開合併檔案,處理合併內容
  2. 執行 git add 指令將衝突檔案加到暫存區
  3. 執行 git commit 提交合併的內容

# 同場加映:實際情境的合併衝突

看完合併衝突的解決方式,可能沒什麼真實感,這裡示範一下實際情境可能會發生的衝突樣貌。

不過如果寫程式碼,不一定每個讀者都能看得懂,不如我們來寫歌詞吧!

來製造一個會發生衝突的情境:
假設有兩個開發者 愛蜜莉雅雷姆 ,他們分別要在自己的分支建立一個歌詞的檔案,把歌詞寫進去,commit 一版。
愛蜜莉雅 被分配到 「童話」,雷姆 被分配到 「突然好想你」。
等到兩個開發者編輯完歌詞檔案後, master 分支再分別把兩個開發者建立的分支合併回來。

如果依照上面的做法,因為兩個分支不會編輯到同一個檔案,理論上不會有衝突。
雷姆 應該只負責「突然好像你」,但她卻也新增 「童話」檔案。

使得衝突一觸即發…

一步一步來看看兩個開發者的行為:

  1. 在最初的 master 分支先來開兩個分支 EmiliaRem
  2. 愛蜜莉雅Emilia 分支提交一個 童話歌詞.txt 檔案:
  3. 雷姆Rem 分支同時新增「兩首」歌詞的檔案結果童話的歌詞寫錯了兩個字…
  4. 執行合併的人員,先切換到 master 分支,合併 Emilia 分支:
  5. 接著再繼續合併 Rem 分支,然後不意外的,有一句話發生衝突了:
  6. 上面這個底色,是 VSCode 偵測到衝突的文字 ( <=> ) 時,自動會出現的內容,可以讓開發者選擇。
  • 接受目前變更:因為 master 在第四步先合併 Emilia 分支,所以 目前變更 意思是 Emilia 分支的內容。 (如果看不出來,你可以回頭看 愛蜜莉雅 開發的截圖)
  • 接受來源變更:因為 master 正在 合併 Rem 分支而發生衝突,所以 來源變更 代表 Rem 的內容。
  • 接受兩者變更:表示要兩筆的內容全都要。
  1. 由於正確的歌詞是「張開雙手 變成翅膀守護你」,所以選擇了接受 目前變更

    注意此時只做到解決衝突三部曲的第一步:編輯衝突檔案。
    還有兩步要處理。

  2. 童話歌詞.txt 加到暫存區。

git add 童話歌詞.txt
  1. 提交暫存區資料
git commit -m "處理 rem 分支合併衝突"
  1. 完成

# 用 Fork GUI 操作分支合併

用 GUI 操作合併時觀念很重要,必須很清楚到底是 誰要吃掉誰 的資料?
我們是站在「要收割」的分支身上,去處理「被收割」的分支。

所以即便是用 GUI ,要執行 masterdev 合併回來的行為,一定要先切到 master 分支上。

Fork GUI 有兩種合併方式可以操作

# 對著要「被合併」的分支點右鍵選擇 merge

這裡從切換分支的行為開始示範,一直處理到合併完成:

當然你也可以關掉快轉模式 --no-ff 進行合併:

# 用「拖曳分支」的方式合併

這種方式就很直覺,相信聰明的大家直接看操作就知道在幹嘛了:

# 合併衝突處理

直接讓線圖長成這樣,來看看兩個分支合併會發生什麼事情:

如字面上的意思,兩個分支在原本的位置,都編輯同一個檔案,並且發佈一個版本。
此時兩個分支進行合併後,已經達成發生衝突的條件:

當兩個分支 同一個檔案同一行 內容不同,只要進行合併,無論是誰合併誰,都會發生衝突。

當執行合併,GUI 會很好心跳出警示,這個行為會發生衝突哦!

很不意外的,按了確定之後,衝突發生了 (\ 打架 / \ 打架 /)

GUI 視窗此時會自動跳出這樣的畫面:
在未暫存區顯示衝突的檔案,並且於主畫面中,讓我們決定想要留下哪個分支的內容。

這裡先示範留下左邊分支的內容給大家看:

如果想留下右邊的分支內容,依樣畫葫蘆,僅勾選右邊即可。

如果兩邊的內容都要留,GUI 會再跳一個視窗,讓我們操作內容先後順序:

完成之後,就是單純的執行 commit 操作了:

# 放棄合併

放棄合併的操作就沒什麼學問,Abort 按鈕點下去就結束了:

# 總結

分支合併是 Git 重要功能之一,同時也是基本分支功能的最後一哩路。

這篇文章記錄了分支合併的指令,認識 Git 分支合併快轉機制 (fast-forward),也學習了解決衝突的方式。

希望這些內容有幫助到正在學習 Git 的朋友!