Jade Dungeon

Git最佳实践

分支管理

图例

主分支

该开发模型的核心基本和现有的模型是一样的。中心代码库永远维持着两个主要的分支:

  • master
  • develop

origin上的master分支和每个 git 用户的保持一致。 而和master分支并行的另一个分支叫做develop

图例

我们认为origin/master是其 HEAD 源代码总是代表了生产环境准备就绪的状态的主分支。

我们认为origin/develop是其 HEAD 源代码总是代表了最后一次交付的可以赶上 下一次发布的状态的主分支。有人也把它叫做「集成分支」。 该源代码还被作为了 nightly build 自动化任务的来源。

每当develop分支到达一个稳定的阶段,可以对外发布时, 所有的改变都会被合并到master分支,并打一个发布版本的 tag。 具体操作方法我们稍后讨论。

因此,每次改动被合并到master的时候,这就是一个真正的新的发布产品。 我们建议对此进行严格的控制, 因此理论上我们可以为每次master分支的提交都挂一个钩子脚本, 向生产环境自动化构建并发布我们的软件。

支持型分支

我们的开发模型里,紧接着masterdevelop主分支的,是多种多样的支持型分支。 它们的目的是帮助团队成员并行处理每次追踪特性、准备发布、快速修复线上问题等开发任务。 和之前的主分支不同,这些分支的生命周期都是有限的,它们最终都会被删除掉。

我们可能会用到的不同类型的分支有:

  • feature 分支
  • release 分支
  • hotfix 分支

每一种分支都有一个特别的目的,并且有严格的规则,诸如哪些分支是它们的起始分支、 哪些分支必须是它们合并的目标等。我们快速把它们过一遍。

这些「特殊」的分支在技术上是没有任何特殊的。分支的类型取决于我们如何运用它们。 它们完完全全都是普通而又平凡的 git 分支。

feature 分支

图例

  • 可能派发自:develop
  • 必须合并回:develop
  • 分支命名规范:除了masterdeveloprelease-*hotfix-*的任何名字

Feature 分支(有时也被称作 topic 分支)用来开发包括即将发布或远期发布的新的特性。 当我们开始开发一个特性的时候,发布合并的目标可能还不太确定。 Feature 分支的生命周期会和新特性的开发周期保持同步, 但是最终会合并回develop或被抛弃。

Feature 分支通常仅存在于开发者的代码库中,并不出现在 origin 里。

创建 feature 分支

当开始一个新特性的时候,从develop分支派发出一个分支

$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
把完成后合并回 develop

完成的特性可以合并回 develop 分支并赶上下一次发布:

$ git checkout develop
Switched to a new branch "develop"
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557)
$ git push origin develop

-no-ff标记使得合并操作总是产生一次新的提交,哪怕合并操作可以快速完成。 这个标记避免将 feature 分支和团队协作的所有提交的历史信息混在主分支的其它提交之后。

比较一下:

图例

在右边的例子里,我们不可能从 git 的历史记录中看出来哪些提交实现了这一特性—— 你可能不得不查看每一笔提交日志。恢复一个完整的特性(比如通过一组提交) 在右边变成了一个头疼事情,而如果使用了--no-ff之后,就变得简单了。

是的,这会创造一些没有必要的空的提交记录,但是得到的是大量的好处。

不幸的是,我还没有找到一个在 git merge 时默认就把--no-ff标记打上的办法,但这很重要。

release 分支

  • 可能派发自:`develop
  • 必须合并回:developmaster
  • 分支命名规范:release-*

Release 分支用来支持新的生产环境发布的准备工作。 允许在最后阶段产生提交点和交汇点。 而且允许小幅度的问题修复以及准备发布时的meta数据(比如版本号、发布日期等)。 在 release 分支做了上述这些工作之后,develop 分支会被翻篇儿, 开始接收下一次发布的新特性。

我们选择几近完成所有预期的开发的时候,作为从 develop 派发出 release 分支的时机。 最起码所有准备构建发布的功能都已经及时合并到了 develop 分支。 而往后才会发布的功能则不应该合并到 develop 分支——他们必须等到 release 分支派发出去之后再做合并。

在一个 release 分支的开始,我们就赋予其一个明确的版本号。直到该分支创建之前, develop 分支上的描述都「是下一次release 的改动」, 但这个下一次release 其实也没说清楚是0.3 release还是1.0 release。 而在一个 release 分支的开始时这一点就会确定。这将成为有关项目版本号晋升的一个守则。

创建 release 分支

Release 分支派发自 develop 分支。比如,我们当前的生产环境发布的版本是1.1.5, 马上有一个 release 要发布了。 develop 分支已经为下一次release 做好了准备, 并且我们已经决定把新的版本号定为1.2(而不是1.1.62.0)。 所以我们派发一个 release 分支并以新的版本号为其命名:

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

创建好并切换到新的分支之后,我们完成对版本号的晋升。 这里的bump-version.sh是一个虚构的用来改变代码库中某些文件以反映新版本的 shell 脚本。 当然你也可以手动完成这些改变——重点是有些文件发生了改变。然后,晋升了的版本号会被提交。

这个新的分支会存在一段时间,直到它确实发布出去了为止。 期间可能会有 bug 修复(这比在 develop 做更合理)。 但我们严格禁止在此开发庞大的新特性,它们应该合并到 develop 分支,并放入下次发布。

完成 release 分支

当 release 分支真正发布成功之后,还有些事情需要收尾。

  • 首先,release 分支会被合并到 master 。 因为master 上的每一次提交都代表一个真正的新的发布);
  • 然后,为 master 上的这次提交打一个 tag,以便作为版本历史的重要参考;
  • 最后,还要把 release 分支产生的改动合并回 develop, 以便后续的发布同样包含对这些 bug 的修复。

前两步在 git 下是这样操作的:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive
(Summary of changes)
$ git tag -a 1.2

现在发布工作已经完成了,同时 tag 也打好了,用在未来做参考。

补充:你也可以通过-s-u <key>标记打 tag。

为了保留 release 分支里的改动记录,我们需要把这些改动合并回 develop。git 操作如下:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

这一步有可能导致冲突的发生(只是有理论上的可能性,因为我们已经改变了版本号), 一旦发现,解决冲突然后提交就好了。

现在我们真正完成了一个 release 分支,该把它删掉了,因为它的使命已经完成了:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

hotfix 分支

  • 可能派发自:master
  • 必须合并回:developmaster
  • 分支命名规范:hotfix-*

图例

Hotfix 分支和 release 分支非常类似, 因为他们都意味着会产生一个新的生产环境的发布, 尽管 hotfix 分支不是先前就计划好的。 他们在实时的生产环境版本出现意外需要快速响应时, 从 master 分支相应的 tag 被派发。

我们这样做的根本原因,是为了让团队其中一个人来快速修复生产环境的问题, 其他成员可以按工作计划继续工作下去而不受太大影响。

创建 hotfix 分支

Hotfix 分支创建自 master 分支。

例如,假设1.2版本是目前的生产环境且出现了一个严重的 bug, 但是目前的 develop 并不足够稳定。 那么我们可以派发出一个 hotfix 分支来开始我们的修复工作:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

别忘了在派发出分支之后晋升版本号!

然后,修复 bug,提交改动。通过一个或多个提交都可以。

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
完成 hotfix 分支

当我们完成之后,对 bug 的修复需要合并回 master,同时也需要合并回 develop, 以保证接下来的发布也都已经解决了这个 bug。 这和 release 分支的完成方式是完全一样的。

首先,更新 master 并为本次发布打一个 tag:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive
(Summary of changes)
$ git tag -a 1.2.1

补充:你也可以通过-s-u <key>标记打 tag。

然后,把已修复的 bug 合并到 develop:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive
(Summary of changes)

这个规矩的一个额外之处是:如果此时已经存在了一个 release 分支, 那么 hotfix 的改变需要合并到这个 release 分支,而不是 develop 分支。

因为把对 bug 的修复合并回 release 分支之后, release 分支最终还是会合并回 develop 分支的。 如果在 develop 分支中立刻需要对这个 bug 的修复,且等不及 release 分支合并回来, 则你还是可以直接合并回 develop 分支的,这是绝对没问题的

最后,删掉这个临时的分支:

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

长期分支

长期分支建议开rerere

git config rerere.enable true

详情参考man git-rerere的文档。

推荐分支规划

master为基础建立release分支作为打包发布的内容:

git checkout -b release master

master为基础建立develop分支作为开发版本:

git checkout -b develop master
git push -u origin develop

每个任务再从develop里再分出分支来:

git checkout -b feature01 develop

如果生产环境上的bug,那再从master分出hotfix分支来:

git checkout -b hotfix master

开发工作流程

开发前

新建临时分支

新任务从develop里分出分支来:

git checkout develop
git pull
git checkout -b feature01 develop

修改软件版本号

版本号改为1.0.0-feature01-SNAPSHOT

vim pom.xml     # update version
vim README.md   # update readme file add TODO info
git commit -a -m 'bla bla bal'

加上TAG

打上tag中的「feature」部分:

git tag -a 'v1.0.0-feature01-SNAPSHOT' -m 'Version 1.0.0-feature-add-modole'
git push origin --tags

开发完成后

develop分支合并过来

开发完以后把最新版本的develop合并过来,因为可以有其他的任务已经提交了:

git checkout develop
git pull
git checkout feature01
git merge --no-ff develop

加上TAG

打上tag后缀-M表示已经合并过develop分支的内容:

git tag -a 'v1.0.0-feature01-SNAPSHOT-M' -m 'Version 1.0.0-feature-add-modole merged'
git push origin --tags

修改版本号:开发版本

增加版本号,去掉「feature」,改为1.0.1-SNAPSHOT

vim README.md   # update readme file add change log
vim pom.xml     # update version
git commit -a -m 'bla bla bal'

提交到develop

git checkout develop
git merge --no-ff feature01   # 注释要写明增加了那些feature

打上标签-M-feature01-

git tag -a 'v1.0.1-M-feature01-SNAPSHOT' -m 'Version 1.0.1-M-feature01-SNAPSHOT'
git push origin --tags

发布版本

  • 检查pom.xml
    • 检查所有依赖,把快照版本变为正式版本。
  • 检查文档README.md
    • TODOList已经完成。

把主干上别人提交的内容合并到当前分支中(比如可能有:hotfix之类的分支有更新内容) :

git checkout master
git pull
git checkout develop
git merge --no-ff master
git tag -a 'v${version.from}-SNAPSHOT-M' -m 'Version v${version.from}-SNAPSHOT merged'

版本号升级合并到主干

  • pom.xml去掉SNAPSHOT,项目的版本号由快照版变为正式版。
git commit -a -m 'v${version.from}-SNAPSHOT-M'
git checkout master
git merge --no-ff develop          # comment : detail of new features
git tag -a 'v${version.from}' -m 'Version ${version.from}'
git push && git push origin --tags
git checkout develop
git merge --no-ff master
  • 检查pom.xml
    • 提升版本号为下一版本的快照。
  • 检查文档README.md
    • 为新的版本号增加TODOList。
git commit -a -m 'v${version.to}-SNAPSHOT'
git tag -a 'v${version.to}-SNAPSHOT' -m 'Version ${version.to}-SNAPSHOT'
git push && git push origin --tags

布署到生产环境

提交到release分支:

git checkout release
git merge master

修改配置文件,参数改为生产环境参数。

修改版本号,加上release,改为1.0.1-RELEASE

git commit

打上标签:

git tag -a 'v1.0.1-RELEASE' -m 'Version 1.0.1 release'
git push origin --tags

场景模拟:合作开发

假设现在有另一个人(用户B)和我合作。他的工作目录在:

mkdir wubi2

从仓库复制源代码到本地

用户B从我的版本库中导出。

在没有服务器的情况下:

git clone wubi wubi2

通过服务器:

git clone git://git.jade.com/wubi wubi2

然后别人可以在我的代码基础上进行开发,然后提交:

git commit -a

发布本地提交的内容

用户B提交的内容只有自己能看到,他想让别人得到他的工作成果的话,就要把他提交的 内容发布给其他开发者。

如果服务器允许直接发布,那可以直接发布到服务器:

git push

有些项目(如:linux kernel)不允许提交到版本库,只能做成补丁文件发邮件给别人:

git format-patch origin

取得其他人提交的内容

回到我这边,我知道别人已经提交了,现在要取得别人的工作成果。如果不放心别人的工作 ,根据别人的远程仓库名为master的分支在本地建立一个名为otherone的分支。

在没有服务器的情况下:

cd wubi
git fetch ../wubi2 master:otherone

有服务器的情况下(区别就是源地址,以后不再说明没有服务器的情况了):

cd wubi
git fetch git://git.jade.com/wubi master:otherone

比较一下别人改了哪些地方

git whatchanged -p master..otherone

如果认为对方改错了,可以删除掉对方的修改

git branch -D otherone

如果觉得没有问题了可以用pull命令导入别人的修改。其实pull命令相当于是fetch 命令和merge命令的一个组合。当然如果信任对方的话,也可以不建立分支检查(略过 上面的所有步骤)直接导入。

从目录:

git pull ../wubi2 master

从服务器:

git pull git://git.jade.com/wubi master

从服务器也可以不加参数,直接:

git pull

其实,用git pull .就相当于git merge

用户B继续开发时先要取得我的工作成果,可以直接pull不用加参数。因为clone的时候 已经记住了来源:

git pull