作者:咕咚移動技術團隊-nanchen
題目看起來很像是提供解決方案的文章,但實際上我並不會給大家直接提供解決方案,我們追求的從來不應該是答案,而是探索的過程。當然,如果你只想檢視答案的話,請直接拉到文章最底部。
寫在前面
相信大家都知道,Git 相比於 SVN,優勢不言而喻,以致於現在大多數公司的專案都在採用 Git 進行管理。作為一個開發人員,對 Git 的使用自然應該是得心應手。
如果你還不會使用 Git 的話,那我勸你還是不要聲張,好好的去學習一番,再自己弄個實驗專案走一下流程,以免遭到同事的鄙視。
每個公司都會有自己不一樣的 Git 分支管理規範,特別是在開發人員較多的公司,Git 的分支管理規範就顯得更加重要。前面比較出名的 Git Flow 分支管理策略相信不少人都已經瞭解了,不熟悉的當然也可以去看看:nvie.com/posts/a-suc…

Git Flow 管理方式把專案分為 5 條線,通常會是下面的管理方式。
- Master:作為穩定主分支,長期有效。不可以在此分支進行任何提交,只能接受從 Hotfix 分支或者 Release 分支發起的 merge request,該分支上的每一個提交都對應一個 Tag。
- Develop:開發主分支,長期有效。不可以在此分支上做任何提交,只接受從 Feature 分支發起的 merge request。所有的 Alpha Release 都應該在這個分支釋出。
- Feature:功能分支,生命週期為產品迭代週期,每個分支對應一期的需求。只可以從 Develop 分支進行 Kick Off。可以 merge Release 分支的程式碼,生命週期結束後,需要 merge 回 Develop 分支。方式需要採用 merge request。
- Release:釋出分支,宣告週期從新需求的預釋出到正式釋出,每一個分支對應一個新版本的版本號。只可以從 Develop 分支 Kick Off。宣告週期結束後,需要 Merge 回 Master 及 Develop 分支,方式同樣需要採用 merge request。所有的 Beta Release 均需要在該分支釋出。
- Hotfix:熱修復分支,生命週期對應一個或者多個需要緊急修復並上線的 Bug,每一個分支對應一個小版本號。只可以從 Master 分支進行 Kick Off。宣告週期結束後,需要 merge 回 Master 分支和 Develop 分支,方式當然也是採用 merge request。
實際上,如果你熟悉 Git 的話,你會很快發現上面的管理方式會存在歷史提交非常混亂的缺點,但覺得不失為一個 Git 分支管理的經典。實際上,我們可以用 rebase 去替換 merge 讓 commit 看起來更加清晰。對 rebase 和 merge 的優劣對比這裡暫不做講解,感興趣的可以直接 Google 搜尋。
下面就給大家分享一下發生在咕咚專案的一次坑爹的 Git 體驗。
從 git revert 說起
咕咚專案組並沒有對開發者限制 Develop 分支和 Master 分支的許可權,我們暫時並沒有一個專門做程式碼 Review 和 PR 的角色,其實一定意義上也提現了團隊對每個人的信任。
我們依然會基於 Develop 做開發主線,每個需求迭代期,團隊成員會從 Develop 拉取自己的分支,並命名於 feture/XX,然後各自在自己的分支上進行開發。
由於大家開發業務上的不同,所以在需求開發完畢,整合程式碼到 Develop 分支的時候,一般不會出現太多衝突的情況。
而我這邊交接一個需求時,採用 merge 的時候出現了一個奇怪的問題,我們姑且來重現一下事故現場。
首先使用 git branch
檢視一下當前我們的本地分支。

這裡先簡單提一下我們要做的操作。
"feature8.28_buyGifts" 是我們同事的分支,基於 "release8.27.0" 拉取,而 "feature8.29.0_nanchen" 是我的分支,基於 "release8.28.0" 分支拉取,所以我這邊的分支包含了最新的程式碼。
現在由於某些原因,我需要把同事的 "feature8.28_buyGifs" 分支程式碼合併到我的分支上,直接接手他的程式碼進行開發。
就不要吐槽為啥不按照功能搞分支開發了,原因是因為他那邊程式碼基本已經完成,現在只需要少量修改。
所以我們就採用 git merge <branch>
命令進行 merge 操作。

我們用 git status
更容易看明白衝突了什麼。

可以看到,上面衝突的檔案全是和同事開發的需求出現的衝突,所以出現這個衝突其實令人非常懊惱,因為是不可能有其他同事改動到這些檔案的。
為了驗證自己的想法,我們隨意開啟一個檔案檢視。這裡就採用 vim <filename>
檢視第一個檔案。

正如我們所想,確實和同事編寫的需求 Presents
類有關係,但看衝突內容就更一臉懵逼了,因為看起來,這應該是一個不會衝突的 merge。
於是趕緊使用 git merge --abort
撤銷這次 merge。再在 "origin/feature8.29.0_nanchen" 檢視我們剛剛的檔案提交歷史。

可以很清晰的看到,確實是最近沒有任何的修改記錄。
一個 7 個月都沒人動的檔案,居然 merge 的時候發生了衝突!這讓我一臉懵逼。(手動黑人問號)
使用 git lg
檢視一下該分支的提交歷史,我們希望從中能得到某些思路。

注意其中紅框中的 commit,我們這位同事之前想往 "release8.28.0" 合併他分支的程式碼,後面又因為某些原因,希望撤銷這次提交,他採用了 revert 進行處理。雖然 revert 對檔案沒有提交記錄,但 Git 卻認為我們在當前分支更改了這些檔案,所以在我們 git merge
的時候,Git 認為這是一次衝突,並選擇了告知我們。
如若如我們所想,那我們只需要撤銷這次 revert 操作即可。
我們當然知道,可以通過 reset 命令放棄這次提交,但這裡後面已經有了非常多的 commit,顯然我們這樣是不行的,我們需要另闢蹊徑。
解決方案?
最容易想到的大概就是直接在 merge 的時候解決衝突了,但通過一系列檢視以後,我們發現檔案改動量非常大,直接解決衝突並非易事。所以我們還是得 想辦法取消掉這次 revert 的 commit,再進行 merge。
我們知道,程式碼回滾有三種方式:reset、checkout,還有我們的 revert。直觀感受,我們應該在 reset 上想辦法。
我們來看看 reset 有些怎樣的操作方法。

主要想給大家講講:--soft 和 --hard 的區別。
我們經常會用到 git reset --hard <commit>
做「毀屍滅跡」的操作,常常爽到不能自已,因為這不僅可以回退到我們想要的版本,而且還「直接丟棄」了後面提交的程式碼,真正的「毀屍滅跡」級別的操作。
而另外一個 --soft 處理,實際上還具備點人性,雖然同樣可以回退到我們想要的版本,但目標版本後面的提交都還會存放在 stage 區域中,以便後面找出證據。
說到這,似乎我們已經有了思路。
- 使用
git reset --soft <revert 操作的 commit ID>
回退到 revert 操作的版本; - 使用
git reset --hard <revert 操作的前一個 commit>
幹掉那次 revert 提交; - 最後再把 stage 區域的所有改動匯聚成一個新的提交 commit 到我們的專案倉庫中。
當然,細心的你一定會發現,在第 1 步操作後,我們還必須執行 git stash
命令把所有的改動存到暫存區,再在第 2 步操作後使用 git stash pop
命令取出來,直接進行第 2 步操作肯定還是會毀滅證據的。
我們後面的提交不見了。
這樣似乎可以解決我們的問題,不過有個弊端:我們後面那麼多的提交被合併成一個提交了,以後我們就沒辦法看到了,萬一...
不少小夥伴會想到進階方案:
- 對 "feature8.29.0_nanchen" 的最新程式碼 checkout -b 一個分支 feature_copy;
- 然後使用
git checkout feature8.29.0_nanchen
回到我們的分支; - 然後直接對當前分支 reset 到 revert 的前一個 commit 後,我們採用 cherry-pick 方式進行傻瓜式改寫便可以把歷史重寫了。(誰說的我們不能改寫歷史?)
改寫歷史?
改寫歷史?等等,好像還有一個操作:rebase。
rebase 是 Git 的一個神奇的命令,前面我也說了,總會有人不喜歡 merge 之後歷史的分叉,這種分叉再匯合後會讓結構看起來非常混亂,以致於無法管理。如果你不喜歡 commit 歷史出現分叉,那 rebase 絕對是你的救星。
改寫歷史是 rebase 與生俱來的能力。我們可以用 git rebase -i <commit>
進行歷史的改寫。
我們試試看在我們的專案中直接使用 git rebase -i <commit>
會怎樣。

我們會拿到分支後面的提交歷史,並且前面還有一個 Commands。我們可以從提示中看到,上面全寫的 pick 就是代表保持這個提交的意思,edit 代表編輯此次提交...
我們希望刪除此次 revert 這次提交,那當然我們最關心的就是 drop 了,甚至我們可以更加簡單粗暴:直接刪掉這一行。
然後我們便開始處理了。

過程中可能會出現衝突,我們只需要解決就好。
解決掉衝突後,再使用 git add <filename>
把它們 merge 進去。

oh,我們看到我們已經 rebase 成功了。我們再使用 git lg
檢視一下提交歷史。

我們成功改寫了歷史!
歷史改寫結束,我們還要做我們最開始想做的事情,進行 merge 操作。

可以看到,這次我們 merge 確實如我們預期的不再發生衝突,方案親測有效!
寫在最後
寫了這麼多,想必大家對解決方案也算比較清楚了。我們主要便是採用 git rebase -i <>
操作進入到 commit 歷史編輯頁面,然後進行歷史改寫處理!