Git subtree 要不要使用 –squash 参数

上一篇文章 中把 Snoopy 理顺了, 其实 Gregarius 使用的是 MagpieRSS, 而 MagpieRSS 又使用了 Snoopy, 是一个两层的包含关系。

Git submodule 的繁琐似乎是世人皆知了, 所以我用 subtree 来解决上面的包含关系。即: 在 Gregarius 中以 subtree 的方式管理 MagpieRSS, 然后在 MagpieRSS 中以 subtree 的方式管理 Snoopy。

问题的产生

subtree 处理多层包含是没有问题的,因为包含进项目之后, 别人根本看不出这是一个 subtree, 所以它本质上还只是管理本地 repo 的一种方法。

使用 Git subtree 新建或更新子项目的时候,可以选用 --squash 参数, 它的作用就是把 subtree 子项目的更新记录进行合并,再合并到主项目中。

所以,在使用 --squash 参数的情况下, subtree add 或者 pull 操作的结果对应两个 commit, 一个是 Squash 了子项目的历史记录, 一个是 Merge 到主项目中。

这种做法下,主项目的历史记录看起来还是比较整齐的。 但在子项目有更新,需要 subtree pull 的时候,却经常需要处理冲突。 严重的,在每次 subtree pull 的时候都需要重复处理同样的冲突,非常烦人。

如果不使用 --squash 参数,子项目更新的时候,subtree pull 很顺利, 能够自动处理已解决过的冲突,缺点就是子项目的更新记录“污染”了主项目的。

原因分析

简单说,subtree add/pull 操作中,需要用到 merge,而 merge 顺利进行的前提, 是要有相同的 parent commit。对照上面的情况:

使用 --squash 参数,原子项目历史记录被合并后就消失了,相当于一个“新”的提交。 下次再进行 add/pull 时,新添加的内容找不到“上一次的修改”, 于是在更新 subtree 内文件的时候,就会提示冲突,需要手工解决。

不使用 --squash 参数,原子项目的历史复制到了父项目中, 下次再进行 add/pull 时,新增的 commit 能够找到“上一次的修改”, 那么他会像在子项目中逐个 am patch 那样更新 subtree 下的内容, 不会提示冲突。

注:我使用的 Git subtree 是 PPA 上的一个 旧版本 , 或许新版已经解决了上面的问题。

解决问题

就像 这篇文章 结尾说的那样,是否使用 squash 都是可以的, 但需要在开始阶段作出选择,并 一直坚持下去 。 如果一会儿用一会儿不用,得到的不是两者的优点,而是两者的缺点之和。

出于个人偏好,我既希望能够比较顺利的更新子项目, 又不希望子项目的历史记录直接合并在主项目中。StackOverflow 上有人提到了 一种做法 , 就是另外建立一个分支进行 –no-squash 的 subtree 更新, 这样就保留了子项目的历史记录,没有烦人的反复冲突问题; 然后在合并到主分支(比如 master)时合并提交( git merge --squash ), 这样主项目的主分支上只会体现一个 commit, 比直接 git subtree add/pull --squash 还要简洁。

这种做法也有缺点,但在能够接受的范围内:

  • 新开分支的历史记录比较乱,无视吧
  • 新开分支与 master 分支不同步,记着每次在新开分支上做 subtree 操作之前 要 merge master

在新开分支上进行 subtree split 操作是没有问题的。 merge master 以后,subtree push 操作也没有大问题, 也许刚开始会出现 push 被 reject 的状况。

在这种情况下,可以先在本地 split 一份,比如 git subtree split -P extlib/magpierss -b test --rejoin , 然后切换到这个 test 分支,可以看到之所以被 reject , 是因为主项目的那个合并提交也被 split 出来了。 这里会麻烦一些,需要通过 rebase 操作,把这些合并的提交删掉, 换成合并内容包含的每个提交(用 pick HASH)。 成功之后,可以在这个分支直接 push 到 子项目: git push remote_of_subtree branch_on_local:branch_on_remote , 注意后面是指定将本地的哪个分支 push 进 remote 的哪个分支。 这次 push 会很顺利。 接下来再作一次正常的 subtree pull 就可以了, 下次再进行 subtree split 操作时, split 出来的临时分支和 remote 是一致的。

通过上面 push 的例子可以看出,为了 split 和 push 顺利, 即使用了 subtree 分支, 如果能在 master 分支中保存子项目历史记录还是有好处的。 同时,我们还可以参考这个来决定 subtree 使用策略:

  • subtree 里面放外围项目,只接收更新,不发送更新, 那么无论是用 squash 还是用 subtree 分支都不麻烦。
  • 将一个大项目拆分成若干小项目, 那么最好不要用 squash,并且活用 subtree, 最好是所有提交都在主项目中作, 然后 subtree split 出子项目来发布, 子项目原则上不直接修改,即和上一条相反, 只向子项目发送更新,不从子项目接收更新。 Symfony2 使用的就是这种做法。

总体上都有些麻烦,subtree 分支算不上是完美解决方法,但看起来好歹清爽了很多。

@link https://github.com/fwolf/magpierss

@link https://github.com/fwolf/gregarius

将 CVS 转到 Git 并和 Github 上 Fork 的项目合并

在捣鼓我的 Gregarius 时,发现无法读取 HTTPS 的 RSS , 追查发现是他所使用的 HTTP 客户端类 Snoopy 的原因。 想升级新版 Snoopy 却发现原作者已经几年都不更新了, Github 上倒是有人弄了几个镜像, 其中 hurrycaner 的这个 还对 README 进行了一些改进。 但所有镜像都没有 SourceForge 上的修改历史。

所以,我想作的是,基于 hurrycaner 的镜像进行 Fork, 但是要把 SourceForge 上的修改历史也弄进来。

CVS –> Git

现在应该没有人用 CVS 了把,SourceForge 也支持 Git 了, 但上面有些古老项目依然只有 CVS 。

把 CVS 转换成 Git 的工具还是有一些的,但从 一些讨论看来 似乎都做不到完美。 也难怪,CVS 的存储格式实在是有些奇怪, 代码、修改记录、修改注释都堆在一个文件中,解析起来肯定头疼。

由于害怕 cvs2git 会像 svn2git 那样转换时把作者缀上 UUID, 我先试了试 parsecvs , 但这货连使用说明都没有,放弃了。 然后用的是 StackOverflow 上最后一个人推荐的 crap 。 和上面的一样,都是简单 make 一下就有可执行文件用, 但比上面的帮助全,还有一个非常简单的例子。

这就可以开始了,先把 SourceForge 上的仓库下载下来:

$ mkdir Snoopy.cvs
 
$ rsync -av rsync://snoopy.cvs.sourceforge.net/cvsroot/snoopy/ Snoopy.cvs
receiving incremental file list
./
CVSROOT/
CVSROOT/.#checkoutlist
CVSROOT/.#commitinfo
CVSROOT/.#config
CVSROOT/.#cvswrappers
CVSROOT/.#editinfo
CVSROOT/.#loginfo
CVSROOT/.#modules
CVSROOT/.#notify
CVSROOT/.#rcsinfo
CVSROOT/.#taginfo
CVSROOT/.#verifymsg
CVSROOT/checkoutlist
CVSROOT/checkoutlist,v
CVSROOT/commitinfo
CVSROOT/commitinfo,v
CVSROOT/config
CVSROOT/config,v
CVSROOT/cvswrappers
CVSROOT/cvswrappers,v
CVSROOT/editinfo
CVSROOT/editinfo,v
CVSROOT/history
CVSROOT/loginfo
CVSROOT/loginfo,v
CVSROOT/modules
CVSROOT/modules,v
CVSROOT/notify
CVSROOT/notify,v
CVSROOT/passwd
CVSROOT/rcsinfo
CVSROOT/rcsinfo,v
CVSROOT/readers
CVSROOT/taginfo
CVSROOT/taginfo,v
CVSROOT/val-tags
CVSROOT/verifymsg
CVSROOT/verifymsg,v
CVSROOT/writers
CVSROOT/Emptydir/
Snoopy/
Snoopy/AUTHORS,v
Snoopy/COPYING.lib,v
Snoopy/ChangeLog,v
Snoopy/FAQ,v
Snoopy/INSTALL,v
Snoopy/Makefile.am,v
Snoopy/NEWS,v
Snoopy/README,v
Snoopy/Snoopy.class.php,v
Snoopy/TODO,v
Snoopy/autogen.sh,v
Snoopy/configure.in,v
Snoopy/Attic/
Snoopy/Attic/.cvsignore,v
Snoopy/Attic/COPYING,v
Snoopy/Attic/Snoopy.class.inc,v
 
sent 1,066 bytes  received 229,013 bytes  17,042.89 bytes/sec
total size is 225,573  speedup is 0.98

注意这和下载 CVS 代码是不一样的,这里下载的是 CVSROOT,仓库的原始码。

然后初始化一个 Git 仓库目录,用 crap 开始转换:

$ mkdir Snoopy.git
$ cd Snoopy.git
 
$ git init
 
$ ../crap/crap-clone /home/fwolf/dev/Snoopy.cvs Snoopy
Valid-requests Root Valid-responses valid-requests Repository Directory Max-dotdot Static-directory Sticky Entry Kopt Checkin-time Modified Is-modified Empty-conflicts UseUnchanged Unchanged Notify Questionable Argument Argumentx Global_option Gzip-stream wrapper-sendme-rcsOptions Set Gssapi-authenticate expand-modules ci co update diff log rlog add remove update-patches gzip-file-contents status rdiff tag rtag import admin export history release watch-on watch-off watch-add watch-remove watchers editors init annotate rannotate noop version
*********** CYCLE **********
Changeset  andrei
*** empty log message ***
 
    INSTALL:1.1
    Makefile.am:1.1
    NEWS:1.1
    autogen.sh:1.1
    configure.in:1.1
    .cvsignore:1.1
Deferring:
    autogen.sh:1.2
Tag 'Snoopy' placing on branch ''
Tag 'start' placing on branch 'Snoopy'
opening version cache failed: No such file or directory
1970-01-01 08:00:00 CST BRANCH
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST BRANCH Snoopy
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST TAG start
2000-02-04 00:10:54 CST COMMIT
2000-02-04 00:10:54 CST COMMIT
2000-02-04 00:28:59 CST COMMIT
2000-02-22 23:44:57 CST COMMIT
2000-03-10 04:52:59 CST COMMIT
2000-03-10 04:54:47 CST COMMIT
2000-05-18 22:50:14 CST COMMIT
2000-05-18 23:36:34 CST COMMIT
2000-05-18 23:44:00 CST COMMIT
2000-06-30 02:37:25 CST COMMIT
2000-08-23 04:36:52 CST COMMIT
2000-09-14 04:52:04 CST COMMIT
2000-09-14 22:09:58 CST COMMIT
2000-09-15 21:11:11 CST COMMIT
2000-09-16 05:57:37 CST COMMIT
2000-09-27 03:34:38 CST COMMIT
2000-09-27 04:28:45 CST COMMIT
2000-10-09 21:13:52 CST COMMIT
2001-03-25 04:15:18 CST COMMIT
2001-07-07 05:24:11 CST COMMIT
2001-08-22 23:43:24 CST COMMIT
2001-11-21 04:23:02 CST COMMIT
2002-10-03 22:38:49 CST COMMIT
2002-10-03 22:55:06 CST COMMIT
2002-10-03 22:57:39 CST COMMIT
2002-10-10 04:25:50 CST COMMITMissed first time round: ChangeLog 1.11
Missed first time round: Snoopy.class.inc 1.21
 
2002-10-10 04:41:24 CST COMMITcvs checkout ChangeLog 1.14 - version is duplicate
cvs checkout Snoopy.class.inc 1.24 - version is duplicate
Missed first time round: ChangeLog 1.12
Missed first time round: Snoopy.class.inc 1.22
 
2002-10-10 04:51:57 CST COMMITcvs checkout ChangeLog 1.14 - version is duplicate
cvs checkout Snoopy.class.inc 1.24 - version is duplicate
Missed first time round: ChangeLog 1.13
Missed first time round: Snoopy.class.inc 1.23
 
2002-10-10 04:56:14 CST COMMIT
2003-03-12 22:40:55 CST COMMIT
2003-09-15 21:58:28 CST COMMIT
2003-10-22 03:18:39 CST COMMIT
2003-11-08 03:52:58 CST COMMIT
2003-12-24 03:34:35 CST COMMIT
2004-01-08 03:16:10 CST COMMIT
2004-07-25 02:23:27 CST COMMITMissed first time round: ChangeLog 1.19
Missed first time round: Snoopy.class.php 1.5
 
2004-07-25 02:34:28 CST COMMITcvs checkout ChangeLog 1.22 - version is duplicate
cvs checkout Snoopy.class.php 1.8 - version is duplicate
Missed first time round: ChangeLog 1.20
Missed first time round: Snoopy.class.php 1.6
 
2004-07-25 08:49:02 CST COMMIT
2004-07-25 10:42:48 CST COMMIT
2004-07-25 10:46:34 CST COMMIT
2004-07-25 10:46:59 CST COMMIT
2004-07-25 11:18:32 CST COMMIT
2004-10-16 13:14:11 CST COMMIT
2004-10-16 13:17:41 CST COMMIT
2004-10-16 13:44:51 CST COMMIT
2004-10-16 14:27:09 CST COMMIT
2004-10-16 14:28:30 CST COMMIT
2004-10-16 14:40:42 CST COMMIT
2004-10-17 00:33:58 CST COMMIT
2004-10-17 00:36:18 CST COMMIT
2004-10-18 13:12:55 CST COMMIT
2004-10-18 13:18:27 CST COMMIT
2004-10-18 13:19:04 CST COMMIT
2004-10-18 13:19:28 CST COMMIT
2004-10-18 13:19:51 CST COMMIT
2004-11-18 13:51:32 CST COMMIT
2004-11-18 13:52:28 CST COMMIT
2004-11-18 14:37:05 CST COMMIT
2005-02-03 12:43:26 CST COMMIT
2005-02-03 12:57:05 CST COMMIT
2005-10-23 10:08:40 CST COMMIT
2005-10-23 10:16:26 CST COMMIT
2005-10-24 00:30:34 CST COMMIT
2005-10-24 23:34:50 CST COMMIT
2005-10-24 23:44:12 CST COMMIT
2005-10-24 23:44:59 CST COMMIT
2005-10-24 23:46:10 CST COMMIT
2005-10-30 13:33:15 CST COMMIT
2005-10-30 13:45:09 CST COMMIT
2005-10-31 02:32:42 CST COMMIT
2005-10-31 02:51:35 CST COMMIT
2005-11-08 14:53:56 CST COMMIT
2005-11-08 15:01:47 CST COMMIT
2008-10-22 23:30:41 CST COMMIT
2008-10-22 23:53:14 CST COMMIT
2008-11-09 05:09:09 CST COMMIT
Emitted 79 commits (= total 79).
Exact     2 +     1 =     3 branches + tags.
Fixup     0 +     0 =     0 branches + tags.
Download 147 cvs versions in 84 transactions.
String cache: 141 items, 132/1024 buckets used, mean search 1.06383
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:          289 (         8 duplicates                  )
      blobs  :          134 (         7 duplicates         46 deltas of        133 attempts)
      trees  :           77 (         0 duplicates         70 deltas of         71 attempts)
      commits:           78 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           3 (         2 loads     )
      marks:           1024 (       220 unique    )
      atoms:             15
Memory total:          2294 KiB
       pools:          2098 KiB
     objects:           195 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize =   33554432
pack_report: core.packedGitLimit      =  268435456
pack_report: pack_used_ctr            =          7
pack_report: pack_mmap_calls          =          3
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =     350104 /     350104
---------------------------------------------------------------------

这样这个 Git 仓库就包含了已经转换过了的 CVS 历史记录, 如果看不到文件可以 reset 一下。

按说后续的操作理论上可以在这个仓库目录中操作,但为了更好的和 Fork 的项目合并, 我使用导出 Patch 的方法,后面再 am:

$ git log --pretty=oneline |wc -l
78
 
$ git format-patch -78

其实在这里,也可以在目标 repo 里面,通过添加 Snoopy.git 为 Git remote, 然后 merge remote 的方式进行,效果更好,还不用修改提交时间。

Fork 项目,移花接木

在 Github 上 Fork https://github.com/hurrycaner/snoopy , 得到 https://github.com/fwolf/snoopy , 但先不下载到本地,后面的操作方法和正常 Fork 项目是 不一样 的。

在本地再新建一个 Git 仓库,这个仓库是我们今后维护 Snoopy 的主仓库:

$ mkdir Snoopy
$ cd Snoopy
$ git init
$ git remote add origin git@github.com:fwolf/snoopy.git
$ touch .gitignore
$ git add .gitignore
$ git commit -a -m "Initial commit"
$ git push -f origin master

和新建项目的方法基本一样,不同点是我们的 origin 是 Fork 后的项目, 并且进行了 push -f 操作,覆盖掉了 hurrycaner 的所有提交。

接下来新建一个 sourceforge 分支,保留 SourceForge 上 CVS 代码的最终状态, 提交是通过 am 导入的, --committer-date-is-author-date 参数是将作者的时间作为提交时间, 也可以不要。Patch 0002 是空的,会导致 am 失败,所以删除掉:

$ git branch sourceforge
$ git checkout sourceforge
 
$ rm ../Snoopy.git/0002-Initial-check-in.patch
$ git am ../Snoopy.git/00* --committer-date-is-author-date
 
$ git checkout master
$ git merge sourceforge
$ git push

现在,master 分支上是我作的一个初始提交,加上 CVS 上导过来的提交内容, 相当于是 CVS 被完整的导入了 Git。

添加只有一个空 .gitignore 文件的初始提交是 Git 的一个习惯, 因为 Git 的初始提交可以视为是“不可以操作”的, 所以最好是空或者只包含最少内容。

接下来,我们要将 hurrycaner 所作的修改合并进来。 由于他是基于 Snoopy 1.2.4 代码修改的, 和我导入的最终代码差距不大,所以合并还比较顺利,只有几处冲突而已:

$ git branch hurrycaner
$ git checkout hurrycaner
$ git remote add upstream git@github.com:hurrycaner/snoopy.git
$ git fetch upstream
$ git merge upstream/master     # 手工解决冲突
 
$ git checkout master
$ git merge hurrycaner
$ git push

这就基本上完成了,保留了从 CVS 到 hurrycaner 的完整修改记录, 并且还能像正常 Fork 的项目那样继续工作。

修改记录看起来是这个样子的:

2013-10-18-000402_722x358_scrot

我已经向原项目作者推送 Pull Request 了。 hurrycaner 在 Github 上并不活跃,不知道能不能看到、会不会收啊。

尾声

Git 的使用是比较灵活的,我相信其他分布式 SCM 也能做到,没研究过,不对比。 话说回来,本文中的做法,是不是有点鸠占鹊巢的感觉?

@link https://github.com/fwolf/snoopy

通过代理使用 GitHub

Git 是非常好用的开发工具,越来越离不开了。 如果要与他人合作项目,GitHub 是很好的平台。 但如果身处受限网络,要管理 GitHub 上的项目, 还是要费一番周折的。

GitHub 网页访问应该不用说了,工具多得是。 我要说的是对项目进行管理,比如 push/pull 操作等。

最简单的方式是通过 https_proxy,比如:

export https_proxy=http://127.0.0.1:8087

然后将仓库地址改为 HTTP 方式。

虽然简单,但有一点不方便,就是进行写操作时, 比如 push ,会需要手工输入用户名和密码, 而不是 GitHub 常用的证书自动认证。

更好的方法还是走 ssh 协议代理, 这需要一个软件 connect-proxy。 Ubuntu 下可以通过 Apt 安装, ArchLinux 下要通过 AUR 安装( 包地址 )。

先要有 Socks 代理,通常,可以使用无限制网络的 VPS, 然后使用 ssh 打个隧道:

# Native ssh
ssh -D 127.0.0.1:22888 -CfNg domain.tld -o ControlPath=/tmp/ssh-22888-domain.tld
# OR
# 使用 authssh 更方便
autossh -M 0 -D 127.0.0.1:22888 -CfNg domain.tld -o ControlPath=/tmp/ssh-22888-domain.tld

可以 telnet localhost 22888 检查通不通。

然后,在 $HOME/.ssh/config 中添加一段:

Host github.com
    # On Ubuntu
    ProxyCommand /usr/bin/connect-proxy -S 127.0.0.1:22888 %h %p
    # OR
    # On ArchLinux
    ProxyCommand /usr/bin/connect -S 127.0.0.1:22888 %h %p

-S 参数如果换成 -H ,就是使用 http 代理, 效果应该和上面的简单方法一样。

最后,将仓库地址改为 SSH 方式。 现在,本地 GitHub 仓库中 push 操作就正常了,简单测试一下 GitHub 登录:

$ ssh -T git@github.com
Hi fwolf! You've successfully authenticated, but GitHub does not provide shell access.

Git commit 注释格式

Git 本身并没有硬性限制注释的格式,不过,对于多人参与的项目来说, 好的注释风格更加有利于团队合作。 即使是自己用,也应当坚持实用好的注释风格, 一来是对自己的工作历史负责,二来得以养成好的注释习惯。 虽然这里标题说的是 Git,其他源代码控制系统也可以参考的。

可以先看看一些著名开源项目源代码管理系统当中的提交注释, 其中也有用 SVN 和 Bazaar 的, Apahe 的源码看不到 logview,可能是使用了 CVS 文件格式的原因:

结合其他参考文章,我总结 Git 的 推荐 注释风格如下:

  1. 第一行为对改动的简要总结,建议长度不超过 50,用语采用命令式而非过去式。

    Vim 很贴心,在默认配置下,注释的第一行文字颜色是黄色, 超过 50 列之后就变成白色了。

  2. 第一行结尾不要英文的句号 . ,中文的就也不要 吧。

    为啥?我给 rst2wp 提交的时候,对方也提出了这个要求 [1] [2] , 后来查了查,大概原因是,第一行被认为是一个“标题”,也会作为邮件标题, 而标题是不需要标点的。 上面那些开源项目也都遵守了这一规则。 不过也有 例外的

  3. 第二行为空行。

    如果配置了自动发送邮件,那么第一行就用来做邮件标题, 第三行开始的内容是邮件正文, 第二行是分隔线,用于区分两者。

  4. 第三行开始,是对改动的详细介绍,可以是多行内容,建议每行长度不超过 72。

    可以包括原因、做法、效果等很多内容,一切你认为与当前改动相关的。 为何是 72 长度呢?因为 git log 输出的时候能显示得比较好看, 前面 4 个空格作为缩进,后面留 4 个空格机动(英文按单词排版)。 Vim 的 gq 命令排版很方便。

    一些项目还对这个部分的内容有特殊要求,比如加一些特定标签什么的。

  5. 建议全部英文,首字母大写。如果一定要用中文,请尽量使用 UTF-8 编码。

  6. 大项目中,可以用 module/submodule: 前缀作为第一行的开头, 前缀首字母不必大写。

    前缀的冒号后面跟一个空格比较好看。 为了控制字符串长度,子模块名称可适当缩写,但应保持统一。

我以前喜欢在注释第一行加上 New: Add: Fix: 这样的前缀, 看来也是没有必要了。

Tips: 提交之前,用 git diff --check 可以检查有无空白字符错误, 比如行尾留有空白什么的。

BTW,在使用 Git 或者其他 SCM 时,还应当避免下面这些做法:

  • 把 SCM 当做备份工具。

    SCM 是项目/代码管理工具,备份功能只是“福利”。 随意将未完成测试或临时的工作结果进行提交, 不仅破坏日志的“纯洁性”,还不利于日后的浏览、整理、汇总等工作。 在开源项目中这么做的话,没人会接受这种提交。 如果确实需要备份当前工作异地继续的话,可以采用这样的折衷方法:

    $ git commit                # 在本地进行提交
    $ git format-patch -n1      # 导出 Patch
                                # 这个 Patch 就是你的备份
    $ git am Path_To_Patch_File # 如果换了工作地点,导入 Patch
    $ git reset --mixed [hash]  # 撤销提交,保留更改,继续工作
  • 一个改动不一次提交完成。

    “提交”的概念是具有独立的功能、修正等作用。 小可以小到只修改一行,大可以到改动很多文件, 但划分的标准不变,一个提交就是解决一个问题的。

    对格式的修正,不应该和其他功能修补一起提交, 这种情况应该考虑使用 git add --editgit add -p 也可以用用,更复杂和强大一些。

  • 注释不清晰。

    比如只有“修正 BUG”、“改错”、“升级”等,没有其他内容,等于没说。

参考

Git 合并 patch 时的冲突处理一例

git version 1.6.0.4

几个新手刚刚开始接触 Git,为了维护核心仓库的“纯洁”,避免太多无关信息被误提交进仓库(再次批评一些图形化工具默认的“Select All”),采用了核心仓库只读,邮件提交 patch,审核后再提交的工作流程。

期间有时会遇到合并冲突,正常的原因一般是未及时下载新版本产生了冲突,特殊一点的原因是手工修改 patch 内容导致的。有时候看注释写得不够准确,忍不住就改了,有时候是 Geany 保存时自动去除了 patch 原文中的行尾空格,有时候是文件回车格式、BOM 等变动了,总之合并 patch 的时候,如果生成 patch 的“原稿”找不到,一般就产生了冲突,比如:

$ git am 0001-BUG-Sybase.patch
Applying: CHG: 读取Sybase如果时间为空,设置默认时间的修改
error: patch failed: source.php:38
error: source.php: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".

刚开始一看有些懵,因为没有任何冲突在哪里的提示,后来找到一种方法,am 操作出问题后先手工 apply:

$ git apply --reject 0001-BUG-Sybase.patch
Checking patch source.php...
error: while searching for:
        // 注释
        // 以下为几行代码片断
error: patch failed: source.php:38
Applying patch source.php with 1 rejects...
Rejected hunk #1.

这样,就把没有冲突的文件先合并了,剩下有冲突的作了标记。先看输出,error: while searching for: 说明是这段代码有冲突,error: patch failed: source.php:38 指明了产生冲突的代码片断的开始行号,相应的,patch 中应该有这么一段:

diff --git a/source.php b/source.php
index 8770441..4e77b8a 100644
--- a/source.php
+++ b/source.php
@@ -38,27 +38,23 @@ class Site extends Module
        // 注释
        // 以下为几行代码片断

同时,还会产生一个 source.php.rej 文件,里面也是上面这段因为冲突无法合并的代码片断。

现在,在这段代码中查找冲突原因,并对文件进行修改,source.php.rej 参考完了可以删掉。改好之后,用 git add 把 source.php 添加到缓冲区,同时也要把其他没有冲突合并成功了的文件也加进来,因为在作 apply 操作的时候他们也发生了变化:

$ git add source.php
$ git add 其他 apply 进来的文件们

最后:

$ git am --resolved
Applying: CHG: 读取Sybase如果时间为空,设置默认时间的修改

大功告成。

中间如果处理乱了,用 git reset 恢复即可,所以合并 patch 在一个“干净”的分支上处理更好。

[Git]初学者注意事项

实在是受不了有些人的 Git 提交,费大力气“回滚”,遂整理了这些刚开始用 git 或者还没有建立 scm 概念时容易犯的错误。

和源码无关的东西,尽量不要进仓库

不得不说一些图形化软件,在提交内容的时候大多提供一个“全选”或者“Select All”功能,这是最不好的了,一些懒惰的同志看都不看就连瓢带碗都提交了。

  • 测试时上传的文件,测试时的临时文件,统统不要
  • 对应上一条,强烈建议把所有文件的上传保存目录另行设置,放到源代码目录以外
  • 编辑器产生的备份文件、临时文件,编译时的中间文件,统统不要
  • 对应上一条,有个例外就是为了实现通过 Git 更新系统,.NET 的 bin 文件要进仓库,导致那个仓库现在都 100+m 了
  • 图片等资源文件,进仓库也可以,但应当使用有意义的文件名,便于后期管理
  • 对应上一条,现在设计网站界面喜欢先作图然后切割,产生一大堆 001_r5_c1.jpg 这样的文件,讨厌之极
  • 使用的外部类库,比如 php 类、js 类等,统统扔到源码目录以外,如果实在没办法要放在目录树中,也可以留出空目录,打包发行的时候再包含进来,依然不进代码仓库
  • 不要中文文件名,主要是跨平台使用有问题,文件名完全能够只用字母数字减号下划线

尽量采用相对小、相对独立的提交

Git 是作什么用的?Git 不是代码上传工具,也不是网站更新工具,而是软件开发过程的记录工具,为了更加准确的定位每个问题、每个功能修改,就需要在每完成一部分可以称得上是“一项”的工作时,就 commit 一次。哪怕只是修改了一两行,只要产生了必要的功能改变,就有价值记录。

当采用代码审核机制或者需要用邮件提交补丁时,较小的提交能够更有效、更容易、更准确的被检查和审核,这个在 linux kernel 开发文档中也有提到。

当然不能矫枉过正,必须有可记录的改变才有提交的价值。对应的,Git 日志大多数情况下主要显示第一行,控制每次提交都能用一句话简单概括,也是有必要的。

注释格式

格式属于个人习惯和团队规范范围,有必要采用相对统一的风格。

Git 本身不允许空注释,同时建议注释的第一行写简要说明,下面留一行空行,再写详细说明。

我的个人习惯,喜欢在每条注释前面用大约三个字母来表示本次修改的性质:

  • Add something
  • Bug [fix|found]: describe the bug or fix.
  • Chg something
  • Del something
  • Enh some treatment
  • New something
  • Tmp for some cause

为了保持语法通顺,也可以采用前三个字母后面加冒号,后面有啥写啥的方法。

最后,我觉得,能够遵守行业规范和团队约定,主动养成良好习惯,应当是鉴别人才的一项重要因素。

Update @ 2012-12-03

关于注释格式,在使用了几年 Git 后,又有了更深的认识, 参见 Git commit 注释格式