Git最佳实践
分支管理
主分支
该开发模型的核心基本和现有的模型是一样的。中心代码库永远维持着两个主要的分支:
-
master
-
develop
在origin
上的master
分支和每个 git 用户的保持一致。
而和master
分支并行的另一个分支叫做develop
。
我们认为origin/master
是其 HEAD 源代码总是代表了生产环境准备就绪的状态的主分支。
我们认为origin/develop
是其 HEAD 源代码总是代表了最后一次交付的可以赶上
下一次发布的状态的主分支。有人也把它叫做「集成分支」。
该源代码还被作为了 nightly build 自动化任务的来源。
每当develop
分支到达一个稳定的阶段,可以对外发布时,
所有的改变都会被合并到master
分支,并打一个发布版本的 tag。
具体操作方法我们稍后讨论。
因此,每次改动被合并到master
的时候,这就是一个真正的新的发布产品。
我们建议对此进行严格的控制,
因此理论上我们可以为每次master
分支的提交都挂一个钩子脚本,
向生产环境自动化构建并发布我们的软件。
支持型分支
我们的开发模型里,紧接着master
和develop
主分支的,是多种多样的支持型分支。
它们的目的是帮助团队成员并行处理每次追踪特性、准备发布、快速修复线上问题等开发任务。
和之前的主分支不同,这些分支的生命周期都是有限的,它们最终都会被删除掉。
我们可能会用到的不同类型的分支有:
- feature 分支
- release 分支
- hotfix 分支
每一种分支都有一个特别的目的,并且有严格的规则,诸如哪些分支是它们的起始分支、 哪些分支必须是它们合并的目标等。我们快速把它们过一遍。
这些「特殊」的分支在技术上是没有任何特殊的。分支的类型取决于我们如何运用它们。 它们完完全全都是普通而又平凡的 git 分支。
feature 分支
-
可能派发自:
develop
-
必须合并回:
develop
-
分支命名规范:除了
master
、develop
、release-*
或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
-
必须合并回:
develop
和master
-
分支命名规范:
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.6
或2.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
-
必须合并回:
develop
和master
-
分支命名规范:
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