构造干净的 Git 历史线索

原文转载自: http://codecampo.com/topics/379?comment_id=1354#comment-1354

用 Git 也有一段时间了,看过一些 Git 工作流的文章,加上工作和业余中参与一些项目开发,对 Git 的工作流有一些心得,写下来整理一下。

如果你对 Git 并不是很熟悉,推荐两份阅读资料:

  1. ProGit 中文版
  2. A successful Git branching model
  3. Understanding the GitHub

本篇文章是基于中心式的代码管理,但如果你理解其内涵,会发现这跟一般的 github 托管的开源项目是兼容的,只要把每个 fork 都当成特性分支,而项目的发源地是中心。

0. 理想的历史线索

首先看一下这个流传很广的图(取自 A successful Git branching model

alt text

这确实是一个理想的版本分支控制流程,至少它有以下优点:

  1. 分支成线性
  2. 可追溯某 commit 什么时候被合并进主干

但是,实际中如果不加注意,很容易变成这样:

alt text

这个历史线索有两个很糟糕的地方:

  1. 本来应该只有3个分支:internal,newbbs,newblog,但是却有5条线索
  2. 分不清哪条是主干(你能看出吗?上数第3个commit连接的红色的那条才是)

实际上这种情况的发生很普遍,如果你自己在维护一个项目,并且有多人参与开发,可以用 gitg 这个工具打开项目目录看一看。

下面将会分析图中问题产生的原因和解决方法。

1. 不要产生多余的分支

留意到上一节的糟糕例子中,有几条 merge 信息是这样的格式:

merge <branch> of <source_url> into <branch>

这类 merge 是在开发者向 git 源 push 的时候发生冲突,然后运行git pull的时候产生的。git pull 确实很便利,但是却产生了副作用:一个特性分支变成了两个。

git push # 冲突
git pull # !产生额外的分支

alt text

如果你认为每个 git 源都是平等的,而且每个开发者都是平等的,这没什么问题。不过如果需要一个中心式的开发历史,这些本地分支的合并信息就会成为干扰。

解决这些多余的 merge 信息的解决方案就是—— rebase。

git push # 冲突
git pull --rebase
git push

pull --rebase 可以基于远程分支重写自己的本地 commit,从而产生干净的线索。

alt text

有人可能会提出异议,pull --rebase 丢失了并行开发的这一信息,没错,这取决于你的项目开发是怎样的管理。如果你跟我一样喜好干净的分支线索,那么同一分支的提交应该用pull --rebase。确实需要保留分支线索的,应该另外开一条分支,而不要pull

注意,rebase 某些时候是个危险的工具,因为它实际改写了你的 commit,千万不要 rebase 一些已经提交到公共源的 commit,详情可以看 progit 中 分支的衍合 这一章节。

2. 避免线索“扭麻花”

依然是这个图

alt text

除了多出的分支外,还有一个严重的问题,就是主干线索(internal)被痛苦的扭到了一边。如果这种情况反复出现,各个线索就会像麻花一样扭在一起。

现在来重现一下这种扭麻花出现的原因:

1) 开发者以本地的 master 为基础新建了一个分支 feature A,以此工作,中途更新过 master,然后在工作完成后 merge 回 master 分支。

alt text

2) 但是向线上的 master push 的时候发生了冲突,因为线上的 master 比自己本地的 master 新。

alt text

3) 然后,开发者执行了 git pull

alt text

4) 最后,开发者愉快的 git push ,线上的 master 分支将会和本地一样

alt text

这样会导致什么问题呢?问题就是如果某个 commit 有 bug,你将无法查证这个 commit 被归入主干分支的上线时间。例如上图的红色色块,看起来是在最新的 merge 之后才进入 master 的,但是它在之前已经进入 master 了。作为 master 分支(甚至任一分支)的管理者,肯定不希望分支的主线索一直在开发者的本地线索中来回切换。

解决方法是,一定要在 merge 之前,将主干分支更新到最新状态。

git checkout master
git pull
git merge feature
git push

如果你事先把 master 更新,线索就会是这样的:

alt text

bug 点在什么时候进入主干分支一目了然。

3. 线上分支合并一定要用 merge --no-ff

假设你已经知道,git 中的合并默认是快进合并(fast-forward)

alt text

线上分支间的快进合并会产生一个问题,就是丢失了合并的时间戳。试问上图的红色有 bug commit 是什么时候进入主分支?

如果每次分支合并时加上 --no-ff 参数,就可以避免信息的丢失

alt text

bug 进入主干的时间将很清晰。

4. 总结

假如上面的规则让你感到混乱,那么下面整理两个法则,分别适用于两种身份。

1) 代码提交者身份

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

2) 分支管理者身份

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

不要混用两种身份,一边进行代码提交,一边进行分支合并。这样你的 git 管理将会轻松很多。

 4

评论

  • avatar

    还好我往master merge的时候都要先更新。。
    我刚才试了试gitg,挺cute的,可是仔细一对比发现不清晰,我repo里这么简单的一个操作就有绕麻花的迹象了
    alt textalt text
    我很好奇你的repo用qgit打出来会不会clean一点儿,能不能发上来看看。。

  • avatar

    @cqpx 类似的地方

    alt text

    分支多不是问题,怕是线上状态和本地状态互换才是扭麻花。绘图的依据在于 merge 节点的 parent node 哪个在前面,如果 pull 的时候产生 merge,本地的节点就作为第一个父节点。

  • avatar

    大部分都清楚了,我还是继续用qgit。。看起来比较容易。。

  • avatar

    其实我觉得,单人项目。。线性推进比较好。。

  • avatar

    在还没有 tag 0.1 之前,怎么办呢?

  • avatar

    @lepture 这里未涉及 tag 管理。如果没有发布版本的需求的话,只 merge 不打 tag 就行了

  • avatar

    我的意思是指,在未有tag前,其实master相当于develop。如上图所示,是没有一个指导性方案的。处于这样一个阶段,应该如何处理分支呢?

  • avatar

    @lepture

    这个还是看项目的需求,像 codecampo 这样,一提交就上线,没有什么严谨数据的,就单master工作。有时也会为了尝试增加某个功能而开一些特性分支。

    如果项目很严谨,关系重要数据,并且要长期维护,就至少需要 master + developer,起码要知道特性上线时间。然后根据工作量和去分 features 分支。

  • avatar

    今天又有些概念不清来读了遍。不过好像有两个地方需要小改动一些:

    merge <branch> of <source_url> to <branch>
    我想应该是
    merge <branch> of <source_url> into <branch>
    

    你在ruby-china的讨论:http://ruby-china.org/topics/112 里面二楼回复也是这样写的。

    git check master
    我想应该是
    git checkout master
    
  • avatar

    @reyesyang 恩,我写错了。按你说的改了。

  • avatar

    =============『2. 避免线索“扭麻花”』
    =============2) 但是向线上的 master push 的时候发生了冲突,因为线上的 master 比自己本地的 master 新。

    这个图是表示origin/master和local/master合并过了的??

    =============如果你事先把 master 更新,线索就会是这样的:

    这个图为什么feature A会有第三个commit?不是merge commit在master线上么??

    最后一个问题,在合并分支的时候,
    1. 首先在local/master pull最新的(与origin/master同步)
    2. 然后merge --no-f OneBranch
    3. 最后 push origin master

    要是在push之前,又有人更新了origin/master,此时我在local/master上push会被reject,这个时候是用git pull --rebase去解决conflict么??
    感觉rebase虽然让分支清晰,使用起来考虑的东西确实太多

    谢谢!

  • avatar
  • avatar

    回复的table宽度不对吧

  • avatar

    @final0pro 我这篇文章对身边人的推广效果是失败的,一开始用 rebase 就会把 push 搞得一团糟,后来我就开始推广不要 rebase 了,除非清楚自己在做什么。

    推荐看 Pro Git http://git-scm.com/book/zh ,比我说的清晰多了。

  • avatar

    @Rei

    自己又实践了下,还是同样的问题

    在合并分支的时候,
    1. 在local/master,pull最新的(与origin/master同步)
    2. 在local/master,执行merge --no-f OneBranch
    3. fix some conflicts(这个时候有人push新的东西到了origin/master)
    4. push origin master

    所以就是
    1. [master] git pull
    2. [master] git merge --no-ff one_branch
    3. // fix conficts(at this time, one guy push new stuffs to origin/master)
    4. [master] git push
    5. //rejected
    6. [master] git pull
    8. [master] git push

    这样git竟然会生产一个新的commit,感觉很奇怪,branch感觉不易读

  • avatar

    楼主是否知道最开始的那张流程图是用何软件来绘制的?看着很舒适

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值