SCM for git-repo

Git 复合工程的 SCM 计划方案

Author

  • Marcus Tang
  • tmc9031@gmail.com

开发模式

现状分析

现有的大型软件项目是由不同相对独立的模块组合而成,而不是单一的个体。而开发模式,一般是基于产品,即一个产品的项目开始和结束,通常会经过“软件资源的汇聚,修正,再稳定”几个过程。

然后,再一个新的产品需要推出,又要重复这一过程。当然,如果这一循环是相互无关的,那没有问题;但是,往往新产品是基于原有项目的提高改进,并且在同一时间新旧产品往往还需要同时维护,会导致软件资源的分离和不可控制。

如果,这个循环的次数越多,每次软件资源的汇聚,都代表着共有资源的分散,也就是说它们无法稳固在一个单纯的状态里以持续的改进,同时开发人员无法确切知道这些共有资源在不同项目产品之间的差异是什么,并且每次都在从头开始,降低了软件开发的效率

改进意见

总之,现有部署中一般是以开发的设备产品为单位建立软件仓库的,所以我们需要另一种方式,来提高共有软件资源的利用率。

基于以上讨论,可以从每个软件资源的角度进行汇聚,以达到网络设备由独立的模块组合而成的特点。

建议,引入开源的repo管理工具,把设备产品为单位的项目拆分为软件资源为单位的独立仓库,并用xml的标记方式把软件仓库组合为设备产品。开发人员就可以通过repo管理工具获取到设备产品的所有仓库集合。

以下是使用repo管理公共模块和独立模块组合成项目产品的开发模型:

公共模块:        c1    c2    c3
                |    /  \   |  \
                |   /    \  |   \
[项目产品]:      td-s     td-l    fdd
                |          |      |
独立模块:        s1        s2     s3

另外,repo需要与版本控制工具git进行配套使用。git与在使用的svn对比有一些使用上的差异,但是git有许多先进的特性,所以使用repo+git也是现有条件下最优的选择。

审核模式

现状分析

现有在软件开发的审核阶段(即code review),使用的是reviewboard的web服务

网络架构

软件仓库(SVN服务) <<---  reviewboard审核web服务
    |       ^            ^
    |       |           /
    |       |          /
    V       |         /
    开发人员(SVN工具)

开发人员把仓库的修改结果先提交到reviewboard上,leader再二次确认,就可以提交入库。但是,这个流程里有许多漏洞和问题:

  • rb与软件仓库并没有有实质上的关联,仓库的读写权限并不都它控制,也就是说开发人员也可以直接推送修改入库
  • rb不支持二进制文件的提交,开发人员只能采取直接推送入库
  • rb并不能保留Linux系统的其中一个文件属性“可执行”,实际开发中,常常发现修改结果没有达到预期都与该问题有关,并且解决方法也是直接推送入库(这也助长了开发人员绕过审核的可能)
  • rb根本不能保留svn merge的合并信息,并且svn status里add状态的文件,经过post-review命令不会产生补丁文件提交

改进意见

所以,为了提高开发中对软件仓库的监管,建议引入gerrit的web服务,它对于软件修改的审核是全面和强制性的。

开发人员的提交必须通过leader在gerrit的web上审核通过,才可以入库。以避免使用reviewboard审核服务过程中,一些弥补问题的中间操作给软件仓库和测试验证带来的影响。

整个改进方案

所需资源清单

  • Git版本控制工具
  • Repo仓库管理工具
  • Gerrit代码审核服务
  • Jenkins自动构建服务【可选】

部署架构

网络架构

Gerrit仓库+审核服务(sshd服务)  --->>   Git备份仓库【可选】
    |       ^
    |       |
    |       |
    V       |
开发人员(Repo+Git工具)

简要流程

  1. Gerrit服务器本身就保存着git仓库,同时可以再简单配置镜像服务器以备份git仓库
  2. 管理员通过webUI配置这些git仓库的访问权限
  3. Gerrit通过内置的sshd服务提供client端获取代码
  4. client端本地修改后推送至Gerrit服务器进行code review,通过后直接入库

部署优势

架构简单,可靠
集成了仓库管理,权限配置,代码审核并入库等功能

工作流对比

开发人员简要流程

初始化manifest.git清单库

$ repo init -b <branch>

依据manifest.xml清单来clone所有git库

$ repo sync

新建本地分支,开始工作

cd <work_dir>
$ repo start <branch> .

任务完成后,本地提交

vim <source_code>
$ git commit -a

推送review

$ repo upload

拉取更新update(与依据xml清单clone命令相同)

$ repo sync

迁移问题

版本控制工具对比

SVN

特点

To:self

目录树式的版本控制,基于最新version开发
version信息存储在每个子目录的.svn文件夹

To:developer

本地操作符合一般的思维,即:checkout版本,本地修改,checkin修改

问题

To:leader

只关心最新版本,但一般最新version是不稳定的,导致开发团队受到不可预知的干扰,影响任务进度
即便最新version看上去是”干净的”,但往往历史是通过添加和删除构成的,难以回溯
项目有必要的上游更新时,很难在最新version上合并改动,延误响应时效
使用的SVN的场景,新项目立项时,需要重用原项目的某些模块,很难导入它们的历史,只能是对应最新version的某一快照

To:developer

虽然允许子目录下svn update,但会使本地version处于一个无法确定的状态
如果任务有并发,本地修改的状态往往会相互干扰,分离困难
尤其review阶段,本地修改的状态要保持住,才能避免review不通过继续修改时受到其他代码改动干扰的情况,这样也阻碍了任务并发
常常无法确认的本地version状态,采取重新checkout版本的方法,时间代价高
协同开发切换branch时,开发人员一次删除本地version再重新检出,并回到工作状态就可能半天过去了
当svn的不同分支需要相同的提交时,只能手动修改再提交,并且一定时间后很难确认是否一个分支包含了某次改动,除非还是手动打开原文件确认

Git

特点

To:self

内容式的版本控制,基于repository历史开发
repository信息仅仅存储在一个项目根目录的.git文件夹
特有的stage暂存区,有利于抽出需要提交的差异,避免编译结果进入版本控制等

To:admin

创建和删除branch代价几乎为0,不会对repository服务器带来过多存储负担

To:developer

本地就是repository,可以游走于任意的版本
只有创建本地branch,可以分离不同任务的修改状态

问题

To:developer

操作灵活多样,起初使用时,可能并不清楚git工具对本地repository做了什么
本地repository操作时,要同时考虑到可能带来的影响,但这让repository变得可控

情景分析及工作流示例

注册用户

To:admin

gerrit采用http认证模式

管理员注册流程

$ htpasswd ~/review_site/etc/http.passwd admin
# 登录gerrit页面,添加ssh公钥,点击continue
$ ssh g2admin gerrit set-account admin --add-email admin@example.com --full-name admin@example.com

开发人员注册流程

[管理员操作]
$ htpasswd ~/review_site/etc/http.passwd user1
# 登录gerrit页面,直接点击continue,仅完成账号激活
$ ssh g2admin gerrit set-account user1 --add-email user1@example.com --full-name user1

[开发人员操作]
$ ssh-keygen    # 在本机生成ssh密钥和公钥
# 从管理员获取http账号密码,登录gerrit页面,添加ssh公钥,即可

新建仓库

To:admin

导入现有仓库

To:admin

仓库权限配置

To:admin+leader

Need:

一般的职能构成是一个部门由几个小组,没有小组有一位leader
需要配置哪些project只能由哪个小组可以访问,并且哪些project可以由几个小组可以访问
这些访问关系只能有一位部门管理员统一配置

Support:

项目层面采用access权限列表

支持project的权限继承自指定的ACL(未指定使用默认)
支持ACL之间的继承,特殊情况可以设置exclusive标志覆写继承的权限

用户层面采用group组管理权限

支持指定某些成员为一个group
支持设置group的所属关系,Owner可以添加删除组成员
支持设置group的包含关系,grpCD1,grpCD2等可以同时属于grpCD,然后grpCD和grpPD又可以同时属于grpALL

Example:

基本思路

指定多个user为group,在project的ACL里添加group的所需权限,避免为单独用户修改project的ACL
尽量不修改project的ACL,而是使用ACL的继承关系定义好一些预制的group,使权限需求相同的project继承自该ACL模板即可

admin可以把一个部门的开发人员定义为一个group,再在相关的project为该group打开read访问权限

配置core开发人员组的附加权限

Reference:     refs/heads/*
Create Reference
Push        # opensource only

Reference:     refs/tags/*
Push Annotated Tag

预配置权限的工程模板

$ cat groups
# UUID                                      Group Name
#
00ea643    leaderA
8495079    coreA
9163180    grpA

$ cat project.config
[submit]
    action = fast forward only
[access "refs/*"]
    owner = group leaderA
    read = group grpA
[access "refs/heads/*"]
    create = group coreA
[access "refs/tags/*"]
    pushTag = group coreA

邮件系统整合

To:admin

Need:

针对一组project设定email通知的用户组
开发人员upload提交后,自动触发一组project预配置好的用户组/用户进行review
另外,开发人员在必要的时候,也可以自定义email通知和review事件

Example:

email通知

工程级别的配置

$ vim project.config
[notify "leaderA"]
    email = group leaderA

review事件

开发人员手动添加review人员

开发人员自己upload提交后,添加leader组到”Need Code-Review”列表,然后点击”Publish Comments”,即可进入review人员的”Incoming reviews”列表

审核人员手动成为review人员

处于open状态的提交,任何人有权限进行review的,就会自动成为该次提交的Reviewer,并进入”Incoming reviews”列表

repo配合review

开发人员配置本机repo upload时,默认的review人员(仅uplaod时生效)

$ git config --global review.<review_url>.autoreviewer <lead_name>

开发人员每次repo upload时,手动指定review人员

$ repo upload --reviewers=<lead_name> [--cc=<other_email>]

开发人员已经upload后,通过ssh命令添加review人员(或者在web页面添加)

$ ssh g2user1 gerrit set-reviewers <Change-Id> --add <lead_name>|<group_name>

review与email

  • 提交者一旦添加review人员,就会发email通知该review人员,提交者Abandoned这次提交同样会通知该review人员
  • 该review人员的review动作,也会触发email给提交者和其他review人员
  • 默认review动作不会发email给自己,可以在”Settings -> Preferences -> CC Me On Comments I Write”勾选配置抄送给自己

获取仓库

To:developer

Example:

初始化repo清单列表,获取manifest.git库到当前目录的.repo目录下

$ repo init --manifest-url=ssh://user2@gerrit-vm:29418/project/manifest.git --no-repo-verify --repo-url=file:///home/gerrit/repo/git-repo --repo-branch=stable

获取repo清单列表上所有的git库

$ repo sync

更新repo清单列表,以下命令都会自动触发获取manifest.git库

$ repo init
or
$ repo sync

本地仓库开发

To:developer

Need:

一般针对单个git库的需要,repo都要能有对应的命令实现

  • 创建分支
  • 显示当前所处分支
  • 切换分支
  • 当前分支的修改与repo仓库对应分支版本的差异,类似status和diff
  • 当前分支的所有的超前提交与repo仓库对应分支版本的差异,类似指定diff的分支引用
  • 保留本地修改,拉取更新,类似fetch和pull
  • 回到repo仓库的某个tag状态
  • 显示repo仓库的不同tag之间的差异

Example:

repo sync结束后,默认没有本地branch,需要根据xml的标记来创建本地branch,开始工作

$ repo start <branch> --all

显示当前所处分支

$ repo branch
or
$ repo info

注意:避免分支杂乱的情况,可能以为在统一的分支,其实某个git库在一个其它的本地分支上
repo branch会显示所有的分支引用,所以命令返回的信息有重叠现象

切换分支

$ repo checkout <branchname> [<project>...]
or
$ repo forall [<project>...] -c git checkout <branchname>

注意:repo checkout只要有一个project检出成功就不会再提示error信息,所以更适合一开始就是repo start指定的project
repo forall -c可以有提示,但无法知道是哪个project,所以需要用repo forall -cpv

分别显示各个项目工作区下的文件差异,基于HEAD引用的提交位置

$ repo diff [--absolute] [<project>...]     # --absolute是去掉project路径
or
$ repo status [--orphans] [<project>...]    # --orphans是显示project以外的差异

当前分支的所有的超前提交与repo仓库对应分支版本的差异,基于跟踪的远程分支

$ repo overview [--current-branch] [<project>...]
or
$ repo info [--diff --local-only] [--overview [--current-branch]] [<project>...]

保留本地修改,拉取更新

$ repo forall -c git stash
$ repo sync
$ repo forall -c git stash pop

[CONFLICT]
$ repo status       # 查看冲突的project,大写字母"U"
$ cd <project>      # fix ...
$ git stash drop    # 手动清理冲突时没有删掉的临时区

回到repo仓库的某个tag状态

$ repo init -b <tag_branch>
$ repo sync --local-only [--detach]

显示repo仓库的不同tag之间的差异

$ repo forall -c git diff --stat refs/tags/<tag_name>
or
$ repo diffmanifests manifest1.xml [manifest2.xml] [--raw]

推送提交

To:developer

本地修改,局部分支法

$ repo init -b <fdd-dev>
$ repo sync --detach

$ cd fdd

$ repo start <fdd-dev> .    # NOTE: symbol "." mean current dir
$ repo branch
*  fdd-dev                   | in fdd
or
$ git co -b <fdd-dev> remotes/m/fdd-dev
$ git branch -vv
* fdd-dev 4c5d518 [gerrit/fdd-dev] Merge branch 'fdd-rel' into fdd-dev

$ vim ...
$ git ci -a
    up: make c02

$ repo upload

不等review完成,直接继续同一git库的其它任务

$ repo start <fdd-dev-b> .    # NOTE: symbol "." mean current dir
$ repo branch
 P fdd-dev                   | in fdd
*  fdd-dev-b                 | in fdd
or
$ git co -b <fdd-dev-b> remotes/m/fdd-dev
$ git branch -vv
  fdd-dev   ef9c17e [gerrit/fdd-dev: ahead 1] up: make c02
* fdd-dev-b 4c5d518 [gerrit/fdd-dev] Merge branch 'fdd-rel' into fdd-dev

$ vim ...
$ git ci -a
    up: make c03
$ git branch -vv
  fdd-dev                ef9c17e [gerrit/fdd-dev: ahead 1] up: make c02
* fdd-dev-b              c132ecc [gerrit/fdd-dev: ahead 1] up: make c03

等到上次提交的review完成,那么当这次需要upload时,先sync一下

$ repo sync
project fdd/
: git rebase --onto ef9c17e c132ecc^1
First, rewinding head to replay your work on top of it...
Applying: up: make c03

$ git branch -vv
  fdd-dev                ef9c17e [gerrit/fdd-dev] up: make c02
* fdd-dev-b              5922080 [gerrit/fdd-dev: ahead 1] up: make c03

$ repo upload

清理已经review通过的分支

# 先确认所有分支已经repo upload,即显示分支状态为大写字母"P"
$ repo branch
 P fdd-dev                   | in fdd
 P fdd-dev-b                 | in fdd

# 安全清理prune
$ repo prune

# 回到无分支状态
$ repo branch
   (no branches)

注意事项

一个分支一次repo upload的提交历史最好只用1个,最多也不要超过3个,由于gerrit需要逐个review
同一分支,永远不要用git pull拉取更新,即在本地用merge合并分叉的远程branch来更新,对于远程来说主线HEAD^会倾斜到本地还未push的提交

提交过合并的节点后,更不可以rebase后再push

Rei总结

1) 代码提交者身份

向远程分支提交代码时,先 git pull –rebase,避免把本地状态当作分支提交。

2) 分支管理者身份

进行线上分支合并时,一律使用 git merge –no-ff,保留合并时间戳。

代码审核

To:developer+leader

Submit Type:

  • Fast Forward Only 严格的快进,合并只在本地完成
  • Merge If Necessary 一般快进,不然就合并
  • Rebase If Necessary 一般快进,不然就换基

服务端的自动Merge类型对于独立的分叉提交可以很好的合并,方便提交者更新本地,但历史不够线性
服务端的自动Rebase类型对于独立的分叉提交可以很好的换基,历史线性,但不方便提交者更新本地

优先review合并提交,并从合并的依赖提交依次review;否则合并提交又因为出现分叉而无法快进时,服务端不管采用哪种Submit类型都无法很好解决,merge会产生多余的合并节点,rebase会丢失合并节点

持续并行的特性分支,如果需要主动upload一个merge –no-ff的合并节点,建议采用Merge类型,并且review时从依赖提交开始,当已经由于分叉通过服务端自动合并,就abandon提交上来的合并节点即可
不过,服务端的自动Merge类型的节点是没有Change-Id也没有显示在评审列表里的,如果需要严格通过本地创建合并节点,还是推荐采用Rebase类型,以保持历史线性
另外,持续并行的特性分支情况,还是需要新建branch,再在需要的时候通过本地主线merge特性branch生成的合并节点来upload(测试下来先submit分叉提交会有问题,还是会自动merge,所以只能牢记先review合并提交,不然不得不用ff-only类型?)

自动构建

To:developer+leader

版本发布

To:developer

To:leader

[repo发布流程]

基于release分支创建tag

$ repo init -b fdd-rel
$ repo sync --detach
[Test OK]
$ repo forall -cpv 'git tag -a fdd-v1.0 -m "release fdd-v1.0"'
$ repo forall -cpv 'git push gerrit --tags'

合并回develop分支

$ repo checkout fdd-dev
$ repo forall -cpv 'git merge --no-ff fdd-rel'
$ repo forall -c 'git ci --amend'   # NOTE: must operation
$ repo upload

[manifest发布流程]

大版本发布

$ git co fdd-rel
$ git co -b fdd-v1.0
$ vim default.xml
    revision="refs/heads/fdd-rel" --> revision="refs/tags/fdd-v1.0"
$ git ci -a
$ git push origin HEAD:fdd-v1.0

$ git tag -a "fdd-v1.0" -m "release fdd-v1.0"
$ git push origin --tags

小版本发布

$ git co fdd-v1.0
$ git co -b fdd-v1.1
$ vim default.xml
    revision="refs/tags/fdd-v1.0" --> revision="refs/tags/fdd-v1.1"
$ git ci -a
$ git push origin HEAD:fdd-v1.1

$ git tag -a "fdd-v1.1" -m "release fdd-v1.1"
$ git push origin --tags

版权声明

作者: Marcus Tang
许可证: 创作共用保留署名-非商业-禁止演绎4.0国际许可证
License: Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
本文永久链接: https://blog.tmc1900.com/scm-repo/