12. 常见工作流比较
多种多样的工作流使得在项目中实施 Git 时变得难以选择。这份教程提供了一个出发点,调查企业团队最常见的 Git 工作流。
阅读的时候,请记住工作流应该是一种规范而不是金科玉律。我们希望向你展示所有工作流,让你融会贯通,因地制宜。
这份教程讨论了下面四种工作流:
中心化的工作流
过渡到分布式分版本控制系统看起来是个令人恐惧的任务,但你不必为了利用 Git 的优点而改变你现有的工作流。你的团队仍然可以用以前SVN的方式开发项目。
然而,使用 Git 来驱动你的开发工作流显示出了一些SVN没有的优点。首先,它让每个开发者都有了自己 本地 的完整项目副本。隔离的环境使得每个开发者的工作独立于项目的其它修改——他们可以在自己的本地仓库中添加提交,完全无视上游的开发,直到需要的时候。
第二,它让你接触到了 Git 鲁棒的分支和合并模型。和 SVN 不同,Git 分支被设计为一种故障安全的机制,用来在仓库之间整合代码和共享更改。
如何工作
和 Subversion 一样,中心化的工作流将中央仓库作为项目中所有修改的唯一入口。和 trunk
不同,默认的开发分支叫做master
,所有更改都被提交到这个分支。这种工作流不需要 master
之外的其它分支。
开发者将中央仓库克隆到本地后开始工作。在他们的本地项目副本中,他们可以像SVN一样修改文件和提交更改;不过,这些新的提交被保存在 本地 ——它们和中央仓库完全隔离。这使得开发者可以将和上游的同步推迟到他们方便的时候。
为了向官方项目发布修改,开发者将他们的本地 master
分支「推送」到中央仓库。这一步等同于 svn commit
,除了Git添加的是所有不在中央 master
分支上的本地提交。
管理冲突
中央仓库代表官方项目,因此它的提交历史应该被视作神圣不可更改的。如果开发者的本地提交和中央仓库分叉了,Git 会拒绝将他们的修改推送上去,因为这会覆盖官方提交。
在开发者发布他们的功能之前,他们需要 fetch 更新的中央提交,在它们之上 rebase 自己的更改。这就像是「我想要在其他人的工作进展之上添加我的修改」,它会产生完美的线性历史,就像和传统的 SVN 工作流一样。
如果本地修改和上游提交冲突时,Git 会暂停 rebase 流程,给你机会手动解决这些冲突。Git 很赞的一点是,它将 git status
和 git add
命令同时用来生成提交和解决合并冲突。这使得开发者能够轻而易举地管理他们的合并。另外,如果他们改错了什么,Git 让他们轻易地退出 rebase 过程,然后重试(或者找人帮忙)。
栗子
让我们一步步观察一个普通的小团队是如何使用这种工作流协作的。我们有两位开发者,John 和 Mary,分别在开发两个功能,他们通过中心化的仓库共享代码。
一人初始化了中央仓库
首先,需要有人在服务器上创建中央仓库。如果这是一个新项目,你可以初始化一个空的仓库。不然,你需要导入一个已经存在的 Git 或 SVN 项目。
中央仓库应该永远是裸仓库(没有工作目录),可以这样创建:
ssh user@host git init --bare /path/to/repo.git
但确保你使用的 SSH 用户名 user
、服务器 host
的域名或 IP 地址、储存仓库的地址 /path/to/repo.git
是有效的。注意 .git
约定俗成地出现在仓库名的后面,表明这是一个裸仓库。
所有人将仓库克隆到本地
接下来,每个开发者在本地创建一份完整项目的副本。使用 git clone
命令:
git clone ssh://user@host/path/to/repo.git
当你克隆仓库时,Git 自动添加了一个名为 origin
的远程连接,指向「父」仓库,以便你以后和这个仓库交换数据。
John 在开发他的功能
在他的本地仓库中,John 可以用标准的 Git 提交流程开发功能:编辑、缓存、提交。如果你对缓存区还不熟悉,你也可以不用记录工作目录中每次的变化。于是你创建了一个高度集中的提交,即使你已经在本地做了很多修改。
git status # 查看仓库状态
git add <some-file> # 缓存一个文件
git commit # 提交一个文件</some-file>
记住,这些命令创建的是本地提交,John 可以周而复始地重复这个过程,而不用考虑中央仓库。对于庞大的功能,需要切成更简单、原子化的片段时,这个特性就很有用。
Mary 在开发她的功能
同时,Mary 在她自己的本地仓库用相同的编辑/缓存/提交流程开发她的功能。和 John 一样,她不需要关心中央仓库的进展,她也 完全 不关心 John 在他自己仓库中做的事,因为所有本地仓库都是私有的。
John 发布了他的功能
一旦 John 完成了他的功能,他应该将本地提交发布到中央仓库,这样其他项目成员就可以访问了。他可以使用git push
命令,就像:
git push origin master
记住,origin
是 John 克隆中央仓库时指向它的远程连接。master
参数告诉 Git 试着将 origin
的 master
分支变得和他本地的 master
分支一样。中央仓库在 John 克隆之后还没有进展,因此这个推送如他所愿,没有产生冲突。
Mary as试图发布她的功能
John 已经成功地将他的更改发布到了中央仓库上,看看当 Mary 试着将她的功能推送到上面时会发生什么。她可以使用同一个推送命令:
git push origin master
但是,她的本地历史和中央仓库已经分叉了,Git 会拒绝这个请求,并显示一段冗长的错误信息:
error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git 防止 Mary 覆盖官方的修改。她需要将 John 的更新拉取到她的仓库,和她的本地修改整合后,然后重试。
Mary在John的提交之上rebase
Mary 可以使用 git pull
来将上游修改并入她的仓库。这个命令和 svn update
很像——它拉取整个上游提交历史到Mary的本地仓库,并和她的本地提交一起整合:
git pull --rebase origin master
--rebase
选项告诉 Git,在同步了中央仓库的修改之后,将 Mary 所有的提交移到 master
分支的顶端,如下图所示:
如果你忽略这个选项拉取同样会成功,只不过你每次和中央仓库同步时都会多出一个「合并提交」。在这种工作流中,rebase 和生成一个合并提交相比,总是一个更好的选择。
Mary 解决了合并冲突
Rebase 的工作是将每个本地提交一个个转移到更新后的 master
分支。也就是说,你可以一个个提交分别解决合并冲突,而不是在一个庞大的合并提交中解决。它会让你的每个提交保持专注,并获得一个干净的项目历史。另一方面,你更容易发现bug是在哪引入的,如果有必要的话,用最小的代价回滚这些修改。
如果 Mary 和 John 开发的功能没有关联,rebase的过程不太可能出现冲突。但如果出现冲突时,Git 在当前提交会暂停 rebase,输出下面的信息,和一些相关的指令:
CONFLICT (content): Merge conflict in <some-file>
Git 的优点在于 每个人 都能解决他们自己的合并冲突。在这个例子中,Mary 只需运行一下 git status
就可以发现问题是什么。冲突的文件会出现在未合并路径中:
# Unmerged paths:
# (use "git reset HEAD <some-file>..." to unstage)
# (use "git add/rm <some-file>..." as appropriate to mark resolution)
#
# both modified: <some-file>
接下来,修改这些文件。如果她对结果满意了,和往常一样缓存这些文件,然后让 git rebase
完成接下来的工作:
git add <some-file>
git rebase --continue
就是这样。Git 会继续检查下个提交,对冲突的提交重复这个流程。
如果你这时候发现不知道自己做了什么,不要惊慌。只要运行下面的命令,你就会回到开始之前的状态:
git rebase --abort
Mary 成功发布了她的分支
在她和中央仓库同步之后,Mary 可以成功地发布她的修改:
git push origin master
接下来该怎么做
正如你所见,使用一丢丢 Git 命令来复制一套传统的 Subversion 开发环境也是可行的。这对于从 SVN 转变而来的团队来说很棒,但这样没有利用到 Git 分布式的本质。
如果你的团队已经习惯了中心化的工作流,但希望提高协作效率,那么探索 Feature 分支工作流 的好处是完全值当的。每个功能在专门的独立分支上进行,在代码并入官方项目之前就可以启动围绕新修改的深度讨论。
Feature 分支的工作流
一旦你掌握了 中心化工作流 的使用方法,在你的开发流程中添加功能分支是一个简单的方式,来促进协作和开发者之间的交流。这种封装使得多个开发者专注自己的功能而不会打扰主代码库。它还保证 master
分支永远不会包含损坏的代码,给持续集成环境带来了是很大的好处。
封装功能的开发使得 Pull Request 的使用成为可能,用来启动围绕一个分支的讨论。它给了其他开发者在功能并入主项目之前参与决策的机会。或者,如果你开发功能时卡在一半,你可以发起一个 Pull Request,向同事寻求建议。重点是,Pull Request 使得你的团队在评论其他人的工作时变得非常简单。
如何工作
Feature 分支工作流同样使用中央仓库,master
同样代表官方的项目历史。但是,与其直接提交在本地的 master
分支,开发者每次进行新的工作时创建一个新的分支。Feature 分支应该包含描述性的名称,比如 animated-menu-items
(菜单项动画)或 issue-#1061
。每个分支都应该有一个清晰、高度集中的目的。
Git 在技术上无法区别 master
和功能分支,所以开发者可以在 feature 分支上编辑、缓存、提交,就和中心化工作流中一样。
此外,feature 分支可以(也应该)被推送到中央仓库。这使得你和其他开发者共享这个功能,而又不改变官方代码。既然 master
只是一个“特殊”的分支,在中央仓库中储存多个 feature 分支不会引出什么问题。当然了,这也是备份每个开发者本地提交的好办法。
Pull Request
除了隔离功能开发之外,分支使得通过 Pull Request 讨论修改成为可能。一旦有人完成了一个功能,他们不会立即将它并入master
。他们将 feature 分支推送到中央服务器上,发布一个 Pull Request,请求将他们的修改并入 master
。这给了其他开发者在修改并入主代码库之前审查的机会。
代码审查是 Pull Request 的主要好处,但他们事实上被设计为成为讨论代码的一般场所。你可以把 Pull Request 看作是专注某个分支的讨论版。也就是说他们可以用于开发流程之前。比如,一个开发者在某个功能上需要帮助,他只需发起一个 Pull Request。感兴趣的小伙伴会自动收到通知,看到相关提交中的问题。
一旦 Pull Request 被接受了,发布功能的行为和中心化的工作流是一样的。首先,确定你本地的 master
和上游的 master
已经同步。然后,将 feature分支并入 master
,将更新的 master
推送回中央仓库。
栗子
下面这