[前端漫談]Git 在項目中的徹底控制實踐

導讀

在上一篇文章[前端漫談]一巴掌拍平Git中的各類概念中,描述了 Git 的一些概念,可是太過虛化,描述的都是一些概念和命令。這篇文章結合實際場景,主要描述我在項目實踐中使用 Git 管理項目、團隊協做的一些經驗。包括 1)mergerebase 使用的區別和選擇;2)多人團隊合做開發流程;3)標準化 commit message;4)commit 精細化管理等。這些都是爲項目的健壯發展和代碼的精細管理所流的淚累積出來的。html

0x000 前言

由上一片文章[前端漫談]一巴掌拍平Git中的各類概念中,能夠知道,Git 世界就像一個 宇宙,每個 commit 都是一顆星球,而 commitId 就是星球的座標,branch 是一條條的航線,穿過無數的 星球,tag 是航線上重要的星球,多是供給站,多是商業中心,而 HEAD 則是探索號飛船,不斷向前探索。中間可能會有岔道,可是永遠有一個真正的方向等待勇敢的船長。前端

0x001 merge 仍是 rebase

merge 仍是 rebase,這是經久不衰的討論點。可是這裏我不去爭論孰優孰略,我只說我在不一樣場景的實踐。git

1. merge

我一般使用 merge 來將多個分支合併到當前分支,好比要發佈的時候,將多個功能分支合併到帶發佈分支:shell

已知:feat/Afeat/Bfeat/C,是從主分支新建的功能分支,feat/Bfeat/C都修改了文件1npm

  • 新建待發布分支:
    # 從主分支新建分支 pub/191205
    $ git checkout -b pub/191205
    Switched to a new branch 'pub/191205'
    複製代碼
  • 合併feat/Apub/191205
    $ git merge feat/A
    Updating 53ab8fd..e443dd4
    Fast-forward
     featA | 1 +
     1 file changed, 1 insertion(+)
     create mode 100644 featA
    複製代碼

pub/191205feat/A 都是從主分支新建,因此 pub/191205 指向的 commitfeat/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/Bpub/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/191205feat/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/Cpub/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/Cfix/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/191205app

feat/C是從主分支新建的分支,pub/191205本來指向的也是feat/C的祖先,可是由於已經和feat/Afeat/B合併了,因此pub/191205再也不是feat/C的祖先。所以,pub/191205feat/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編輯器

歷史以下:

2. rebase

注:rebase 的功能很強大,這裏先介紹和 merge 相對應的功能。

我一般用它來和主分支同步,好比一個新版本發佈,主分支比我當前的功能分支超前,我使用rebase將當前分支和主分支「合併(變基)」。

已知:feat/Afeat/B 是從主分支新建,feat/A開發完成以後合併到主分支。feat/B繼續開發,須要將master的功能合併到當前分支上,使用merge能夠這麼作:

  • 切換到 feat/B
    $ git switch feat/B
    Switched to branch 'feat/B'
    複製代碼
  • 將 master 合併到 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是從mastercheckout出來的,可是使用git rebase master以後,就會以master最新的節點做爲feat/B分支的基礎。就像feat/B上全部的commit都是基於最新的master提交的。

歷史以下:

因爲rebase以後,master始終是feat/B的祖先節點,所以,以後將feat/B合併到master將執行Fast-Farword,不會產生衝突(若是有衝突,rebase的時候就須要解決了),也不會產生新節點。

3. merge 仍是 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 分支 - 變基 - 變基的風險

0x002 多人合做開發

1. 新功能開發

開發方式 新功能開發的時候從主分支新建新分支,全部該功能的開發工做都在這個分支上完成。若是主分支有新的發佈,使用rebase同步主分支功能:

名稱規範 功能分支的命名方式是feat/${name}_${featName},它的構成以下:

  • 常量feat:表示這是一個功能分支
  • 變量name:你的名字
  • 變量featName:功能名字 好處是見名知意,一看就知道是功能分支,是誰負責,是什麼功能

2. bug 修復及其發佈

開發方式 bug修復大致上和新功能的開發相似,可是bug修復通常時間短,立立刻線。 bug修復從主分支新建新分支,全部的bug修復工做都在這個分支上完成。若是主分支有新的發佈,使用rebase同步主分支功能(這個步驟其實和新功能開發同樣):

名稱規範 bug修復分支的命名方式是hotfix/${name_${bugName}},它的構成以下:

  • 常量hotfix:表示這是一個功能分支
  • 變量name:你的名字
  • 變量bugNamebug名字 好處是見名知意,一看就知道是bug修復分支,是誰負責,是什麼bug

bug 發佈 bug發佈能夠直接推送到待發布版本分支,好比1.1.1,而後CodeReview(若是有),而後合併主分支部署上線。

完整過程以下:

2.5 stash

通常咱們修復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 工具 - 儲藏與清理瞭解更多。

3. 新功能發佈

新功能發佈和bug發佈有些不一樣,1)可能會有多個功能共同發佈,須要提早合併,避免大沖突;2)可能有bug修復須要插隊。3)可能須要等待後端發佈,時間長。

由於(2),全部沒法像bug發佈那樣直接推送到版本分支,等待發布。由於在真正發佈以前,是沒法知道準確版本的。

由於(1)、(3),因此須要提早合併,因此引入一個「日期分支」的概念,即以日期爲分支名,好比pub/191205

因此發佈過程以下:

(其實我還畫了一張賊複雜的圖,把本身都噁心了,有空仍是畫個動態圖吧(沒空))

0x003 標準化 commit message

標準化 commit message 能夠參考阮一峯 - Commit message 和 Change log 編寫指南。(阮一峯真的是寫博客跨不過去的坎😢,寫啥均可以引用他)

  1. 安裝commitizen
$ npm install -g commitizen
複製代碼
  1. 在項目目錄中初始化commitizen
$ commitizen init cz-conventional-changelog --save --save-exact
複製代碼
  1. 使用git cz代替git commit,如下是我經常使用的類型:
$ git cz
feat:       一個新功能
fix:        一個 bug 修復
docs:       只改變文檔
refactor:   改變代碼可是不添加或者修復功能,我通常用於優化或者重構
test:       添加測試代碼
chore:      其餘改變
style:      樣式更新
複製代碼

0x004 commit 精細化管理

首先是爲何?爲何要管理commitcommit有啥好管理的?

在之前,我以爲git是用來記錄代碼操做的,我對代碼的任何操做都應該被記錄下來,並且就像歷史同樣,是神聖不可侵犯的。經過git歷史,我必需要能夠知道我在某一刻作了什麼,就算我在一個commit添加了一行代碼,而後在後一個commit刪除了它,我也必須能夠從log中看出來。

因此個人git歷史中充滿了各類無效的commit,由於有時候真的不知道如何爲命名。

可是後來,我就想通了,我使用git的目標是否是爲了記錄,而是爲了項目的穩定發展。只要實現了這個目的,手段不是問題,更況且git只是一個工具,工具是用來用的,不是用來供奉的。讓本身快樂快樂才叫作意義。

所謂的管理commit,就是對commit執行增、刪、改、拆操做。會在後面的章節一一列出。而管理的目的,是爲了讓每個commit都有存在的意義,讓Git成爲項目管理真正的後盾。

後面的例子將同時提供SourceTree的操做,命令式能夠看上一篇文章[前端漫談]一巴掌拍平Git中的各類概念

1. 排序 commit

場景:完成登錄頁面以後,提交一個commitmessagefeat(登錄): 完成登錄頁面。而後進入其餘功能的開發,以後又回到登錄頁面的開發。提交記錄以下:

咱們有兩個feat(登錄)或者多個相關的的commit,可是卻分佈於不一樣的地方,假設每個feat(登錄)只會與前一個feat(登錄)有文件修改的交集,那麼咱們但願feat(登錄)相關的功能能夠放在一塊兒。以下:

如何實現:

2. 合併 commit

場景:完成登錄頁面以後,提交一個commitmessagefeat(登錄): 完成登錄頁面。而後進入其餘功能的開發,後來發現登錄有一個文案錯誤,繼續修改,完成以後又提交一個commitmessagefeat(登錄): 修改文案。提交記錄以下:

在我看來,feat(登錄): 修改文案這個commit的存在是不該該的,好比,1)若是有一天咱們須要單獨上「登錄」功能,還有可能被遺漏;2)單獨佔據一個commit可能只是爲了修復一個符號問題,在回溯歷史的時候有沒必要要的浪費。也就是我但願一個commit它是獨立的,不依賴後續commit的存在。

因此我但願將這兩個commit合併:

操做過程:

3. 更新 commit

更新commit的場景有兩個:

  1. 更新message
    • 正好上面有一個不符合標準的message:
    • 我但願改成:
    • 操做說明
  2. 更新、添加、刪除
    • 1)有時候咱們會經過修改某個變量來作一些測試,而後提交的時候忽然發現忘記改回來;2)忘記或者誤添加文件;3)忘記刪除文件。這時候能夠經過再建立一個commit再改回來,可是誤添加的文件依舊會在歷史中存在,佔據必定的空間。咱們能夠根據上面的「合併」方式合併commit消除影響,也能夠一步到位:
    • feat(mine): 我的中心提交中有一個mime.html文件,我但願刪掉bad line;還有一個mineBad.bad這麼一個看起來壞壞的文件,我但願刪除它。
    • 操做過程(略複雜):

4. 增長/分離 commit

  1. 增長一個commit的意義其實不大,在更新commit的過程當中咱們選擇的是更正上一次提交,也就是git commit --amend,可是若是咱們不選擇,而是建立一個提交,其實就是增長一個commit了。

    • 我但願在feat(mine): 完成我的中心feat(main): 完成主頁中間添加一個commit,能夠經過新建一個commit而後以後經過前面的排序手段來作到,也能夠一步到位:

    • 操做過程(和前面差很少,只是不選擇更正上一次提交):
  2. 分離commit的意義重大,有時候咱們但願只發佈一個功能,卻發現這個功能的commit中包含咱們不但願發佈的另外一個功能,多是由於原本要放到兩個commit的功能誤添加到一個commit

    • 咱們有一個feat(detail): 完成詳情頁commit,卻不當心把other的功能給包含進去了,這時候我但願只發布detail頁面,所以,對於commit的分離是必須的:

    • 操做過程

5. 刪除 commit

當咱們作了一次修改後來發現這個修改沒有必要,就能夠刪除這個commit,可是不推薦,除非真的確認。

  • feat(detail): 完成詳情頁面後面作了一個不須要的提交:

  • 刪除步驟

7. cherry-pick

有時候,咱們須要發佈一個分支中的幾個功能,好比咱們在一次統一優化中修復了 5 個 bug,作了 5 個優化,可是其中幾個並無經過驗證:

  • refactor/A 分支中有 3 個commit,經過了 2(用 ok 標記) 個

  • fix/A 分支中有 3 個 commit,經過了 2(用 ok 標記) 個

咱們只能發佈經過的 bug 修復和優化(標註了 ok 的),而這些修復和優化並不必定在哪一個分支,是隨機分佈的:

在這種場景中,雖然能夠用分支去處理,可是有點麻煩,這個時候 cherry-pick 是最好的工具。

  • 操做過程

0x005 reflog

上面的不少操做都涉及到歷史的操做,用普通的 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
    複製代碼
  • 效果

  • 又又後悔啦!!!滾

0x006 總結

  1. 勿忘初心,砥礪前行。咱們一開始使用git是爲了更好的輔助項目,而不是讓項目更加複雜,若是不使用這些方式可讓你的項目更加簡單,那就不要用,爲了使用git而使用git,不如不使用。

  2. 要理解工具的原理,再去使用,不要盲目。使用上面的命令以前,務必瞭解這些命令或者操做背後發生了什麼。

0x007 後記

  • 我一直在尋找一種好的表達方式,從截圖標註、繪圖,到 gif 等,但願能夠將文章講的更加透徹。如今看來,可能仍是 gif 比較好。

  • 寫一篇文章真的有點難啊,構思、佈局、實驗、總結,每一步都須要花很大的功夫,可是一篇精心總結的文章,對本身的幫助仍是很大的,但願對各位也有幫助把。

0x008 資源

0x009 帶貨

最近發現一個好玩的庫,做者是個大佬啊--基於 React 的現象級微場景編輯器

相關文章
相關標籤/搜索