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.

用 rst2wp 来写 WordPress

很久没有写 Blog 了,没想到一下子拉了这么长时间。 想想最初停止的原因,是因为在看到 reStructuredText 之后, 觉得用来写文章、文档或者 Blog 非常好,就想学着用用。 谁知 rst 太复杂了,光 说明文档 就那么长, 也没有完整的中文版,就想来翻译一下,翻译的结果还是用 rst 来写。 结果卡死在这里了,后来忙了就没有再继续了, Blog 也就没有再更新,翻译也没有进行完。

现在,随着一切在发展,似乎 WordPress 有些没落了, 原来的 rst 支持插件居然也几乎没有了。 大家都热衷于使用 Jekyll, Octopress, Pelican 类似的软件来生成静态 Blog, 摈弃了以前 PHP+Mysql 的动态网站方式, github 还提供配套的 托管服务 。 我不喜欢全静态化的实现方式,网站大了以后更新效率肯定会降低, 好在这些生成工具大多都支持 MarkdownreStructuredText , 继续使用 rst 来写 Blog 也不会太影响将来可能的迁移。

继续 Blog 吧,懒人又开始面临一个不是问题的问题:网络不好,连接速度慢, 直接登录 WordPress 后台写简直太痛苦了。 所以有一段时间我都是在本地编辑,然后粘贴到后台看效果。 这种方式在 rst 在线预览工具 的帮助下还算凑合 (原来有个 rst2a 也挺好用,但已经挂了很久了)。 看过一些 Blog 客户端软件,感觉大多仍然不理想,功能不足, WordPress 推荐工具 多半也不好用, 很多都很长时间没更新了。 于是,继续搜索,找到了现在用的 rst2wp

rst2wp 的工作方式是在本地用任何你喜欢的编辑器写 rst 文档, rst2wp 帮你转换成 html、预览或者上传到 WordPress 中。 rst 文件中可以指定 Category 和 Tag, 上传时会自动记录 Post Id 用于以后的编辑。 我 Fork 之后还增加了记录 date 和 slug 的功能。 一个比较完整的 rst 文档示例如下:

..  -*- mode: rst -*-
..  -*- coding: utf-8 -*-
 
:id: 6
:title: 用 rst2wp 来写 WordPress
:slug: 201211-test-rst2wp
:date: 2012-11-27 00:19:37
:publish: yes
:categories:
    - Blog
    - Tools
:tags:
    - blog
    - git
    - github
    - WordPress
    - RestructuredText
 
 
blah 测试 中文。
 
+---+---+---+
| a | b | c |
+===+===+===+
| 1 | 2 | 3 |
+---+---+---+

先用着吧,顺便熟悉 reStructuredText 的语法。