常见工作流比较
BY 童仲毅(geeeeeeeeek@github)
这是一篇在原文(BY atlassian)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名(CC BY 2.5 AU)协议共享。
多种多样的工作流使得在项目中实施 Git 时变得难以选择。这份教程提供了一个出发点,调查企业团队最常见的 Git 工作流。
阅读的时候,请记住工作流应该是一种规范而不是金科玉律。我们希望向你展示所有工作流,让你融会贯通,因地制宜。
这份教程讨论了下面四种工作流:
中心化的工作流
过渡到分布式分版本控制系统看起来是个令人恐惧的任务,但你不必为了利用 Git 的优点而改变你现有的工作流。你的团队仍然可以用以前SVN的方式开发项目。
然而,使用 Git 来驱动你的开发工作流显示出了一些SVN没有的优点。首先,它让每个开发者都有了自己 本地 的完整项目副本。隔离的环境使得每个开发者的工作独立于项目的其它修改——他们可以在自己的本地仓库中添加提交,完全无视上游的开发,直到需要的时候。
第二,它让你接触到了 Git 鲁棒的分支和合并模型。和 SVN 不同,Git 分支被设计为一种故障安全的机制,用来在仓库之间整合代码和共享更改。
如何工作
和 Subversion 一样,中心化的工作流将中央仓库作为项目中所有修改的唯一入口。和 trunk
不同,默认的开发分支叫做master
,所有更改都被提交到这个分支。这种工作流不需要 master
之外的其它分支。
开发者将中央仓库克隆到本地后开始工作。在他们的本地项目副本中,他们可以像SVN一样修改文件和提交更改;不过,这些新的提交被保存在 本地 ——它们和中央仓库完全隔离。这使得开发者可以将和上游的同步推迟到他们方便的时候。
为了向官方项目发布修改,开发者将他们的本地 master
分支「推送」到中央仓库。这一步等同于 svn commit
,除了Git添加的是所有不在中央 master
分支上的本地提交。
.svg)
管理冲突
中央仓库代表官方项目,因此它的提交历史应该被视作神圣不可更改的。如果开发者的本地提交和中央仓库分叉了,Git 会拒绝将他们的修改推送上去,因为这会覆盖官方提交。
.svg)
在开发者发布他们的功能之前,他们需要 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 在开发他的功能
.svg)
在他的本地仓库中,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
推送回中央仓库。
栗子
下面这🌰演示了代码审查使用到的 Pull Request,但记住 Pull Request 有多种用途。
Mary 开始了一个新功能
在她开始开发一个功能之前,Mary 需要一个独立的分支。她可以用下面的命令创建新分支:
git checkout -b marys-feature master
一个基于 master
、名为 marys-feature
的分支将会被checkout,-b
标记告诉Git在分支不存在时创建它。在这个分支上,Mary和往常一样编辑、缓存、提交更改,用足够多的提交来构建这个功能:
git status
git add <some-file>
git commit
Mary 去吃饭了
.svg)
Mary 在早上给她的功能添加了一些提交。在她去吃午饭前,将她的分支推送到中央仓库是个不错的想法。这是一种方便的备份,但如果Mary和其他开发者一起协作,他们也可以看到她的初始提交了。
git push -u origin marys-feature
这个命令将 marys-feature
推送到中央仓库(origin
),-u
标记将它添加为远程跟踪的分支。在设置完跟踪的分支之后,Mary 调用不带任何参数的 git push
来推送她的功能。
Mary 完成了她的工作
当 Mary 吃完午饭回来,她完成了她的功能。在并入 master
之前,她需要发布一个 Pull Request,让其他的团队成员知道她所做的工作。但首先,她应该保证中央仓库包含了她最新的提交:
git push
然后,她在她的 Git 界面上发起了一个 Pull Request,请求将 marys-feature
合并进 master
,团队成员会收到自动的通知。Pull Request 的好处是,评论显示在相关的提交正下方,方便讨论特定的修改。
Bill 收到了 Pull Request
.svg)
Bill 收到了 Pull Request,并且查看了 marys-feature
。他决定在并入官方项目之前做一些小修改,通过 Pull Request 和 Mary 进行了沟通。
Mary 作了修改
.svg)
为了做这些更改,Mary 重复了之前创建功能时相同的流程,她编辑、缓存、提交、将更新推送到中央仓库。她所有的活动显示在 Pull Request 中,Bill 可以一直评论。
如果 Bill 想要的话,也可以将 marys-feature
分支 pull 到他自己的本地仓库,继续工作。后续的任何提交都会显示在 Pull Request 上。
Mary 发布了她的功能
一旦 Bill 准备接受这个 Pull Request,某个人(Bill 或者 Mary 都可)需要将功能并入稳定的项目:
git checkout master
git pull
git pull origin marys-feature
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 工作流一节源于 nvie 网站上的作者 Vincent Driessen。
GitFlow 工作流围绕项目发布定义了一个严格的分支模型。有些地方比功能分支工作流更复杂,为管理大型项目提供了鲁棒的框架。
和功能分支工作流相比,这种工作流没有增加任何新的概念或命令。它给不同的分支指定了特定的角色,定义它们应该如何、什么时候交流。除了功能分支之外,它还为准备发布、维护发布、记录发布分别使用了单独的分支。当然,你还能享受到功能分支工作流带来的所有好处:Pull Request、隔离实验和更高效的协作。
如何工作
GitFlow 工作流仍然使用中央仓库作为开发者沟通的中心。和其他工作流一样,开发者在本地工作,将分支推送到中央仓库。唯一的区别在于项目的分支结构。
历史分支
和单独的 master
分支不同,这种工作流使用两个分支来记录项目历史。master
分支储存官方发布历史,develop
分支用来整合功能分支。同时,这还方便了在 master
分支上给所有提交打上版本号标签。
.svg)
工作流剩下的部分围绕这两个分支的差别展开。
功能分支
每个新功能都放置在自己的分支中,可以在备份/协作时推送到中央仓库。但是,与其合并到 master
,功能分支将开发分支作为父分支。当一个功能完成时,它将被合并回 develop
。功能永远不应该直接在 master
上交互。
.svg)
注意,功能分支加上 develop
分支就是我们之前所说的功能分支工作流。但是,GitFlow 工作流不止于此。
发布分支
.svg)
一旦 develop
分支的新功能足够发布(或者预先确定的发布日期即将到来),你可以从 develop
分支 fork 一个发布分支。这个分支的创建开始了下个发布周期,只有和发布相关的任务应该在这个分支进行,如修复 bug、生成文档等。一旦准备好了发布,发布分支将合并进 master
,打上版本号的标签。另外,它也应该合并回 develop
,后者可能在发布启动之后有了新的进展。
使用一个专门的分支来准备发布确保一个团队完善当前的发布,其他团队可以继续开发下一个发布的功能。它还建立了清晰的开发阶段(比如说,「这周我们准备 4.0 版本的发布」,而我们在仓库的结构中也能看到这个阶段)。
通常我们约定:
- 从
develop
创建分支 - 合并进
master
分支 - 命名规范
release-* or release/*
维护分支
.svg)
维护或者「紧急修复」分支用来快速给产品的发布打上补丁。这是唯一可以从 master
上 fork 的分支。一旦修复完成了,它应该被并入 master
和 develop
分支(或者当前的发布分支),master
应该打上更新的版本号的标签。
有一个专门的 bug 修复开发线使得你的团队能够处理 issues,而不打断其他工作流或是要等到下一个发布周期。你可以将维护分支看作在 master
分支上工作的临时发布分支。
栗子
下面的栗子演示了这种工作流如何用来管理发布周期。假设你已经创建了中央仓库。
创建一个开发分支
你要做的第一步是为默认的 master
分支创建一个互补的 develop
分支。最简单的办法是在本地创建一个空的 develop 分支,将它推送到服务器上:
git branch develop
git push -u origin develop
这个分支将会包含项目中所有的历史,而 master
将包含不完全的版本。其他开发者应该将中央仓库克隆到本地,创建一个分支来追踪 develop 分支:
git clone ssh://user@host/path/to/repo.git
git checkout -b develop origin/develop
现在所有人都有了一份历史分支的本地副本。
Mary 和 John 开始了新功能
我们的栗子从 John 和 Mary 在不同分支上工作开始。他们都要为自己的功能创建单独的分支。他们的功能分支都应该基于develop
,而不是 master
:
git checkout -b some-feature develop
他们都使用「编辑、缓存、提交」的一般约定来向功能分支添加提交:
git status
git add <some-file>
git commit
Mary 完成了她的功能
在添加了一些提交之后,Mary 确信她的功能以及准备好了。如果她的团队使用 Pull Request,现在正是发起 Pull Request 的好时候,请求将她的功能并入 develop
分支。否则,她可以向下面一样,将它并入本地的 develop
分支,推送到中央仓库:
git pull origin develop
git checkout develop
git merge some-feature
git push
git branch -d some-feature
第一个命令在尝试并入功能分支之前确保 develop
分支已是最新。注意,功能绝不该被直接并入 master
。冲突的处理方式和中心化工作流相同。
Mary 开始准备发布
当 John 仍然在他的功能分支上工作时,Mary s开始准备项目的第一个官方发布。和开发功能一样,她新建了一个分支来封装发布的准备工作。这也正是发布的版本号创建的一步:
git checkout -b release-0.1 develop
这个分支用来整理提交,充分测试,更新文档,为即将到来的发布做各种准备。它就像是一个专门用来完善发布的功能分支。
一旦 Mary 创建了这个分支,推送到中央仓库,这次发布的功能便被锁定了。不在 develop
分支中的功能将被推迟到下个发布周期。
Mary完成了她的发布
一旦发布准备稳妥,Mary 将它并入 master
和 develop
,然后删除发布分支。合并回 develop
很重要,因为可能已经有关键的更新添加到了发布分支上,而开发新功能需要用到它们。同样的,如果 Mary 的团队重视代码审查,现在将是发起 Pull Request 的完美时机。
git checkout master
git merge release-0.1
git push
git checkout develop
git merge release-0.1
git push
git branch -d release-0.1
发布分支是功能开发(develop
)和公开发布(master
)之间的过渡阶段。不论什么时候将提交并入 master
时,你应该为提交打上方便引用的标签:
git tag -a 0.1 -m "Initial public release" master
git push --tags
Git 提供了许多钩子,即仓库中特定事件发生时被执行的脚本。当你向中央仓库推送 master
分支或者标签时,你可以配置一个钩子来自动化构建公开发布。
终端用户发现了一个 bug
正式发布之后,Mary 回过头来和 John 一起为下一个发布开发功能。这时,一个终端用户开了一个 issue 抱怨说当前发布中存在一个 bug。为了解决这个 bug,Mary(或 John)从 master
创建了一个维护分支,用几个提交修复这个 issue,然后直接合并回 master
。
git checkout -b issue-#001 master
# Fix the bug
git checkout master
git merge issue-#001
git push
和发布分支一样,维护分支包含了 develop
中需要的重要更新,因此 Mary 同样需要执行这个合并。接下来,她可以删除这个分支了:
git checkout develop
git merge issue-#001
git push
git branch -d issue-#001
接下来该怎么做
现在,希望你已经很熟悉中心化的工作流、功能分支工作流和 GitFlow 工作流。你应该已经可以抓住本地仓库、推送/拉取模式,和 Git 鲁棒的分支和合并模型的无限潜力。
请记住,教程中呈现的工作流只是可行的实践——而非工作中使用 Git 的金科玉律。因此,尽情地取其精华,去其糟粕吧。不变的是要让 Git 为你所用,而不是相反。
Fork 工作流
Fork 工作流和教程中讨论的其它工作流截然不同。与其使用唯一的服务端仓库作为「中央」代码库,它给予 每个 开发者一个服务端仓库。也就是说每个贡献者都有两个 Git 仓库,而不是一个:一个私有的本地仓库和一个公开的服务端仓库。
Fork 工作流的主要优点在于贡献可以轻易地整合进项目,而不需要每个人都推送到单一的中央仓库。开发者推送到他们 自己的 服务端仓库,只有项目管理者可以推送到官方仓库。这使得管理者可以接受任何开发者的提交,却不需要给他们中央仓库的权限。
结论是,这种分布式的工作流为大型、组织性强的团队(包括不可信的第三方)提供了安全的协作方式。它同时也是开源项目理想的工作流。
如何工作
和其它 Git 工作流一样,Fork 工作流以一个储存在服务端的官方公开项目开场。但新的开发者想参与项目时,他们不直接克隆官方项目。
取而代之地,他们 fork 一份官方项目,在服务端创建一份副本。这份新建的副本作为他们私有的公开仓库——没有其他开发者可以在上面推送,但他们可以从上面拉取修改(在后面我们会讨论为什么这一点很重要)。在他们创建了服务端副本之后,开发者执行 git clone
操作,在他们的本地机器上复制一份。这是他们私有的开发环境,正如其他工作流中一样。
当他们准备好发布本地提交时,他们将提交推送到自己的公开仓库——而非官方仓库。然后,他们向主仓库发起一个 Pull Request,让项目维护者知道一个更新做好了合并的准备。如果贡献的代码有什么问题的话,Pull Request 可以作为一个方便的讨论版。
我为了将功能并入官方代码库,维护者将贡献者的修改拉取到他们的本地仓库,确保修改不会破坏项目,将它合并到本地的 master 分支,然后将 master
分支推送到服务端的官方仓库。贡献现在已是项目的一部分,其他开发者应该从官方仓库拉取并同步他们的本地仓库。
中央仓库
「官方」仓库这个概念在 Fork 工作流中只是一个约定,理解这一点很重要。从技术的角度,Git 并看不出每个开发者和官方的公开仓库有什么区别。事实上,官方仓库唯一官方的原因是,它是项目维护者的仓库。
Fork 工作流中的分支
所有这些个人的公开仓库只是一个在开发者之间共享分支的约定。每个人仍然可以使用分支来隔离功能,就像在功能分支工作流]()和 GitFlow 工作流中一样。唯一的区别在于这些分支是如何开始的。在 Fork 工作流中,它们从另一个开发者的本地仓库拉取而来,而在功能分支和 GitFlow 分支它们被推送到官方仓库。
栗子
项目维护者初始化了中央仓库
和任何基于 Git 的项目一样,第一步是在服务端创建一个可以被所有项目成员访问到的官方仓库。一般来说,这个仓库同时还是项目维护者的公开仓库。
公开的仓库应该永远是裸的,不管它们是否代表官方代码库。所以项目维护者应该运行下面这样的命令来设置官方仓库:
ssh user@host
git init --bare /path/to/repo.git
GitHub 同时提供了一个图形化界面来替代上面的操作。这和教程中其它工作流设置中央仓库的流程完全一致。如果有必要的话,项目维护者应该将已有的代码库推送到这个仓库中。
开发者 fork 仓库
接下来,所有开发者需要 fork 官方仓库。你可以用 SSH 到服务器,运行 git clone
将它复制到服务器的另一个地址—— fork 其实只是服务端的 clone。但同样地,GitHub上开发者只需点一点按钮就可以 fork 仓库。
在这步之后,每个开发者应该都有了自己的服务端仓库。像官方仓库一样,所有这些仓库都应该是裸仓库。
开发者将 fork 的仓库克隆到本地
接下来开发者需要克隆他们自己的公开仓库。他们可以用熟悉的 git clone
命令来完成这一步。
我们的栗子假设使用他们使用 GitHub 来托管仓库。记住,在这种情况下,每个开发者应该有他们自己的 GitHub 账号,应该用下面的命令克隆服务端仓库:
git clone https://user@github.com/user/repo.git
而教程中的其他工作流使用单一的 origin
远程连接,指向中央仓库,Fork 工作流需要两个远程连接,一个是中央仓库,另一个是开发者个人的服务端仓库。你可以给这些远端取任何名字,约定的做法是将 origin
作为你 fork 后的仓库的远端(运行 git clone
是会自动创建)和 upstream
作为官方项目。
git remote add upstream https://github.com/maintainer/repo
你需要使用上面的命令来创建上游仓库的远程连接。它使得你轻易地保持本地仓库和官方仓库的进展同步。注意如果你的上游仓库开启了认证(比如它没有开源),你需要提供一个用户名,就像这样:
git remote add upstream https://user@bitbucket.org/maintainer/repo.git
它需要用户从官方代码库克隆或拉取之前提供有效的密码。
开发者进行自己的开发
在他们刚克隆的本地仓库中,开发者可以编辑代码、提交更改,和其它分支中一样创建分支:
git checkout -b some-feature
# 编辑代码
git commit -a -m "Add first draft of some feature"
他们所有的更改在推送到公开仓库之前都是完全私有的。而且,如果官方项目已经向前进展了,他们可以用 git pull
获取新的提交:
git pull upstream master
因为开发者应该在专门的功能分支开发,这一般会产生一个快速向前的合并。
开发者发布他们的功能
一旦开发者准备好共享他们的新功能,他们需要做两件事情。第一,他们必须将贡献的代码推送到自己的公开仓库,让其他开发者能够访问到。他们的 origin
远端应该已经设置好了,所以他们只需要:
git push origin feature-branch
这和其他工作流不同之处在于,origin
远端指向开发者个人的服务端仓库,而不是主代码库。
第二,他们需要通知项目维护者,他们想要将功能并入官方代码库。GitHub 提供了一个「New Pull Request」按钮,跳转到一个网页,让你指明想要并入主仓库的分支。一般来说,你希望将功能分支并入上游远端的 master
分支。
项目维护者整合他们的功能
当项目维护者收到 Pull Request 时,他们的工作是决定是否将它并入官方的代码库。他们可以使用下面两种方式之一:
- 直接检查 Pull Request 中检查代码
- 将代码拉取到本地仓库然后手动合并
第一个选项更简单,让维护者查看修改前后的差异,在上面评论,然后通过图形界面执行合并。然而,如果 Pull Request 会导致合并冲突,第二个选项就有了必要。在这个情况中,维护者需要从开发者的服务端仓库 fetch 功能分支,合并到他们本地的 master
分支,然后解决冲突:
git fetch https://bitbucket.org/user/repo feature-branch
# 检查修改
git checkout master
git merge FETCH_HEAD
一旦修改被整合进本地的 master
,维护者需要将它推送到服务器上的官方仓库,这样其他开发者也可以访问它:
git push origin master
记住,维护者的 origin
指向他们的公开仓库,也就是项目的官方代码库。开发者的贡献现在完全并入了项目。
开发者和中央仓库保持同步
因为主代码库已经取得了新的进展,其他开发者应该和官方仓库同步:
git pull upstream master
接下来该怎么做
如果你从 SVN 迁移而来,Fork 工作流看上去是一个比较大的转变。但不要害怕——它只是在 Feature 分支工作流之上引入了一层抽象。贡献的代码发布到开发者在服务端自己的仓库,而不是在唯一的中央仓库中直接共享分支。
这篇文章解释了一次代码贡献是如何从一个开发者流入官方的 master
分支的,但相同的方法可以用在将代码贡献整合进任何仓库。比如,如果你团队的一部分成员在一个特定功能上协作,他们可以用自己约定的行为共享修改——而不改变主仓库。
这使得 Fork 工作流对于松散的团队来说是个非常强大的工具。任何开发者都可以轻而易举地和其他开发者共享修改,任何分支都能高效地并入主代码库。
这篇文章是「git-recipes」的一部分,点击 目录 查看所有章节。
如果你觉得文章对你有帮助,欢迎点击右上角的 Star :star2: 或 Fork :fork_and_knife:。
如果你发现了错误,或是想要加入协作,请参阅 Wiki 协作说明。