一杯茶的时间,上手 Git 团队协作开发
本文由图雀社区成员 mRc 写作而成,欢迎加入[图雀社区](https://tuture.co),一起创作精彩的免费技术教程,予力编程行业发展。
在大多数工作中,我们都将使用 Git 作为团队协作开发的工具。
本文总结了图雀团队协作开发的流程与规范,仅供参考。最优的解决方案还是需要结合团队的实际情况,具体问题具体分析。
为了让大家能够非常清晰直观地了解协作开发的流程,大家在看的时候可以打开 Learn Git Branching 的沙箱运行环境来实践(可以直接输入提供的代码)。在左边的终端中输入命令,就会在右边看到相应的动画。其中左边的实线圆圈代表本地仓库,右边的虚线圆圈代表远程仓库,*
号指向的是当前分支,o/master
就是远程分支( o
就相当于 origin
)。
由于 Learn Git Branching 为了演示和学习的方便对部分命令做了简化,我将指出在实际操作中应当输入的命令。
基本流程
接下来将重点讲述以下两个流程:
贡献代码
更新本地仓库
贡献代码
接下来的流程描述了在接到开发任务后,如何为中心仓库贡献代码。
将仓库 clone 到本地
实际命令应当提供 URI 参数,例如:
```bash
$ git clone https://github.com/dhucst/cooperation.git
```
开启新分支
为了便于演示,我们将新分支命名为 B1
。在实际开发中,新分支的命名应当遵循以下原则:
使用 kebab-case,例如
new-branch
,而不是new_branch
或newBranch
尽量能概括这个分支所要完成的任务
如果是为了解决某个 Issue,在最后加上 Issue 的编号,例如
fix-75
编写代码并提交
实际命令应当要先执行
git add
来将修改的文件添加到暂存区,例如:```bash
$ git add .
$ git commit
```
Commit Message (Log) 的书写是有比较严格的规范的,会在后文的 提交信息书写规范 中详细阐述。
推送分支
实际命令在第一次 push 任何分支时,应当指定 remote 和分支名称:
```bash
$ git push origin B1
```
有时候我们的分支会在一夜之间“过时”。什么是过时的分支,我们该怎样处理?不要方,后面会讲到。
提交 Pull Request
这一步骤无需在 Learn Git Branching 中操作。
将分支提交到远程仓库后,打开仓库的 GitHub 页面,应该会看到下面这样黄色的提示框:
然后点击 Compare & pull request 按钮,即可进入到提交 Pull Request 页面。
填写 Pull Request 标题所遵循的原则与 Commit message 大致相似。在填写 Pull Request 的详细内容时,如果是为了解决某个或多个 Issue 时,可以使用 Close(s)
, Fix(es)
或 Resolve(s)
关键词来关闭某个 Issue,例如 Fix #75
。
点击 Create pull request 按钮后,即可完成本次 PR。如果经讨论后发现需要修改,则在本地仓库修改后直接 git push
继续提交即可。如果代码通过了评审,则会由项目管理者将此分支并入 master 中,本次贡献代码流程结束。
更新本地仓库
接下来的流程介绍了当团队其他成员贡献代码后,如何将远程仓库的更新同步到本地。
如果你在使用 Learn Git Branching 边看边练,请输入以下命令:
其他成员贡献代码
实际没有这条 Git 命令 😂,是 Learn Git Branching 提供用于练习协作的。
这时候你会发现远程的仓库有了本地没有的提交 C2
和 C3
。
拉取远程代码
我们先来看第一种比较简单的情况:
这时候一眼就可以看出,只需把远程的 C2
和 C3
直接拉取过来接在本地的 C1
后面就可以了:
接着我们来看另一种比较棘手的情况:
需要输入的命令如下:
对着图看,我们在 B2
分支上在开发某个新功能,这时候远程仓库已经更新到了 C4
,很显然我们本地的 master 分支和 B2
分支都不是最新的了。这种情况很常见:几个小伙伴从同一个起点(在这里就是 C1
)各自开发新功能时,其他人先于我们提交。
大多数情况下,请遵循这一条原则:只更新 master 分支。
这一原则对于并行开发并不适用,我们会在本知识库后续文档中讲解。
这时候,我们就会认为 B2
分支已经过时(outdated),因为它没有最新的 C3
和 C4
。但过时的分支并不意味着没有价值了,我们可以像前面所讲解的那样 push 到远程仓库:
然后一样可以发起 Pull Request。GitHub 会提示你这条分支已经过时,你可以点击 Update Branch 按钮来更新这一条分支(通常由项目管理者来执行这一操作)。
小结
团队协作开发的模型只涉及两个核心流程:贡献代码和更新本地仓库。
贡献代码的流程:
更新代码库的流程:
并行开发
一个项目的开发往往由多个开发任务组成,每个人都会负责承担一个或多个开发任务。最简单、最理想的情况当然是:同学 A 开始贡献代码,成功合并后所有人更新本地代码库;接着同学 B 开始贡献代码,合并后所有人更新本地代码库;然后是同学 C、D、E……
不会有任何冲突,只需用到前面 基本流程 所介绍到的命令,多么轻松愉快!
唯一的问题就是:这样的开发显然进度很慢,而且大家的时间安排也不够自由。这种串行开发的方式过于同步化,对于一个追求效率的团队来说是不能接受的。我们需要高度并行、*完全异步*的协作开发模式。
接下来我们将描述三个典型的并行开发场景,其中的主角是大唐同学和煨鸽同学。
互不依赖且没有修改同一文件
例如有个着陆页开发的任务,大唐负责做“关于我们”页面,叫 about-us.html,煨鸽负责做“联系我们”页面,叫 contact.html,这两个文件相互独立的。
这里我们假定大唐同学率先完成了任务并且已经合并到 origin/master
。这时候根据前一章 更新本地仓库 一节的说法,煨鸽正在工作的分支已经“过时”。这时候他只需要继续完成他的 contact.html 页面,然后提交就可以了。
这是最简单的,也是最常见的情况(合理的任务划分应当如此):相互独立的分支只需依次 push,不管是否过时。
存在依赖关系且没有修改同一文件
现在我们又假设大唐在开发着陆页的首页 index.html,煨鸽负责写着陆页的样式 index.css,很明显大唐的开发任务依赖煨鸽。经过一天的开发,大唐写完了主体部分 C2
,煨鸽也写好了样式 C3
并且已经提交到远程仓库,现在他需要把煨鸽的样式表加进来,才能完成自己的开发任务。
在 Learn Git Branching 中输入以下代码:
然后大唐使用 fetch 命令将远程的 C3 抓取下来(其实更严格的说法是将本地的 o/master
分支与远程的 master
同步):
可以看到,html 文件的提交和 css 文件的提交在不同的分支上。html 是我们工作的分支(也是当前所在的分支),因此要把 C3
所在的 o/master
合并过来:
这个形状看上去有点吓人!实际上,你只要真正理解分支的本质就会觉得非常好理解。
分支不能简单地理解为一串 commit(虽然说在大多数情况下这种理解非常直观),而应该理解为指向某个 commit 的指针,而该 commit 的所有父节点都是该分支上的节点(commit)。因此在执行合并后,我们可以说 C2
和 C3
都已经在 html
分支上了。
合并之后,我们再修改点东西,提交为 C5
,然后推送到远程仓库:
再次提醒真正的 push 命令在第一次推送某一分支时要加上远程仓库名称和分支名称,例如
git push origin html
。
接下来就是提交 Pull Request、等待合并就可以了。
修改同一文件
首先声明这种情况非常少见,合理的任务划分会尽量避免这种情况出现。但是我们还是会讲解一下这种比较棘手的情况。由于 Learn Git Branching 没有提供冲突(conflict)的演示,所以我们需要自己在本地开仓库进行演示。
为什么在本地开仓库练习就可以了,而不需要搭一个远程仓库吗?因为本小节的操作流程和命令跟上一节相比,除了增加了一个处理冲突的步骤,其余完全相同,因此我们关注的重点是怎么处理冲突。
然后我们开启一个新分支 add-func
:
在 index.js 中增添一个 add
函数:
保存并提交:
然后我们切回主分支,并开启一个叫 origin-master
的分支(听这名字也知道,它模拟了远程的主分支):
接着再在 index.js 中添加一个叫 multiply
的函数:
好了,现在本地的 add-func
工作分支和“远程”的 origin-master
分支修改了同一文件 index.js,冲突一触即发!让我们来点燃这根导火索!
其实你可以不停地把分支切来切去(轮流输入
git checkout add-func
和git checkout origin-master
),你会看到 index.js 的内容会随之变来变去,版本控制系统的魅力可见一斑。
我们会发现 Git 会输出你从未见过的信息:
划重点:index.js 在合并时发生冲突,请处理冲突然后提交。
我们查看 index.js 的内容,发现了很神奇的东西(在命令行中用 cat 查看):
如果我们用 VSCode 打开,会看到更炫酷的结果:
这就一目了然了!绿色部分是我们当前分支 add-func
的内容,蓝色部分是 origin-master
的内容。由于我们两者都要,所以点击 Accept Both Changes。然后略经修正,将 index.js 改为如下:
提交我们用于处理冲突的 commit:
冲突处理完成,我们提交此分支,任务完成。
小结
并行开发是 Git 团队协作中比较高级却又非常重要的部分。平时大多数情况下,我们遇到的都是第一种情况。如果“不幸”遇到了后面两种情况,不熟悉时可以回来看一看这篇文档。
PS:对于后面两种情况,有一点需要补充:如果想要撤销 merge,使用下面这条命令:
提交信息书写规范
提交信息,又称为 commit messages 或者 commit logs,是每一步提交所必需的信息。我们可以看一下 React 仓库的提交记录:
由此我们可以对项目每一步做了什么有了比较好的了解。
格式
每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。
其中,Header 是必需的,Body 和 Footer 可以省略。
Header
Header部分只有一行,是对 commit 的简短概述,是一个包括动宾结构和*修改对象(可选)*的祈使句(不要加句号!)。
我们来看几个例子。
这里的动宾结构是 Remove 'warning' module,修改对象是 JS scheduler。
这里的动宾结构是 Add @flow directive,修改对象是 findDOMNode shim。
这里动宾结构是 Update www warning shim,由于修改对象已经很明确(在动宾结构中),所以无需再写。
Body
Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。
有两个注意点。
使用第一人称现在时,比如使用
change
而不是changed
或changes
。
应该说明代码变动的动机,以及与以前行为的对比。
Footer
Footer 部分只用于两种情况。
(1)不兼容变动
如果当前代码与上一个版本不兼容,则 Footer 部分以 BREAKING CHANGE
开头,后面是对变动的描述、以及变动理由和迁移方法。
(2)关闭 Issue
如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue。
也可以一次关闭多个 issue 。
我们团队建议在 Pull Request 中关闭 Issue,如前面基本流程所描述的那样。
Revert
还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以 revert:
开头,后面跟着被撤销 Commit 的 Header。
Body 部分的格式是固定的,必须写成 This reverts commit <hash>.
,其中的 hash
是被撤销 commit 的 SHA 标识符。
如何修改
一开始写 Commit Message 的时候难免会出现写得不好的情况,一般情况下会有人建议你如何写得更好,或者你自己想到了更合适的写法。这时候该如何修改呢?
修改最近一次提交
如果你要修改的就是最近一次提交,那就非常简单了。Git 有专门的命令用于轻松修改刚才的提交:
然后就会进入 vi 界面重新编辑你的提交信息。当然也可以直接用 -m
选项指定提交信息:
想要在 Learn Git Branching 看看怎么回事?输入下面的命令体验一下吧:
修改倒数第 n 次的提交
下面要介绍的 rebase
命令威力可以说是非常巨大,但是要掌握却实属不易。没事,我们先来看看如何用 rebase
修改倒数第 3 次提交:
-i
的意思是 --interactive
,输入后 Git 就会打开一个 vi 编辑器,并出现下面的内容:
其实如果你认真看一下下面的注释,基本上知道怎么做了:把倒数第三次提交前面的 pick
命令改为 reword
。保存后,Git 就会把你带到倒数第 3 次提交的 vi 编辑页面,这时候重新写提交信息就可以了。
你也可以在 Learn Git Branching 中体验一下 rebase
命令:
Learn Git Branching 提供的是图形化界面,和 Git 的 vi 界面略有区别。
强制推送修改
有时候可能你已经把分支 push
到远程仓库、甚至已经提交了 Pull Request 了。如果直接 push
,Git 会因为远程和本地的分支冲突而拒绝推送。这时候只需要加上 -f
选项,强制用本地的分支覆盖远程的分支即可:
参考
阮一峰《Commit message 和 Change log 编写指南》
代码评审
代码评审,又称代码审查,是软件开发流程中必不可少的一环。
代码审查是计算机源代码的系统性检验(有时被称为同行评审)。其目的在于找到开发初期所忽略的错误,从而提高软件的整体质量。
——Wikipedia
为什么要代码评审
代码评审并不意味着被评审者的能力不足。有下面这些原因表明代码评审的重要性。
降低风险
写出存在 bug 的代码再正常不过了。每个人贡献的代码先要经过持续集成(CI,Continuous Integration)的一系列构建测试,然后是人工代码审查,因此代码审查可以说是最后一道防线。
显著提高代码质量
代码评审不仅仅是单纯地查找 bug 或是修正格式问题,还包括使代码更高效。
在一个团队里,每个人都有自己的背景和特长,因此总有人可能提出更聪明的解决方案,更合适的设计模式,或者能降低复杂性或提高性能的方法。
有助于熟悉项目
当一个团队在做一个项目时,想要每个开发人员致力于应用的每个部分,这是极不可能的。有时候,会出现这种情况:在某一段时间,一个开发人员正为项目的大部分模块辛苦地工作,而另一个人则完全在做别的东西。
知识共享
通过合作,每个人都可以相互学习并取得进步。提交代码者很有可能从该工作中得到反馈,并意识到可能存在的问题和需要改进的部分;而审查者也可以通过阅读他人代码学到新的东西,并找出适用于他们自己的工作方案。
如何进行代码评审
发起代码评审
代码评审发生在 Pull Request 阶段,代码提交者可以请求其他成员的 Review,如下图所示。
然后被请求进行评审的成员打开这条 Pull Request 页面时会出现一个提示框:
我们点击 Add your review 按钮,即进入到 Review 页面(或者也可以点击 Files changed 这个 Tab)。Review 页面展示了本次 Pull Request 所有发生改动的文件,评审的过程也就是审查这些发生改动的代码。
在 GitHub 上评审
直接在 GitHub 的 Pull Request 页面评审是最基本的方法。对于改动比较小的分支,这种方法完全足够。
有时候我们发现了他人代码的问题。千万不要保留你的意见!要把自己的想法有条理地写下来。我们可以选择特定一行来发表评论,只需把鼠标移到行首,就会显示一个加号,如下图。
点击加号,就可以对这一行进行评论了:
拉取到本地评审
有时候某些分支的改动非常大,大到需要你在本地亲自运行一下,看看是否真的达到了预期的目标。
然后你的本地仓库就完成切换到待评审分支的状态了!你可以试着运行,做各种尝试,还可以在自己熟悉的编辑器里面更加舒适地阅读代码,美滋滋。
提交评审结果
无论是直接在 GitHub 还是在本地审查,最后都要提交评审结果。评审结果包括你在代码行中的所有评论、Review summary 和最终意见。
Review summary 主要是一些总结性的话语。如果代码提交者确实做得非常优秀,当然是要夸奖一下喔;如果有些地方做得不足,则要给出改进的方向和一些鼓励。
最终意见有以下三种:
Comment:只是做一些客观评价,对此分支是否可以合并不给出明确意见
Approve:同意此分支合并进主分支
Request changes:不同意此分支合并,需要进一步修改
接着代码提交者根据其他人的评审进行修改后提交,然后再继续评审,如此迭代,直到分支可以合并。
最佳实践
对于代码提交者
任务最小化
每个开发任务都应当只做一件事情,因此所需评审的代码应可能地少。事实表明,超过 200 行的代码评审的有效性显著降低,超过 400 行时代码评审几乎没有意义。
提供足够的上下文
在编写代码时应有意识地添加足够的注释或文档,因为你的代码会被很多人阅读。良好的注释能够让团队其他成员评审你的代码时更加轻松,也更容易发现问题所在。另外,在填写 Pull Request 说明信息时,也应该将所解决的问题、发生的相应改变说明清楚。
对于评审者
评审最重要的事情
不要纠结于代码风格或是格式问题,这些事情会有专门的工具代劳。你应当关注的是下面这些问题:
代码是否具备良好的可读性?
能否实现得更简洁、更地道?
代码是否遵循了良好的设计原则?
代码的空间效率和时间效率怎么样?
保持积极开放的心态
不必过于挑剔,乐于赞扬他人的劳动,学会欣赏他人的代码。
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。
版权声明: 本文为 InfoQ 作者【图雀社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/ad8852c4f9326fdcd58a5424f】。文章转载请联系作者。
评论 (8 条评论)