git 开发一例

对于不善于事前搞清楚一个任务如何分割的程序员来说,git 真是太方便了。比如我要解决一个事情,但是却很难将这个宏观的问题直接与 code base 里面的文件直接关联上或者说很难清楚的认识到每个文件需要做一些什么修改,因为他们彼此还存在先后关系,一般这种情况下,程序员或者能把整个逻辑想清楚再动手,这样每步干啥都很明确,但是更多的情况下,写程序也有一点“试错”的感觉,就是我想到大概这里需要加一点,那边需要改一点,还有些什么协调起来看还差一点,最后你的 workspace 充斥着各种修改,但是你还是不满意,不过经过几个尝试,你逐渐的明白了其中哪些可以分割成更小的能 manage 的任务,比如你的流程里面还有 code review,将一堆修改无头无脑的提交怕是让 reviewer 最头疼的事情了。这里讲一些“试错”过程的 git 技巧,可以帮助你解决这个过程中的反复。

混沌

我把第一个阶段称为混沌,也就是程序员什么也没想清楚的试错阶段。

分割

从混沌走出来的第一步就是分割,这个时候你大致清楚将整个问题如何划分成可以被 review 的部分了,那么面对 workspace 里面乱糟糟的修改,我们第一件事情就是分割任务。分割任务最简单的策略就是利用 branch 和 stash,在当前 branch 将可以一个子任务搞定的东西 git add 甚至 git add -p 加入到一个 commit,然后 stash 剩下的。基于此,如果你有一个相关的任务,就基于此 branch 创建新的 branch,否则可以创建单独的 tracking branch(假定你一般都需要 track 远端的 origin/master),在新的 branch 里面应用 stash,然后再剥离出来一个子任务,如此反复下去,任务就被各个 local branch 的 HEAD 所 track 了。

s - a
     \- b
         \- c

各个击破

这个阶段每个子任务可以单独的解决,更新(如完成或者提交 review 根据 feedback 修改)之后有两种选择:

  • 新的修改进入到新的 commit 里面
  • 新的修改通过 git commit –amend 融合到前面一个 commit 里面

这两种策略都会在你有一个“树”状的 branch 结构下出现一些困难,比如说我们的 local branch 在分割之后有 A 之上创建了 B,B 之上创建了 C,记现在 ABC 的 head 分别为 abc。

新的修改进入新的 commit,比如 A 上你完成了结果,多一个 commit a1,提交了 CR 之后你进入 B,然后开始在 B 里面干活。你发现你需要一个来自 A 的 working feature,这时你就得 rebase B onto A。比如 B 很简单解决了之后(添加 b1)你进入了 C,

s - a - a1
     \- b - b1
         \- c

此时 C 本质上还是 abc 三个 commit,很可能你得 rebase C onto B,这样 C 上才会有 a、a1、b、b1 和 c。问题出现在如果在 B 的 rebase 阶段出现了 conflict(比如 a1 和 b 存在冲突),那么在 rebase C 的时候一样会出现类似的 conflict,因为本质上 B 那时是 a-a1-b-b1,C 是 a-b-c 其中因为 B 的 rebase conflict,两个 b 已经不一样了,这时 rebase 的 common head 是 a,最后 C 试图将 b-c 应用到 a-a1-b-b1 上,这样之前 b 和 a1 的冲突就会重现。

使用 amend 可以减少每个 branch 的 commit 数目,但是一旦 A 进行了 amend,比如 A: a’,那么 B 和 C 已经与其有差距了,B 的 rebase 需要 resolve a 与 a’ 的差异,而 b 一旦修改,那么 C 的 rebase 又会重复 a、b 产生的 conflict。

s - a'
 \- a - b
 \- a - b - c

一个比较“假”的解决方案是使用 git rerere,他会录入每次 merge/rebase 的 resolution,在碰到相似的情况下自动的应用。比如前一种策略下,解决 B 的 rebase conflict 之后,可以顺带将 C 也按 A 进行 rebase,这时的 conflict 就会重复从而自动的解决。注意这里并不能将 C 对 B 进行 rebase,因为 B 完成 rebase 后头上的 commit 已经变化了,如此 rebase 反而不能利用 rerere。注意这里的结果很有意思,本来 B 是 A 的子树,C 是 B 的子树,B 和 C 对 A 进行 rebase 后 B、C 就分道扬镳了。

s - a'
     \- b' - b1
     \- b' - c

一种可能的解决方案是寻求一个 branch subtree 的 rebase,这样 rebase B 的时候也能顺带解决 C 的问题就更好了。一般来说是没有终极解决方案的,因为一旦 subbranch 各自又有新的 update,那是不可能通过一次 rebase 稳妥的解决的,但是如果他们尚未被更新,理论上是有保持这个结构的可能的,因为这等价于先把 C 当作 B 的 commit,rebase 之后再将 C 那个  commit 剥离到新的 branch 里面去,这种思路可以解决 linear graph,如果 C 不是 B 的唯一子 branch,这个大概也是行不通的。

看样子 rebase 要真正跟人们编辑的习惯对应起来有一定的距离啊。

—————–
And Leah said, A troop cometh: and she called his name Gad.

Advertisements
git 开发一例

git 最近感悟

这次 scrum 里面学到了一些 project management 的东西,暂记于此。部分内容来自这个 tutorial 和这个流程介绍

如果你有一个 release 周期的话…

曾经以为类似 gitflow 这类 workflow (见此讨论)对于稍微大一点的 team 才适用,其实某些原则对很小的 team 一样是 make sense 的。比如,devs 应该 push commits 到一个单独的 dev branch,而 release 的版本应该出现在另一个 branch 上(比如 release)。至于为什么,很多 CD 系统都有一个“截断”过程,比如可以 approve 某些 commit,之后的就会被拦截起来,那么看起来似乎就能够解决 release 的问题了,大家纷纷开发,然后某天 manager 说不错现在可以发布了,于是 approve 让当前版本进入 production。

这出现的基本问题就是一旦出现 bug,如何修正?如果只有一个 branch,那可好,万一有人 push 了新的 commit,那么 bug fix 这个 commit 就会在其后出现,因此进入 production 的是新的 commit + bug fix,这样新的 commit 又可能带入新的 bug… 这时你就意识到,一个 branch 是不够的了吧。因此 best practice 是 devs 只能 push to dev branch,manager 决定什么时候(往往是 feature complete 的时候)进行从 dev 到 release 的 merge。这样做几个好处:

  • devs 不会因为需要 release 而不能及时的 push(为了避免前面单线开发的那个情况势必需要等到下个 release 开始才适合 push)
  • manager 可以在 merge 的时候通过 non-fast-forward merge 的 commit message 记录一些事情,比如什么 feature 发布了等

Okay 如果你说我们就是单线的作风怎么办?那么取消他人的 commit 可以用 git revert 将 bug fix 前面的那么一两个 commit 去掉,等 bug fix 进入 production,再 revert 那个 revert 的操作就行了。

如果你有一个 code review 流程的话…

使用 git 很大的好处是可以很方便的进行 code review,通常你可以建立很多 local branch 以便你完成一项任务之后提交 CR 然后切换到另外的 branch 做另外的事情。一般性的原则有:

  • 短期开发使用 local branch 即可,如 bug fix、小 feature(几天的),注意不同类型的问题应该使用不同的 tracking branch,如 bug fix 应该对 release 或者专门的 hot fix branch 来做;而小 feature 应该考虑起简单明了的名称 track 最新的 dev branch
  • 长期开发使用单独的 remote tracking branch,这是因为 local commit 需要在远端进行备份以免丢失,而在这个 topic 完成之前应该避免进入 release 或者 dev 影响别的开发

往往 CR 意味着同一段代码上反复的迭代,这可以利用 git commit –amend 将头上尚未 push 的 commit 反复的修改,同时 git 也提供了重写 commit 的功能(见 magit),如果是一个 CR 多个 commits,开发者还有机会在 push 之前将整个 commits 逻辑调整到一般人容易理解的顺序。

某些 fork workflow 里面常提到的 pull request 和 CR 有点像,但是发生的环境往往是一个开发者完成了一个 feature 后希望 maintainer 从自己的 public repository 里面获取这个 feature。这种环境下是不能通过 git commit –amend 来纠正问题的(因为已经完成了 push),为此新的代码只能通过新的 commit 让对方看到。

merge 还是 rebase

从某种意义上来说 merge 和 rebase 的区分不是那么明显,似乎都是把一个 branch 的东西放进另一个,但是语义上是截然不同的,也应该尽量避免混用:

  • merge 的潜台词往往是在 trunk 上需要 merge 一个 feature branch 的新东西,merge 完之后 feature branch 就可以消失了,这个 merge 会导致 feature branch 的内容(区别于创建这个 feature branch 创建时 trunk 的内容)进入 trunk
  • rebase 的潜台词是,我在 feature branch 上,我觉得我的内容可能过时了,需要获得 trunk 上最新的内容(可能是因为我想 merge 到 trunk 了,可能是需要 trunk 上的新 feature 了,还可能是需要 bug fix 了,等等),rebase 之后我的 local commits 应该仍然出现在 rebase 的“新头”之后,这导致对应的 hash 也会被重写

发生 merge 的时候存在三种可能:

  • 当前的 head 和被 merge 进来的 branch 的曾经的 head 是一个,也就是说被 merge 进来的肯定是“新加的”内容时会进行 fast forward merge,这时可以理解成为简单的将被 merge 的 branch 里面的新 commit 加到当前 branch 的 head
  • 如果当前的 head 与被 merge 的 branch 的 head 不一样,这意味着当前 branch 又出现了新的 commit,因此无法“安全的”加入新的 commit,这时会做 recursive merge (3-way merge),这个 merge 如果成功就产生一个 merge commit message
  • 一旦不成功,也就是说两个 branch 至少对一段代码做出了不同的修改,那就需要 resolve conflict,这往往会在 git status 里面显示为 both modified,文件里面出现 >> == << 表示的不同版本,开发者需要选择其中一个或者重写这部分使得它合乎要求,之后 git add 并 git commit 得到 merge commit message

而发生 rebase 的时候类似也可能出现 conflict,这同样需要 resolve(编辑后 git add 即可),但是 rebase 同时提供了一个修改(message 甚至 commit 内容)的机会,某些 branch 上如果意料之中出现很多的 conflict 的话,应使用 git rebase -i 这样在 replay local commit 的时候可以判断哪些 commit message 需要改动,碰到某个 commit 出现 conflict 之后修改、git add 就可以 git rebase –continue。如果决定放弃 rebase 可以 git rebase –abort 回到 rebase 开始前的状态。

使用 rebase 最大的好处在于 commit history 会好看很多:比如大家都在 push 的 dev 上,如果大家选择的是简单的 merge,结果往往是 dev 的 history 显得错综复杂,而 rebase 会使得最后的 history 仍然是单线条。为什么?很简单,rebase 之后的 feature branch 里面的 commit 只会出现在头上,这意味着进行 merge 的时候一定是 fast-forward merge,而如果直接 git fetch/merge (从 tracking branch 或者当地的 trunk)到 feature branch 就会产生 merge commit,当这个部分 merge(push 到远端的 branch)时,这个 merge 产生的交错情况就会进入 commit history。

简化 rebase 最常见的就是直接 checkout -t -b feature_branch remote/dev 建立自己的 feature branch,之后 git pull –rebase 即可。

为什么每次我 rebase 都需要 resolve conflict?

什么时候你会碰到这个问题?其实短期的 local feature branch 不会碰到这个问题,因为你很可能就最后 merge 前进行 rebase,碰到 conflict 就 resolve 之后 push 了。因此碰到这个问题的一定是需要反复 rebase 的长期 topic branch。比如一个 topic branch 可能会开发一个季度,里面除了最后 rebase(如果你希望看到不错的 history 的话),中间还会有从 trunk 里面获得 fix、new feature 的各种情形,因此反复的 rebase 是不可避免的。

每次 rebase 的时候因为相当于是重放 local commits,那么势必可能多次(每次 rebase)与以前的编辑冲突,一个简化这类 conflict resolution 的手段是使用 git-rerere(reuse recorded resolution),这个会在 merge/rebase 等操作里面留下 record 的钩子,今后碰到一样的问题就会照葫芦画瓢了。

这里有个问题是如果你希望将这个 branch push 到另外的 remote branch 备份,那是否应该从 trunk 来做 rebase?感觉答案是否定的,因为 rebase 重放后 local commits 的 hash 就会与 remote branch 不同,merge 后就会出问题(出现重复的 commit),似乎这种情况下还是 merge 比较稳妥。

多个 branch 很要命吗?

其实初用 git 的人都会问这个问题,答案尽管是否定的,你可能还是不大敢创建一堆 branch 切换来切换去。但其实应该明白,只是新手不知道怎么处理切换时候碰到的问题:

  • 没啥修改,有 commit,那就放心吧,直接 checkout
  • 有修改,你有两个选择,或者 add 然后 commit 了,回来做 commit –amend 或者 git reset –soft HEAD^ 将这个 commit 取消掉或者直接 stash 回来再 stash pop

当然 stash 功能比较强大,你可以把 stash 在 branch 之间应用,比如你在某个 branch 里面多做了一些工作,临到提交 code review 了,这部分想继续下去,但不想 CR feedback 来了之后影响这部分工作的进度,那么可以将 CR 的部分 commit 之后 stash 其余的工作,然后建立新的 branch,在上面应用 stash,并且继续干活,顺便等 CR。

当然频繁的切换 branch 出现的问题是编辑器打开的文件怎么办?有的编辑器比较大胆,一旦文件被其他进程修改了,直接 reload;但是像 emacs 这种比较谨慎的编辑器就会问你是否 reread,设置 global-auto-revert-mode 是个不错的选择。我比较喜欢谨慎一点的,一次误操作可能让你硬盘上的文件丢失(git reset –hard 往往是万恶之源)那么 emacs buffer 里面的东西就是你最后的救命稻草。

最后一个小问题是创建的 branch 不用了就赶紧删掉吧 git branch -d 可以删掉,remote branch 可以用 push repos :branch_name 来删。

我想要非线性的 merge…

这个问题出现在比如我们使用 gitflow,已经准备 release 的情况下,突然决定把 dev 上的某个新完成的 feature 加入,或者 dev 上已经出现了当前 bug 的 fix 了,可是离那个 commit 中间还有好几个 commit 怎么办?这时有几种方法:

  • git cherry-pick 将对应的 commit 切出来加在本 branch 上,这会导致一个新的 commit
  • git format-patch + git am,前者构造 patch 后者写入当前 branch 并导致新的 commit
  • git format-patch 和 git apply,后者不产生新的 branch 而是要求编辑,之后手动 commit

一般说来 cherry pick 还是会导致重复记录(如果后来两个 branch 进行了 merge),慎用(经典的用例是 pick 之后,删掉被 pick 的 branch,也就是说一个 feature branch 几个 commit,但是有用的可能就一个,于是 pick 那一个之后这个 feature branch 就可以删掉了)!

—————–
And Jacob said unto Laban, Give me my wife, for my days are fulfilled, that I may go in unto her.

git 最近感悟

magit

看名字大家就知道这玩意是 emacs + git,其实 emacs 通过自己的 version control 系统就能简单的控制很多版本控制系统,但是 git 的高级功能就没法用了。magit 填补了这一空白,安装其实很简单,大约就是让 emacs 把下载的 el 文件编译好,放到 .eamcs.d 这类目录下面,如果需要路径设置加上 add-to-list ‘load-path 即可。在 .emacs 里面通过 require ‘magit 载入。

之后我们可以通过打开 magit-status 这个 buffer 进行 git 操作。详细的见这里,想看看现场版的看这里。就说几个好处吧

  • 以前 local branching 的时候所有的文件需要重新打开,现在不需要了,我土死了
  • merge/rebase 都能做了,还能 push
  • 可以 rewrite log 虽然看起来还是复杂了一点,不过现在这个版本有提示功能更喜欢了
magit
magit

嗯,很可惜内部对 emacs 的支持比较有限,在这个基础上加上 code review 应该不难,不过很可惜居然内部都没有 build system 的 emacs 包,每次还得到命令行下面解决,太不给力。可能 eclipse 是王道吧…

——————-
Bring me venison, and make me savoury meat, that I may eat, and bless thee before the LORD before my death.

magit