GitCorp Flow - 安居客Git开发流程规范

代码仓库 - Git Repositories

原始代码仓库

在这篇文章的例子中,我们以git@git.corp.anjuke.com:corp/flow-demo.git为原始代码仓库。文章后面提到的origin都是指原始代码仓库。

origin上只包含有三个分支,masterbetaga;每一个发布的版本将有一个tag。

  • master 主要的开发分支,待发布的新功能都在这个分支上。通常对这个分支进行每日构建、集成测试等;新的功能开发也都在这个分支的基础上进行。通常这个分支是最新的。

  • beta 这个分支的代码是已经部署到预发布环境的指针,通常其HEAD指向当前的预发布版本。当BETA环境有重要的hotfix时,在这个分支上开始;同时这个hotfix还需要合并回master。

  • ga 这个分支是部署到对外公开的环境,通常其HEAD指向当前的对外公开版本。当GA环境有重要的hotfix时,在这个分支上开始;同时这个hotfix还需要合并回master;如果必要改hotfix也将合并至beta分支。

ga/beta分支的HEAD会被release发布命令改变。

本地开发仓库

当我们进行项目开发的时候,需要将origin克隆到本地,这时我们称呼本地的git仓库为本地开发仓库。下面这个命令将原始的flow-demo.git克隆到本地。

$ git clone git@git.corp.anjuke.com:corp/flow-demo.git

在本地仓库查看所有分支

$ git branch -a

将显示

* master
  remotes/origin/beta
  remotes/origin/ga
  remotes/origin/master

共享开发仓库

当我们有项目需要多人共同开发时,又希望其他合作者可以直接push代码,可以设置共享仓库。

创建共享仓库

例如想要将的本地仓库推到服务器上,

$ git remote add enzhang git@git.corp.anjuke.com:enzhang/flow-demo.git
$ git push enzhang master:master

则两个命令的格式是,

  • git remote add (共享仓库的名字) (共享仓库的地址)
  • git push (共享仓库的名字) (要推送的本地的分支):(推送到远程仓库上的分支)

其中共享仓库名在GitCorp环境下建议使用开发者的用户名,或者开发者的用户名为前缀

缺省情况下,这个共享仓库还只有创建者一个人有写权限,希望其他同事可以直接推送代码,还需要设置权限。可以先查看现有的用户权限

$ ssh git@git.corp.anjuke.com getperms enzhang/flow-demo.git

输出:

READERS @all

上面的getperms命令看到所有人有读权限。下面我们在原有权限的基础上给所有人增加写权限,

$ (ssh git@git.corp.anjuke.com getperms enzhang/flow-demo.git \
    && echo "WRITERS @all") | \
    ssh git@git.corp.anjuke.com setperms enzhang/flow-demo.git

输出:

New perms are:
READERS @all
WRITERS @all

现在所有人对这个共享的代码仓库都拥有读写权限了。

使用共享仓库

当你需要加入别人创建的共享代码仓库时,只需要将其共享仓库加入git的remote里。

$ git remote add enzhang git@git.corp.anjuke.com:enzhang/flow-demo.git

建议使用对方的开发者目录名作为git的remote名称,这样同时加入多个开发者的共享仓库也不容易混淆。

遵守我们之前的约定,origin都应该指向原始代码仓库

项目开发 - Feature Development

已经准备好了本地代码仓库,现在看看项目开发的具体流程。

开发者信息

对于公司的项目,建议采用真实姓名和公司的邮箱地址作为git用户的信息。

$ git config user.name "张尔宁"
$ git config user.email "enzhang@anjuke.com"

项目开发分支

通常对代码的改进都应该在分支上进行,确认完成后再合并回去。特别是项目的开发,由于改动一般比较多,应该新建立一个对应该项目的分支。我们约定这个分支以PMT的编号开始,例如pmt1001

正常的项目分支从master分支拉出,(本地的master分支,跟踪origin/master的)

$ git checkout -b pmt1001 master

写代码,提交

$ echo "这是PMT1001的文件" > pmt1001.txt
$ git add pmt1001.txt
$ git commit -m "增加某功能"

再写代码,提交

$ echo "增加了一项功能" >> pmt1001.txt
$ git commit -a -m "又增加了一项功能"

分支里的代码可以部署到测试环境,测试通过之后项目完成。这时分支里的代码合并回master。

$ git checkout master
$ git merge --no-ff pmt1001 -m '项目pmt1001完成,合并回master'

注意--no-ff参数。

此时,pmt1001这个分支已经不用,可以删除了。

$ git branch -d pmt1001

git的日志大致如下

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s'

*   43fe646 - (HEAD, master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - (origin/master) GitCorp Flow Demo Initial Version

将改动push回原始仓库

$ git push origin master:master

并行的项目

同一时候会有很多个项目在并行开发,假设现在有两个并行的项目pmt1002pmt1003。我们先从master创建两个项目分支

$ git branch pmt1002 master
$ git branch pmt1003 master

然后在pmt1002分支里进行开发

$ git checkout pmt1002
$ echo "这是PMT1002的文件" > pmt1002.txt
$ git add pmt1002.txt
$ git commit -m '增加pmt1002文件'
$ echo "修改PMT1002的文件" >> pmt1002.txt
$ git commit -a -m '修改pmt1002文件'

同时,在pmt1003分支里进行开发

$ git checkout pmt1003
$ echo "这是PMT1003的文件" > pmt1003.txt
$ git add pmt1003.txt
$ git commit -m '增加pmt1003文件'
$ echo "修改PMT1003的文件" >> pmt1003.txt
$ git commit -a -m '修改pmt1003文件'

看一下两个分支的情况,

pmt1003

$ git log pmt1003 --graph --abbrev-commit --pretty=format:'%h -%d %s'
* dad3119 - (HEAD, pmt1003) 修改pmt1003文件
* c69f390 - 增加pmt1003文件
*   43fe646 - (origin/master, master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - GitCorp Flow Demo Initial Version

pmt1002

$ git log pmt1002 --graph --abbrev-commit --pretty=format:'%h -%d %s'
* e174508 - (pmt1002) 修改pmt1002文件
* b2698ed - 增加pmt1002文件
*   43fe646 - (origin/master, master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 -  GitCorp Flow Demo Initial Version

这时pmt1003开发完毕,我们按照之前的流程,将pmt1003分支合并回master

$ git checkout master
$ git merge --no-ff pmt1003 -m '项目pmt1003完成,合并回master'

将得到这样的master分支

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s'
*   2110b32 - (HEAD, master) 项目pmt1003完成,合并回master
|\
| * dad3119 - (pmt1003) 修改pmt1003文件
| * c69f390 - 增加pmt1003文件
|/
*   43fe646 - (origin/master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - GitCorp Flow Demo Initial Version

然后pmt1002继续开发

$ git checkout pmt1002
$ echo "这是PMT1002的文件" > pmt1003.txt
$ git add pmt1003.txt
$ git commit -m '创建了一个和pmt1003冲突的文件'

同样,pmt1002开发完毕,将pmt1002分支合并回master

$ git checkout master
$ git merge --no-ff pmt1002 -m '项目pmt1002完成,合并回master'

如我们的例子,合并时冲突了。需要解决冲突再提交

$ echo "这是PMT1003,PMT1002的共同文件" > pmt1003.txt
$ git add pmt1003.txt
$ git commit -m '项目pmt1002完成,合并回master'

此时,master分支为

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s'
*   f7c4c36 - (HEAD, master) 项目pmt1002完成,合并回master
|\
| * 6ec4909 - (pmt1002) 创建了一个和pmt1003冲突的文件
| * e174508 - 修改pmt1002文件
| * b2698ed - 增加pmt1002文件
* |   2110b32 - 项目pmt1003完成,合并回master
|\ \
| |/
|/|
| * dad3119 - (pmt1003) 修改pmt1003文件
| * c69f390 - 增加pmt1003文件
|/
*   43fe646 - (origin/master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - GitCorp Flow Demo Initial Version

分支合并的线路图显示,pmt1003分支从43fe646开始,合并到2110b32pmt1002分支也从43fe646开始,合并到f7c4c36

rebase

除了直接合并回master,还可以选择在项目分支内先进行rebase再合并回去的方法。为了演示,我们将master分支回退到pmt1002合并前的状态,然后执行rebase

$ git reset --hard 2110b32
$ git checkout pmt1002
$ git rebase master

这时遇到冲突,需要解决

$ echo "这是PMT1003,PMT1002的共同文件" > pmt1003.txt
$ git add pmt1003.txt
$ git rebase --continue

完成后pmt1002的分支成为

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s'
* b0fe0ac - (HEAD, pmt1002) 创建了一个和pmt1003冲突的文件
* e402ad1 - 修改pmt1002文件
* 48faf83 - 增加pmt1002文件
*   2110b32 - (master) 项目pmt1003完成,合并回master
|\
| * dad3119 - (pmt1003) 修改pmt1003文件
| * c69f390 - 增加pmt1003文件
|/
*   43fe646 - (origin/master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - GitCorp Flow Demo Initial Version

这时再将pmt1002分支合并回master

$ git checkout master
$ git merge --no-ff pmt1002 -m '项目pmt1002完成,合并回master'

此时,master分支为

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s'
*   b42d61d - (HEAD, master) 项目pmt1002完成,合并回master
|\
| * b0fe0ac - (pmt1002) 创建了一个和pmt1003冲突的文件
| * e402ad1 - 修改pmt1002文件
| * 48faf83 - 增加pmt1002文件
|/
*   2110b32 - 项目pmt1003完成,合并回master
|\
| * dad3119 - (pmt1003) 修改pmt1003文件
| * c69f390 - 增加pmt1003文件
|/
*   43fe646 - (origin/master) 项目pmt1001完成,合并回master
|\
| * 43a7407 - 又增加了一项功能
| * 757c89e - 增加某功能
|/
* 0774825 - GitCorp Flow Demo Initial Version

这里rebase的优点是,冲突在项目的分支里解决,并且master上图看起来比较清晰。

不再使用的分支可以删掉了

$ git branch -d pmt1002 pmt1003

多人共同开发一个项目

现在来说一下,多个人共同开发一个项目分支的情况。这在我们平时的项目开发中是最常见的一种情况。

        /------------\    +------------+      +------------+ +------------+
        | origin     |    | enzhang    |      | bob        | | alice      |
        |  * master  |    |  * master  |  ... |  * master  | |  * master  |
        |  * beta    |    |  - pmt1004 |      |  - pmt1005 | |  - pmt1006 |
        |  * ga      |    |            |      |            | |            |
        \------------/    +------------+      +------------+ +------------+
         origin repo \    / shared repo         shared repo    shared repo
              |       \  /       |
              |        \/        |
              |        /\        |
              |       /  \       |
 local repo   |      /    \      |    local repo
+--------------------+    +---------------------+
| enzhang            |    | liangshan           |
|  * master          |    |   * master          |
|  - pmt1004         |    |   - pmt1004         |
|  * origin/master   |    |   * origin/master   |
|  * enzhang/master  |    |   * enzhang/master  |
|  * enzhang/pmt1004 |    |   * enzhang/pmt1004 |
+--------------------+    +---------------------+

举例开发项目pmt1004

enzhang首先开始。如果之前的例子,首先在本地仓库建立pmt1004分支

$ git checkout -b pmt1004 master

将需要共同开发的项目分支推送到共享代码仓库中 (假设共享的代码仓库之前已经设置好)

$ git push enzhang pmt1004:pmt1004

并通知合作伙伴liangshan,在共享仓库的pmt1004分支下一起开发。同时自己还在继续进行开发

$ echo "PMT1004" > pmt1004.txt
$ git add pmt1004.txt
$ git commit -m '完成pmt1004第一个功能点'
$ echo "PMT1004.balabala" >> pmt1004.txt
$ git commit -a -m '完成pmt1004第二个功能点'

假设liangshan已经从原始仓库clone了一份代码,如果还没有需要执行

$ git clone git@git.corp.anjuke.com:corp/flow-demo.git

liangshan在接到共享的开发分支位置后,开始工作

$ git remote add enzhang git@git.corp.anjuke.com:enzhang/flow-demo.git
$ git fetch enzhang
$ git checkout -b pmt1004 enzhang/pmt1004
$ echo "PMT1004" > pmt1004.ls.txt
$ git add pmt1004.ls.txt
$ git commit -m '完成pmt1004第三个功能点'
$ echo "PMT1004.balabala" >> pmt1004.ls.txt
$ git commit -a -m '完成pmt1004第四个功能点'

这时,enzhang将本地的最新修改push到共享仓库

$ git push enzhang pmt1004

之后,liangshan也将最新的修改push到共享仓库

$ git push enzhang pmt1004

这时可以看到一窜错误提示,无法fast-forward合并

To git@git.corp.anjuke.com:enzhang/flow-demo.git
 ! [rejected]        pmt1004 -> pmt1004 (non-fast-forward)
error: failed to push some refs to 'git@git.corp.anjuke.com:enzhang/flow-demo.git'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes (e.g. 'git pull') before pushing again.  See the
'Note about fast-forwards' section of 'git push --help' for details.

应该在本地先合并好再push到远程的共享仓库,并且建议使用rebase参数

$ git pull --rebase enzhang
$ git push enzhang pmt1004

enzhang还继续在本来进行新的开发

$ echo "PMT1004.foobar" >> pmt1004.txt
$ git commit -a -m '完成pmt1004第五个功能点'

当enzhang需要push代码到共享仓库的时候,同样需要pull再push

$ git pull --rebase enzhang
$ git push enzhang

现在项目开发完成,由liangshan来负责合并代码到master分支

$ git fetch --all
$ git checkout pmt1004
$ git rebase enzhang/pmt1004
$ git checkout master
$ git rebase origin/master
$ git merge pmt1004 --no-ff -m '项目pmt1004完成'
$ git push origin

由于项目pmt1004已经开发完毕,而且已经合并入master分支,可以删除这个分支了

$ git branch -d pmt1004

enzhang的共享代码仓库里的pmt1004分支也可以删除

$ git push enzhang :pmt1004

看看此时master分支的情况

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s <%an>'
*   6f6cb8f - (HEAD, origin/master, master) 项目pmt1004完成 <梁山>
|\
| * fe42061 - 完成pmt1004第五个功能点 <张尔宁>
| * a581641 - 完成pmt1004第四个功能点 <梁山>
| * 35a2cc4 - 完成pmt1004第三个功能点 <梁山>
| * 3b92be2 - 完成pmt1004第二个功能点 <张尔宁>
| * 74746a5 - 完成pmt1004第一个功能点 <张尔宁>
|/
*   b42d61d - 项目pmt1002完成,合并回master <张尔宁>
|\
| * b0fe0ac - 创建了一个和pmt1003冲突的文件 <张尔宁>
| * e402ad1 - 修改pmt1002文件 <张尔宁>
| * 48faf83 - 增加pmt1002文件 <张尔宁>
|/
*   2110b32 - 项目pmt1003完成,合并回master <张尔宁>
|\
| * dad3119 - 修改pmt1003文件 <张尔宁>
| * c69f390 - 增加pmt1003文件 <张尔宁>
|/
*   43fe646 - 项目pmt1001完成,合并回master <张尔宁>
|\
| * 43a7407 - 又增加了一项功能 <张尔宁>
| * 757c89e - 增加某功能 <张尔宁>
|/
* 0774825 - GitCorp Flow Demo Initial Version

也可以仅查看在master分支上的提交

$ git log --graph --abbrev-commit --pretty=format:'%h -%d %s <%an>' --first-parent
* 6f6cb8f - (HEAD, origin/master, master) 项目pmt1004完成 <梁山>
* b42d61d - 项目pmt1002完成,合并回master <张尔宁>
* 2110b32 - 项目pmt1003完成,合并回master <张尔宁>
* 43fe646 - 项目pmt1001完成,合并回master <张尔宁>
* 0774825 - GitCorp Flow Demo Initial Version

代码发布

与传统软件的版本发布不同,我们每周都会有新的版本发布上线。而各事业部的网站上线策略不完全一致。实际情况,我们每天都会有新的版本发布上线,一天多次。

为了和现有的系统结合,版本号续使用YYYY_WW_patch格式的规则。

初始情况,betaga都指向0774825,标签为RELEASE-2012_01

$ git tag -a -m '初始标签' RELEASE-2012_01 0774825

有两个程序负责代码的分发和版本的切换 * deploy * release

其中release程序会自动更新beta/ga两个分支的HEAD位置。

BETA预发布

举例我们想要将master分支上的b42d61d部署到预发布环境,下一周版本为2012_02

$ deploy b42d61d 2012_02 # deploy ($COMMIT) ($VERSION)

部署程序deploy自动选择合适的tag打在b42d61d上,并返回新打上的tag。例如

$ git tag RELEASE-2012_02 b42d61d
$ git push --tags origin

我们得到标签RELEASE-2012_02。然后发布程序release切换预发布的版本

$ release BETA RELEASE-2012_02 # release ($ENV) ($TAG)

GA正式发布

如果标签RELEASE-2012_02对应的版本在预发布环境运行稳定,经测试通过可以升级到GA环境。这时只需要执行切换版本的操作。

$ release GA RELEASE-2012_02

自定义版本的发布

在发布到BETA、GA能够访问到的环境前,我们还有通过cookie选择版本的发布机制。例如我们要将master分支的6f6cb8f发布到这个环境。

$ deploy 6f6cb8f 2012_02 #

我们得到新的标签RELEASE-2012_02_01

$ tag RELEASE-2012_02_01 6f6cb8f
$ git push --tags origin

hotfix

生产环境的bug需要在hotfix分支上修复,然后合并回ga或beta分支,这样可以及时发布这个修复而不意外将其他内容带到生产环境。