Git 协同工作流
0x00 写在前
对于工具的学习,先尽可能从各个渠道搜集尽可能多的信息,进行分类整理。应用时,应该多做减法,只记住最有用的部分,那些奇淫技巧、黑魔法不记也罢,把精力投入到更有价值的事情中去。
问 2 个问题:
-
大家觉得 GitFlow、GitHub Flow、GitLab Flow 三者是什么关系?
-
大家有没有在自己项目中使用过
git pull --rebase
指令?
0x01 基本概念
先从 git 的三个分区开始,这三个状态都是在未提交到远程仓库之前出现的
-
本地 work directory:是本次工作目录,也就是我们电脑上,大家肉眼能看到的文件
-
该区域的代码,使用
git status
查看为红色 -
暂存区 stage:执行
git add
相关命令后,就会把 work dir 中的修改添加到暂存区 -
该区域的代码,使用
git status
查看为绿色 -
历史提交区: 当代码在 stage 区时,使用
git commit
指令,每次 commit 都会产生一个对应的唯一 hash 指,并把 HEAD 指向该指针
上图中流转的过程:
1、本地 dir => 暂存区 stage
# 添加全部文件
$git add .
# 添加单个文件
$git add a.js
2、暂存区 stage => 本地 dir
# 全部恢复
$git checkout .
# 恢复单个文件
$git checkout a.js
该操作存在一定风险:本地 dir 里 a.js 中做出的修改会被 stage 覆盖,无法恢复。所以需要确定本地 a.js 中进行的操作可以被抛弃。
使用场景:切换到了 master 分支的代码,查看某个文件,如果这个文件与自己本地 eslint 代码自动保存的规范不一致,自己的 IDE 会把该文自动格式化,但我们又不想提交这个修改。这时可以使用 git checkout 指令,来恢复文件。
3、暂存区 statge => 历史提交区 histroy
$git commit -m 'commit message'
再简单提一些常见场景, 比如说 commit 完之后,突然发现一些错别字需要修改,又不想为改几个错别字而新开一个 commit 到 history 区,那么就可以使用下面这个命令:
$git commit --amend
这样就是把错别字的修改和之前的那个 commit 中的修改合并,作为一个 commit 提交到history 区。
5、历史提交区 histroy => 本地 dir
这个场景,我说一个极端一点的例子:比如我从 GitLab 上 clone 了一个别人的项目,然后乱改了一通代码,结果发现我写的代码根本跑不通,于是后悔了,干脆不改了,我想恢复成最初的模样,怎么办?
# 还是 checkout 命令,但和之前有一些不同
$git checkout HEAD .
0x02 常见协同工作流都有哪些?
2.1 中心式协同工作流
现在大家应该很少接触 SVN 了,我当时实习的时候, SVN 还是很流行。Git 其实是可以像 SVN 这样的中心工作流一样工作的。咱们目前很多底层框架也都是在采用这样的工作方式。
中心式工作流一般是这样:
-
拉取代码:执行
git pull origin master
把代码同步下来 -
提交代码:执行
git add . && git commit
把代码添加到缓存区 -
推送代码:执行
git push origin maste
r 把代码推动远程仓库
如果第 3 步的时候发现 push 失败了,因为别人已经提交了,这里就涉及到协同的部分:
那么你需要先把服务器上的代码给 pull 下来,为了避免有 merge 动作,你可以使用 git pull --rebase
,这样就可以把服务器上的提交直接合并到你的代码中,但此时还没有完。
Git 此时的操作是:
-
先把你本地提交的代码放到一边。
-
然后把服务器上的改动下载下来。
-
然后在本地把你之前的改动再重新一个一个地做 commit,直到全部成功。
如下图所示。Git 会把 origin/feature/v3.2.0 的远程分支下载下来(紫色的),然后把本地的 feature/v3.2.0 分支上的改动一个一个地提交上去(蓝色的)。
最终效果如下图:
如果没有冲突,则可以直接 push:git push origin feature/v3.2.0
如果有冲突,需要先解决冲突,然后做 git rebase --continue
,如下图所示,git 在做 pull --rebase 时,会一个一个地应用(apply)本地提交的代码,如果有冲突就会停下来,等你解决冲突。
2.2 功能分支协同工作流
中心式协同工作流,只适合 2-3 个人,并且大家都在主干上开发。
“功能分支” 常见的协同工作流开发过程如下:
-
首先使用
git checkout -b new-feature
创建 new-feature 功能分支。 -
然后共同开发这个功能的程序员就在这个分支上工作,进行 add、commit 等操作。
-
然后通过
git push -u origin new-feature
把分支代码 push 到服务器上。 -
其他同学可以通过 git pull --rebase 来拿到最新的这个分支的代码。最后通过 Pull Request 的方式做完 Code Review 后合并到 Master 分支上。
其实,这种形式也是中心化的开发,只不过是以服务器为中心,使用分支的方式来完成代码的隔离。
如果一个项目分支开的太久,时间长了,越难合并回去,这种功能分支的方式也是为了我们可以把一个庞大的项目拆成若干个小项目来执行。Git 的最佳实践希望大家在开发的过程中,快速提交,快速合并,快速完成。这样可以少很多冲突。
2.3 GitFlow 协同工作流
上面的方式还是不能满足我们的实际需求,因为只有一个 master 分支。我们要在不停地开发新代码的同时,维护线上的代码,于是,就有了下面这些需求。
-
希望有一个分支是非常干净的,上面是可以发布的代码,上面的改动永远都是可以发布到生产环境中的。这个分支上不能有中间开发过程中不可以上生产线的代码提交。
-
对于已经发布的代码,经常会有一些 Bug-fix,不会将正在开发的代码提交到生产线上去。
上面两点,引出了一个概念:环境
为了解决这些问题,GitFlow 协同工作流就出来了。GitFlow 协同工作流是由 Vincent Driessen 于 2010 年在 A successful Git branching model 这篇文章中引出的。
整个代码路一共五种分支:
-
Master分支。也就是主干分支,用作发布环境,上面的每一次提交都是可以发布的。
-
Feature 分支。也就是功能分支,用于开发功能,其对应的是开发环境。
-
Developer 分支。是开发分支,一旦功能开发完成,就向 Developer 分支合并,合并完成后,删除功能分支。
-
Release 分支。当 Developer 分支测试达到可以发布状态时,开出一个 Release 分支来,然后做发布前的准备工作。这个分支对应的是预发环境。之所以需要这个 Release 分支,是我们的开发可以继续向前。(一旦 Release 分支上的代码达到可以上线的状态,那么需要把 Release 分支向 Master 分支和 Developer 分支同时合并,以保证代码的一致性。然后再把 Release 分支删除掉。)
-
Hotfix 分支。是用于处理生产线上代码的 Bug-fix,每个线上代码的 Bug-fix 都需要开一个 Hotfix 分支,完成后,向 Developer 分支和 Master 分支上合并。合并完成后,删除 Hotfix 分支。
核心流程如下图所示:(网上找了一张图,实在是画不动了 - -)
基于这个工作流,需要做的是:
-
需要长期维护 Master 和 Developer 两个分支。
-
这其中的方式还是有一定复杂度的,尤其是 Release 和 Hotfix 分支需要同时向两个分支作合并。所以,如果没有一个好的工具来支撑的话,这会因为我们可能会忘了做一些操作而导致代码不一致
-
GitFlow 协同虽然工作流比较重。但是它几乎可以应对所有公司的各种开发流程。
2.4 GitHub / GitLab 协同工作流
上面的 GitFlow 工作流,大家觉得有问题吗?毕竟已经过去 11 年了。但业内其实是有很多吐槽的:
上面两张图是我在文章 GitFlow considered harmful | End of Line Blog 中摘取的,主要问题就是分支太多。 最终就会出现上面图一的情况。
-
层级太多
-
分支交叉太多
主要是 git-flow 使用 git merge --no-ff
来合并分支,在 git-flow 这样多个分支的环境下会让你的分支管理的 log 变得很难看。如下所示,左边是使用–no-ff 参数在多个分支下的问题。
所谓--no-ff参数的意思是 no fast forward 的意思。也就是说,合并的方法不要把这个分支的提交以前置合并的方式,而是留下一个 merge 的提交。这是把双刃剑,我们希望我们的--no-ff能像右边那样,而不是像左边那样。
对此的建议是:只有 feature 合并到 developer 分支时,使用–no-ff 参数,其他的合并都不使用--no-ff参数来做合并。
GitLab 一开始是 GitFlow 的坚定支持者,后来因为大家对 GitFlow 的不满,GitLab 创造了一个新的工作流:GitLab Flow,这个 GitLab Flow 是基于 GitHub Flow 来做的。
2.4.1 GitHub Flow
所谓 GitHub Flow,其实也叫 Forking flow,也就是 GitHub 上的那个开发方式。大家如果经常有在github 上贡献代码的,一定很熟悉 fork、pr 这两个单词。
GitHub 的工作流程:
-
每个开发人员都把“官方库”的代码 fork 到自己的代码仓库中。
-
然后,开发人员在自己的代码仓库中做开发,想干啥干啥。
-
因此,开发人员的代码库中,需要配两个远程仓库,一个是自己的库,一个是官方库(用户的库用于提交代码改动,官方库用于同步代码)。
-
然后在本地建“功能分支”,在这个分支上做代码开发。
-
这个功能分支被 push 到开发人员自己的代码仓库中。
-
然后,向“官方库”发起 pull request,并做 Code Review。一旦通过,就向官方库进行合并。
如果你有“官方库”的权限,那么就可以直接在“官方库”中建功能分支开发,然后提交 pull request。通过 Code Review 后,合并进 Master 分支,而 Master 一旦有代码被合并就可以马上 release
题外话:
经常参与开源代码合并的话,肯定要熟悉一些 Code Review 行话:
1、LGTM:Looks Good To Me「对我来说,还不错」表示认可这次PR,同意merge合并代码到远程仓库 2、ASAP:As Soon As Possible「尽快」 3、ACK:Acknowledgement「承认,确认,同意」i.e. agreed/accepted change 4、NACK/NAK:Negative acknowledgement「不同意」 i.e. disagree with change and/or concept 5、RFC:Request For Comments「请求进行讨论」 i.e. I think this is a good idea, lets discuss 6、WIP:Work In Progress 「进展中」常见词汇,这里作为 Best Practice 单独提出来,主要针对改动较多的 PR,可以先提交部分,标题或 Tag 加上 WIP,表示尚未完成,这样别人可以先 review 已提交的部分 7、AFAIK/AFAICT:As Far As I Know / Can Tell 「据我所知;就我所知」 8、IIRC:If I Recall Correctly「如果我没有记错的话」 9、IANAL:I am not a lawyer , but I smell licensing issues「-」 10、IMO:In My Opinion 「在我看来」 11、TL;DR:Too Long; Didn’t Read 「太长懒得看」README 文档常见。 12、PR:Pull Request「合并请求」 13、CR:Code Review 「代码审查」 14、PTAL:Please Take A Look.「你来瞅瞅?」用来提示别人来看一下 15、TBR:To Be Reviewed「提示维护者进行 review」 16、TBD:To Be Done(or Defined/Discussed/Decided/Determined). 「未完成,将被做」根据语境不同意义有所区别,但一般都是还没搞定的意思。 17、TBH:To Be Honest 「老实说」 18、atm:at the moment 「现阶段」 19、YYDS:永远的神,表示对这个PR中所展示出来的惊人编程技巧的赞叹 20、SSDS:屎山堆屎,表示在一段不可维护的垃圾代码上继续开发,无暇重构 💩⛰
2.4.2 GitLab Flow
GitHub Flow 这种玩法依然会有好多问题,因为其虽然变得很简单,但是没有把我们的代码和我们的运行环境给联系在一起。所以,GitLab 提出了几个优化点。
1、引入环境分支,如下图所示,其包含了预发布(Pre-Production)和生产(Production)分支。
2、有些时候,我们还会有不同版本的发布,所以,还需要有各种 release 的分支。如下图所示。Master 分支是一个 roadmap 分支,然后,一旦稳定了就建稳定版的分支,如 2.3.stable 分支和 2.4.stable 分支,其中可以 cherry-pick master 分支上的一些改动过去。
这样也就解决了两个问题:
-
环境和代码分支对应的问题;
-
版本和代码分支对应的问题;
看了很多关于工作流的文章,总结下结论:对于互联网公司来说,环境和代码分支对应这个事,只要有个比较好的 CI/CD 生产线,这种环境分支应该也是没有必要的。而对于版本和代码分支的问题,我觉得这应该是有意义的,但是,最好不要维护太多的版本,版本应该是短暂的,等新的版本发布时,老的版本就应该删除掉了。
2.5 协同工作流的本质
协同工作流的本质,并不是怎么玩好代码仓库的分支策略,而是玩好我们的软件架构和软件开发流程。
协同工作需要考虑四个问题:
-
不同的团队能够尽可能地并行开发。
-
不同版本和代码的一致性。
-
不同环境和代码的一致性。
-
代码总是会在稳定和不稳定间交替。我们希望生产线上的代码总是能对应到稳定的代码
协同工作流也没有银弹,在可以满足上面条件的情况下,我们只需要选择适合我们的方式即可。
0x03 提升效率的小技巧
3.1 指令
3.1.1 开胃小菜:开启全局错误纠错
每个人都不时在输入时犯拼写错误,但是如果你使能了 Git 的自动纠错功能,你就能让 Git 自动纠正一些输入错误的子命令。
$git config --global help.autocorrect 1
效率提升:
-
开启自动纠错前:只会给出建议,让你重新输入指令(上图情况 1)
-
开启自动纠错后:不仅会给出建议,而且会把第一个建议对应的打印结果直接输出(上图情况 3 为正常输入 status 的打印结果),避免再次输入指令
3.1.2 查看另外一个分支上的某一个文件
有时,你会想要浏览另一个分支下某个文件的内容。这其实用一个简单的 Git 命令就可以实现,甚至都不用切换分支。
$git show main:README.md
效率提升:
-
减少 2 次分支切换(切过去、切回来),也不用暂存(git stash)当前代码再恢复(git stash pop),可以直接查看某个文件在某个分支的情况。
3.1.3 多账号登录
这里会引入一个概念,git config 的作用域
# 查看所有全局 git config
$git config --list
# 以下为打印结果
user.email=hello@company.com
user.name=hello
help.autocorrect=1
# 设置全局账号
$git config --global user.name "zhangsan"
$git config --global user.email "zhangsan@company.com"
# 单独为某个仓库设置账号
$cd myProjectDir
$git config user.name "lisi"
$git config user.email "lisi@gmail.com"
3.1.4 暂存代码 git stash
暂存区,合理使用暂存区会大大提升我们的并行开发效率,可以让大家在各个项目中自由自在的穿梭 💃🏻
使用场景:
我们有时会遇到这样的情况,正在 dev 分支开发新功能,做到一半时有人过来反馈一个bug,让马上解决,但是新功能做到了一半你又不想提交,这时就可以使用 git stash 命令先把当前进度保存起来,然后切换到另一个分支去修改 bug,修改完提交后,再切回 dev 分支,使用 git stash pop 来恢复之前的进度继续开发新功能。
常用指令:
# 把当前代码存在暂存区
$git stash
# 恢复最新的一个进度
$git stash pop
# 删除所有存储进度
$git stash clear
效率提升:
以上三个常用指令的用法升级:
# git stash 可以加上 save 指令,可以让我们的快速恢复到想要的版本
$git stash save '本次暂存的功能描述'
# pop 时加上 --index 可以选择想要恢复到某个指定的版本
$git stash pop --index 3
# clear 太暴力了,会把所有暂存区内容全部删掉, drop 指令可以删除某个制定的版本
$git stash drop 3
可以看到在直接执行 stash 指令时,大量 WIP on master,并且后面的信息为上一次提交的 commit,功能区分不明确,没有办法区分具体改了什么功能。
3.1.5 git push -u 与 git branch --set-upstream-to 的区别
# 方法一:
$git push -u origin master
# 方法二:
$git branch --set-upstream-to=origin/master master
举个例子:
我要把一个本地分支 master 与远程仓库 origin 里的分支 master 建立关联,这两种方式都可以达到目的。但是方法一更通用,因为你的远程库有可能并没有 master 分支,这种情况下用方法二就不可行,连目标分支都不存在,怎么进行关联呢?
总结一下:
方法一相当于 git push origin master
+ git branch --set-upstream-to=origin/master master
的合体
提升效率:
在日后的开发中,命名较长的分支, 首次 push 代码到远程仓库时,可以使用 git push -u origin new_branch
来执行,之后在该分支可以直接执行 git push
来提升效率。
更改后可以在 .git 文件夹中查看 config 文件:
[branch "master"]
remote = origin
merge = refs/heads/master
[branch "new_branch"]
remote = origin
merge = refs/heads/new_branch
3.1.6 git revert 与 reset 的区别
revert(反做)
使用场景:
git revert是用于“反做”某一个版本,以达到撤销该版本的修改的目的。比如,我们commit了三个版本(版本一、版本二、 版本三),突然发现版本二不行(如:有bug),想要撤销版本二,但又不想影响撤销版本三的提交,就可以用 git revert 命令来反做版本二,生成新的版本四,这个版本四里会保留版本三的东西,但撤销了版本二的东西
首先要明确一个概念:commit 分为两种,常规 commit 和 merge commit,revert 两种 commit 操作是不同的
-
Revert 常规 commit:使用 git revert 即可,git 会生成一个新的 commit,将指定的 commit 内容从当前分支上撤除。
-
Revert merge commit:不建议这么操作
常见场景:
1、Revert 之后怎么回退?
# 查看之前所有的操作记录,找到之前 HEAD 指向的 commit hash
$git reflog
# 回退到该 commit
$git reset --hard commit-hash
reset (回退)
**使用场景:**如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。
常见场景:
1、回退已经 push 到远程仓库的代码
# 查看要回退的版本
$git log
# 回退到想要回退的版本
$git reset 某个commit
# 强制推送到远程仓库 (不推荐)
$git push -f origin master
2、合并多个未 push 的 commit
# 本次 history 中有多个 commit,如果提交不太美观
# 我们可以 reset 到 17bd20c 这个 commit
$git reset 17bd20c
# 然后重新提交代码
$git add .
$git commit -m 'balabala'
# 这样多个 commit 就被合并为了一个
两者差别:
两者最大的差别是 reset 会使 HEAD 回退, revert 会使 HEAD 继续向前
3.1.7 git rebase 与 git merge 的区别
不用再用 merge 一把梭了,可以试着去了解下更合理的 rebase 指令。
1、不要在公共分支使用rebase
为什么不要在公共分支使用rebase?
因为往后放的这些 commit 都是新的,这样其他从这个公共分支拉出去的人,都需要再 rebase,相当于你 rebase 东西进来,就都是新的 commit 了
2、本地和远端对应同一条分支时,优先使用 rebase,而不是 merge
在使用 merge 的时候,一定要注意上面讲到的对 --no-ff 参数的应用
总结: 1. 在本和远端对应同一条分支时,请使用 rebase,在处理冲突的时候 merge 但不要加 --no-ff 参数。 2. 在不同分支间合并时,请使用 merge,但一定要添加 --no-ff 参数。
3.2 工具
3.2.1 Git alias
大家如果在使用命令行工具的情况下,建议安装 iTerm,并且使用 zsh 替代自带的 bash,顺便升级 oh-my-zsh 进行样式升级和功能扩充
1、可以使用 alias(别名)来缩减 git 操作的速度,如:
$gp='git push'
$gl='git pull'
$ga='git add'
$gaa='git add --all'
$gcmsg='git commit -m'
...
2、一些组合指令放在 npm script 中提升效率
# 在所有提交记录中搜索包含 'xgplayer' 的 commit
$git rev-list --all | xargs git grep -F 'xgplayer'
# 不建议
$git add . && git commit -m 'fast commit' && git push
3.2.2 Vscode 里面的实用插件
Git Graph
查看分支情况
GitLens
-
看某行代码的提交记录
-
处理 merge 冲突
3.2.3 cli 工具
commitizen
帮助用户输出符合规范的
Husky
协助 commitizen,增加 git hooks 来强制完成该操作
"husky":{
"hooks": {
"commit-msg": "commitlint -e $GIT_PARAMS"
}
}
0x04 感悟
1、使用命令行工具:
虽然上面说了,可以大家可以花更多的时间去做更有意义的事情,但对于使用命令行还是可视化工具来操作 git,我建议是使用命令行。
-
感觉在与 git 进行对话:我要把远程的代码拉取(git pull)下来,并且与自己本地代码进行合并(git merge),然后推送到远程仓库(git push)。
-
类似使用 Vim,使用 Vim 不仅是为了效率提升,而是希望自己的每次操作都是经过思考的。
-
不用切换软件,甚至不用切换窗口,完全使用键盘完成这些操作。
2、工具的使用也是成本,一定要化繁为简:
-
熟悉工作流:对于 git 来说,我们只需要掌握每种 git 协同工作流适用于那种场景。
-
熟练使用基本指令:不浪费额外精力去记各种奇淫技巧,只掌握最基本的指令,并知道如何使用更加高效。(如:首次 push 时加上 -u 参数)
3、复杂的工作流一定是架构不合理导致的
转载自:https://juejin.cn/post/7046302856604811301