是否要 git rebase -i,合并 commit log 引发的思考
2016-06-20 #Coding

image.png

前言

团队项目代码管理从 SVN 整体 切换到 Git 已经有一段时间了,小伙伴们也在不断的适应新的版本控制开发规范。

就在最近的一个项目的 code review 中,一位同学抱怨另一位跟他协同开发的 git log 太杂乱了,经常出现一些意义不明确或者重复的 commit。于是我就和他们一块查看下项目的 git log。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
commit 45e488ac83cc25c8d8d26425aac36f6c568b7e61 (HEAD -> master)
Author: XXX <XXX@xxx.com>
Date: Mon Jan 15 18:03:48 2016 +0800

Bugfix: fix add XXX menu again

commit b19ad90984bde09e69ea10697801709b636b0bfc
Author: XXX <XXX@xxx.com>
Date: Mon Jan 15 17:42:26 2016 +0800

Bugfix: fix add XXX menu

commit 47136b590137dd1c34a378ce9713dada9a554cb8
Author: XXX <XXX@xxx.com>
Date: Mon Jan 15 16:03:41 2016 +0800

Feature: add XXX menu

commit 36579b1de689d029b158d3919cace6147ac4b784
Author: XXX <XXX@xxx.com>
Date: Fri Jan 5 18:34:28 2016 +0800

Feature: add XXX modal

从 log 中能看出来,这名同学在提交某块功能代码时候经常出现反复修改的情况,具体看了下提交的内容,有一些还真是因为粗心导致的补救型提交。这位同学也表示粗心确实不应该,只是放着问题也不能不补救提交下啊,那么要如何合并一些意义不大的 commit 那?

至此,也就引到了 git rabase 上来了,合并 commit 需要用到 git rebase -i

git rebase -i

如上面的例子,获取下简略 log:

1
2
3
4
5
➜ git log --oneline
45e488a (HEAD -> master) Bugfix: fix add XXX menu again
b19ad90 Bugfix: fix add XXX menu
47136b5 Feature: add XXX menu
36579b1 Feature: add XXX modal

需要把最近三次提交,45e488a b19ad90 47136b5 合并到一起

使用 git rebase -i,-i 是 –interactive 的简写,交互式 rebase,接下会有交互提示编辑 commit。

1
git rebase -i 36579b1

参数 36579b1 指最新一个想完整保留 commit,也就是之后所有提交的 45e488a b19ad90 47136b5 会被合并修改,虽然看似合并至47136b5 Feature: add XXX menu,但其实本身也是进行了修改。

或者可以 HEAD~3这样更直观的表达合并最近3次 commit

1
git rebase -i HEAD~3

然后进入 commit 选择界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pick 47136b5 Feature: add XXX menu
pick b19ad90 Bugfix: fix add XXX menu
pick 45e488a Bugfix: fix add XXX menu again

# Rebase 36579b1..45e488a onto 36579b1 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

上面这个界面会倒序(靠下越新)列出来指定的最近3次提交,下面还列出来一些选择命令,默认为 Pick

因为大部分情况肯定是要保留合并 commit 的,这里着重说明下 use commit 相关:

p, pick :使用这个 commit
r, reword:使用这个 commit,同时编辑 commit 信息
e, edit:使用这个 commit,rebase 执行中会暂停让用户修正 commit
s, squash:使用这个 commit,同时和上一个 commit 信息合并
f, fixup:与 squash 类似,但丢弃这个 commit 信息

这里选择使用 squash,进入接下里的 commit 信息编辑界面后还会列出来之前的 commit 信息,方便我们思考合并后的 commit 信息内容,当然也能手动忽略之前的。

1
2
3
pick 47136b5 Feature: add XXX menu
s b19ad90 Bugfix: fix add XXX menu
s 45e488a Bugfix: fix add XXX menu again

INSERT键进入编辑模式,类似上面把需要向上合并的 commit 改为 s ,是squash的简写,节省时间。

然后按ESC,输入:wq保存退出,会自动进入 commit 编辑界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# This is a combination of 3 commits.
# This is the 1st commit message:

Feature: add XXX menu

# This is the commit message #2:

Bugfix: fix add XXX menu

# This is the commit message #3:

Bugfix: fix add XXX menu again

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Jan 16 10:11:22 2016 +0800
#
# interactive rebase in progress; onto 36579b1
# You are currently rebasing branch 'feature/xxxMenu' on '36579b1'.
#
# Changes to be committed:
# modified: xxx.js
#

上面就列出来了要合并的3个 commit 信息,这里被 # 注释是掉的都会被忽略,剩下的作为 rebase 后的 commit 信息。这里就注掉最新的2个,在add XXX menu 的基础上进行修改,当然也可全删除直接写新的。

1
2
3
4
5
6
7
8
9
10
11
12
# This is a combination of 3 commits.
# This is the 1st commit message:

Feature: add XXX menu and fix bugs on XXX menu

# This is the commit message #2:

# Bugfix: fix add XXX menu

# This is the commit message #3:

# Bugfix: fix add XXX menu again

同样输入:wq保存退出,git 会自动执行,然后检查下现在的 git log

1
2
3
➜ git log --oneline
3f7bd31 (HEAD -> master) Feature: add XXX menu and fix bugs on XXX menu
36579b1 Feature: add XXX modal

修改完成,使用git push -f 强制覆盖远端(注意!一定要确定远端无其他人同时在修改当前分支,防止将其他人提交覆盖

引起的反思

虽然通过 rebase 合并了部分不必要的 commit 让 log 看起来更整洁了些,但是 git log 存在的意义就是真实的反馈代码的开发记录。尤其在多人协作时,强制使用git push -f 覆盖远端可能会造成不可预计的后果。

因此看了一些讨论,有两个观念:

  • log 历史应该真实的反馈记录,被尊重并且不应该被修改
  • log 历史应该清晰明确,方便被查阅

从哲学上讲,存在即合理,凡事都有相对的一面,不极端的去禁止或者推崇。

如何判断是否进行 git rebase -i 重写 git log

个人理解的判断标准:

  • 多人协作的分支,所修改 commit 已提交远端,绝对禁止进行重写并强制覆盖
  • 多人协作的分支,所要修改的 commit 在本地尚未被提交,允许重新 git log 但仅限于个人新增历史
  • 分支仅个人使用的,远端可以强制覆盖更新,允许重新 git log

这样适当的使用 git rebase -i 可以帮助项目的 commit log 更清晰。

参考资料