疫情期間我感覺整個人懶散了不少,慢慢有意識要振作起來了,恢復到正常的節奏。最近團隊代碼庫從Gerrit遷移到了Gitlab,為了讓前端團隊日常開發工作有條不紊,高效運轉,開發歷史可追溯,我也查閱和學習了不少資料。參考業界主流的Git工作流,結合公司業務特質,我也梳理了一套適合自己團隊的Git工作流,在這里做下分享。
分支管理
首先要說的是分支管理,分支管理是git工作流的基礎,好的分支設計有助於規范開發流程,也是CI/CD的基礎。
分支策略
業界主流的git工作流,一般會分為develop, release, master, hotfix/xxx, feature/xxx等分支。各個分支各司其職,貫穿了整個開發,測試,部署流程。我這里也基於主流的分支策略做了一些定制,下面用一張表格簡單概括下:
| 分支名 | 分支定位 | 描述 | 權限控制 |
|---|---|---|---|
| develop | 開發分支 | 不可以在develop分支push代碼,應新建feature/xxx進行需求開發。迭代功能開發完成后的代碼都會merge到develop分支。 | Develper不可直接push,可發起merge request |
| feature/xxx | 特性分支 | 針對每一項需求,新建feature分支,如feature/user_login,用於開發用戶登錄功能。 | Develper可直接push |
| release | 提測分支 | 由develop分支合入release分支。ps: 應配置此分支觸發CI/CD,部署至測試環境。 | Maintainer可發起merge request |
| bug/xxx | 缺陷分支 | 提測后發現的bug,應基於develop分支創建bug/xxx分支修復缺陷,修改完畢后應合入develop分支等待回歸測試。 |
|
| master | 發布分支 | master應處於隨時可發布的狀態,用於對外發布正式版本。ps: 應配置此分支觸發CI/CD,部署至生產環境。 | Maintainer可發起merge request |
| hotfix/xxx | 熱修復分支 | 處理線上最新版本出現的bug | Develper可直接push |
| fix/xxx | 舊版本修復分支 | 處理線上舊版本的bug | Develper可直接push |
一般來說,develop, release, master分支是必備的。而feature/xxx, bug/xxx, hotfix/xxx, fix/xxx等分支純屬一種語義化的分支命名,如果要簡單粗暴一點,這些分支可以不分類,都命名為issue/issue號,比如issue/1,但是要在issue中說明具體問題和待辦事項,保證開發工作可追溯。
保護分支
利用Protected Branches,我們可以防止開發人員錯誤地將代碼push到某些分支。對於普通開發人員,我們僅對develop分支提供merge權限。

具體操作案例請前往下面的實戰案例一節查看。
issue驅動工作
我們團隊采用的敏捷開發協作平台是騰訊的TAPD,日常迭代需求,缺陷等都會在TAPD上記錄。為了讓Gitlab代碼庫能與迭代日常事務關聯上,我決定用Gitlab issues來做記錄,方便追溯問題。
里程碑
里程碑Milestone可以認為是一個階段性的目標,比如是一輪迭代計划。里程碑可以設定時間范圍,用來約束和提醒開發人員。

里程碑可以拆解為N個issue,新建issue時可以關聯里程碑。比如這輪迭代一共5個需求,那么就可以新建5個issue。在約定的時間范圍內,如果一個里程碑關聯的所有issue都Closed掉了,就意味着目標順利達成。

標簽
Gitlab提供了label來標識和分類issue,我覺得這是一個非常好的功能。我這里列舉了幾種label,用來標識issue的分類和緊急程度。

issue分類
所有的開發工作應該通過issue記錄,包括但不限於需求,缺陷,開發自測試,用戶體驗等范疇。
需求&缺陷
這里大概又分為兩種情況,一種是TAPD記錄在案的需求和缺陷,另一種是與產品或測試人員口頭溝通時傳達的簡單需求或缺陷(小公司會有這種情況...)。
對於TAPD記錄的需求和缺陷,創建issue時應附上鏈接,方便查閱(上文中已有提到)。
對於口頭溝通的需求和缺陷,我定了個規則,要求提出人本人在Gitlab上創建issue,並將需求或缺陷簡單描述清楚,否則口頭溝通的開發工作我不接(也是為了避免事后扯皮)。
ps:其實要求產品或者測試提issue,還不如上Tapd記錄。我定這么個規則,其實就是借Gitlab找個說辭,杜絕口頭類需求或缺陷,哈哈。
開發自測試
開發者自己發現了系統缺陷或問題,此時應該通過issue記錄問題,並創建相應分支修改代碼。

實戰案例
我前面也說了,我的原則是issue驅動開發工作。
下面用幾個例子來簡單說明基本的開發流程。小公司里整個流程比較簡單,沒有復雜的集成測試,多輪驗收測試,灰度測試等。我甚至連單元測試都沒做(捂臉...)。
公共庫和公共組件其實是很有必要做單元測試的,這里立個flag,后面一定補上單元測試。
需求開發
feature/1,一個特性分支,對應issue 1
創建需求
正常的需求當然來源於產品經理等需求提出方,由於是通過示例說明,這里我自己在TAPD上模擬着寫一個需求。

創建issue
創建Gitlab issue,鏈接到TAPD中的相關需求。


創建分支&功能開發
基於develop分支創建feature分支進行功能開發(要保證本地git倉庫當前處於develop分支,且與遠程倉庫develop分支同步)。
git checkout -b feature/1
或者直接以遠程倉庫的develop分支為基礎創建分支。
git checkout -b feature/1 origin/develop
ps:我這里用的feature/1作為分支名,其實這里的1是用的issue號,並沒有用諸如feature/login_verify之類的名字,是因為我覺得用issue號可以更方便地找到對應的issue,更容易追蹤代碼。
接着我們開始開發新功能......

commit & push
完成功能開發后,我們需要提交代碼並同步到遠程倉庫。
PS D:\projects\gitlab\project_xxx> git add .
PS D:\projects\gitlab\project_xxx> git cz
cz-cli@4.0.3, cz-conventional-changelog@3.1.0
? Select the type of change that you're committing: feat: A new feature
? What is the scope of this change (e.g. component or file name): (press enter to skip)
? Write a short, imperative tense description of the change (max 94 chars):
(9) 登錄校驗功能
? Provide a longer description of the change: (press enter to skip)
? Are there any breaking changes? No
? Does this change affect any open issues? Yes
? If issues are closed, the commit requires a body. Please enter a longer description of the commit itself:
-
? Add issue references (e.g. "fix #123", "re #123".):
fix #1
git push origin HEAD
git cz是利用了commitizen來替代git commit。詳情請點擊前端自動化部署的深度實踐深入了解。
fix #1用於關閉issue 1。
git push origin HEAD則代表推送到遠程倉庫同名分支。
創建Merge Request
開發人員發起Merge Request,請求將自己開發的功能特性合入develop分支。

接着Maintainer需要Review代碼,確認無誤后同意Merge。然后這部分代碼就在遠程Git倉庫入庫了,其他開發同學拉取develop分支就能看到了。
版本提測
issue/2,處理更新日志,版本號等內容,對應issue 2
每個團隊的開發節奏都不同,有的團隊會每日持續集成發版本提測,有的可能兩天一次,這個就不深入討論了......
那么當我們准備提測時,應該怎么做呢?
通過上節的了解,我們已經知道,迭代內的功能需求都會通過feature/xxx分支合入到develop分支。
提測前,一般來說,還是要更新下CHANGELOG.md和package.json的版本號,可以由Maintainer或其他負責該項事務的同學執行。
主要是執行npm version major/minor/patch -m 'something done',具體操作可以參考前端自動化部署的深度實踐一文。
git checkout -b issue/2 origin/develop
npm version minor -m '迭代1第一次提測'
git push origin HEAD
然后發起merge request合入develop分支
此時,應以最新的develop分支代碼在開發環境跑一遍功能,保證版本自測通過。
提測時,由Maintainer發起Merge Request,將develop分支代碼合入release分支,此時自動觸發Gitlab CI/CD,自動構建並發布至測試環境。
版本提測后,各責任人應在TAPD上將相關需求和缺陷的狀態變更為“測試中”。
修復測試環境bug
bug/3,一個bug分支,對應issue 3
這里說的是在迭代周期內由測試工程師發現的測試環境中的系統bug,這些bug會被記錄在敏捷開發協作平台TAPD上。修復測試環境bug的步驟與開發需求類似,這里簡單說下步驟:
-
在Gitlab上創建issue
創建issue,並附上TAPD上的缺陷鏈接,方便追溯
-
創建分支&修復缺陷
基於
develop分支創建分支:// 3是issue號 git checkout -b bug/3 origin/develop接着改代碼......
-
commit & push
PS D:\projects\gitlab\project_xxx> git cz cz-cli@4.0.3, cz-conventional-changelog@3.1.0 ? Select the type of change that you're committing: fix: A bug fix ? What is the scope of this change (e.g. component or file name): (press enter to skip) ? Write a short, imperative tense description of the change (max 95 chars): (11) 修復一個測試環境bug ? Provide a longer description of the change: (press enter to skip) ? Are there any breaking changes? No ? Does this change affect any open issues? Yes ? If issues are closed, the commit requires a body. Please enter a longer description of the commit itself: - ? Add issue references (e.g. "fix #123", "re #123".): fix #3 git push origin HEAD -
發起Merge Request
開發人員發起
Merge Request,請求將自己修復缺陷引入的代碼合入develop分支。然后
Maintainer需要Review代碼,同意本次Merge Request。 -
等待回歸測試
該
bug將在下一次CI/CD后,進入回歸測試流程。 -
級別高的測試環境Bug
如果是級別很高的
bug,比如影響了系統運行,導致測試人員無法正常測試的,應以release分支為基礎創建bug分支,修改完畢后合入release分支,再發起cherry pick合入develop分支。
發布至生產環境
經過幾輪持續集成和回歸測試后,一個迭代版本也慢慢趨於穩定,此時也迎來了激動人心的上線時間。我們要做的就是把通過了測試的release分支合入master分支。

這一步相對簡單,但是要特別注意權限控制(防止生產環境事故),具體權限控制可以回頭看第一章節分支管理。
與release分支類似,master分支自動觸發Gitlab CI/CD,自動構建並發布至生產環境。
線上回滾
revert/4,一個回滾分支,對應issue 4
代碼升級到線上,但是發現報錯,系統無法正常運行。此時如果不能及時排查出問題,那么只能先進行版本回退操作。
先說說慣性思維下,我的版本回退做法。
首先還是創建issue記錄下:

基於master分支創建revert/4分支
git checkout -b revert/4 origin/master
假設當前版本是1.1.0,我們想回退到上個版本1.0.3。那么我們需要先查看下1.0.3版本的信息。
PS D:\tusi\projects\gitlab\projectname> git show 1.0.3
commit 90c9170a499c2c5f8f8cf4e97fc49a91d714be50 (tag: 1.0.3, fix/1.0.2_has_bug)
Author: tusi <tusi@xxx.com.cn>
Date: Thu Feb 20 13:29:31 2020 +0800
fix:1.0.2
diff --git a/README.md b/README.md
index ac831d0..2ee623b 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@
只想修改舊版本的bug,不改最新的master
+1.0.2版本還是有個版本,再修復下
+
特性2提交
特性3提交
主要是取到1.0.3版本對應的commit id,其值為90c9170a499c2c5f8f8cf4e97fc49a91d714be50。
接着,我們根據commit id進行reset操作,再推送到遠程同名分支。
git reset --hard 90c9170a499c2c5f8f8cf4e97fc49a91d714be50
git push origin HEAD
接着發起Merge Request把revert/4分支合入master分支。

一般來說,這波操作沒什么問題,能解決常規的回滾問題。
臨時變通
由於master分支是保護分支,設置了不可push。如果不想通過merge的方式回滾,所以只能先臨時設置Maintainer擁有push權限,然后由Maintainer進行回滾操作。
git checkout master
git pull
git show 1.0.3
git reset --hard 90c9170a499c2c5f8f8cf4e97fc49a91d714be50
git push origin HEAD
完事后,還需要記得把master設置為不可push。
Q: 為什么不讓
Maintainer一直擁有master的push權限?A: 主要還是為了防止出現生產環境事故,給予臨時性權限更穩妥!
git reset --hard存在什么問題?
如題,git reset --hard確實是存在問題的。git reset --hard屬於霸道玩法,直接移動HEAD指針,會丟棄之后的提交記錄,如果不慎誤操作了也別慌,還是可以通過查詢git reflog找到commitId搶救回來的;git reset后還存在一個隱性的問題,如果與舊的branch進行merge操作時,會把git reset回滾的代碼重新引入。那么怎么解決這些問題呢?

別慌,這個時候你必須掏出git revert了。
Q:
git revert的優勢在哪?A: 首先,
git revert是通過一次新的commit來進行回滾操作的,HEAD指針向前移動,這樣就不會丟失記錄;另外,git revert也不會引起merge舊分支時誤引入回滾的代碼。最重要的是,git revert在回滾的細節控制上更加優秀,可解決部分回滾的需求。
舉個栗子,本輪迭代團隊共完成需求2項,而上線后發現其中1項需求有致命性缺陷,需要回滾這個需求相關的代碼,同時要保留另一個需求的代碼。

首先我們查看下日志,找下這兩個需求的commitId
PS D:\tusi\projects\test\git_test> git log --oneline
86252da (HEAD -> master, origin/master, origin/HEAD) 解決沖突
e3cef4e (origin/release, release) Merge branch 'develop' into 'release'
f247f38 (origin/develop, develop) 需求2
89502c2 需求1
我們利用git revert回滾需求1相關的代碼
git revert -n 89502c2
這時可能要解決沖突,解決完沖突后就可以push到遠程master分支了。
git add .
git commit -m '回滾需求1的相關代碼,並解決沖突'
git push origin master
感覺還是菜菜的,如果大佬們有更優雅的解決方案,求指導啊!
修復線上bug
hotfix/5,一個線上熱修復分支,對應issue 5
比如線上出現了系統無法登錄的bug,測試工程師也在TAPD提交了缺陷記錄。那么修復線上bug的步驟是什么呢?
-
創建
issue,標題可以從TAPD中的Bug單中copy過來,而描述就貼上Bug單的鏈接即可。 -
基於
master分支創建分支hotfix/5。git checkout -b hotfix/5 origin/master -
擼代碼,修復此bug......
-
修復完此
bug后,提交該分支代碼到遠程倉庫同名分支git push origin HEAD -
然后發起
cherry pick合入到master和develop分支,並在master分支打上新的版本tag(假設當前最大的tag是1.0.0,那么新的版本tag應為1.0.1)。
修復線上舊版本bug
fix/6,一個線上舊版本修復分支,對應issue 6
某些項目產品可能會有多個線上版本同時在運行,那么不可避免要解決舊版本的bug。針對線上舊版本出現的bug,修復步驟與上節類似。
-
創建
issue,描述清楚問題 -
假設當前版本是
1.0.0,而0.9.0版本出了一個bug,應基於tag 0.9.0創建fix分支。git checkout -b fix/6 0.9.0 -
修復缺陷后,應打上新的
tag 0.9.1,並推送到遠程。git tag 0.9.1 git push origin tag 0.9.1 -
如果此
bug也存在於最新的master分支,則需要git push origin HEAD提交該fix分支代碼到遠程倉庫同名分支,然后發起cherry pick合入到master,此時很大可能存在沖突,需要解決沖突。
cherry pick
在了解到cherry pick之前,我一直認為只有git merge可以合並代碼,也好幾次遇到合入了不想要的代碼的問題。有了cherry pick,我們就可以合並單次提交記錄,解決git merge時合並太多不想要的內容的煩惱,在解決bug時特別有用。
git rebase
經過這段時間的使用,我發現使用git merge合並分支時,會讓git log的Graph圖看起來有點吃力。
PS D:\tusi\projects\gitlab\projectname> git log --pretty --oneline --graph
* 7f513b0 (HEAD -> develop) Merge branch 'issue/55' into 'release'
|\
| * 1c94437 (origin/issue/55, issue/55) fix: 【bug】XXX1
| * c84edd6 Merge branch 'release' of host:project_repository into release
| |\
| |/
|/|
* | 115a26c Merge branch 'develop' into 'release'
|\ \
| * \ 60d7de6 Merge branch 'issue/30' into 'develop'
| |\ \
| | * | 27c59e8 (origin/issue/30, issue/30) fix: 【bug】XXX2
| | | * ea17250 Merge branch 'release' of host:project_repository into release
| | | |\
| |_|_|/
|/| | |
* | | | 9fd704b Merge branch 'develop' into 'release'
|\ \ \ \
| |/ / /
| * | | a774d26 Merge branch 'issue/30' into 'develop'
| |\ \ \
| | |/ /
接着我就了解到了git rebase,變基,哈哈哈。由於對rebase了解不深,目前也不敢輕易改用rebase,畢竟rebase還是有很多隱藏的坑的,使用起來要慎重!在這里先挖個坑吧,后面搞懂了再填坑......
注意事項
- 一般而言,自己發起的
Merge Request必須由別的同事Review並同意合入,這樣更有利於發現代碼問題。 - 對了,
TAPD還支持與Gitlab協同的。詳情見源碼關聯指引。
結語
實踐證明,這套Git工作流目前能覆蓋我項目開發過程中的絕大部分場景。不過要注意的是,適合自己的才是最好的,盲目采用別人的方案有時候是會水土不服的。
以上所述純屬前端小微團隊內部的Gitlab實踐,必然存在着很多不足之處,如有錯誤之處還請指正,歡迎交流。

