作實(shí)戰(zhàn):分支策略、提交規(guī)范與沖突處理完整指南)
這幾天我?guī)еM里的幾個(gè)新同學(xué)做了一次完整的Git多人項(xiàng)目開發(fā)流程演練從分支規(guī)劃、日常提交、合并請求到?jīng)_突處理、CI門禁把一條真實(shí)團(tuán)隊(duì)日常走了一遍。演練結(jié)束后我自己也把整個(gè)流程重新捋了一遍發(fā)現(xiàn)很多東西如果不親手跑一次光看文檔確實(shí)記不住。這篇文章就把這次演練的完整過程、我當(dāng)時(shí)怎么設(shè)計(jì)流程、踩過的坑、以及最終沉淀下來的團(tuán)隊(duì)規(guī)范整理出來。如果你正在帶新團(tuán)隊(duì)、帶實(shí)習(xí)生或者自己剛進(jìn)一個(gè)用Git協(xié)作的項(xiàng)目組這套流程可以直接拿來參照。哪怕你只有兩三個(gè)人的小項(xiàng)目里面關(guān)于分支策略、提交規(guī)范、沖突處理的思路也完全適用。1. 多人協(xié)作的痛點(diǎn)和Git工作流選型1.1 為什么三人以上的倉庫就開始亂很多人覺得Git嘛不就是commit、push、pull一個(gè)人寫的時(shí)候確實(shí)怎么折騰都行本地分支隨便開提交信息隨便寫反正自己看得懂。但人數(shù)一旦超過三個(gè)同時(shí)改的模塊開始重疊問題就一批批往外冒。最常見的是這幾種情況提交信息亂七八糟update、fix、111這種占了一半回頭翻歷史根本不知道當(dāng)時(shí)改了什么。直接往主分支推代碼推完發(fā)現(xiàn)編譯不過別人pull下來一臉懵。兩個(gè)人同時(shí)改了同一個(gè)文件后push的人默認(rèn)覆蓋另一個(gè)人半天的工作白做。以為自己在自己的分支上沒事結(jié)果分支長期不更新等要合并的時(shí)候發(fā)現(xiàn)已經(jīng)落后主分支幾百個(gè)提交沖突大到無從下手。這次演練我故意沒有用單人開發(fā)那種隨便的方式而是從一開始就按正規(guī)團(tuán)隊(duì)協(xié)作來設(shè)計(jì)。目的就是讓參與者體會多人協(xié)作的難點(diǎn)不是Git命令本身而是流程紀(jì)律。1.2 主流工作流對比與選型行業(yè)里現(xiàn)在常見的幾種工作流我簡單列個(gè)對比。工作流核心策略適合場景主要缺點(diǎn)主干集成Trunk-Based所有人短分支工作頻繁合入主干快速發(fā)布、DevOps成熟團(tuán)隊(duì)對CI要求高主干必須時(shí)刻可發(fā)布Git Flow分master、develop、release、feature、hotfix版本發(fā)布節(jié)奏固定的項(xiàng)目分支多流程重GitHub Flow只有main feature分支PR合入以PR為中心的協(xié)作環(huán)境部署策略需要配合Fork式每個(gè)開發(fā)者fork獨(dú)立倉庫開源跨團(tuán)隊(duì)協(xié)作同步上游比較繁瑣這次演練我選的是偏向GitHub Flow但稍作裁剪的模式原因很簡單團(tuán)隊(duì)不大發(fā)布節(jié)奏是靠流水線自動化的不需要Git Flow那種重型分支體系。核心規(guī)則就幾條默認(rèn)分支設(shè)為主干所有正式代碼都在這里。任何改動先從主干拉一個(gè)功能分支。分支命名帶上類型和描述比如fix/login-bug。代碼寫完后發(fā)起合并請求必須經(jīng)過審查和自動化檢查才能合入。主干永遠(yuǎn)是干凈、可用的狀態(tài)。有人會問為什么不用Fork式因?yàn)槟鞘强鐐}庫協(xié)作場景團(tuán)隊(duì)內(nèi)部共享一個(gè)倉庫效率更高沒必要引入雙份倉庫的同步成本。還有一點(diǎn)很重要分支策略的選擇要跟發(fā)布方式綁定如果你的項(xiàng)目是固定版本打包發(fā)布的Git Flow可能更合適如果是持續(xù)滾動的那主干集成和GitHub Flow這類輕量模式更舒服。2. 分支策略與提交規(guī)范設(shè)計(jì)2.1 分支命名不是小事很多人覺得分支名無所謂其實(shí)分支名是給團(tuán)隊(duì)看的不是給Git看的。一個(gè)清晰的分支名等于把你的工作意圖寫在了倉庫門口。我這次給團(tuán)隊(duì)定的規(guī)則是功能開發(fā)feature/xxx比如feature/user-login。缺陷修復(fù)fix/xxx比如fix/order-timezone。性能優(yōu)化perf/xxx。重構(gòu)調(diào)整refactor/xxx。實(shí)驗(yàn)驗(yàn)證experiment/xxx這個(gè)分支用完即棄。這里有個(gè)容易被忽略的點(diǎn)分支名不要帶個(gè)人昵稱或者臨時(shí)日期比如dev/zhangsan/update。因?yàn)榉种г谡麄€(gè)生命周期里可能被多個(gè)人接手按人名命名會造成歸屬誤解而且從分支名完全看不出業(yè)務(wù)意圖。另外我建議分支名用英文小寫加連字符不要用中文、不要用駝峰。這不只是習(xí)慣問題很多終端工具、CI腳本對分支名的解析方式不一樣特殊字符容易出問題。2.2 分支保護(hù)與權(quán)限控制這次演練開始前我在倉庫設(shè)置里把主干分支的保護(hù)打開了。保護(hù)分支的作用不是限制大家干活而是防止誤操作。我配置的規(guī)則供你參考不允許任何人直接push到主干。合并請求必須至少1個(gè)審查人批準(zhǔn)。所有自動化檢查必須通過。分支合并后自動刪除源分支。實(shí)際操作中很多人覺得“我是管理員我就可以直接推主干這樣快”這種想法在團(tuán)隊(duì)協(xié)作里特別危險(xiǎn)。管理員能直接推出了事故沒人攔得住輕則影響大家拿代碼重則把事故帶上線。保護(hù)分支不是限制權(quán)限是給所有人一道安全網(wǎng)。2.3 提交信息的語義化規(guī)范提交信息這塊我是吃過虧的。之前有次排查線上問題git log翻出來全是“fix”“update”“dev”根本沒法定位只能靠猜。從那以后我強(qiáng)制團(tuán)隊(duì)用約定式提交的格式。標(biāo)準(zhǔn)格式是這樣type(scope): subject bodytype表示類型常用有feat、fix、docs、style、refactor、perf、test、chore。scope是影響范圍比如模塊名或文件名可省略。subject用一句話說明做了什么祈使句不超過50個(gè)字符。舉例fix(order): correct timezone calculation on settlement date The previous logic used server local time, which caused incorrect settlement dates for overseas users.這里有個(gè)細(xì)節(jié)想提醒你subject不要寫過去式比如fixed、added那是在寫日記不是在描述代碼變更。規(guī)范要求的是這條提交合入后會做什么不是回想你剛做了什么。至于正文body超過一行的改動建議都寫上解釋為什么改這比代碼注釋重要得多。3. 實(shí)操演練從拉分支到合并的完整閉環(huán)3.1 同步最新代碼與拉取功能分支演練第一步每個(gè)人都從主干拉一個(gè)功能分支。當(dāng)時(shí)我給他們演示了標(biāo)準(zhǔn)操作git checkout main git fetch origin git pull origin main git checkout -b feature/import-batch這三步看起來簡單但很多人會漏掉git fetch origin這一步。直接用git pull雖然也會拉取遠(yuǎn)端但它依賴當(dāng)前分支和遠(yuǎn)端分支的追蹤關(guān)系在多人共用倉庫時(shí)如果本地分支追蹤信息亂了pull的行為可能不符合預(yù)期。fetch是純拉取遠(yuǎn)端元數(shù)據(jù)不會改變工作區(qū)所以習(xí)慣上先fetch再看情況合并更安全。還有一個(gè)常見毛病直接從別人的分支上再拉分支。比如git checkout -b feature/xxx的時(shí)候當(dāng)前在dev/other下新分支基于別人的舊代碼分支結(jié)果自己代碼里全是別人的改動。創(chuàng)建功能分支永遠(yuǎn)從最新主干出發(fā)。3.2 日常提交與推送的節(jié)奏在演練中我要求大家在功能開發(fā)階段盡量保持小步提交而不是攢一天之后一次提交。我聽到最多的問題是小步提交會不會讓歷史很碎其實(shí)不會關(guān)鍵在于每個(gè)提交都要保持項(xiàng)目可運(yùn)行。我當(dāng)時(shí)給團(tuán)隊(duì)定了一個(gè)“三步走”節(jié)奏完成一個(gè)可運(yùn)行的子功能提交一次。提交前先看git status和git diff確認(rèn)沒有多余文件。提交信息必須寫清楚。代碼寫完后推送到遠(yuǎn)端git push origin feature/import-batch這里有個(gè)新手很容易踩的坑第一次push的時(shí)候直接git push會報(bào)錯(cuò)因?yàn)檫h(yuǎn)端沒有這個(gè)分支。需要用git push -u origin feature/import-batch來建立上游關(guān)系。一旦加上-u之后就可以直接git push了。3.3 發(fā)起合并請求并做Code Review功能開發(fā)完成并推送之后進(jìn)入合并請求環(huán)節(jié)。這一步我們團(tuán)隊(duì)沒有直接用網(wǎng)頁版那種大提交的做法而是要求先自己在本地把主干合一次確認(rèn)沒問題后再發(fā)起。發(fā)起合并請求時(shí)模板里必須說明三件事改動目的、改動內(nèi)容、測試記錄。我當(dāng)時(shí)在演練里演示了一個(gè)反例直接寫“添加功能”四個(gè)字。這種描述等于沒寫審查人根本不知道你動了什么。Code Review我定的原則是先看改動整體描述再看具體diff。優(yōu)先關(guān)注邏輯正確性不要只抓代碼風(fēng)格。發(fā)現(xiàn)問題用評論直接指出帶上具體行號和修改建議。提交者收到評論后在當(dāng)前分支繼續(xù)提交不要開新分支。這里我想特別說下Code Review不是找茬是團(tuán)隊(duì)互相補(bǔ)齊盲區(qū)的過程。寫代碼的人容易陷入自己的思路審查人反而能發(fā)現(xiàn)一些邊界情況的漏洞。我見過一些團(tuán)隊(duì)Review就是走個(gè)過場“LGTM”點(diǎn)一下就完事那這個(gè)環(huán)節(jié)就失去了意義。3.4 合并策略選擇與沖突現(xiàn)場合并請求通過后會出現(xiàn)合并方式的選擇。我見過有的平臺提供三種Merge Commit、Squash and Merge、Rebase and Merge。我們團(tuán)隊(duì)默認(rèn)用Squash and Merge。為什么因?yàn)楣δ芊种峡赡苡衅甙藗€(gè)瑣碎的提交比如“修了個(gè)標(biāo)點(diǎn)”“補(bǔ)了個(gè)注釋”這些過程性提交對主干歷史沒有價(jià)值。Squash會把它們壓成一個(gè)干凈的提交再合入主干歷史看起來邏輯清晰。但Squash也帶來一個(gè)注意點(diǎn)合并后源分支和主干的關(guān)系不是簡單的快進(jìn)源分支從技術(shù)上來說和主干已經(jīng)分叉了所以務(wù)必在合并后刪除源分支否則后續(xù)繼續(xù)在這個(gè)分支上開發(fā)會遇到莫名其妙的重復(fù)提交問題。演練中就實(shí)際發(fā)生了一次沖突。當(dāng)時(shí)兩個(gè)隊(duì)友同時(shí)改了config.js一個(gè)在頂部加了一個(gè)配置項(xiàng)另一個(gè)在底部改了一個(gè)默認(rèn)值。等他們做合并時(shí)Git提示CONFLICT (content): Merge conflict in config.js這時(shí)候我讓他們不要慌按這個(gè)流程走git status找到?jīng)_突文件編輯器里會看到類似這樣的標(biāo)記 HEAD defaultLang en; defaultLang zh-CN; feature/lang-option HEAD到之間是當(dāng)前分支的內(nèi)容到之間是正在合并進(jìn)來的分支的內(nèi)容。手動決定保留哪邊或者都保留然后刪掉三行標(biāo)記保存再執(zhí)行g(shù)it add config.js git commit這里我想重點(diǎn)說**永遠(yuǎn)不要直接拷貝對方的文件來覆蓋沖突。**有人說沖突麻煩干脆把整個(gè)文件替換掉。那會丟失掉另一半改動而且這種問題不是當(dāng)場能發(fā)現(xiàn)的可能要很久之后才爆出來。正確思路是逐塊判斷保留雙方的有效內(nèi)容。4. 沖突根因分析與日常避免沖突的套路4.1 沖突發(fā)生的三種典型時(shí)機(jī)沖突不是隨機(jī)的它有自己的規(guī)律。我總結(jié)下來主要在這三種場景高發(fā)多人同時(shí)修改同一文件的不同區(qū)域比如一個(gè)人改了文件頭部import另一個(gè)人改了文件底部配置。分支存在時(shí)間過長和主干漸行漸遠(yuǎn)合并時(shí)大量重疊。配置文件、公共組件、接口定義這類全局文件被頻繁修改。很多人誤以為沖突是Git的Bug或者覺得只要pull勤快就不會沖突。其實(shí)沖突的本質(zhì)是雙方改了同一塊代碼Git無法自動判斷誰是對的。沖突不是壞事它恰恰是Git在保護(hù)你的代碼不被靜默覆蓋。4.2 用合并前同步代替事后解決預(yù)防沖突最有效的辦法不是解決得好而是讓分支保持新鮮。分支活得越久沖突概率越大。我在團(tuán)隊(duì)里定的一個(gè)硬性要求是功能分支的生命周期不超過三天。如果超過三天每天至少從主干同步一次。同步操作我推薦用merge而不是rebase。雖然很多人推崇rebase讓歷史更線性但在共享分支上rebase會重寫歷史一旦分支已經(jīng)推送到遠(yuǎn)端rebase會導(dǎo)致其他人本地分支和遠(yuǎn)端對不上反而制造混亂。所以團(tuán)隊(duì)內(nèi)統(tǒng)一用mergegit fetch origin git merge origin/main執(zhí)行完之后照例解決沖突然后繼續(xù)開發(fā)。有個(gè)實(shí)用技巧可以提前暴露沖突如果你改動的是某個(gè)全局文件改之前先看一眼主干上這個(gè)文件最新變動能避開很多沖突。4.3 文件級分工降低碰撞概率沖突還有一個(gè)來源是團(tuán)隊(duì)的代碼結(jié)構(gòu)沒有邊界。兩個(gè)人都在往一個(gè)大文件里加?xùn)|西那是必然撞車。我們在演練中討論過這個(gè)問題最后的結(jié)論是如果某個(gè)文件經(jīng)常被多個(gè)人修改應(yīng)該把它拆成更小的模塊。比如配置項(xiàng)按模塊拆分成多個(gè)配置文件公共組件按職責(zé)拆分。這看起來像是代碼架構(gòu)問題但實(shí)際影響的是Git協(xié)作效率。這次演練之后有個(gè)新同學(xué)跟我說他以前都以為沖突是個(gè)技術(shù)問題現(xiàn)在明白了大部分沖突其實(shí)是溝通問題。深表認(rèn)同。5. 自動化檢查與合并門禁5.1 提交前攔截pre-commit鉤子光靠人的自覺不夠流程里必須加入自動化。這次演練我配置了幾道檢查關(guān)卡第一道是pre-commit鉤子。pre-commit的作用是在提交之前自動執(zhí)行一些檢查比如代碼格式、靜態(tài)檢查、敏感信息掃描。一個(gè)典型配置包含這些環(huán)節(jié)保存一個(gè)隱藏目錄來放鉤子腳本。團(tuán)隊(duì)統(tǒng)一裝好之后每次git commit的時(shí)候鉤子會自動跑一遍檢查是否按約定式提交填寫信息。檢查是否有調(diào)試代碼或者臨時(shí)文件。運(yùn)行代碼格式化器。運(yùn)行單元測試中的冒煙用例。有同學(xué)覺得這些檢查浪費(fèi)時(shí)間覺得“先提交之后再說”。但實(shí)戰(zhàn)經(jīng)驗(yàn)是提交那一刻是代碼狀態(tài)最新鮮的時(shí)候這時(shí)候暴露問題修復(fù)成本最低。等代碼合入主干再跑檢查上下文已經(jīng)丟了。5.2 合并前的CI門禁第二道關(guān)卡是CI。我們在合并請求上接入了一條輕量流水線每次push都會自動跑編譯或構(gòu)建。跑核心單元測試。生成覆蓋率報(bào)告。做一次依賴安全檢查。CI結(jié)果會直接顯示在合并請求頁面上。我們在分支保護(hù)規(guī)則里設(shè)置了“必須全部通過才能合并”。這樣一來誰也沒法把一個(gè)編譯都不過的改動合進(jìn)主干。當(dāng)時(shí)演練中有人問本來本地已經(jīng)跑過測試了CI還跑一遍不是浪費(fèi)嗎這個(gè)想法我理解但本地環(huán)境每個(gè)人千差萬別有人依賴沒裝全有人跑的是緩存結(jié)果CI的價(jià)值在于提供一個(gè)統(tǒng)一干凈的環(huán)境從零開始驗(yàn)證。本地通過只能代表你機(jī)器上沒問題CI通過才能代表項(xiàng)目沒問題。5.3 自動合并的隱藏風(fēng)險(xiǎn)有些團(tuán)隊(duì)圖省事把合并設(shè)置為“檢查通過后自動合并”。聽起來很高效但我不推薦。原因很簡單看代碼的人需要時(shí)間而自動化檢查通過只能代表機(jī)器驗(yàn)證過不能代表邏輯正確。有一次我在一個(gè)群里看到有人抱怨CI全綠合并后第二天線上出了問題。一查代碼邏輯把兩個(gè)字段的賦值搞反了但測試用例沒有覆蓋這條路徑。CI只能保證你測過的場景沒問題不能保證你沒測過的場景也沒問題。所以人在合并前的那道Review還是得人工來。6. 常見問題與排查技巧實(shí)錄6.1 誤提交到錯(cuò)誤分支怎么辦演練中出現(xiàn)了一個(gè)很典型的失誤有人在自己功能分支上做著A任務(wù)不小心把B任務(wù)的改動也提交了。處理辦法很簡單先用git log找到誤提交的上一個(gè)提交序號。把分支回退但保留工作區(qū)改動git reset --soft HEAD~1這樣commit被撤銷改動還會留在暫存區(qū)。然后把屬于B任務(wù)的改動挑出來放到另一個(gè)分支上git stash git checkout -b fix/task-b git stash pop這里最忌諱的是用git reset --hard那會把改動全部丟掉。reset --soft是保留修改只撤銷提交reset --hard是連改動一起扔掉除非你確定不要了否則少用--hard。6.2 本地分支落后主分支太多功能開發(fā)周期長的人一定會遇到這個(gè)問題自己的分支拉了半個(gè)月主干上已經(jīng)幾十個(gè)新提交功能分支又一直沒合。這時(shí)候千萬別硬合并沖突會大到懷疑人生。我推薦的做法是分階段同步# 第一次同步先把主干拉到本地 git fetch origin git merge origin/main # 解決沖突后提交 git commit # 推送自己分支 git push origin feature/xxx如果沖突文件特別多不要試圖一次性全部解決??梢韵扔胓it status列出所有沖突文件從核心文件開始一個(gè)一個(gè)來每解決完一個(gè)就git add一個(gè)。這樣即使中途有事離開也不會丟進(jìn)度。還有一個(gè)分散沖突的技巧先合入一些已經(jīng)塵埃落定的分支比如別人已經(jīng)合進(jìn)主干的功能分支再合并主干。這相當(dāng)于分步消化。6.3 誤刪分支后的恢復(fù)團(tuán)隊(duì)協(xié)作中容易發(fā)生一種狀況合并請求關(guān)掉之后順手把源分支刪了第二天發(fā)現(xiàn)那個(gè)分支上還有一段沒整理完的東西。不用擔(dān)心Git在刪除分支時(shí)只是刪了引用提交對象還留在對象庫里?;謴?fù)方法# 查看所有分支的提交包括已刪除的 git reflog # 或者查看被刪分支最后一次提交 git log -g找到最后一次提交的哈希值后直接從這個(gè)哈希創(chuàng)建新分支git checkout -b feature/restore-xxx 4f8a1b2reflog是本地操作日志默認(rèn)保留90天它記錄了HEAD指針的所有變動。這就是為什么我建議剛碰Git的人一定要記住在Git里只要你提交過大部分東西都能找回來真正的丟失是那些從未提交過的內(nèi)容。6.4 如何處理一團(tuán)亂的提交歷史最后說一下歷史整理。我見過功能分支明明只改十幾個(gè)文件但提交了三十多次一半是“調(diào)試中”“臨時(shí)保存”這種。合入主干前我會讓大家做一件事情交互式變基來整理提交。git rebase -i HEAD~5執(zhí)行后會出現(xiàn)一個(gè)編輯列表把想合并的提交前改成squash或s保存退出Git會提示你重新編輯提交信息??梢园盐鍌€(gè)瑣碎提交合并成一個(gè)清晰的功能提交。但要記住一條鐵律**rebase只能用在還沒推送過的提交上。**如果分支已經(jīng)推送到遠(yuǎn)端又rebase過你的本地歷史和遠(yuǎn)端歷史就對不上了。之后push需要強(qiáng)制推送這會把遠(yuǎn)端別人的改動也搞亂。團(tuán)隊(duì)公共分支嚴(yán)禁rebase功能分支自己沒推送的部分可以隨意整理。7. 這次演練沉淀下來的團(tuán)隊(duì)行為準(zhǔn)則最后把這次演練過程中形成的幾條行為準(zhǔn)則列在這里這些都是我們踩過坑之后總結(jié)的你直接照抄大概率會有幫助。第一主干永遠(yuǎn)保持綠色。誰往主干推了編譯不過的代碼誰負(fù)責(zé)在最短時(shí)間內(nèi)修復(fù)不計(jì)代價(jià)。第二每個(gè)功能分支只做一件事。一件事做完、合并、刪除分支再開下一個(gè)。分支里塞的任務(wù)越多生命周期越長沖突概率越大。第三代碼合入前必須通過三道門本地自測、自動化檢查、人工Review。缺一道就合就是給未來埋雷。第四提交信息是寫給未來的自己和同事看的不是寫個(gè)流程交差。我自己在實(shí)際操作中的體會是Git命令背得再熟也不如一次真實(shí)演練帶來的體感深。工具始終是工具真正讓協(xié)作順暢的是背后那套紀(jì)律和共識。你可以先從一個(gè)小項(xiàng)目開始嘗試這套流程把分支保護(hù)打開把合并請求用起來哪怕只有兩三個(gè)人堅(jiān)持兩三個(gè)星期后再回頭看你會明顯感覺到代碼庫的歷史變得干凈有序了。