使用Git在多人协作的过程中,我们也会面临如何运用好Git的问题。这种情况下,就出现了各种各样的​​Git Workflow​​​,而本文将介绍一种基于​​rebase​​的工作流,这种工作流也是目前开源社区所比较推崇的做法,了解了这种工作流之后可以更规范的使用Git

一、Rebase和Squash

1、Rebase是什么,为什么使用Rebase

​rebase​​​是能够将我们对代码的更改从一个分支集成到另一个分支中的git命令之一(另一个命令是​​Merge​​​)。使用​​rebase​​​的一个风险在于,它会改写​​commit​​历史,如果操作不当那么会是一种破坏性的操作。

风险总是与利益并存的,使用​​rebase​​也好处良多,体现在于:

  • 能够保持提交记录的清爽,不带来额外的提交历史(而使用​​merge​​会生成一条​​merge​​的​​commit​​)
  • 能够保持commit线的线性,保持成一条直线,从而能够容易地看出代码是如何推进的

2、Squash是什么,为什么使用Squash

​rebase​​​可以运行在​​“交互(interactive)”​​模式下,交互模式下的rebase操作允许我们将多次​commit​进行压缩,从而合并成更少的、甚至单一的一次提交

之所以这么做,是因为在我们有很多​​commit​​​时,在最终合并到​​master​​​分支时,这些​​commit​​​就都会进入​​master​​​的​​commit​​​历史中,那么这会导致​​master​​中的提交历史不那么可读。

举个例子,假设我们为某个​​feature​​​进行了多次​​commit​​​,每次​​commit​​都只是做了一点点的更新,那么commit历史看起来就会杂乱无章。

比如我们的commit历史是这样的:


commit a commit b commit c


我们可以利用:


git rebase -i HEAD~3


把三个commit 合并成一个 commit 

 

我们之所以频繁地提交​​commit​​​,是为了能够及时地存档,以便能够在需要的时候方便地回滚代码。但是为了修复特定问题、开发某个特性的多次小提交,其实是仅仅在当前开发上下文中有意义的,可能我们为此提交了十几个​​commit​​​,但是对于其他同事而言,他们可能就只想知道我们这次做了什么事情。那么通过​​Squash​​​这个操作,我们就能够将这些小​​commit​​​进行合并,组成一个有意义的​​commit​​。

二、基本法则

在这套工作流中,有一些重要的法则,即:

  • 在fork的仓库上拉分支  每个贡献者都应该​​fork​​代码仓库并且在该​​fork​​出的仓库上进行开发。通过​​fork​​仓库,就可以拥有独立的工作空间,从而可以放心大胆地进行开发、修改等,对于多人开发而言,还无需事先获得分支权限也能继续开发(Tips:其实并不建议多人在同一个分支上进行开发,因为这样子很容易导致合并冲突,如果某个​​feature​​需要不止一个人去开发,相比只拉一个​​feature​​分支,把大的​​feature​​拆分为小的可独立工作单元其实是一种更好的实践)
  • 禁止对一个多人协作的分支进行rebase  这一点非常重要,贡献者只能对自己​​fork​​仓库的分支进行​​rebase​​,这是因为​​rebase​​会改写​​commit​​历史,是比较危险的一种操作,只有对自己​​fork​​仓库的分支进行​​rebase​​,才不会有任何问题

 

三、工作角色

在这套工作流中,涉及两个角色:

  • ​Maintainer(维护者)​​​:维护者拥有仓库的写权限,他们可对​​Pull Request​​进行​​Review​​来决定通过或者拒绝,并且能创建​​Git Tag​​用于发布
  • ​Contributor(贡献者)​​​:贡献者拥有仓库的读和​​fork​​权限,可以查看和创建​​issue​​,也可以提交​​Pull Request​​。贡献者也负责解决合并冲突,此外贡献者仅能推送分支到自己​​fork​​的仓库里

 

四、准备工作

在开始这套工作流之前,我们需要先做一些起始步骤。首先,需要先​​fork​​​项目仓库(通常原项目仓库惯称为​​upstream​​​,上游仓库);然后,在本地​​clone​​​这个​​fork​​​后的仓库(与此对应的远程仓库惯称为​​origin​​​);之后,在本地的​​remote​​​记录中添加这个​​upstream​​仓库,具体步骤如下:

  1. ​Fork​​​上游仓库,如在​​Gitlab​​中可以点击右上角的“Fork”按钮,选择自己的空间,确定
  2. 在本地​​clone​​这个​​fork​​后的仓库,如:

$ git clone git@git.phpmianshi.com:yang/app.git


  1. 添加上游仓库

$ git remote add upstream git@git.phpmianshi.com:php/app.git


  1. 确认信息:通过​​git remote -v​​确认我们是否成功进行了配置,正常而言需要显示两组记录(一组​​origin​​,一组​​upstream​​),如:
origin git@phpmianshi.com:yang/app.git (fetch)
origin git@phpmianshi.com:yang/app.git (push)
upstream git@phpmianshi.com:php/app.git (fetch)
upstream git@phpmianshi.com:php/app.git (push)


 

五、工作流

好了,说了这么多前置知识,总算应该开始进入正题了。在工作流中,建议所提交的代码是关联一个​​user story(用户故事)​​​或者一个​​issue​​​的,可以和一些外部系统相关联(如​​JIRA​​​、​​VersionOne​​),本套工作流的步骤总结如下:

  • 第一步:​fetch​​上游仓库的更新
  • 第二步: 将​​upstream/master​​分支和本地​​master​​合并
  • 第三步: 在本地新建分支
  • 第四步: 写代码、提交代码
  • 第五步: 再次​​fetch​​上游仓库的更新(以同步在拉取分支之后​​upstream/master​​中更新的内容)
  • 第六步: 对分支基于​​upstream/master​​进行​​rebase​​和​​squash​​,并且解决合并冲突(如果有)
  • 第七步: 推送分支
  • 第八步: 发起一个​​Pull Request​​,进行​​Code Review​​,​​Code Review​​通过后,合并到​​master​​,此后可以删除本地分支

以下是具体的细节:

1、​​fetch​​上游仓库的更新

我们开发时,应当基于最新版本的代码库进行开发。国际惯例,我们一般将原始被fork的那个仓库成为​​upstream​​​仓库(上游仓库),通过​​fetch​​​命令,我们可以将​​upstream​​中的内容获取下来存在本地,即执行:


$ git fetch upstream


2、将​​upstream/master​​​分支和本地​​master​​合并

一般情况下,我们都是先拉取远程分支,然后基于此创建本地新分支,这样子就能够拥有最新的代码。不过在创建本地新分支前,我们可以先执行以下命令:


$ git checkout master $ git merge upstream/master


这样子将会执行一个​​快速合并(Fast-Forward)​​​来让​​master​​​和​​upstream/master​​指向同一个提交

3、创建本地新分支

现在​​master​​分支是最新的了,因此我们可以基于此来拉新分支:


$ git checkout -b feat-xyz


4、编写代码、提交代码

这一步就是开发的主要步骤了,开发过程中我们可以时不时地提交​​commit​​​(当然也要确保​​commit​​是有意义的)

5、再次拉取代码

编码结束后,再发起一个​​Pull Request​​​合并上游仓库的​​master​​​分支之前,我们需要抓取一些​​upstream​​里的新提交记录(这是因为对于原仓库而言,我们不是唯一的一个开发者)

为了保持和​​upstream/master​​​的同步,我们需要使用​​git fetch​​:


$ git fetch upstream


6、Rebase和Squash

​rebase​​​会改变原​​commit​​​所基于的分支(简称​​变基​​​),并且创建带着新​​SHA-1​​​哈希的新​​commit​​​(与此同时会保留原有提交信息)。而​​squash​​​会将多个​​commit​​​压缩为一个新的​​commit​​​(也可以是多个​​commit​​​)并且带上新的​​SHA-1​​​哈希值。通常情况下,我们会想要对目标合并分支进行​​rebase​​​,当最终创建​​Pull Request​​​的时候,这个​​rebase​​​和​​squash​​​后的分支就会从​​origin​​​仓库的分支进入到​​upstream​​​仓库的master分支里,所以我们需要​​rebase upstream/master​​​,为了执行​​squash​​​操作,我们需要运行交互模式的​​rebase​​,如下:


$ git rebase --interactive upstream/master


这将会打开默认的编辑器,然后呈现将被​​rebase​​​的​​commit​​列表,如:


pick 40d9fc0 feat: Add headline
pick e8021a2 feat: Add Beijing
pick 1525479 feat: Add Shanghai
pick 81d2258 feat: Add Guangzhou
pick e94d2fb feat: Add Shenzhen# Rebase ce9657d..e94d2fb onto ce9657d (5 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


开头即为​​commit​​​列表,而​​#​​​中的内容则是对一些操作的解释。​​commit​​​列表中列出了从旧到新的每次​​commit​​​,然后我们可以使用说明里的命令选择对这些​​commit​​​执行怎样的一个操作,当我们操作完了后,可以保存文件然后退出编辑器。这之后,​​rebase​​​就会基于所选择的命令进行处理。
在​​​rebase​​​过程中,可能会有合并冲突(比如你和​​upstream/master​​里都修改了同个文件的同一部分),那么这时候,需要手动解决冲突,步骤如下:

  • 通过​​git status​​查看哪些文件发生了冲突
  • 手动解决冲突
  • 运行​​git add​​来暂存文件
  • 运行​​git rebase --continue​​来继续合并过程(不需要用​​git commit​​来解决合并冲突)

​git​​​会提示我们输入​​commit message​​​来作为这次压缩后的​​commit message​​​,所以我们通常需要让这条​​message​​​能够概括多次​​commit​​的内容

7、推送分支

为了创建一个​​Pull Request​​​,我们需要将分支推送到​​origin​​仓库,可以运行:


$ git push --set-upstream origin search


如果你已经推送过你的分支了,并且想要更新它,那么以上的命令会执行失败。这是因此​​rebase​​​改写了​​commit​​​历史,所以不再有一个公共的​​commit​​​,因此需要使用​​--force​​​选项(-f简写)来告诉​​git​​放弃并覆盖远程分支,即:


$ git push origin HEAD:search -f
Enumerating objects: 92, done.
Counting objects: 100% (92/92), done.
Delta compression using up to 8 threads
Compressing objects: 100% (59/59), done.
Writing objects: 100% (74/74), 12.02 KiB | 947.00 KiB/s, done.
Total 74 (delta 58), reused 25 (delta 15), pack-reused 0
remote:
remote: To create a merge request for search, visit:
remote: https://git.phpmianshi.com.com/yang/app/merge_requests/new?merge_request%5Bsource_branch%5D=search
remote:
To git.phpmianshi.com:yang/app.git
* [new branch] HEAD -> search


Tips: 以上也是为什么我们要在​​fork​​分支上独立写代码的主要原因

8、发起一个Pull Request

到这里,是时候对上游仓库的​​master​​​分支发起一个来自​​origin​​​仓库​​search​​​分支的​​PR​​了,可以直接复制上一步的输出结果中的url:


​https://git.phpmianshi.com.com/yang/app/merge_requests/new?merge_request%5Bsource_branch%5D=search​


 

就可以直接打开gitlab进行merge了,这时候点击​​“Compare && pull request”​​​便可进入发起​​PR​​的流程

 

进入​​PR​​​创建页面后,我们可以填写对这个​​PR​​​的描述信息,然后点击​​Create pull request​​​便可成功发起​​PR​

 

我们可以通过填写​​Reviewers​​​邀请同事进行​​Code Review​​​(与此同时,还可以通过​​CI​​​处理一些流程,比如校验代码格式、判断是否已经​​Review​​通过,必须完成这项步骤才能合并代码)

在​​CI​​​(如果有)通过和​​Code Review​​​通过后,我们便可以点击​​Squash and merge​​​或者​​Rebase and merge​​​来合并代码到​​master​​​(这里我们推荐​​Squash and merge​​​,可以压缩多次​​commit​​为一次):

此后,维护者接受PR,代码就会被自动合并到​​master​​​分支里,并且关闭该​​PR​​​。然后,我们也可以做一些后期清理工作:删除本地分支和远程分支(如​​search​​​和​​origin/search​​),可以执行如下命令:


$ git branch -D search $ git push origin --delete search


 

到这里基本就结束了,欢迎留言沟通其他的git workflow 。谢谢