一杯茶的时间,上手 Git 团队协作开发
发布于 2 个月前 作者 huan1043269994 3430 次浏览 来自 分享

我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star

如果你想快速了解如何使用,欢迎阅读我们的 教程文档哦

在大多数工作中,我们都将使用 Git 作为团队协作开发的工具。

本文总结了图雀团队协作开发的流程与规范,仅供参考。最优的解决方案还是需要结合团队的实际情况,具体问题具体分析。

为了让大家能够非常清晰直观地了解协作开发的流程,大家在看的时候可以打开 Learn Git Branching 的沙箱运行环境来实践(可以直接输入提供的代码)。在左边的终端中输入命令,就会在右边看到相应的动画。其中左边的实线圆圈代表本地仓库,右边的虚线圆圈代表远程仓库,* 号指向的是当前分支,o/master 就是远程分支( o 就相当于 origin )。

由于 Learn Git Branching 为了演示和学习的方便对部分命令做了简化,我将指出在实际操作中应当输入的命令。

基本流程

接下来将重点讲述以下两个流程:

  • 贡献代码

  • 更新本地仓库

贡献代码

接下来的流程描述了在接到开发任务后,如何为中心仓库贡献代码。

将仓库 clone 到本地

$ git clone

实际命令应当提供 URI 参数,例如:

$ git clone https://github.com/dhucst/cooperation.git

开启新分支

$ git checkout -b B1

为了便于演示,我们将新分支命名为 B1。在实际开发中,新分支的命名应当遵循以下原则:

  • 使用 kebab-case,例如 new-branch ,而不是 new_branchnewBranch

  • 尽量能概括这个分支所要完成的任务

  • 如果是为了解决某个 Issue,在最后加上 Issue 的编号,例如 fix-75

编写代码并提交

$ git commit

实际命令应当要先执行 git add 来将修改的文件添加到暂存区,例如:

$ git add .
$ git commit

Commit Message (Log) 的书写是有比较严格的规范的,会在后文的 提交信息书写规范 中详细阐述。

推送分支
$ git push

实际命令在第一次 push 任何分支时,应当指定 remote 和分支名称:

$ git push origin B1

有时候我们的分支会在一夜之间“过时”。什么是过时的分支,我们该怎样处理?不要方,后面会讲到。

提交 Pull Request

这一步骤无需在 Learn Git Branching 中操作。

将分支提交到远程仓库后,打开仓库的 GitHub 页面,应该会看到下面这样黄色的提示框:

image-20200429091859922

然后点击 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 边看边练,请输入以下命令:

$ reset
$ git clone

其他成员贡献代码

$ git fakeTeamwork 2

实际没有这条 Git 命令 😂,是 Learn Git Branching 提供用于练习协作的。

这时候你会发现远程的仓库有了本地没有的提交 C2C3

拉取远程代码

我们先来看第一种比较简单的情况:

这时候一眼就可以看出,只需把远程的 C2C3 直接拉取过来接在本地的 C1 后面就可以了:

$ git pull

接着我们来看另一种比较棘手的情况:

需要输入的命令如下:

$ reset
$ git clone
$ git checkout -b B2
$ git commit
$ git fakeTeamwork 2

对着图看,我们在 B2 分支上在开发某个新功能,这时候远程仓库已经更新到了 C4,很显然我们本地的 master 分支和 B2 分支都不是最新的了。这种情况很常见:几个小伙伴从同一个起点(在这里就是 C1 )各自开发新功能时,其他人先于我们提交。

大多数情况下,请遵循这一条原则:只更新 master 分支。

这一原则对于并行开发并不适用,我们会在本知识库后续文档中讲解。

$ git checkout master
$ git pull

这时候,我们就会认为 B2 分支已经过时(outdated),因为它没有最新的 C3C4 。但过时的分支并不意味着没有价值了,我们可以像前面所讲解的那样 push 到远程仓库:

$ git checkout B2
$ git push

然后一样可以发起 Pull Request。GitHub 会提示你这条分支已经过时,你可以点击 Update Branch 按钮来更新这一条分支(通常由项目管理者来执行这一操作)。

小结

团队协作开发的模型只涉及两个核心流程:贡献代码和更新本地仓库。

贡献代码的流程:

$ git clone <REPO_URI>
$ git checkout -b new-branch
$ git add .
$ git commit
$ git push origin new-branch

更新代码库的流程:

$ git checkout master
$ git pull

并行开发

一个项目的开发往往由多个开发任务组成,每个人都会负责承担一个或多个开发任务。最简单、最理想的情况当然是:同学 A 开始贡献代码,成功合并后所有人更新本地代码库;接着同学 B 开始贡献代码,合并后所有人更新本地代码库;然后是同学 C、D、E……

不会有任何冲突,只需用到前面 基本流程 所介绍到的命令,多么轻松愉快!

唯一的问题就是:这样的开发显然进度很慢,而且大家的时间安排也不够自由。这种串行开发的方式过于同步化,对于一个追求效率的团队来说是不能接受的。我们需要高度并行完全异步的协作开发模式。

接下来我们将描述三个典型的并行开发场景,其中的主角是大唐同学和煨鸽同学。

互不依赖且没有修改同一文件

例如有个着陆页开发的任务,大唐负责做“关于我们”页面,叫 about-us.html,煨鸽负责做“联系我们”页面,叫 contact.html,这两个文件相互独立的。

这里我们假定大唐同学率先完成了任务并且已经合并到 origin/master 。这时候根据前一章 更新本地仓库 一节的说法,煨鸽正在工作的分支已经“过时”。这时候他只需要继续完成他的 contact.html 页面,然后提交就可以了。

这是最简单的,也是最常见的情况(合理的任务划分应当如此):相互独立的分支只需依次 push,不管是否过时。

存在依赖关系且没有修改同一文件

现在我们又假设大唐在开发着陆页的首页 index.html,煨鸽负责写着陆页的样式 index.css,很明显大唐的开发任务依赖煨鸽。经过一天的开发,大唐写完了主体部分 C2,煨鸽也写好了样式 C3 并且已经提交到远程仓库,现在他需要把煨鸽的样式表加进来,才能完成自己的开发任务。

Learn Git Branching 中输入以下代码:

$ git clone
$ git checkout -b html
$ git commit
$ git fakeTeamwork

然后大唐使用 fetch 命令将远程的 C3 抓取下来(其实更严格的说法是将本地的 o/master 分支与远程的 master 同步):

$ git fetch

可以看到,html 文件的提交和 css 文件的提交在不同的分支上。html 是我们工作的分支(也是当前所在的分支),因此要把 C3 所在的 o/master 合并过来:

$ git merge o/master

这个形状看上去有点吓人!实际上,你只要真正理解分支的本质就会觉得非常好理解。

分支不能简单地理解为一串 commit(虽然说在大多数情况下这种理解非常直观),而应该理解为指向某个 commit 的指针,而该 commit 的所有父节点都是该分支上的节点(commit)。因此在执行合并后,我们可以说 C2C3 都已经在 html 分支上了。

合并之后,我们再修改点东西,提交为 C5 ,然后推送到远程仓库:

$ git commit
$ git push

再次提醒真正的 push 命令在第一次推送某一分支时要加上远程仓库名称和分支名称,例如 git push origin html

接下来就是提交 Pull Request、等待合并就可以了。

修改同一文件

首先声明这种情况非常少见,合理的任务划分会尽量避免这种情况出现。但是我们还是会讲解一下这种比较棘手的情况。由于 Learn Git Branching 没有提供冲突(conflict)的演示,所以我们需要自己在本地开仓库进行演示。

为什么在本地开仓库练习就可以了,而不需要搭一个远程仓库吗?因为本小节的操作流程和命令跟上一节相比,除了增加了一个处理冲突的步骤,其余完全相同,因此我们关注的重点是怎么处理冲突。

$ mkdir conflict-demo && cd conflict-demo
$ git init
$ touch index.js
$ git add .
$ git commit -m "Add index.js"

然后我们开启一个新分支 add-func :

$ git checkout -b add-func

在 index.js 中增添一个 add 函数:

function add(x, y) {
  return x + y;
}

保存并提交:

$ git add .
$ git commit -m "Implement add function"

然后我们切回主分支,并开启一个叫 origin-master 的分支(听这名字也知道,它模拟了远程的主分支):

$ git checkout master
$ git checkout -b origin-master

接着再在 index.js 中添加一个叫 multiply 的函数:

function multiply(x, y) {
  return x * y;
}

好了,现在本地的 add-func 工作分支和“远程”的 origin-master 分支修改了同一文件 index.js,冲突一触即发!让我们来点燃这根导火索!

其实你可以不停地把分支切来切去(轮流输入 git checkout add-funcgit checkout origin-master ),你会看到 index.js 的内容会随之变来变去,版本控制系统的魅力可见一斑。

$ git checkout add-func
$ git merge origin-master

我们会发现 Git 会输出你从未见过的信息:

Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

划重点:index.js 在合并时发生冲突,请处理冲突然后提交。

我们查看 index.js 的内容,发现了很神奇的东西(在命令行中用 cat 查看):

<<<<<<< HEAD
function add(x, y) {
  return x + y;
=======
function multiply(x, y) {
  return x * y;
>>>>>>> origin-master
}

如果我们用 VSCode 打开,会看到更炫酷的结果:

这就一目了然了!绿色部分是我们当前分支 add-func 的内容,蓝色部分是 origin-master 的内容。由于我们两者都要,所以点击 Accept Both Changes。然后略经修正,将 index.js 改为如下:

function add(x, y) {
  return x + y;
}

function multiply(x, y) {
  return x * y;
}

提交我们用于处理冲突的 commit:

$ git add .
$ git commit -m "Merge conflict of index.js"

冲突处理完成,我们提交此分支,任务完成。

小结

并行开发是 Git 团队协作中比较高级却又非常重要的部分。平时大多数情况下,我们遇到的都是第一种情况。如果“不幸”遇到了后面两种情况,不熟悉时可以回来看一看这篇文档。

PS:对于后面两种情况,有一点需要补充:如果想要撤销 merge,使用下面这条命令:

$ git merge --abort

提交信息书写规范

提交信息,又称为 commit messages 或者 commit logs,是每一步提交所必需的信息。我们可以看一下 React 仓库的提交记录:

由此我们可以对项目每一步做了什么有了比较好的了解。

格式

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。

<header>
// 空一行
<body>
// 空一行
<footer>

其中,Header 是必需的,Body 和 Footer 可以省略。

Header

Header部分只有一行,是对 commit 的简短概述,是一个包括动宾结构修改对象(可选)祈使句(不要加句号!)

我们来看几个例子。

Remove 'warning' module from the JS scheduler

这里的动宾结构是 Remove ‘warning’ module,修改对象是 JS scheduler。

Add [@flow](/user/flow) directive to findDOMNode shim

这里的动宾结构是 Add @flow directive,修改对象是 findDOMNode shim。

Update www warning shim

这里动宾结构是 Update www warning shim,由于修改对象已经很明确(在动宾结构中),所以无需再写。

Body

Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

More detailed explanatory text, if necessary.  Wrap it to 
about 72 characters or so. 

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Use a hanging indent

有两个注意点。

  • 使用第一人称现在时,比如使用 change 而不是 changedchanges

  • 应该说明代码变动的动机,以及与以前行为的对比。

Footer

Footer 部分只用于两种情况。

(1)不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以 BREAKING CHANGE 开头,后面是对变动的描述、以及变动理由和迁移方法。

(2)关闭 Issue

如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue。

Closes #234

也可以一次关闭多个 issue 。

Closes #123, #245, #992

我们团队建议在 Pull Request 中关闭 Issue,如前面基本流程所描述的那样。

Revert

还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以 revert: 开头,后面跟着被撤销 Commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body 部分的格式是固定的,必须写成 This reverts commit <hash>.,其中的 hash 是被撤销 commit 的 SHA 标识符。

如何修改

一开始写 Commit Message 的时候难免会出现写得不好的情况,一般情况下会有人建议你如何写得更好,或者你自己想到了更合适的写法。这时候该如何修改呢?

修改最近一次提交

如果你要修改的就是最近一次提交,那就非常简单了。Git 有专门的命令用于轻松修改刚才的提交:

$ git commit --amend

然后就会进入 vi 界面重新编辑你的提交信息。当然也可以直接用 -m 选项指定提交信息:

$ git commit --amend -m "Updated commit message"

想要在 Learn Git Branching 看看怎么回事?输入下面的命令体验一下吧:

$ git commit
$ git commit --amend

修改倒数第 n 次的提交

下面要介绍的 rebase 命令威力可以说是非常巨大,但是要掌握却实属不易。没事,我们先来看看如何用 rebase 修改倒数第 3 次提交:

$ git rebase -i HEAD~3

-i 的意思是 --interactive ,输入后 Git 就会打开一个 vi 编辑器,并出现下面的内容:

pick 0f78800 倒数第4次提交
pick 459014c 倒数第3次提交
pick 38009c7 倒数第2次提交
pick dff7f7d 最新的提交

# Rebase 500d110..dff7f7d onto 500d110 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

其实如果你认真看一下下面的注释,基本上知道怎么做了:把倒数第三次提交前面的 pick 命令改为 reword 。保存后,Git 就会把你带到倒数第 3 次提交的 vi 编辑页面,这时候重新写提交信息就可以了。

你也可以在 Learn Git Branching 中体验一下 rebase 命令:

$ reset
$ git commit
$ git commit
$ git rebase -i HEAD~3

Learn Git Branching 提供的是图形化界面,和 Git 的 vi 界面略有区别。

强制推送修改

有时候可能你已经把分支 push 到远程仓库、甚至已经提交了 Pull Request 了。如果直接 push ,Git 会因为远程和本地的分支冲突而拒绝推送。这时候只需要加上 -f 选项,强制用本地的分支覆盖远程的分支即可:

$ git push -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 页面评审是最基本的方法。对于改动比较小的分支,这种方法完全足够。

有时候我们发现了他人代码的问题。千万不要保留你的意见!要把自己的想法有条理地写下来。我们可以选择特定一行来发表评论,只需把鼠标移到行首,就会显示一个加号,如下图。

点击加号,就可以对这一行进行评论了:

拉取到本地评审

有时候某些分支的改动非常大,大到需要你在本地亲自运行一下,看看是否真的达到了预期的目标。

$ git fetch origin lots-of-changes
$ git checkout logs-of-changes

然后你的本地仓库就完成切换到待评审分支的状态了!你可以试着运行,做各种尝试,还可以在自己熟悉的编辑器里面更加舒适地阅读代码,美滋滋。

提交评审结果

无论是直接在 GitHub 还是在本地审查,最后都要提交评审结果。评审结果包括你在代码行中的所有评论、Review summary 和最终意见。

Review summary 主要是一些总结性的话语。如果代码提交者确实做得非常优秀,当然是要夸奖一下喔;如果有些地方做得不足,则要给出改进的方向和一些鼓励。

最终意见有以下三种:

  • Comment:只是做一些客观评价,对此分支是否可以合并不给出明确意见

  • Approve:同意此分支合并进主分支

  • Request changes:不同意此分支合并,需要进一步修改

接着代码提交者根据其他人的评审进行修改后提交,然后再继续评审,如此迭代,直到分支可以合并。

最佳实践

对于代码提交者

任务最小化

每个开发任务都应当只做一件事情,因此所需评审的代码应可能地少。事实表明,超过 200 行的代码评审的有效性显著降低,超过 400 行时代码评审几乎没有意义。

提供足够的上下文

在编写代码时应有意识地添加足够的注释或文档,因为你的代码会被很多人阅读。良好的注释能够让团队其他成员评审你的代码时更加轻松,也更容易发现问题所在。另外,在填写 Pull Request 说明信息时,也应该将所解决的问题、发生的相应改变说明清楚。

对于评审者

评审最重要的事情

不要纠结于代码风格或是格式问题,这些事情会有专门的工具代劳。你应当关注的是下面这些问题:

  • 代码是否具备良好的可读性?

  • 能否实现得更简洁、更地道?

  • 代码是否遵循了良好的设计原则?

  • 代码的空间效率和时间效率怎么样?

保持积极开放的心态

不必过于挑剔,乐于赞扬他人的劳动,学会欣赏他人的代码。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

回到顶部