3.6. 补丁文件交互

之前各个章节版本库间的交互都是通过git push和/或git pull命令实现的,这是Git最主要的交互模式,但并不是全部。使用补丁文件是另外一种交互方式,适用于参与者众多的大型项目进行分布式开发。例如Git项目本身的代码提交就主要由贡献者通过邮件传递补丁文件实现的。作者在写书过程中发现了Git的两个bug,就是以补丁形式通过邮件贡献给Git项目的,下面两个链接就是相关邮件的存档。

这种使用补丁文件进行提交的方式可以提高项目的参与度。因为任何人都可以参与项目的开发,只要会将提交转化为补丁,会发邮件即可。

3.6.1. 创建补丁

Git提供了将提交批量转换为补丁文件的命令:git format-patch。该命令后面的参数是一个版本范围列表,会将包含在此列表中的提交一一转换为补丁文件,每个补丁文件包含一个序号并从提交说明中提取字符串作为文件名。

下面演示一下在user1工作区中,如何将master分支的最近3个提交转换为补丁文件。

  • 进入user1工作区,切换到master分支。

    $ cd /path/to/user1/workspace/hello-world/
    $ git checkout master
    $ git pull
    
  • 执行下面的命令将最近三个提交转换为补丁文件。

    $ git format-patch -s HEAD~3..HEAD
    0001-Fix-typo-help-to-help.patch
    0002-Add-I18N-support.patch
    0003-Translate-for-Chinese.patch
    

在上面的git format-patch命令中使用了-s参数,会在导出的补丁文件中添加当前用户的签名。这个签名并非GnuPG式的数字签名,不过是将作者姓名添加到提交说明中而已,和在本书第2篇开头介绍的git commit -s命令的效果相同。虽然签名很不起眼,但是对于以补丁方式提交数据却非常重要,因为以补丁方式提交可能因为合并冲突或其他原因使得最终提交的作者显示为管理员(提交者)的ID,在提交说明中加入原始作者的署名信息大概是作者唯一露脸的机会。如果在提交时忘了使用-s参数添加签名,可以在用git format-path命令创建补丁文件的时候补救。

看一下补丁文件的文件头,在下面代码中的第7行可以看到新增的签名。

 1 From d81896e60673771ef1873b27a33f52df75f70515 Mon Sep 17 00:00:00 2001
 2 From: user1 <user1@sun.ossxp.com>
 3 Date: Mon, 3 Jan 2011 23:48:56 +0800
 4 Subject: [PATCH 1/3] Fix typo: -help to --help.
 5
 6
 7 Signed-off-by: user1 <user1@sun.ossxp.com>
 8 ---
 9  src/main.c |    2 +-
10  1 files changed, 1 insertions(+), 1 deletions(-)

补丁文件有一个类似邮件一样的文件头(第1-4行),提交日志的第一行作为邮件标题(Subject),其余提交说明作为邮件内容(如果有的话),文件补丁用三个横线和提交说明分开。

实际上这些补丁文件可以直接拿来作为邮件发送给项目的负责人。Git提供了一个辅助邮件发送的命令git send-email。下面用该命令将这三个补丁文件以邮件形式发送出去。

$ git send-email *.patch
0001-Fix-typo-help-to-help.patch
0002-Add-I18N-support.patch
0003-Translate-for-Chinese.patch
The following files are 8bit, but do not declare a Content-Transfer-Encoding.
    0002-Add-I18N-support.patch
    0003-Translate-for-Chinese.patch
Which 8bit encoding should I declare [UTF-8]?
Who should the emails appear to be from? [user1 <user1@sun.ossxp.com>]

Emails will be sent from: user1 <user1@sun.ossxp.com>
Who should the emails be sent to? jiangxin
Message-ID to be used as In-Reply-To for the first email?
...
Send this email? ([y]es|[n]o|[q]uit|[a]ll): a
...

命令git send-email提供交互式字符界面,输入正确的收件人地址,邮件就批量地发送出去了。

3.6.2. 应用补丁

在前面通过git send-email命令发送邮件给jiangxin用户。现在使用 Linux 上的mail命令检查一下邮件。

$ mail
Mail version 8.1.2 01/15/2001.  Type ? for help.
"/var/mail/jiangxin": 3 messages 3 unread
>N  1 user1@sun.ossxp.c  Thu Jan 13 18:02   38/1120  [PATCH 1/3] Fix typo: -help to --help.
 N  2 user1@sun.ossxp.c  Thu Jan 13 18:02  227/6207  =?UTF-8?q?=5BPATCH=202/3=5D=20Add=20I18N=20support=2E?=
 N  3 user1@sun.ossxp.c  Thu Jan 13 18:02   95/2893  =?UTF-8?q?=5BPATCH=203/3=5D=20Translate=20for=20Chinese=2E?=
&

如果邮件不止这三封,需要将三个包含补丁的邮件挑选出来保存到另外的文件中。 在 mail 命令的提示符(&)下输入命令。

& s 1-3 user1-mail-archive
"user1-mail-archive" [New file]
& q

上面的操作在本地创建了一个由开发者user1的补丁邮件组成的归档文件user1-mail-archive,这个文件是mbox格式的,可以用mail命令打开。

$ mail -f user1-mail-archive
Mail version 8.1.2 01/15/2001.  Type ? for help.
"user1-mail-archive": 3 messages
>   1 user1@sun.ossxp.c  Thu Jan 13 18:02   38/1121  [PATCH 1/3] Fix typo: -help to --help.
    2 user1@sun.ossxp.c  Thu Jan 13 18:02  227/6208  =?UTF-8?q?=5BPATCH=202/3=5D=20Add=20I18N=20support=2E?=
    3 user1@sun.ossxp.c  Thu Jan 13 18:02   95/2894  =?UTF-8?q?=5BPATCH=203/3=5D=20Translate=20for=20Chinese=2E?=
& q

保存在mbox中的邮件可以批量的应用在版本库中,使用git am命令。amapply email的缩写。下面就演示一下如何应用补丁。

  • 基于HEAD~3版本创建一个本地分支,以便在该分支下应用补丁。

    $ git checkout -b user1 HEAD~3
    Switched to a new branch 'user1'
    
  • 将mbox文件user1-mail-archive中的补丁全部应用在当前分支上。

    $ git am user1-mail-archive
    Applying: Fix typo: -help to --help.
    Applying: Add I18N support.
    Applying: Translate for Chinese.
    
  • 补丁成功应用上了,看看提交日志。

    $ git log -3 --pretty=fuller
    commit 2d9276af9df1a2fdb71d1e7c9ac6dff88b2920a1
    Author:     Jiang Xin <jiangxin@ossxp.com>
    AuthorDate: Thu Jan 13 18:02:03 2011 +0800
    Commit:     user1 <user1@sun.ossxp.com>
    CommitDate: Thu Jan 13 18:21:16 2011 +0800
    
        Translate for Chinese.
    
        Signed-off-by: Jiang Xin <jiangxin@ossxp.com>
        Signed-off-by: user1 <user1@sun.ossxp.com>
    
    commit 41227f492ad37cdd99444a5f5cc0c27288f2bca4
    Author:     Jiang Xin <jiangxin@ossxp.com>
    AuthorDate: Thu Jan 13 18:02:02 2011 +0800
    Commit:     user1 <user1@sun.ossxp.com>
    CommitDate: Thu Jan 13 18:21:15 2011 +0800
    
        Add I18N support.
    
        Signed-off-by: Jiang Xin <jiangxin@ossxp.com>
        Signed-off-by: user1 <user1@sun.ossxp.com>
    
    commit 4a3380fb7ae90039633dec84acc2aab85398efad
    Author:     user1 <user1@sun.ossxp.com>
    AuthorDate: Thu Jan 13 18:02:01 2011 +0800
    Commit:     user1 <user1@sun.ossxp.com>
    CommitDate: Thu Jan 13 18:21:15 2011 +0800
    
        Fix typo: -help to --help.
    
        Signed-off-by: user1 <user1@sun.ossxp.com>
    

从提交信息上可以看出:

  • 提交的时间信息使用了邮件发送的时间。
  • 作者(Author)的信息被保留,和补丁文件中的一致。
  • 提交者(Commit)全都设置为user1,因为提交是在user1的工作区完成的。
  • 提交说明中的签名信息被保留。实际上git am命令也可以提供-s参数,在提交说明中附加执行命令用户的签名。

对于不习惯在控制台用mail命令接收邮件的用户,可以通过邮件附件,U盘或其他方式获取git format-patch生成的补丁文件,将补丁文件保存在本地,通过管道符调用git am命令应用补丁。

$ ls *.patch
0001-Fix-typo-help-to-help.patch  0002-Add-I18N-support.patch  0003-Translate-for-Chinese.patch
$ cat *.patch | git am
Applying: Fix typo: -help to --help.
Applying: Add I18N support.
Applying: Translate for Chinese.

Git还提供一个命令git apply,可以应用一般格式的补丁文件,但是不能执行提交,也不能保持补丁中的作者信息。实际上git apply命令和 GNUpatch命令类似,细微差别在本书第7篇第38章“补丁中的二进制文件”予以介绍。

3.6.3. StGit和Quilt

一个复杂功能的开发一定是由多个提交来完成的,对于在以接收和应用补丁文件为开发模式的项目中,复杂的功能需要通过多个补丁文件来完成。补丁文件因为要经过审核才能被接受,因此针对一个功能的多个补丁文件一定要保证各个都是精品:补丁1用来完成一个功能点,补丁2用来完成第二个功能点,等等。一定不能出现这样的情况:补丁3用于修正补丁1的错误,补丁10改正了补丁7中的文字错误,等等。这样就带来补丁管理的难题。

实际上基于特性分支的开发又何尝不是如此?在将特性分支归并到开发主线前,要接受团队的评审,特性分支的开发者一定想将特性分支上的提交进行重整,把一些提交合并或者拆分。使用变基命令可以实现提交的重整,但是操作起来会比较困难,有什么好办法呢?

3.6.3.1. StGit

Stacked Git(http://www.procode.org/stgit/)简称StGit就是解决上述两个难题的答案。实际上StGit在设计上参考了一个著名的补丁管理工具Quilt,并且可以输出Quilt兼容的补丁列表。在本节的后半部分会介绍Quilt。

StGit是一个Python项目,安装起来还是很方便的。在Debian/Ubuntu下,可以直接通过包管理器安装:

$ sudo aptitude install stgit stgit-contrib

下面还是用hello-world版本库,进行StGit的实践。

  • 首先检出hello-world版本库。

    $ cd /path/to/my/workspace/
    $ git clone file:///path/to/repos/hello-world.git stgit-demo
    $ cd stgit-demo
    
  • 在当前工作区初始化StGit。

    $ stg init
    
  • 现在补丁列表为空。

    $ stg series
    
  • 将最新的三个提交转换为StGit补丁。

    $ stg uncommit -n 3
    Uncommitting 3 patches ...
      Now at patch "translate-for-chinese"
    done
    
  • 现在补丁列表中有三个文件了。

    第一列是补丁的状态符号。加号(+)代表该补丁已经应用在版本库中,大于号(>)用于标识当前的补丁。

    $ stg ser
    + fix-typo-help-to-help
    + add-i18n-support
    > translate-for-chinese
    
  • 现在查看master分支的日志,发现和之前没有两样。

    $ git log -3 --oneline
    c4acab2 Translate for Chinese.
    683448a Add I18N support.
    d81896e Fix typo: -help to --help.
    
  • 执行StGit补丁出栈的命令,会将补丁撤出应用。使用-a参数会将所有补丁撤出应用。

    $ stg pop
    Popped translate-for-chinese
    Now at patch "add-i18n-support"
    $ stg pop -a
    Popped add-i18n-support -- fix-typo-help-to-help
    No patch applied
    
  • 再来看版本库的日志,会发现最新的三个提交都不见了。

    $ git log -3 --oneline
    10765a7 Bugfix: allow spaces in username.
    0881ca3 Refactor: use getopt_long for arguments parsing.
    ebcf6d6 blank commit for GnuPG-signed tag test.
    
  • 查看补丁列表的状态,会看到每个补丁前都用减号(-)标识。

    $ stg ser
    - fix-typo-help-to-help
    - add-i18n-support
    - translate-for-chinese
    
  • 执行补丁入栈,即应用补丁,使用命令stg push或者stg goto命令,注意stg push命令和git push命令风马牛不相及。

    $ stg push
    Pushing patch "fix-typo-help-to-help" ... done (unmodified)
    Now at patch "fix-typo-help-to-help"
    $ stg goto add-i18n-support
    Pushing patch "add-i18n-support" ... done (unmodified)
    Now at patch "add-i18n-support"
    
  • 现在处于应用add-i18n-support补丁的状态。这个补丁有些问题,本地化语言模板有错误,我们来修改一下。

    $ cd src/
    $ rm locale/helloworld.pot
    $ make po
    xgettext -s -k_ -o locale/helloworld.pot main.c
    msgmerge locale/zh_CN/LC_MESSAGES/helloworld.po locale/helloworld.pot -o locale/temp.po
    . 完成。
    mv locale/temp.po locale/zh_CN/LC_MESSAGES/helloworld.po
    
  • 现在查看工作区,发现工作区有改动。

    $ git status -s
     M locale/helloworld.pot
     M locale/zh_CN/LC_MESSAGES/helloworld.po
    
  • 不要将改动添加暂存区,也不要提交,而是执行stg refresh命令,更新补丁。

    $ stg refresh
    Now at patch "add-i18n-support"
    
  • 这时再查看工作区,发现本地修改不见了。

    $ git status -s
    
  • 执行stg show会看到当前的补丁add-i18n-support已经更新。

    $ stg show
    ...
    
  • 将最后一个补丁应用到版本库,遇到冲突。这是因为最后一个补丁是对中文本地化文件的翻译,因为翻译前的模板文件被更改了所以造成了冲突。

    $ stg push
    Pushing patch "translate-for-chinese" ... done (conflict)
    Error: 1 merge conflict(s)
           CONFLICT (content): Merge conflict in
           src/locale/zh_CN/LC_MESSAGES/helloworld.po
    Now at patch "translate-for-chinese"
    
  • 这个冲突文件很好解决,直接编辑冲突文件helloworld.po即可。编辑好之后,注意一下第50行和第62行是否像下面写的一样。

    50 "    hello -h, --help\n"
    51 "            显示本帮助页。\n"
    ...
    61 msgid "Hi,"
    62 msgstr "您好,"
    
  • 执行git add命令完成冲突解决。

    $ git add locale/zh_CN/LC_MESSAGES/helloworld.po
    
  • 不要提交,而是使用stg refresh命令更新补丁,同时更新提交。

    $ stg refresh
    Now at patch "translate-for-chinese"
    $ git status -s
    
  • 看看修改后的程序,是不是都能显示中文了。

    $ ./hello
    世界你好。
    (version: v1.0-5-g733c6ea)
    $ ./hello Jiang Xin
    您好, Jiang Xin.
    (version: v1.0-5-g733c6ea)
    $ ./hello -h
    ...
    
  • 导出补丁,使用命令stg export。导出的是Quilt格式的补丁集。

    $ cd /path/to/my/workspace/stgit-demo/
    $ stg export -d patches
    Checking for changes in the working directory ... done
    
  • 看看导出补丁的目标目录。

    $ ls patches/
    add-i18n-support  fix-typo-help-to-help  series  translate-for-chinese
    
  • 其中文件series是补丁文件的列表,列在前面的补丁先被应用。

    $ cat patches/series
    # This series applies on GIT commit d81896e60673771ef1873b27a33f52df75f70515
    fix-typo-help-to-help
    add-i18n-support
    translate-for-chinese
    

通过上面的演示可以看出StGit可以非常方便的对提交进行整理,整理提交时无需使用复杂的变基命令,而是采用:提交StGit化,修改文件,执行stg refresh的工作流程即可更新补丁和提交。StGit还可以将补丁导出为补丁文件,虽然导出的补丁文件没有像git format-patch那样加上代表顺序的数字前缀,但是用文件series标注了补丁文件的先后顺序。实际上可以在执行stg export时添加-n参数为补丁文件添加数字前缀。

StGit还有一些功能,如合并补丁/提交,插入新补丁/提交等,请参照StGit帮助,恕不一一举例。

3.6.3.2. Quilt

Quilt是一款补丁列表管理软件,用Shell语言开发,安装也很简单,在Debian/Ubuntu上直接用下面的命令即可安装:

$ sudo aptitude install quilt

Quilt约定俗成将补丁集放在项目根目录下的子目录patches中,否则需要通过环境变量QUILT_PATCHES对路径进行设置。为了减少麻烦,在上面用stg export导出补丁的时候就导出到了patches目录下。

简单说一下Quilt的使用,会发现真的和StGit很像,实际上是先有的Quilt,后有的StGit。

  • 重置到三个提交前的版本,否则应用补丁的时候会失败。还不要忘了删除src/locale目录。

    $ git reset --hard HEAD~3
    $ rm -rf src/locale/
    
  • 显示补丁列表

    $ quilt series
    01-fix-typo-help-to-help
    02-add-i18n-support
    03-translate-for-chinese
    
  • 应用一个补丁。

    $ quilt push
    Applying patch 01-fix-typo-help-to-help
    patching file src/main.c
    
    Now at patch 01-fix-typo-help-to-help
    
  • 下一个补丁是什么?

    $ quilt next
    02-add-i18n-support
    
  • 应用全部补丁。

    $ quilt push -a
    Applying patch 02-add-i18n-support
    patching file src/Makefile
    patching file src/locale/helloworld.pot
    patching file src/locale/zh_CN/LC_MESSAGES/helloworld.po
    patching file src/main.c
    
    Applying patch 03-translate-for-chinese
    patching file src/locale/zh_CN/LC_MESSAGES/helloworld.po
    
    Now at patch 03-translate-for-chinese
    

Quilt的功能还有很多,请参照Quilt的联机帮助,恕不一一举例。