构造干净的 Git 历史线索

git 工作流 Rei created at
5aec84cd0b5479a0d1d89b6ffa2a9a20

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

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

  1. ProGit 中文版
  2. A successful Git branching model

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

0. 理想的历史线索

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

alt text

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

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

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

alt text

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

  1. 本来应该只有3个分支:internal,new_bbs,new_blog,但是却有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 管理将会轻松很多。

1 people marked
C6c57c07843274735d6f5dc451a203ee
updated at

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

5aec84cd0b5479a0d1d89b6ffa2a9a20
Rei

@cqpx 类似的地方

alt text

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

C6c57c07843274735d6f5dc451a203ee
updated at

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

60fbc6a9f457b3d7401a54e61e468857

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

83a5c46f52b4835e6048159bfa85478e

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

5aec84cd0b5479a0d1d89b6ffa2a9a20
Rei
updated at

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

83a5c46f52b4835e6048159bfa85478e

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

5aec84cd0b5479a0d1d89b6ffa2a9a20
Rei
updated at

@lepture

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

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

33faa1c3d66f588964f4ab75a69e5304

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

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
5aec84cd0b5479a0d1d89b6ffa2a9a20
Rei

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