作者:咕咚移動技術(shù)團隊-nanchen
題目看起來很像是提供解決方案的文章,但實際上我并不會給大家直接提供解決方案添吗,我們追求的從來不應(yīng)該是答案沥曹,而是探索的過程。當然碟联,如果你只想查看答案的話妓美,請直接拉到文章最底部。
寫在前面
相信大家都知道鲤孵,Git 相比于 SVN壶栋,優(yōu)勢不言而喻,以致于現(xiàn)在大多數(shù)公司的項目都在采用 Git 進行管理普监。作為一個開發(fā)人員贵试,對 Git 的使用自然應(yīng)該是得心應(yīng)手。
如果你還不會使用 Git 的話凯正,那我勸你還是不要聲張毙玻,好好的去學習一番,再自己弄個實驗項目走一下流程漆际,以免遭到同事的鄙視淆珊。
每個公司都會有自己不一樣的 Git 分支管理規(guī)范,特別是在開發(fā)人員較多的公司奸汇,Git 的分支管理規(guī)范就顯得更加重要施符。前面比較出名的 Git Flow 分支管理策略相信不少人都已經(jīng)了解了,不熟悉的當然也可以去看看:http://nvie.com/posts/a-successful-git-branching-model/
Git Flow 管理方式把項目分為 5 條線擂找,通常會是下面的管理方式戳吝。
- Master:作為穩(wěn)定主分支,長期有效贯涎。不可以在此分支進行任何提交听哭,只能接受從 Hotfix 分支或者 Release 分支發(fā)起的 merge request,該分支上的每一個提交都對應(yīng)一個 Tag。
- Develop:開發(fā)主分支陆盘,長期有效普筹。不可以在此分支上做任何提交,只接受從 Feature 分支發(fā)起的 merge request隘马。所有的 Alpha Release 都應(yīng)該在這個分支發(fā)布太防。
- Feature:功能分支,生命周期為產(chǎn)品迭代周期酸员,每個分支對應(yīng)一期的需求蜒车。只可以從 Develop 分支進行 Kick Off♂`拢可以 merge Release 分支的代碼酿愧,生命周期結(jié)束后,需要 merge 回 Develop 分支邀泉。方式需要采用 merge request嬉挡。
- Release:發(fā)布分支,聲明周期從新需求的預發(fā)布到正式發(fā)布呼渣,每一個分支對應(yīng)一個新版本的版本號棘伴。只可以從 Develop 分支 Kick Off寞埠。聲明周期結(jié)束后屁置,需要 Merge 回 Master 及 Develop 分支,方式同樣需要采用 merge request仁连。所有的 Beta Release 均需要在該分支發(fā)布蓝角。
- Hotfix:熱修復分支,生命周期對應(yīng)一個或者多個需要緊急修復并上線的 Bug饭冬,每一個分支對應(yīng)一個小版本號使鹅。只可以從 Master 分支進行 Kick Off。聲明周期結(jié)束后昌抠,需要 merge 回 Master 分支和 Develop 分支患朱,方式當然也是采用 merge request。
實際上炊苫,如果你熟悉 Git 的話裁厅,你會很快發(fā)現(xiàn)上面的管理方式會存在歷史提交非常混亂的缺點侨艾,但覺得不失為一個 Git 分支管理的經(jīng)典执虹。實際上,我們可以用 rebase 去替換 merge 讓 commit 看起來更加清晰唠梨。對 rebase 和 merge 的優(yōu)劣對比這里暫不做講解袋励,感興趣的可以直接 Google 搜索。
下面就給大家分享一下發(fā)生在咕咚項目的一次坑爹的 Git 體驗。
從 git revert 說起
咕咚項目組并沒有對開發(fā)者限制 Develop 分支和 Master 分支的權(quán)限茬故,我們暫時并沒有一個專門做代碼 Review 和 PR 的角色盖灸,其實一定意義上也提現(xiàn)了團隊對每個人的信任。
我們依然會基于 Develop 做開發(fā)主線磺芭,每個需求迭代期糠雨,團隊成員會從 Develop 拉取自己的分支,并命名于 feture/XX徘跪,然后各自在自己的分支上進行開發(fā)甘邀。
由于大家開發(fā)業(yè)務(wù)上的不同,所以在需求開發(fā)完畢垮庐,整合代碼到 Develop 分支的時候松邪,一般不會出現(xiàn)太多沖突的情況。
而我這邊交接一個需求時哨查,采用 merge 的時候出現(xiàn)了一個奇怪的問題逗抑,我們姑且來重現(xiàn)一下事故現(xiàn)場。
首先使用 git branch
查看一下當前我們的本地分支寒亥。
這里先簡單提一下我們要做的操作邮府。
"feature8.28_buyGifts" 是我們同事的分支,基于 "release8.27.0" 拉取溉奕,而 "feature8.29.0_nanchen" 是我的分支褂傀,基于 "release8.28.0" 分支拉取,所以我這邊的分支包含了最新的代碼加勤。
現(xiàn)在由于某些原因仙辟,我需要把同事的 "feature8.28_buyGifs" 分支代碼合并到我的分支上,直接接手他的代碼進行開發(fā)鳄梅。
就不要吐槽為啥不按照功能搞分支開發(fā)了叠国,原因是因為他那邊代碼基本已經(jīng)完成,現(xiàn)在只需要少量修改戴尸。
所以我們就采用 git merge <branch>
命令進行 merge 操作粟焊。
我們用 git status
更容易看明白沖突了什么。
可以看到孙蒙,上面沖突的文件全是和同事開發(fā)的需求出現(xiàn)的沖突项棠,所以出現(xiàn)這個沖突其實令人非常懊惱,因為是不可能有其他同事改動到這些文件的马篮。
為了驗證自己的想法沾乘,我們隨意打開一個文件查看。這里就采用 vim <filename>
查看第一個文件浑测。
正如我們所想翅阵,確實和同事編寫的需求 Presents
類有關(guān)系歪玲,但看沖突內(nèi)容就更一臉懵逼了,因為看起來掷匠,這應(yīng)該是一個不會沖突的 merge滥崩。
于是趕緊使用 git merge --abort
撤銷這次 merge。再在 "origin/feature8.29.0_nanchen" 查看我們剛剛的文件提交歷史讹语。
可以很清晰的看到钙皮,確實是最近沒有任何的修改記錄。
一個 7 個月都沒人動的文件顽决,居然 merge 的時候發(fā)生了沖突短条!這讓我一臉懵逼。(手動黑人問號)
使用 git lg
查看一下該分支的提交歷史才菠,我們希望從中能得到某些思路茸时。
注意其中紅框中的 commit,我們這位同事之前想往 "release8.28.0" 合并他分支的代碼赋访,后面又因為某些原因可都,希望撤銷這次提交,他采用了 revert 進行處理蚓耽。雖然 revert 對文件沒有提交記錄渠牲,但 Git 卻認為我們在當前分支更改了這些文件,所以在我們 git merge
的時候步悠,Git 認為這是一次沖突签杈,并選擇了告知我們。
如若如我們所想贤徒,那我們只需要撤銷這次 revert 操作即可芹壕。
我們當然知道,可以通過 reset 命令放棄這次提交接奈,但這里后面已經(jīng)有了非常多的 commit,顯然我們這樣是不行的通孽,我們需要另辟蹊徑序宦。
解決方案?
最容易想到的大概就是直接在 merge 的時候解決沖突了背苦,但通過一系列查看以后互捌,我們發(fā)現(xiàn)文件改動量非常大,直接解決沖突并非易事行剂。所以我們還是得 想辦法取消掉這次 revert 的 commit秕噪,再進行 merge。
我們知道厚宰,代碼回滾有三種方式:reset笆怠、checkout,還有我們的 revert吠昭。直觀感受沟沙,我們應(yīng)該在 reset 上想辦法。
我們來看看 reset 有些怎樣的操作方法仪缸。
主要想給大家講講:--soft 和 --hard 的區(qū)別。
我們經(jīng)常會用到 git reset --hard <commit>
做「毀尸滅跡」的操作,常常爽到不能自已礁击,因為這不僅可以回退到我們想要的版本,而且還「直接丟棄」了后面提交的代碼逗载,真正的「毀尸滅跡」級別的操作哆窿。
而另外一個 --soft 處理,實際上還具備點人性厉斟,雖然同樣可以回退到我們想要的版本更耻,但目標版本后面的提交都還會存放在 stage 區(qū)域中,以便后面找出證據(jù)捏膨。
說到這秧均,似乎我們已經(jīng)有了思路。
- 使用
git reset --soft <revert 操作的 commit ID>
回退到 revert 操作的版本号涯; - 使用
git reset --hard <revert 操作的前一個 commit>
干掉那次 revert 提交目胡; - 最后再把 stage 區(qū)域的所有改動匯聚成一個新的提交 commit 到我們的項目倉庫中。
當然链快,細心的你一定會發(fā)現(xiàn)誉己,在第 1 步操作后,我們還必須執(zhí)行 git stash
命令把所有的改動存到暫存區(qū)域蜗,再在第 2 步操作后使用 git stash pop
命令取出來巨双,直接進行第 2 步操作肯定還是會毀滅證據(jù)的。
我們后面的提交不見了霉祸。
這樣似乎可以解決我們的問題筑累,不過有個弊端:我們后面那么多的提交被合并成一個提交了,以后我們就沒辦法看到了丝蹭,萬一...
不少小伙伴會想到進階方案:
- 對 "feature8.29.0_nanchen" 的最新代碼 checkout -b 一個分支 feature_copy慢宗;
- 然后使用
git checkout feature8.29.0_nanchen
回到我們的分支; - 然后直接對當前分支 reset 到 revert 的前一個 commit 后奔穿,我們采用 cherry-pick 方式進行傻瓜式改寫便可以把歷史重寫了镜沽。(誰說的我們不能改寫歷史?)
改寫歷史贱田?
改寫歷史缅茉?等等,好像還有一個操作:rebase男摧。
rebase 是 Git 的一個神奇的命令蔬墩,前面我也說了译打,總會有人不喜歡 merge 之后歷史的分叉,這種分叉再匯合后會讓結(jié)構(gòu)看起來非吵镂遥混亂扶平,以致于無法管理。如果你不喜歡 commit 歷史出現(xiàn)分叉蔬蕊,那 rebase 絕對是你的救星结澄。
改寫歷史是 rebase 與生俱來的能力。我們可以用 git rebase -i <commit>
進行歷史的改寫岸夯。
我們試試看在我們的項目中直接使用 git rebase -i <commit>
會怎樣麻献。
我們會拿到分支后面的提交歷史,并且前面還有一個 Commands猜扮。我們可以從提示中看到勉吻,上面全寫的 pick 就是代表保持這個提交的意思,edit 代表編輯此次提交...
我們希望刪除此次 revert 這次提交旅赢,那當然我們最關(guān)心的就是 drop 了齿桃,甚至我們可以更加簡單粗暴:直接刪掉這一行。
然后我們便開始處理了煮盼。
過程中可能會出現(xiàn)沖突短纵,我們只需要解決就好。
解決掉沖突后僵控,再使用 git add <filename>
把它們 merge 進去香到。
oh,我們看到我們已經(jīng) rebase 成功了报破。我們再使用 git lg
查看一下提交歷史悠就。
我們成功改寫了歷史!
歷史改寫結(jié)束充易,我們還要做我們最開始想做的事情梗脾,進行 merge 操作。
可以看到蔽氨,這次我們 merge 確實如我們預期的不再發(fā)生沖突藐唠,方案親測有效!
寫在最后
寫了這么多鹉究,想必大家對解決方案也算比較清楚了。我們主要便是采用 git rebase -i <>
操作進入到 commit 歷史編輯頁面踪宠,然后進行歷史改寫處理自赔!