前言
在之前我們有提到 Git 主要被使用在多人協作開發,每個人各自完成屬於自己的工作,最後透過合併即可完成應用,但想的簡單,實作起來卻有些困難,過程中難免會有意外發生,比如說 A 同事與 B 同事先前在初始檔案各開了一個分支,但 A 同事發現這一份檔案存在 Bug,於是做了修復的動作,而 B 同事正好也發現了這一個 Bug 也做了修復,此時如果進行合併,就會造成所謂的合併衝突,此狀況不只會發生在本地分支,遠端分支也同樣會發生,該如何解決此類的衝突,也就是本篇的主題。
筆記重點
- 本地分支合併衝突
- 遠端分支合併衝突
- GitHub 保護機制
- Git 指令回顧
本地分支合併衝突
讓我們先來模擬衝突發生的情境:
1 | mkdir project |
修改 index.html
檔案:
1 |
|
再次提交 commit:
1 | git add . |
到這邊已經完成初始化的動作了,假設目前有一位工程人員開了一個分支並提交了一次 commit 紀錄:
1 | git checkout -b dog |
此時他發現原本的 index.html
檔案標題打錯了,進行了修改:
1 |
|
並提交了 commit 紀錄:
1 | git add . |
假設又有一位工程人員開了一個分支並修復 title 這個問題:
1 | git checkout master |
讓我們來看目前的日誌:
你會發現在 dog
分支與 cat
分支同時修改了 index.html
檔案的標題,這邊要注意,並不是修改同一份檔案就會發生衝突,而是修改同一份檔案的同一行代碼才會發生衝突,基本上 Git 有自己判定的標準,讓我們繼續來看衝突是如何發生的:
1 | git checkout cat |
此時會跳出合併發生衝突的警告:
此時千萬不要慌張,讓我們先用 git status
壓壓驚:
從上面可以得知,目前 all.css
已經被提交至索引區,代表這一個檔案沒有發生衝突,而 index.html
這個檔案就不一樣了,出現了 Unmerged paths
的狀態,且提示 both modified
字樣,代表兩個分支同時修改到了同份檔案的同行代碼,這時候 Git 提示可執行以下命令還原到未合併前的狀態:
1 | git merge --abort |
這很明顯是半途而廢的行為!遇到問題,解決它不就行了?讓我們先打開發生衝突的這一個檔案:
1 |
|
神奇的事情發生了!發生衝突的這個檔案居然出現了奇怪的符號,其實這是 Git 用來告訴我們何處發生了衝突,上半部是 HEAD
,也就是請求合併的 cat
分支,中間是分隔線,接下是 dog
分支的內容,這時請去與 dog
分支的人討論究竟該用誰的修改,假設我們要用 dog
分支的人修改好了,請把 cat
內容與其餘標記都給它刪除:
1 |
|
在這邊補充一點,如果你的編輯器是 VSCode,它會有選擇的提示喔!挺方便的,效果如下:
修改完後,老樣子,目前這個檔案還是存在於工作目錄,將它提交至索引區吧:
1 | git add . |
這時我們來看狀態長什麼樣子:
你會發現提示改變了,告訴我們所有衝突都已被修復,但此時還沒有結束喔,我們目前還是處於索引區,使用以下指令提交至本地數據庫:
1 | git commit |
這時你可能會想,為什麼沒有加 -m
參數呢?事實上,在這種情況下,我習慣使用預設的訊息,如果你跟我一樣單純使用 git commit
,此時會跳出預設編輯器請你輸入提交訊息,而分支衝突本身就有預設的訊息內容了,也就代表直接關閉視窗即可,但如果你想要自訂訊息內容,那就照往常的加入 -m 'message'
即可,這時候讓我們來看究竟合併成功了沒:
大功告成!分支已被成功合併,事實上,本地分支發生衝突的機率確實是挺高的,但只要跑過一次流程,就沒什麼好害怕的了,接下來進入到遠端分支合併衝突的部分。
遠端分支合併衝突
在這邊我們需要先釐清遠端數據庫是怎麼處理我們的上傳檔案的,你可能會認為遠端是以 “更新” 的方式進行處理,但這個觀念是錯的,事實上遠端與本地端同樣都是使用合併的方式處理檔案,這也就導致可能發生與本地端相同的合併衝突問題。
請先在 GitHub 隨便開一個遠端數據庫,並將本地端內容推上去:
1 | git remote add origin git@github.com:awdr74100/conflict-demo.git |
此時的日誌應該為:
在這邊補充一個指令:
1 | git commit --amend -m 'merge dog branch' |
這個指令主要可用來修改最後一次提交的 commit 訊息,假設你不小心在提交 commit 時打錯字,這個指令就很用好,但不建議使用在以推至遠端數據庫的 commit 節點,必定會發生衝突:
你會發現原本的 6aa88f4
節點目前只剩遠端的 cat
分支指著,本地的 cat
分支反而指向了一個全新的 commit 節點,這邊你可能會有所誤會,修改訊息對於 Git 來說也算是一次全新的 commit 紀錄,但假設你是使用在本地尚未推至遠端的 commit 節點,原有的 6aa88f4
應該是會被 “隱藏” 才對,使用 git log
是看不到這一個節點的,上面為什麼看的到是因為遠端的 cat
分支指著,才導致這個節點被顯示出來。
此時如果將本地推至遠端,就會產生所謂的遠端合併分支衝突:
對於遠端分支來說,應該是存在 6aa88f4
這一個節點的,但我們透過修改形成了一個全新的節點,原有的 6aa88f4
就被隱藏了,只要合併前的舊有紀錄有被更改的情形,就有可能發生衝突,因為版本對不上阿!
有沒有發現 non-fast-forward
字樣?這不就是之前介紹的取消快轉合併嗎?這也證實了遠端是以合併的方式處理推上來的檔案,讓我們先來看目前的檔案狀態:
Git 也直接跟你表明目前有衝突發生,請將遠端內容下載到本地端並進行合併,我們可以依造它指示的來做:
1 | git pull |
此時會跳出請你輸入此次合併提交的訊息:
再次執行 git push
:
這一次 Push 就成功了,之前我們有說過 git pull
主要會將 git fetch
的內容直接執行 git merge
,這也才導致直接跳出合併的訊息視窗,這樣子看起來是不是使用 commit --amend
挺麻煩的?我建議此命令不要用在以推至遠端的 commit 紀錄上,以免造成自己與夥伴的困擾。
跑過一次上面的流程你大概就知道怎麼修復遠端分支的衝突了,你也可以嘗試使用 Fetch 來跑上面的流程,之後再透過與本地端發生衝突的修復方法來解決這一個衝突,兩者的原理是一模一樣的。
GitHub 保護機制
如果你是乖乖依照上面方法去修復衝突,倒是不必動用到 GitHub 的保護機制,但如果你是使用以下指令可就麻煩了:
1 | git push -f |
這個 -f
等同於 --force
,表示強制的意思,這個指令主要用在遠端分支發生衝突時,可以強迫上傳,並且覆蓋掉遠端的分支,你可以把它想像成最高權限的覆蓋動作,如果以我們剛才的例子來講,遠端發生衝突時,就可以直接使用這個指令,免去修復的困擾,但建議這個指令只用在自己身上,你可以想像,團隊裡有人沒有先知會大家就突然使用這個指令,此時會發生什麼事?回家吃自己吧!
也因為這個指令帶來的後果太過於可怕,像是 GitHub 網站就有提供所謂的保護機制,可以避免某個分支被 Force Push,以下為示範:
路徑:Settings > Branches > Branch protection
點擊 Add rule
並挑選適合的選項:
master
分支已被保護:
這樣就完成囉,根據你挑選的保護選項,在每次推送前都會觸發,避免可能發生的可怕後果。
Git 指令回顧
1 | # 還原至合併前狀態 |