常见工作流比较

BY 童仲毅(geeeeeeeeek@github

这是一篇在原文(BY atlassian)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名(CC BY 2.5 AU)协议共享。

多种多样的工作流使得在项目中实施 Git 时变得难以选择。这份教程提供了一个出发点,调查企业团队最常见的 Git 工作流。

阅读的时候,请记住工作流应该是一种规范而不是金科玉律。我们希望向你展示所有工作流,让你融会贯通,因地制宜。

这份教程讨论了下面四种工作流:

中心化的工作流

Git Workflows: SVN-style Workflow

过渡到分布式分版本控制系统看起来是个令人恐惧的任务,但你不必为了利用 Git 的优点而改变你现有的工作流。你的团队仍然可以用以前SVN的方式开发项目。

然而,使用 Git 来驱动你的开发工作流显示出了一些SVN没有的优点。首先,它让每个开发者都有了自己 本地 的完整项目副本。隔离的环境使得每个开发者的工作独立于项目的其它修改——他们可以在自己的本地仓库中添加提交,完全无视上游的开发,直到需要的时候。

第二,它让你接触到了 Git 鲁棒的分支和合并模型。和 SVN 不同,Git 分支被设计为一种故障安全的机制,用来在仓库之间整合代码和共享更改。

如何工作

和 Subversion 一样,中心化的工作流将中央仓库作为项目中所有修改的唯一入口。和 trunk 不同,默认的开发分支叫做master,所有更改都被提交到这个分支。这种工作流不需要 master 之外的其它分支。

开发者将中央仓库克隆到本地后开始工作。在他们的本地项目副本中,他们可以像SVN一样修改文件和提交更改;不过,这些新的提交被保存在 本地 ——它们和中央仓库完全隔离。这使得开发者可以将和上游的同步推迟到他们方便的时候。

为了向官方项目发布修改,开发者将他们的本地 master 分支「推送」到中央仓库。这一步等同于 svn commit,除了Git添加的是所有不在中央 master 分支上的本地提交。

Central and local repositories.svg)

管理冲突

中央仓库代表官方项目,因此它的提交历史应该被视作神圣不可更改的。如果开发者的本地提交和中央仓库分叉了,Git 会拒绝将他们的修改推送上去,因为这会覆盖官方提交。

Managing Conflicts.svg)

在开发者发布他们的功能之前,他们需要 fetch 更新的中央提交,在它们之上 rebase 自己的更改。这就像是「我想要在其他人的工作进展之上添加我的修改」,它会产生完美的线性历史,就像和传统的 SVN 工作流一样。

如果本地修改和上游提交冲突时,Git 会暂停 rebase 流程,给你机会手动解决这些冲突。Git 很赞的一点是,它将 git statusgit add 命令同时用来生成提交和解决合并冲突。这使得开发者能够轻而易举地管理他们的合并。另外,如果他们改错了什么,Git 让他们轻易地退出 rebase 过程,然后重试(或者找人帮忙)。

栗子

让我们一步步观察一个普通的小团队是如何使用这种工作流协作的。我们有两位开发者,John 和 Mary,分别在开发两个功能,他们通过中心化的仓库共享代码。

一人初始化了中央仓库

Git Workflows: Initialize Central Bare Repository

首先,需要有人在服务器上创建中央仓库。如果这是一个新项目,你可以初始化一个空的仓库。不然,你需要导入一个已经存在的 Git 或 SVN 项目。

中央仓库应该永远是裸仓库(没有工作目录),可以这样创建:

  1. ssh user@host git init --bare /path/to/repo.git

但确保你使用的 SSH 用户名 user、服务器 host 的域名或 IP 地址、储存仓库的地址 /path/to/repo.git 是有效的。注意 .git 约定俗成地出现在仓库名的后面,表明这是一个裸仓库。

所有人将仓库克隆到本地

Git Workflows: Clone Central Repo

接下来,每个开发者在本地创建一份完整项目的副本。使用 git clone 命令:

  1. git clone ssh://user@host/path/to/repo.git

当你克隆仓库时,Git 自动添加了一个名为 origin 的远程连接,指向「父」仓库,以便你以后和这个仓库交换数据。

John 在开发他的功能

Git Workflows: Edit Stage Commit Feature Process .svg)

在他的本地仓库中,John 可以用标准的 Git 提交流程开发功能:编辑、缓存、提交。如果你对缓存区还不熟悉,你也可以不用记录工作目录中每次的变化。于是你创建了一个高度集中的提交,即使你已经在本地做了很多修改。

  1. git status # 查看仓库状态
  2. git add <some-file> # 缓存一个文件
  3. git commit # 提交一个文件</some-file>

记住,这些命令创建的是本地提交,John 可以周而复始地重复这个过程,而不用考虑中央仓库。对于庞大的功能,需要切成更简单、原子化的片段时,这个特性就很有用。

Mary 在开发她的功能

Git Workflows: Edit Stage Commit Feature

同时,Mary 在她自己的本地仓库用相同的编辑/缓存/提交流程开发她的功能。和 John 一样,她不需要关心中央仓库的进展,她也 完全 不关心 John 在他自己仓库中做的事,因为所有本地仓库都是私有的。

John 发布了他的功能

Git Workflows: Publish Feature

一旦 John 完成了他的功能,他应该将本地提交发布到中央仓库,这样其他项目成员就可以访问了。他可以使用git push命令,就像:

  1. git push origin master

记住,origin 是 John 克隆中央仓库时指向它的远程连接。master 参数告诉 Git 试着将 originmaster 分支变得和他本地的 master 分支一样。中央仓库在 John 克隆之后还没有进展,因此这个推送如他所愿,没有产生冲突。

Mary as试图发布她的功能

Git Workflows: Push Command Error

John 已经成功地将他的更改发布到了中央仓库上,看看当 Mary 试着将她的功能推送到上面时会发生什么。她可以使用同一个推送命令:

  1. git push origin master

但是,她的本地历史和中央仓库已经分叉了,Git 会拒绝这个请求,并显示一段冗长的错误信息:

  1. error: failed to push some refs to '/path/to/repo.git'
  2. hint: Updates were rejected because the tip of your current branch is behind
  3. hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
  4. hint: before pushing again.
  5. hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Git 防止 Mary 覆盖官方的修改。她需要将 John 的更新拉取到她的仓库,和她的本地修改整合后,然后重试。

Mary在John的提交之上rebase

Git Workflows: Git Pull Rebase

Mary 可以使用 git pull 来将上游修改并入她的仓库。这个命令和 svn update 很像——它拉取整个上游提交历史到Mary的本地仓库,并和她的本地提交一起整合:

  1. git pull --rebase origin master

--rebase 选项告诉 Git,在同步了中央仓库的修改之后,将 Mary 所有的提交移到 master 分支的顶端,如下图所示:

Rebasing to Master

如果你忽略这个选项拉取同样会成功,只不过你每次和中央仓库同步时都会多出一个「合并提交」。在这种工作流中,rebase 和生成一个合并提交相比,总是一个更好的选择。

Mary 解决了合并冲突

Git Workflows: Rebasing on Commits

Rebase 的工作是将每个本地提交一个个转移到更新后的 master 分支。也就是说,你可以一个个提交分别解决合并冲突,而不是在一个庞大的合并提交中解决。它会让你的每个提交保持专注,并获得一个干净的项目历史。另一方面,你更容易发现bug是在哪引入的,如果有必要的话,用最小的代价回滚这些修改。

如果 Mary 和 John 开发的功能没有关联,rebase的过程不太可能出现冲突。但如果出现冲突时,Git 在当前提交会暂停 rebase,输出下面的信息,和一些相关的指令:

  1. CONFLICT (content): Merge conflict in <some-file>

Conflict Resolution

Git 的优点在于 每个人 都能解决他们自己的合并冲突。在这个例子中,Mary 只需运行一下 git status 就可以发现问题是什么。冲突的文件会出现在未合并路径中:

  1. # Unmerged paths:
  2. # (use "git reset HEAD <some-file>..." to unstage)
  3. # (use "git add/rm <some-file>..." as appropriate to mark resolution)
  4. #
  5. # both modified: <some-file>

接下来,修改这些文件。如果她对结果满意了,和往常一样缓存这些文件,然后让 git rebase 完成接下来的工作:

  1. git add <some-file>
  2. git rebase --continue

就是这样。Git 会继续检查下个提交,对冲突的提交重复这个流程。

如果你这时候发现不知道自己做了什么,不要惊慌。只要运行下面的命令,你就会回到开始之前的状态:

  1. git rebase --abort

Mary 成功发布了她的分支

Git Workflows: Synchronize Central Repo

在她和中央仓库同步之后,Mary 可以成功地发布她的修改:

  1. git push origin master

接下来该怎么做

正如你所见,使用一丢丢 Git 命令来复制一套传统的 Subversion 开发环境也是可行的。这对于从 SVN 转变而来的团队来说很棒,但这样没有利用到 Git 分布式的本质。

如果你的团队已经习惯了中心化的工作流,但希望提高协作效率,那么探索 Feature 分支工作流 的好处是完全值当的。每个功能在专门的独立分支上进行,在代码并入官方项目之前就可以启动围绕新修改的深度讨论。

Feature 分支的工作流

Feature Branch Workflow

一旦你掌握了 中心化工作流 的使用方法,在你的开发流程中添加功能分支是一个简单的方式,来促进协作和开发者之间的交流。这种封装使得多个开发者专注自己的功能而不会打扰主代码库。它还保证 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 推送回中央仓库。

栗子

下面这🌰演示了代码审查使用到的 Pull Request,但记住 Pull Request 有多种用途。

Mary 开始了一个新功能

New Feature Branch

在她开始开发一个功能之前,Mary 需要一个独立的分支。她可以用下面的命令创建新分支

  1. git checkout -b marys-feature master

一个基于 master、名为 marys-feature 的分支将会被checkout,-b 标记告诉Git在分支不存在时创建它。在这个分支上,Mary和往常一样编辑、缓存、提交更改,用足够多的提交来构建这个功能:

  1. git status
  2. git add <some-file>
  3. git commit

Mary 去吃饭了

Git Workflows: Feature Commits.svg)

Mary 在早上给她的功能添加了一些提交。在她去吃午饭前,将她的分支推送到中央仓库是个不错的想法。这是一种方便的备份,但如果Mary和其他开发者一起协作,他们也可以看到她的初始提交了。

  1. git push -u origin marys-feature

这个命令将 marys-feature 推送到中央仓库(origin),-u 标记将它添加为远程跟踪的分支。在设置完跟踪的分支之后,Mary 调用不带任何参数的 git push 来推送她的功能。

Mary 完成了她的工作

Git Workflows: Pull Request

当 Mary 吃完午饭回来,她完成了她的功能。在并入 master 之前,她需要发布一个 Pull Request,让其他的团队成员知道她所做的工作。但首先,她应该保证中央仓库包含了她最新的提交:

  1. git push

然后,她在她的 Git 界面上发起了一个 Pull Request,请求将 marys-feature 合并进 master,团队成员会收到自动的通知。Pull Request 的好处是,评论显示在相关的提交正下方,方便讨论特定的修改。

Bill 收到了 Pull Request

Git Workflows: Feature Pull Requests.svg)

Bill 收到了 Pull Request,并且查看了 marys-feature。他决定在并入官方项目之前做一些小修改,通过 Pull Request 和 Mary 进行了沟通。

Mary 作了修改

Git Workflows: Central Repository Push.svg)

为了做这些更改,Mary 重复了之前创建功能时相同的流程,她编辑、缓存、提交、将更新推送到中央仓库。她所有的活动显示在 Pull Request 中,Bill 可以一直评论。

如果 Bill 想要的话,也可以将 marys-feature 分支 pull 到他自己的本地仓库,继续工作。后续的任何提交都会显示在 Pull Request 上。

Mary 发布了她的功能

Merging a Feature Branch

一旦 Bill 准备接受这个 Pull Request,某个人(Bill 或者 Mary 都可)需要将功能并入稳定的项目:

  1. git checkout master
  2. git pull
  3. git pull origin marys-feature
  4. git push

首先,不管是谁在执行合并,都要保证他们的 master 分支是最新的。然后,运行 git pull origin marys-feature 合并中央仓库的 marys-feature 副本。你也可以使用简单的 git merge marys-feature,但之前的命令保证你拉取下来的一定是功能分支最新的版本。最后,更新的 master 需要被推送回 origin

这个过程导致了一个合并提交。一些开发者喜欢它,因为它是功能和其余代码合并的标志。但,如果你希望得到线性的历史,你可以在执行 merge 之前将功能 rebase 到 master 分支的顶端,产生一个快速向前的合并。

一些界面会自动化接受 Pull Request 的流程,只需点击一下「Merge Pull Request」。如果你的没有的话,它至少在合并之后应该可以自动地关闭 Pull Request。

同时,John 以同样的方式工作着

Mary 和 Bill 一起开发 marys-feature,在 Pull Request 上讨论的同时,John 还在开发他自己的 feature分支。通过将功能用不同分支隔离开来,每个人可以独立地工作,但很容易和其他开发者共享修改。

接下来该怎么做

为了彻底了解 GitHub 上的功能分支,你应该查看使用分支一章。现在,你应该已经看到了功能分支极大地增强了中心化工作流中单一 master 分支的作用。除此之外,功能分支还便利了 Pull Request 的使用,在版本控制界面上直接讨论特定的提交。GitFlow 工作流是管理功能开发、发布准备、维护的常见模式。

GitFlow 工作流

Gitflow Workflow

下面的 GitFlow 工作流一节源于 nvie 网站上的作者 Vincent Driessen。

GitFlow 工作流围绕项目发布定义了一个严格的分支模型。有些地方比功能分支工作流更复杂,为管理大型项目提供了鲁棒的框架。

和功能分支工作流相比,这种工作流没有增加任何新的概念或命令。它给不同的分支指定了特定的角色,定义它们应该如何、什么时候交流。除了功能分支之外,它还为准备发布、维护发布、记录发布分别使用了单独的分支。当然,你还能享受到功能分支工作流带来的所有好处:Pull Request、隔离实验和更高效的协作。

如何工作

GitFlow 工作流仍然使用中央仓库作为开发者沟通的中心。和其他工作流一样,开发者在本地工作,将分支推送到中央仓库。唯一的区别在于项目的分支结构。

历史分支

和单独的 master 分支不同,这种工作流使用两个分支来记录项目历史。master 分支储存官方发布历史,develop 分支用来整合功能分支。同时,这还方便了在 master 分支上给所有提交打上版本号标签。

Historical Branches.svg)

工作流剩下的部分围绕这两个分支的差别展开。

功能分支

每个新功能都放置在自己的分支中,可以在备份/协作时推送到中央仓库。但是,与其合并到 master,功能分支将开发分支作为父分支。当一个功能完成时,它将被合并回 develop。功能永远不应该直接在 master 上交互。

Feature Branches.svg)

注意,功能分支加上 develop 分支就是我们之前所说的功能分支工作流。但是,GitFlow 工作流不止于此。

发布分支

Release Branches.svg)

一旦 develop分支的新功能足够发布(或者预先确定的发布日期即将到来),你可以从 develop 分支 fork 一个发布分支。这个分支的创建开始了下个发布周期,只有和发布相关的任务应该在这个分支进行,如修复 bug、生成文档等。一旦准备好了发布,发布分支将合并进 master,打上版本号的标签。另外,它也应该合并回 develop,后者可能在发布启动之后有了新的进展。

使用一个专门的分支来准备发布确保一个团队完善当前的发布,其他团队可以继续开发下一个发布的功能。它还建立了清晰的开发阶段(比如说,「这周我们准备 4.0 版本的发布」,而我们在仓库的结构中也能看到这个阶段)。

通常我们约定:

  • develop 创建分支
  • 合并进 master 分支
  • 命名规范 release-* or release/*

维护分支

Maintenance Branches.svg)

维护或者「紧急修复」分支用来快速给产品的发布打上补丁。这是唯一可以从 master 上 fork 的分支。一旦修复完成了,它应该被并入 masterdevelop 分支(或者当前的发布分支),master 应该打上更新的版本号的标签。

有一个专门的 bug 修复开发线使得你的团队能够处理 issues,而不打断其他工作流或是要等到下一个发布周期。你可以将维护分支看作在 master 分支上工作的临时发布分支。

栗子

下面的栗子演示了这种工作流如何用来管理发布周期。假设你已经创建了中央仓库。

创建一个开发分支

Create a Develop Branch

你要做的第一步是为默认的 master 分支创建一个互补的 develop 分支。最简单的办法是在本地创建一个空的 develop 分支,将它推送到服务器上:

  1. git branch develop
  2. git push -u origin develop

这个分支将会包含项目中所有的历史,而 master 将包含不完全的版本。其他开发者应该将中央仓库克隆到本地,创建一个分支来追踪 develop 分支:

  1. git clone ssh://user@host/path/to/repo.git
  2. git checkout -b develop origin/develop

现在所有人都有了一份历史分支的本地副本。

Mary 和 John 开始了新功能

New Feature Branches

我们的栗子从 John 和 Mary 在不同分支上工作开始。他们都要为自己的功能创建单独的分支。他们的功能分支都应该基于develop,而不是 master

  1. git checkout -b some-feature develop

他们都使用「编辑、缓存、提交」的一般约定来向功能分支添加提交:

  1. git status
  2. git add <some-file>
  3. git commit

Mary 完成了她的功能

Merging a Feature Branch

在添加了一些提交之后,Mary 确信她的功能以及准备好了。如果她的团队使用 Pull Request,现在正是发起 Pull Request 的好时候,请求将她的功能并入 develop 分支。否则,她可以向下面一样,将它并入本地的 develop 分支,推送到中央仓库:

  1. git pull origin develop
  2. git checkout develop
  3. git merge some-feature
  4. git push
  5. git branch -d some-feature

第一个命令在尝试并入功能分支之前确保 develop 分支已是最新。注意,功能绝不该被直接并入 master。冲突的处理方式和中心化工作流相同。

Mary 开始准备发布

Preparing a Release

当 John 仍然在他的功能分支上工作时,Mary s开始准备项目的第一个官方发布。和开发功能一样,她新建了一个分支来封装发布的准备工作。这也正是发布的版本号创建的一步:

  1. git checkout -b release-0.1 develop

这个分支用来整理提交,充分测试,更新文档,为即将到来的发布做各种准备。它就像是一个专门用来完善发布的功能分支。

一旦 Mary 创建了这个分支,推送到中央仓库,这次发布的功能便被锁定了。不在 develop 分支中的功能将被推迟到下个发布周期。

Mary完成了她的发布

Merging Release into Master

一旦发布准备稳妥,Mary 将它并入 masterdevelop,然后删除发布分支。合并回 develop 很重要,因为可能已经有关键的更新添加到了发布分支上,而开发新功能需要用到它们。同样的,如果 Mary 的团队重视代码审查,现在将是发起 Pull Request 的完美时机。

  1. git checkout master
  2. git merge release-0.1
  3. git push
  4. git checkout develop
  5. git merge release-0.1
  6. git push
  7. git branch -d release-0.1

发布分支是功能开发(develop)和公开发布(master)之间的过渡阶段。不论什么时候将提交并入 master 时,你应该为提交打上方便引用的标签:

  1. git tag -a 0.1 -m "Initial public release" master
  2. git push --tags

Git 提供了许多钩子,即仓库中特定事件发生时被执行的脚本。当你向中央仓库推送 master 分支或者标签时,你可以配置一个钩子来自动化构建公开发布。

终端用户发现了一个 bug

Maintenance Branch

正式发布之后,Mary 回过头来和 John 一起为下一个发布开发功能。这时,一个终端用户开了一个 issue 抱怨说当前发布中存在一个 bug。为了解决这个 bug,Mary(或 John)从 master 创建了一个维护分支,用几个提交修复这个 issue,然后直接合并回 master

  1. git checkout -b issue-#001 master
  2. # Fix the bug
  3. git checkout master
  4. git merge issue-#001
  5. git push

和发布分支一样,维护分支包含了 develop 中需要的重要更新,因此 Mary 同样需要执行这个合并。接下来,她可以删除这个分支了:

  1. git checkout develop
  2. git merge issue-#001
  3. git push
  4. git branch -d issue-#001

接下来该怎么做

现在,希望你已经很熟悉中心化的工作流功能分支工作流和 GitFlow 工作流。你应该已经可以抓住本地仓库、推送/拉取模式,和 Git 鲁棒的分支和合并模型的无限潜力。

请记住,教程中呈现的工作流只是可行的实践——而非工作中使用 Git 的金科玉律。因此,尽情地取其精华,去其糟粕吧。不变的是要让 Git 为你所用,而不是相反。

Fork 工作流

Fork 工作流和教程中讨论的其它工作流截然不同。与其使用唯一的服务端仓库作为「中央」代码库,它给予 每个 开发者一个服务端仓库。也就是说每个贡献者都有两个 Git 仓库,而不是一个:一个私有的本地仓库和一个公开的服务端仓库。

Git Workflows: Forking

Fork 工作流的主要优点在于贡献可以轻易地整合进项目,而不需要每个人都推送到单一的中央仓库。开发者推送到他们 自己的 服务端仓库,只有项目管理者可以推送到官方仓库。这使得管理者可以接受任何开发者的提交,却不需要给他们中央仓库的权限。

结论是,这种分布式的工作流为大型、组织性强的团队(包括不可信的第三方)提供了安全的协作方式。它同时也是开源项目理想的工作流。

如何工作

和其它 Git 工作流一样,Fork 工作流以一个储存在服务端的官方公开项目开场。但新的开发者想参与项目时,他们不直接克隆官方项目。

取而代之地,他们 fork 一份官方项目,在服务端创建一份副本。这份新建的副本作为他们私有的公开仓库——没有其他开发者可以在上面推送,但他们可以从上面拉取修改(在后面我们会讨论为什么这一点很重要)。在他们创建了服务端副本之后,开发者执行 git clone 操作,在他们的本地机器上复制一份。这是他们私有的开发环境,正如其他工作流中一样。

当他们准备好发布本地提交时,他们将提交推送到自己的公开仓库——而非官方仓库。然后,他们向主仓库发起一个 Pull Request,让项目维护者知道一个更新做好了合并的准备。如果贡献的代码有什么问题的话,Pull Request 可以作为一个方便的讨论版。

我为了将功能并入官方代码库,维护者将贡献者的修改拉取到他们的本地仓库,确保修改不会破坏项目,将它合并到本地的 master 分支,然后将 master 分支推送到服务端的官方仓库。贡献现在已是项目的一部分,其他开发者应该从官方仓库拉取并同步他们的本地仓库。

中央仓库

「官方」仓库这个概念在 Fork 工作流中只是一个约定,理解这一点很重要。从技术的角度,Git 并看不出每个开发者和官方的公开仓库有什么区别。事实上,官方仓库唯一官方的原因是,它是项目维护者的仓库。

Fork 工作流中的分支

所有这些个人的公开仓库只是一个在开发者之间共享分支的约定。每个人仍然可以使用分支来隔离功能,就像在功能分支工作流]($sources-)和 GitFlow 工作流中一样。唯一的区别在于这些分支是如何开始的。在 Fork 工作流中,它们从另一个开发者的本地仓库拉取而来,而在功能分支和 GitFlow 分支它们被推送到官方仓库。

栗子

项目维护者初始化了中央仓库

Forking Workflow: Shared Repository

和任何基于 Git 的项目一样,第一步是在服务端创建一个可以被所有项目成员访问到的官方仓库。一般来说,这个仓库同时还是项目维护者的公开仓库。

公开的仓库应该永远是裸的,不管它们是否代表官方代码库。所以项目维护者应该运行下面这样的命令来设置官方仓库:

  1. ssh user@host
  2. git init --bare /path/to/repo.git

GitHub 同时提供了一个图形化界面来替代上面的操作。这和教程中其它工作流设置中央仓库的流程完全一致。如果有必要的话,项目维护者应该将已有的代码库推送到这个仓库中。

开发者 fork 仓库

Forking Workflow: Forking the official repository.

接下来,所有开发者需要 fork 官方仓库。你可以用 SSH 到服务器,运行 git clone 将它复制到服务器的另一个地址—— fork 其实只是服务端的 clone。但同样地,GitHub上开发者只需点一点按钮就可以 fork 仓库。

在这步之后,每个开发者应该都有了自己的服务端仓库。像官方仓库一样,所有这些仓库都应该是裸仓库。

开发者将 fork 的仓库克隆到本地

Forking Workflow: Cloning the forked repositories

接下来开发者需要克隆他们自己的公开仓库。他们可以用熟悉的 git clone 命令来完成这一步。

我们的栗子假设使用他们使用 GitHub 来托管仓库。记住,在这种情况下,每个开发者应该有他们自己的 GitHub 账号,应该用下面的命令克隆服务端仓库:

  1. git clone https://user@github.com/user/repo.git

而教程中的其他工作流使用单一的 origin 远程连接,指向中央仓库,Fork 工作流需要两个远程连接,一个是中央仓库,另一个是开发者个人的服务端仓库。你可以给这些远端取任何名字,约定的做法是将 origin 作为你 fork 后的仓库的远端(运行 git clone 是会自动创建)和 upstream 作为官方项目。

  1. git remote add upstream https://github.com/maintainer/repo

你需要使用上面的命令来创建上游仓库的远程连接。它使得你轻易地保持本地仓库和官方仓库的进展同步。注意如果你的上游仓库开启了认证(比如它没有开源),你需要提供一个用户名,就像这样:

  1. git remote add upstream https://user@bitbucket.org/maintainer/repo.git

它需要用户从官方代码库克隆或拉取之前提供有效的密码。

开发者进行自己的开发

Forking Workflow: Developers work on features

在他们刚克隆的本地仓库中,开发者可以编辑代码、提交更改,和其它分支中一样创建分支

  1. git checkout -b some-feature
  2. # 编辑代码
  3. git commit -a -m "Add first draft of some feature"

他们所有的更改在推送到公开仓库之前都是完全私有的。而且,如果官方项目已经向前进展了,他们可以用 git pull 获取新的提交:

  1. git pull upstream master

因为开发者应该在专门的功能分支开发,这一般会产生一个快速向前的合并

开发者发布他们的功能

Forking Workflow: Developers publish features

一旦开发者准备好共享他们的新功能,他们需要做两件事情。第一,他们必须将贡献的代码推送到自己的公开仓库,让其他开发者能够访问到。他们的 origin 远端应该已经设置好了,所以他们只需要:

  1. git push origin feature-branch

这和其他工作流不同之处在于,origin 远端指向开发者个人的服务端仓库,而不是主代码库。

第二,他们需要通知项目维护者,他们想要将功能并入官方代码库。GitHub 提供了一个「New Pull Request」按钮,跳转到一个网页,让你指明想要并入主仓库的分支。一般来说,你希望将功能分支并入上游远端的 master 分支。

项目维护者整合他们的功能

Forking Workflow: Integrate Features

当项目维护者收到 Pull Request 时,他们的工作是决定是否将它并入官方的代码库。他们可以使用下面两种方式之一:

  1. 直接检查 Pull Request 中检查代码
  2. 将代码拉取到本地仓库然后手动合并

第一个选项更简单,让维护者查看修改前后的差异,在上面评论,然后通过图形界面执行合并。然而,如果 Pull Request 会导致合并冲突,第二个选项就有了必要。在这个情况中,维护者需要从开发者的服务端仓库 fetch 功能分支,合并到他们本地的 master 分支,然后解决冲突:

  1. git fetch https://bitbucket.org/user/repo feature-branch
  2. # 检查修改
  3. git checkout master
  4. git merge FETCH_HEAD

一旦修改被整合进本地的 master,维护者需要将它推送到服务器上的官方仓库,这样其他开发者也可以访问它:

  1. git push origin master

记住,维护者的 origin 指向他们的公开仓库,也就是项目的官方代码库。开发者的贡献现在完全并入了项目。

开发者和中央仓库保持同步

Forking Workflow: Synchronize with the official repository

因为主代码库已经取得了新的进展,其他开发者应该和官方仓库同步:

  1. git pull upstream master

接下来该怎么做

如果你从 SVN 迁移而来,Fork 工作流看上去是一个比较大的转变。但不要害怕——它只是在 Feature 分支工作流之上引入了一层抽象。贡献的代码发布到开发者在服务端自己的仓库,而不是在唯一的中央仓库中直接共享分支。

这篇文章解释了一次代码贡献是如何从一个开发者流入官方的 master 分支的,但相同的方法可以用在将代码贡献整合进任何仓库。比如,如果你团队的一部分成员在一个特定功能上协作,他们可以用自己约定的行为共享修改——而不改变主仓库。

这使得 Fork 工作流对于松散的团队来说是个非常强大的工具。任何开发者都可以轻而易举地和其他开发者共享修改,任何分支都能高效地并入主代码库。

这篇文章是「git-recipes」的一部分,点击 目录 查看所有章节。

如果你觉得文章对你有帮助,欢迎点击右上角的 Star :star2: 或 Fork :fork_and_knife:。

如果你发现了错误,或是想要加入协作,请参阅 Wiki 协作说明