在上一篇文章[前端漫谈]一巴掌拍平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 的现象级微场景编辑器。