前一段時間接觸一個新團隊時,發現他們因沒有”流程”,常發生代碼合併困難,或者合併後漏掉幾個commit,上線後把別人代碼搞壞的情況。由此想探討一下“流程”問題。
兩大流程
調查時發現阮一峯老師在Git 工作流程一文中已經探討了3種經典流程,由此瞭解到幾種經典流程的名字:git flow, github flow, 和 gitlab flow。不過我認爲git flow不適合多人協作,所以只探討github流程和gitlab流程。
先談談基本原則
兩種流程區別只在於gitlab流程比github流程多一個線上分支。他們的原則是一樣的:
- 不在主分支開發,保證主分支“乾淨”,只接受開發完成的代碼合併,或補丁。
- 每個功能在單獨分支上開發。
- 避免多人在同一個模塊上做修改。按模塊分工,錯開排期。
哪怕自己一個人玩,也總有進行到一半意外終止,或不小心把分支玩壞了的時候。創建一個開發分支可避免別人受到自己還沒“完成”的代碼的影響,保證主分支“乾淨”。
當兩人動了同一塊代碼,後合併的一方得解決很多衝突。合併大塊代碼衝突極費時間。需搞清楚來龍去脈,且產生新bug機率很高。因此儘量避免多人在同一塊代碼上修改。
團隊小,大家都知道對方在幹什麼的時候,通過適當排期很容易避免衝突。而團隊大了後,便需要分割成小團隊,明確負責的功能模塊來避免。
Github flow:主分支 + 多開發分支
Github flow只存在一個長期維護的主分支,通常叫master
。接下來粗略分3種情況討論:
- 新功能開發、非緊急bug修復、小範圍不影響接口的重構
- 打補丁
- 大範圍重構
新功能開發/非緊急bug修復/小範圍重構
首先,基於最新的master創建獨立的功能開發分支。
Git樹如下:
待功能開發完成,測試通過後,向master發起PR (pull request 合併請求),請求維護人員審覈代碼。
代碼被合併之前,需定期rebase或merge主分支的最新代碼,減少大塊衝突發生的機率,並保證代碼在最新版上能正常工作。
審覈通過後,由維護人員合併代碼。被合併的分支可立即清理,或定期用git工具提供的一鍵清除已合併分支清理。
合併之後git樹如下:
發版
一個階段的開發完成,或到指定時間點,在master分支上發版:
- 更新版本號(如npm version)。新功能可更新次要版本。如
v16.11.9
更新到v16.12.0
。改動較大時,則應更新主版本。如v1.0.1
更新到v2.0.0
。 - 在最新的master commit上打版本tag。如React的v16.12.0。
- 適當地添加版本說明、更新記錄。
- npm包發佈,或部署。
打補丁
爲已發佈版本打補丁,不適合基於master分支直接打,而要用上節中打的tag。這是因爲主分支時不時有新功能合併進來,代碼上下文隨時會發生變化,我們不能在爲線上打補丁的時候帶進不能在此時上線的代碼。
假設在v1.5.0
版本上打補丁。我們先基於該版本tag創建一個分支:
$>master: git fetch
$>master: git checkout v1.5.0
$>v1.5.0: git checkout -b jennie/fix/xxx
然後在新分支上開發、測試、再提交合並請求。合併請求的目標可以是:
- master - 如果master主版本仍舊在v1.5。
- 舊的主版本分支 - 如果master主版本已經是v2,有些項目會保留一個v1的分支,用於維護舊版本。
代碼審覈、合併後,倉庫負責人應發一個新的補丁版本,如v1.5.1
。過程同上節。
大範圍重構
大範圍重構一般能分成小重構一點點來就一點點來吧,除非像Angular 1到2那樣整個接口都重新設計。
遇到這種“重新設計”的坑,基本都是舊代碼扔掉,從新再寫了。這時最容易的是從初始或一個比較空白的commit創建新的主分支。如master主版本爲v1,現在要開始開發v2,就建一個v2主分支。合理規劃,在v2分支基礎上創建新功能分支,同上面章節所述的新功能開發流程相同。
v2開發告一斷落,併發布預覽版之後,因爲master與v2的git樹相差甚遠,已不適合走合併的路子,而且也沒必要。那此時最容易的就是讓倉庫管理員將現在的master更名爲v1,v2更名爲master,讓v2分支變爲主分支。
評價
Github flow比較適合庫、組件、框架、工具、應用、開源項目,或是發佈控制比較簡單的小團隊。它的優點是流程比較直觀清晰,容易理解上手。但像網站一類的代碼倉庫,並不需要爲舊版打補丁,需要補丁的永遠是當前線上部署的版本。並且功能或修復進入主分支後需要等待一段時間才能上線,期間有可能因爲誤操作、更改排期導致改動被回退。因此單一主分支不夠,需要再加一個發佈的第二主分支(release),也就是下面我們要討論的gitlab flow。
Gitlab flow: 預發佈分支(主分支)+ 已發佈分支 + 多個開發分支
Gitlab flow長期維護兩個主分支master和release。這種模式通常更適合於網站代碼一類不太需要維護舊版本,但經常爲線上版本打補丁的項目。
Gitlab flow中做新功能,修非緊急bug,以及重構的部分與github flow相同,使用master主分支。但在設計上線發版和打補丁時會有些區別。
發版
與github flow不同的是,gitlab flow在代碼合併到master分支後,並不直接從master發版。
master可用於做上線前的迴歸測試,爲上線做準備。一旦上線準備完成,需要將master合併入release分支,再從release分支發版。
發版的tag在master或release打沒有什麼區別,因爲對應的commit應最終同時存在於兩個分支中。
master合併入release分支的操作,應是沒有任何衝突的,除非release上有補丁沒有被及時合併入master,這部分在下節作詳細討論。master合併入release可考慮由維護者手動進行。
打補丁
有了release分支後,所有需要緊急上線的補丁應直接打在release分支上。即基於release分支創建補丁分支。修復,測試,審覈之後合併入release分支而非master分支。再直接從release發版。
打過補丁的release分支會與master分支產生分歧:
此時要記得及時將release分支的改動合併入master,以保證後續的開發能順利進行。
team足夠大的時候,這步發生衝突的機率很高。一旦發生衝突,因爲網頁版的合併工具一般都不太可靠,推薦維護人員手動線下合併。
解決衝突時,先從最新的master創建一個merge-release分支。在這個分支裏,我們合併release分支,解決衝突並提交一個合併commit。然後將代碼推送至遠端,並向master提交PR,讓相關人員審覈。
這就是兩個主流流程。下面我們補充一些實際操作過程中涉及處理和需要注意的細節。
併入主分支後回退
導致併入主分支後回退有以下可能:
- 維護者誤操作。畢竟合併只是點一個按鈕,是人都有誤操作的時候。
- 合併後上線日程推遲。比如,某國皇族去世了,爲了政治正確,一段時間內不許有搞活動。
- 合併後發現其他小夥伴的代碼掛了。
主分支的內容分分鐘可能被小夥伴用來創建自己的功能分支,想要回退的commit可能已經存在在小夥伴們的功能分支上。爲了避免小夥伴們受到影響,我們用雙git revert
:
- 可在主分支上直接操作
git revert <commit_sha>
或創建PR。Git revert會新建一個回退commit。 - 再從主分支創建一個新分支,然後
git revert <revert_commit_sha>
。 - 待需要上線時,再合併第二個revert。
多環境
企業級開發中,爲保證發佈應用的可靠性,會引入更多複雜的流程。比如QA測試,用戶驗收測試,迴歸測試等等。同時會引入多個獨立環境以保證能各自運行不同版本的代碼,用作不同目的。
當然這事又增加了流程的複雜性。
共用測試環境
先說個反例。有些團隊爲QA測試環境建了個分支叫test。每當開發完成需要QA測試的時候,他們就把代碼併入test分支,開發分支保留。待QA測試完成之後再併入master主分支等待上線。
一段時間後大家發現測試環境非常不穩定,迴歸測試bug非常多。QA表示很委屈,測試明明是通過的,不能是我的鍋。
這流程裏代碼被合併了至少兩次,而且兩次的合併目標裏的內容不同,QA測試的上下文跟最終上線的上下文相差甚遠,增加了不穩定性。一旦遇到衝突也極可能需要解決兩次,增加了額外的時間成本。同時解決衝突量越多,越容易引入新bug。
獨立測試環境
實踐經驗得出以下比較好的解決方案:
- QA和用戶驗收測試用以已包含release最新代碼的功能分支,如果master極少發生回退操作最好用master。即每個需要測試的功能分支享有獨立環境。
- 迴歸測試使用待“上線的”master分支,或master分支上待發的版本標籤(tag) 。
以上方案缺陷是環境的控制。團隊需要有足夠的多環境部署控制能力或有牛X的DevOps做容器化支持來保證大家不搶環境,不浪費資源。
功能開關
還有種成本稍低一些的做法是功能開關(feature toggle)。它允許還未開發完成的代碼存在於上線的分支中,可使用構建工具將不上線的代碼塊移除,在測試環境中根據需要打開。經過適當的設計,也可以做A/B testing等更多的控制。
當然事情不會如此完美。
功能開關可以選擇將“關閉”的代碼移除。前端的代碼移除靠AST操作,玩過的都知道有多複雜。多數功能開關工具都沒法做好內連代碼的移除。因此會建議使用特定的編程方式,比如只能用if else來判斷代碼塊的開與關。
如果關的代碼不被移除,又難保不會影響其他開着的功能。且開發需要兼顧在同一文件上多個版本,增加開發成本。如果開發2個不同文件服務功能的開與關,那沒有改動的部分就要一起維護,增加維護成本。
因此我認爲它不適用於作爲節省環境成本的解決方案,團隊小時可以藉此一用。
處理複雜依賴關係
有時有“大功能”需要多位開發一起分工合作,或是涉及同一模塊的不同改動需要同期進行但有先後依賴關係。當然其實項目和產品夠給力這都可以通過好好計劃避免的。
然並卵。一旦遇到此類情況,可先從master創建一個功能主分支,比如:feature-a/master。
分工合作的小夥伴們再從feature-a/shared分出自己的模塊分支。如:feature-a/module-1, feature-a/module-2。模塊分支之間在分工時就需避免互相依賴。
如果有共用的方法、常量,線下商量清楚後放入功能主分支,模塊分支rebase或merge功能主分支。
模塊開發完成後,可互相審覈一下代碼,審覈通過,就可以合併入功能主分支。待所有模塊開發完成,便可在功能主分支上進行測試。
期間因爲項目大開發週期長,注意功能主分支需定期合併master或release。注意功能主分支不要rebase,一旦rebase,所有依賴它的模塊分支需特殊處理,不然git樹很混亂,還會面臨一些奇怪的衝突。
rebase功能主分支後
如果不小心rebase了,我發現有兩種方式處理比較容易:
方法一:cherry-pick。從最新的功能主分支重新創建分支feature-a/module-1,將改動commit從原模塊分支上cherry-pick到新的feature-a/module-1上。
$>feature-a/shared: git checkout -b feature-a/module-1
$>feature-a/module-1: git cherry-pick xxx
如果commit很多,事前flush一下commit就不必cherry-pick這麼多commit了。
方法二: soft reset + stash。在git log中找到創建分支時的commit SHA。soft reset到該commit就能得到所有你在此分支做的所有改動。把它們git stash起來。重新從最新的功能主分支創建模塊分支,再把stash的改動放回來。
$>old-branch: git reset --soft xxx
$>old-branch: git stash
$>old-branch: git checkout feature-a/shared
$>feature-a/shared: git checkout -b feature-a/module-1
$>feature-a/module-1: git stash pop
制定規則
實行流程中,制定一些規則可以幫助團隊工作更高效。
分支名稱
**規則一:使用有意義的分支名。**有意義的分支名能幫助自己和小夥伴用關鍵字尋找分支。
舉例:
- 小夥伴建了個功能主分支,我不記得叫啥,但知道通常按功能命名,在git網頁版工具中通過關鍵字搜索一下就找到了功能主分支和小夥伴的分支。
- 我有時一天在無數分支之間切換,切換時按照我自己的命名規則用關鍵字就可以迅速在命令行自動完成分支名並切換。
反例:小夥伴偷懶直接用了需求單的ID(只是幾位數字)命名,隔兩天後不找到那個需求單,看它的標題,壓根不知道這分支是幹啥的。
規則二:用名字縮寫/修改類型/修改內容命名。 git的GUI工具如SourceTree,Github官方工具,都會把這種命名方式處理成樹狀顯示,分支多的時候就凸顯出這類歸類顯示的整潔清晰。
commit消息
規則一:包含在哪個功能上修復、增加、刪除、修改了什麼,還有爲啥。 以個人經驗來講,commit消息的用處如下:
- 追責。git blame並不一定能幫你找到誰是搞壞代碼的罪魁禍首,也不能幫你搞清楚來龍去脈,但清晰的commit消息可以。
- 代碼回退時容易找到回退目標。
- 識別操作失誤中額外增加的commit。
- 生成更新日誌。
規則二:用鏈接解釋爲啥。 比如Github可以通過特定格式連接Issue或PR,當簡短的消息無法讓人瞭解事情的全貌,提供記錄會有所幫助。
權限
一般git社區裏的代碼倉庫至少有4種角色:管理員、維護人員、其他開發、訪客。利用這4種角色做適當的權限控制,我們可以避免新手誤操作,和非相關人員不正當使用代碼。
假設團隊隊長是管理員,資深隊員是維護人員,剩餘不足年的新人和不靠譜的小夥伴都只是普通開發角色,非開發都是訪客。
常見操作有:
- 保護主分支。不允許任何人直接往主分支推代碼。只允許管理員和維護人員合併PR。
- 代碼評審。只有通過管理員和維護人員審覈過的代碼才能進入合併流程。
貼標籤label
標籤可以用於對PR或Issue進行分類。比較有用的標籤有以下幾類:
- 表明類型:功能、bug、問問題、討論等。
- 項目名稱。常見於monorepo之類多項目共享一個代碼倉庫的情況。
- 狀態:代碼評審狀態、過久沒人處理但需要處理的triage狀態等。
- 參加活動。比如Hacktoberfest。
- 適合人羣。比如Good first issue,對於想爲開源做貢獻的新手是一個很好的起始點。
PR和Issue樣板
創建合適的PR和Issue樣板,列出希望對方說清楚的點,可以減少很多交流成本。
如上圖爲React的Issue樣板,要求大家在提bug的時候描述bug表現,期望表現,發生的環境等。
Codeowners
Codeowner是一個文件,用於列出代碼倉庫路徑對應的維護者。一旦有了這個文件,大家在創建PR的時候就不用愁發給誰了,像Github就會自動根據PR的更改推薦評審人。
Github詳細的說明看這裏。
自動化測試
自動化測試、lint、還有類型驗證可以幫開發避免許多bug發生。因此,有必要在工作流程中強制一些測試,如:
- 用Husky在commit代碼之前做一些檢測,比如ESlint。
- 用CI設置測試步驟,並設置必須通過的步驟。