在上一篇文章[前端漫談]一巴掌拍平Git中的各類概念中,描述了 Git 的一些概念,可是太過虛化,描述的都是一些概念和命令。這篇文章結合實際場景,主要描述我在項目實踐中使用 Git
管理項目、團隊協做的一些經驗。包括 1)merge
和 rebase
使用的區別和選擇;2)多人團隊合做開發流程;3)標準化 commit message
;4)commit
精細化管理等。這些都是爲項目的健壯發展和代碼的精細管理所流的淚累積出來的。html
由上一片文章[前端漫談]一巴掌拍平Git中的各類概念中,能夠知道,Git
世界就像一個 宇宙,每個 commit 都是一顆星球,而 commitId
就是星球的座標,branch
是一條條的航線,穿過無數的 星球,tag
是航線上重要的星球,多是供給站,多是商業中心,而 HEAD
則是探索號飛船,不斷向前探索。中間可能會有岔道,可是永遠有一個真正的方向等待勇敢的船長。前端
merge
仍是 rebase
merge
仍是 rebase
,這是經久不衰的討論點。可是這裏我不去爭論孰優孰略,我只說我在不一樣場景的實踐。git
我一般使用 merge
來將多個分支合併到當前分支,好比要發佈的時候,將多個功能分支合併到帶發佈分支:shell
已知:feat/A
、feat/B
、feat/C
,是從主分支新建的功能分支,feat/B
和feat/C
都修改了文件1
。npm
# 從主分支新建分支 pub/191205
$ git checkout -b pub/191205
Switched to a new branch 'pub/191205'
複製代碼
feat/A
到pub/191205
:$ git merge feat/A
Updating 53ab8fd..e443dd4
Fast-forward
featA | 1 +
1 file changed, 1 insertion(+)
create mode 100644 featA
複製代碼
pub/191205
和 feat/A
都是從主分支新建,因此 pub/191205
指向的 commit
是 feat/A
的祖先,當把 feat/A
合併到pub/191205
的時候,會發生快速合併(Fast-forward)。不會新建一個合併節點(固然也能夠經過--no-ff(no-fast-forward)
來強制生成一個節點):vim
# 查看 log
$ git log --oneline
e443dd4 (HEAD -> pub/191205, feat/A) feat: a
53ab8fd (master) chore: first commit
複製代碼
feat/B
到pub/191205
$ git merge feat/B
# 進入 vim 填寫合併信息
Merge made by the 'recursive' strategy.
1 | 2 +-
featB | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 featB
複製代碼
feat/B
是從主分支新建的分支,pub/191205
本來指向的也是feat/B
的祖先,可是由於已經和feat/A
合併了,因此pub/191205
再也不是feat/B
的祖先。所以,pub/191205
和feat/B
的合併再也不是快速合併(Fast-forward),而是Merge made by the 'recursive' strategy.
。會產生一個新的節點:後端
$ git log --oneline
5d0ee9b (HEAD -> pub/191205) Merge branch 'feat/B' into pub/191205
d7773d6 (feat/B) feat: b
e443dd4 (feat/A) feat: a
53ab8fd (master) chore: first commit
複製代碼
feat/C
到pub/191205
$ git merge feat/C
Auto-merging 1
CONFLICT (content): Merge conflict in 1
Automatic merge failed; fix conflicts and then commit the result.
複製代碼
feat/C
和fix/B
修改了相同文件,因此產生衝突,所以,會提示解決衝突。這時候查看狀態,能夠發現,處於you have unmerged paths
狀態: ``` $ git status On branch pub/191205 You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge)bash
Changes to be committed:
new file: featC
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: 1
```
複製代碼
這時候能夠執行git merge --abort
放棄繼續合併,恢復合併以前的狀態。也能夠解決衝突以後,執行git merge
。這裏選擇解決衝突: # 解決衝突 $ git commit # 進入 vim 編寫 message [pub/191205 98d63aa] Merge branch 'feat/C' into pub/191205
app
feat/C
是從主分支新建的分支,pub/191205
本來指向的也是feat/C
的祖先,可是由於已經和feat/A
、feat/B
合併了,因此pub/191205
再也不是feat/C
的祖先。所以,pub/191205
和feat/C
的合併再也不是快速合併(Fast-forward),會產生一個新的節點: $ git log --oneline 98d63aa (HEAD -> pub/191205) Merge branch 'feat/C' into pub/191205 5d0ee9b Merge branch 'feat/B' into pub/191205 d7773d6 (feat/B) feat: b 52dd922 (feat/C) feat: c e443dd4 (feat/A) feat: a 53ab8fd (master) chore: first commit
編輯器
歷史以下:
注:rebase
的功能很強大,這裏先介紹和 merge
相對應的功能。
我一般用它來和主分支同步,好比一個新版本發佈,主分支比我當前的功能分支超前,我使用rebase
將當前分支和主分支「合併(變基)」。
已知:feat/A
、feat/B
是從主分支新建,feat/A
開發完成以後合併到主分支。feat/B
繼續開發,須要將master
的功能合併到當前分支上,使用merge
能夠這麼作:
$ git switch feat/B
Switched to branch 'feat/B'
複製代碼
$ git merge master
# 進入 vim 編寫 message
Merge made by the 'recursive' strategy.
featA | 1 +
1 file changed, 1 insertion(+)
create mode 100644 featA
複製代碼
$ git log
b4f178e (HEAD -> feat/B) Merge branch 'master' into feat/B
d7773d6 feat: b
e443dd4 (pub/191205, master, feat/A) feat: a
53ab8fd chore: first commit
複製代碼
由於master
合併了feat/A
,所以再也不是feat/B
的祖先節點,不會進行快速合併(Fast-forward),會產生一個新的節點。歷史以下
這麼作是能夠,可是我不喜歡這個合併產生的節點,因此我選擇使用rebase
:
feat/B
以前$ git reset e443dd4 --hard
HEAD is now at e443dd4 feat: a
複製代碼
rebase
「合併(變基)」master
$ git rebase master
git rebase master
First, rewinding head to replay your work on top of it...
Applying: feat: b
複製代碼
$ git log --oneline
ef3450c (HEAD -> feat/B) feat: b
e443dd4 (pub/191205, master, feat/A) feat: a
53ab8fd chore: first commit
複製代碼
能夠發現沒有新的節點產生,可是rebase
的操做過程並不僅是不產生一個合併節點而已,它的中文翻譯是變基
,聽起來很 Gay 的樣子。但它的意思是「改變基礎」。那改變的是什麼基礎呢?就是這個分支checkout
出來的commit
,本來feat/B
是從master
中checkout
出來的,可是使用git rebase master
以後,就會以master
最新的節點做爲feat/B
分支的基礎。就像feat/B
上全部的commit
都是基於最新的master
提交的。
歷史以下:
因爲rebase
以後,master
始終是feat/B
的祖先節點,所以,以後將feat/B
合併到master
將執行Fast-Farword
,不會產生衝突(若是有衝突,rebase
的時候就須要解決了),也不會產生新節點。
merge
仍是rebase
,有人提倡不要使用rebase
,應該rebase
改變了歷史(在上一小節中一直在改變分支的啓始節點),有人提倡使用merge
,保留完整的歷史。
我是這麼作的,在私有的分支上,我始終使用rebase
將主分支的更新合併到私有的分支上(後面還有不少使用rebase
的操做,都是在私有的分支,這裏的私有的分支,指的是隻有本身使用的分支,一旦分享出去,或者有人基於你的分支開發,那就再也不是私有),而在將本身的分支合併到其餘分支(主分支或者待發布分支),則使用merge
。
$ git switch mater
Switched to branch 'master'
複製代碼
feat/B
合併到主分支$ git merge feat/B
Updating e443dd4..ef3450c
Fast-forward
1 | 2 +-
featB | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 featB
複製代碼
這樣在長時間開發(master
中間發佈過n
多版本)的feat/B
就不會有無數亂七八糟的分支合併。而在master
也不會存在rebase
致使的歷史變動後果。
歷史以下:
準則:不要對在你的倉庫外有副本的分支執行變基。
若是你遵循這條金科玉律,就不會出差錯。 不然,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。-- 3.6 Git 分支 - 變基 - 變基的風險
開發方式 新功能開發的時候從主分支新建新分支,全部該功能的開發工做都在這個分支上完成。若是主分支有新的發佈,使用rebase
同步主分支功能:
名稱規範 功能分支的命名方式是feat/${name}_${featName}
,它的構成以下:
feat
:表示這是一個功能分支name
:你的名字featName
:功能名字 好處是見名知意,一看就知道是功能分支,是誰負責,是什麼功能開發方式 bug
修復大致上和新功能的開發相似,可是bug
修復通常時間短,立立刻線。 bug
修復從主分支新建新分支,全部的bug
修復工做都在這個分支上完成。若是主分支有新的發佈,使用rebase
同步主分支功能(這個步驟其實和新功能開發同樣):
名稱規範 bug
修復分支的命名方式是hotfix/${name_${bugName}}
,它的構成以下:
hotfix
:表示這是一個功能分支name
:你的名字bugName
:bug
名字 好處是見名知意,一看就知道是bug
修復分支,是誰負責,是什麼bug
bug 發佈 bug
發佈能夠直接推送到待發布版本分支,好比1.1.1
,而後CodeReview
(若是有),而後合併主分支部署上線。
完整過程以下:
通常咱們修復bug
的時候都在開發新功能,也就是在feat/*
上,這時候如何快速進入bug
修復狀態呢?能夠保存當前代碼,提交commit
,可是這時候會有一些問題,好比,1)當前的代碼並未完成,並不想提交;2)commit
有鉤子,好比ESLint
,必須修復語法問題才能提交。
這時候就是使用stash
了。stash
能夠將當前工做區和暫存區的內容暫時保存起來,以後再使用。
以下:
$ echo "this is a feat" >> feat.txt
複製代碼
bug
通知$ git stash
Saved working directory and index state WIP on master: ef3450c feat: b
$ git switch master
$ git checkout -b hotfix/bugA
Switched to a new branch 'hotfix/bugA'
複製代碼
bug
以後$ git switch feat/A
$ git stash pop
On branch hotfix/bugA
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: feat.txt
Dropped refs/stash@{0} (32cf119fc1dcbe7088d1a12e290b868d6707526d)
複製代碼
stash
命令有一整套完整的增刪改查指令,能夠查看git-pro 7.3 Git 工具 - 儲藏與清理瞭解更多。
新功能發佈和bug
發佈有些不一樣,1)可能會有多個功能共同發佈,須要提早合併,避免大沖突;2)可能有bug
修復須要插隊。3)可能須要等待後端發佈,時間長。
由於(2),全部沒法像bug
發佈那樣直接推送到版本分支,等待發布。由於在真正發佈以前,是沒法知道準確版本的。
由於(1)、(3),因此須要提早合併,因此引入一個「日期分支」的概念,即以日期爲分支名,好比pub/191205
。
因此發佈過程以下:
(其實我還畫了一張賊複雜的圖,把本身都噁心了,有空仍是畫個動態圖吧(沒空))
標準化 commit message 能夠參考阮一峯 - Commit message 和 Change log 編寫指南。(阮一峯真的是寫博客跨不過去的坎😢,寫啥均可以引用他)
commitizen
$ npm install -g commitizen
複製代碼
commitizen
$ commitizen init cz-conventional-changelog --save --save-exact
複製代碼
git cz
代替git commit
,如下是我經常使用的類型:$ git cz
feat: 一個新功能
fix: 一個 bug 修復
docs: 只改變文檔
refactor: 改變代碼可是不添加或者修復功能,我通常用於優化或者重構
test: 添加測試代碼
chore: 其餘改變
style: 樣式更新
複製代碼
首先是爲何?爲何要管理commit
,commit
有啥好管理的?
在之前,我以爲git
是用來記錄代碼操做的,我對代碼的任何操做都應該被記錄下來,並且就像歷史同樣,是神聖不可侵犯的。經過git
歷史,我必需要能夠知道我在某一刻作了什麼,就算我在一個commit
添加了一行代碼,而後在後一個commit
刪除了它,我也必須能夠從log
中看出來。
因此個人git
歷史中充滿了各類無效的commit
,由於有時候真的不知道如何爲命名。
可是後來,我就想通了,我使用git
的目標是否是爲了記錄,而是爲了項目的穩定發展。只要實現了這個目的,手段不是問題,更況且git
只是一個工具,工具是用來用的,不是用來供奉的。讓本身快樂快樂才叫作意義。
所謂的管理commit
,就是對commit
執行增、刪、改、拆操做。會在後面的章節一一列出。而管理的目的,是爲了讓每個commit
都有存在的意義,讓Git
成爲項目管理真正的後盾。
後面的例子將同時提供SourceTree
的操做,命令式能夠看上一篇文章[前端漫談]一巴掌拍平Git中的各類概念。
場景:完成登錄頁面以後,提交一個commit
,message
是feat(登錄): 完成登錄頁面
。而後進入其餘功能的開發,以後又回到登錄頁面的開發。提交記錄以下:
咱們有兩個feat(登錄)
或者多個相關的的commit
,可是卻分佈於不一樣的地方,假設每個feat(登錄)
只會與前一個feat(登錄)
有文件修改的交集,那麼咱們但願feat(登錄)
相關的功能能夠放在一塊兒。以下:
如何實現:
場景:完成登錄頁面以後,提交一個commit
,message
是feat(登錄): 完成登錄頁面
。而後進入其餘功能的開發,後來發現登錄有一個文案錯誤,繼續修改,完成以後又提交一個commit
,message
爲feat(登錄): 修改文案
。提交記錄以下:
在我看來,feat(登錄): 修改文案
這個commit
的存在是不該該的,好比,1)若是有一天咱們須要單獨上「登錄」功能,還有可能被遺漏;2)單獨佔據一個commit
可能只是爲了修復一個符號問題,在回溯歷史的時候有沒必要要的浪費。也就是我但願一個commit
它是獨立的,不依賴後續commit
的存在。
因此我但願將這兩個commit
合併:
操做過程:
更新commit
的場景有兩個:
message
message
:
commit
再改回來,可是誤添加的文件依舊會在歷史中存在,佔據必定的空間。咱們能夠根據上面的「合併」方式合併commit
消除影響,也能夠一步到位:feat(mine): 我的中心
提交中有一個mime.html
文件,我但願刪掉bad line
;還有一個mineBad.bad
這麼一個看起來壞壞的文件,我但願刪除它。
增長一個commit
的意義其實不大,在更新commit
的過程當中咱們選擇的是更正上一次提交
,也就是git commit --amend
,可是若是咱們不選擇,而是建立一個提交,其實就是增長一個commit
了。
feat(mine): 完成我的中心
和feat(main): 完成主頁
中間添加一個commit
,能夠經過新建一個commit
而後以後經過前面的排序
手段來作到,也能夠一步到位:分離commit
的意義重大,有時候咱們但願只發佈一個功能,卻發現這個功能的commit
中包含咱們不但願發佈的另外一個功能,多是由於原本要放到兩個commit
的功能誤添加到一個commit
,
feat(detail): 完成詳情頁
的commit
,卻不當心把other
的功能給包含進去了,這時候我但願只發布detail
頁面,所以,對於commit
的分離是必須的:當咱們作了一次修改後來發現這個修改沒有必要,就能夠刪除這個commit
,可是不推薦,除非真的確認。
在feat(detail): 完成詳情頁面
後面作了一個不須要的提交:
刪除步驟
有時候,咱們須要發佈一個分支中的幾個功能,好比咱們在一次統一優化中修復了 5 個 bug,作了 5 個優化,可是其中幾個並無經過驗證:
refactor/A
分支中有 3 個commit,經過了 2(用 ok 標記) 個
fix/A
分支中有 3 個 commit,經過了 2(用 ok 標記) 個
咱們只能發佈經過的 bug 修復和優化(標註了 ok 的),而這些修復和優化並不必定在哪一個分支,是隨機分佈的:
在這種場景中,雖然能夠用分支去處理,可是有點麻煩,這個時候 cherry-pick 是最好的工具。
操做過程
上面的不少操做都涉及到歷史的操做,用普通的 revert 或者 reset 是沒法消除影響的,只有在清楚這些命令的原理和本質的狀況下才應該使用這些命令。可是對於這些操做也是有辦法處理的,那就是 reflog
:
在git
中,全部的操做都會被記錄下來,好比切換分支
、合併分支
等,可使用 reflog
查看這個記錄,下面是cherry-pick
例子產生的記錄:
$ git reflog
# 執行 cherry-pick,一共 4 個 commit
b185e09 (HEAD -> pub/191206) HEAD@{0}: cherry-pick: feat(A): ok
dd67bf5 HEAD@{1}: cherry-pick: fix(A): ok
1d0237e HEAD@{2}: cherry-pick: feat(A): ok
51f808e HEAD@{3}: cherry-pick: refactor(A): ok
### 從 master 新建分支 pub/191206
a48cdd2 (master) HEAD@{4}: checkout: moving from master to pub/191206
複製代碼
若是咱們撤銷cherry-pick
,能夠執行如下命令:
$ git reset --hard HEAD@{4}
HEAD is now at a48cdd2 chore: 項目初始化
複製代碼
就沒啦
再次查看reflog
,多了一條記錄
$ git reflog
a48cdd2 (HEAD -> pub/191206, master) HEAD@{0}: reset: moving to HEAD@{4}
b185e09 HEAD@{1}: cherry-pick: feat(A): ok
dd67bf5 HEAD@{2}: cherry-pick: fix(A): ok
1d0237e HEAD@{3}: cherry-pick: feat(A): ok
51f808e HEAD@{4}: cherry-pick: refactor(A): ok
a48cdd2 (HEAD -> pub/191206, master) HEAD@{5}: checkout: moving from master to pub/191206
複製代碼
撤銷cherry-pick
又後悔啦
$ git reset --hard HEAD@{1}
HEAD is now at b185e09 feat(A): ok
複製代碼
效果
又又後悔啦!!!滾
勿忘初心,砥礪前行。咱們一開始使用git
是爲了更好的輔助項目,而不是讓項目更加複雜,若是不使用這些方式可讓你的項目更加簡單,那就不要用,爲了使用git
而使用git
,不如不使用。
要理解工具的原理,再去使用,不要盲目。使用上面的命令以前,務必瞭解這些命令或者操做背後發生了什麼。
我一直在尋找一種好的表達方式,從截圖標註、繪圖,到 gif
等,但願能夠將文章講的更加透徹。如今看來,可能仍是 gif
比較好。
寫一篇文章真的有點難啊,構思、佈局、實驗、總結,每一步都須要花很大的功夫,可是一篇精心總結的文章,對本身的幫助仍是很大的,但願對各位也有幫助把。
最近發現一個好玩的庫,做者是個大佬啊--基於 React 的現象級微場景編輯器。