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