Git实现了自己的传输协议用于版本库间交换数据。出于效率方面的考虑(为了节约时间和空间),Git传输协议会进行握手,以确定原版本库中的哪些提交不在目标版本库中,最终传输提交的二进制压缩形式。接收的版本库会给新的提交合并到其本地历史记录中,同时添加到提交图中,并按需要更新分支和标签。
之前提到,要交换提交并保持分布式版本库同步,可以使用Git原生协议和HTTP协议。但在很多情况下使用这些协议是不可行的,Git还支持补丁操作,通常通过email来进行数据交换。
Git实现三条特定的命令帮助交换补丁:
其理由为:
git format-patch命令会以邮件消息的形式生成一个补丁。其会为你指定的每次提交都创建一封邮件。
Git的diff机制是git format-patch命令的核心,但是其与git diff还有区别:
这里看个例子,首先使用下面的代码初始化一个版本库,并提交几次修改:
- git init
- echo abcd > file1
- git add file1
- git commit -m "commit file1"
- echo abcde > file2
- git add file2
- git commit -m "commit file2"
- echo abcdef > file3
- git add file3
- git commit -m "commit file3"
- echo abcdefg > file4
- git add file4
- git commit -m "commit file4"
此时会存在几次提交:
- $ git show-branch --more=4 master
- [master] commit file4
- [master^] commit file3
- [master~2] commit file2
- [master~3] commit file1
从上面来看,已经存在了4次提交。然后为最近n次提交生成补丁:
- $ git format-patch -1
- 0001-commit-file4.patch
-
- $ git format-patch -2
- 0001-commit-file3.patch
- 0002-commit-file4.patch
-
- $ git format-patch -3
- 0001-commit-file2.patch
- 0002-commit-file3.patch
- 0003-commit-file4.patch
-
- $ git format-patch -4
- 0001-commit-file1.patch
- 0002-commit-file2.patch
- 0003-commit-file3.patch
- 0004-commit-file4.patch
默认情况下,Git会为每个补丁生成单独的文件,用一序列数字加上提交日志信息为其命名。
也可以使用一个提交范围来指定把哪些提交格式化为补丁。在上边的打印中,可以看到file4的版本号为master,file3的版本号为master^,file2的版本号为master~2,执行下边的指令:
- $ git format-patch master~2..master
- 0001-commit-file3.patch
- 0002-commit-file4.patch
从上面来看,虽然file2到file4一共存在3次提交,但是只有两条邮件信息:
- $ cat 0001-commit-file3.patch
- From 43f9a45cc872f7bda33f1b0ab5422b3e861318af Mon Sep 17 00:00:00 2001
- From: wood_glb
- Date: Sun, 21 Aug 2022 11:30:05 +0800
- Subject: [PATCH 1/2] commit file3
-
- ---
- file3 | 1 +
- 1 file changed, 1 insertion(+)
- create mode 100644 file3
-
- diff --git a/file3 b/file3
- new file mode 100644
- index 0000000..0373d93
- --- /dev/null
- +++ b/file3
- @@ -0,0 +1 @@
- +abcdef
- --
- 2.31.1.windows.1
-
- $ cat 0002-commit-file4.patch
- From 00bf3a31efe83ea41f129881ee0dcac006653c4c Mon Sep 17 00:00:00 2001
- From: wood_glb
- Date: Sun, 21 Aug 2022 11:30:09 +0800
- Subject: [PATCH 2/2] commit file4
-
- ---
- file4 | 1 +
- 1 file changed, 1 insertion(+)
- create mode 100644 file4
-
- diff --git a/file4 b/file4
- new file mode 100644
- index 0000000..b5b9ef6
- --- /dev/null
- +++ b/file4
- @@ -0,0 +1 @@
- +abcdefg
- --
- 2.31.1.windows.1
上面的打印形式和git diff的形式很相似,也就是每个邮件信息其实是两次提交间的diff信息。
这里再master~2处添加一个新的分支alt,并执行如下代码:
- git checkout -b alt master~2
-
- echo AAA > file5
- git add file5
- git commit -m "commit file5"
- echo BBB > file6
- git add file6
- git commit -m "commit file6"
- echo CCC > file7
- git add file7
- git commit -m "commit file7"
此时的版本库变为:
- $ git log --graph --pretty=oneline --abbrev-commit --all
- * b3a44b4 (HEAD -> alt) commit file7
- * 42afe97 commit file6
- * 691bb0b commit file5
- | * 00bf3a3 (master) commit file4
- | * 43f9a45 commit file3
- |/
- * aef04b2 commit file2
- * 4b0b17c commit file1
然后合并分支alt到master,并在合并基础上添加新修改:
- git checkout master
- git merge alt
- echo DDD > file8
- git add file8
- git commit -m "commit file8"
此时的版本库变为:
- $ git log --graph --pretty=oneline --abbrev-commit --all
- * 41fbc84 (HEAD -> master) commit file8
- * e66c0fc Merge branch 'alt'
- |\
- | * b3a44b4 (alt) commit file7
- | * 42afe97 commit file6
- | * 691bb0b commit file5
- * | 00bf3a3 commit file4
- * | 43f9a45 commit file3
- |/
- * aef04b2 commit file2
- * 4b0b17c commit file1
此时再次执行之前的git format-patch命令:
- $ git format-patch master~2..master
- 0001-commit-file5.patch
- 0002-commit-file6.patch
- 0003-commit-file7.patch
- 0004-commit-file8.patch
可以看出,此时还存在file5,file6和file7的变更。
而如果范围没有指定重点,则默认为HEAD,但其具体的打印内容可能会和想象的结果不同。
补丁是通过git format-patch命令按照拓扑顺序创建的,对于一个给定的提交,所有父提交的补丁会先于该提交的补丁生成和发出。这里可以看一下上面例子:
- $ git format-patch master~5
- 0001-commit-file2.patch
- 0002-commit-file3.patch
- 0003-commit-file4.patch
- 0004-commit-file5.patch
- 0005-commit-file6.patch
- 0006-commit-file7.patch
- 0007-commit-file8.patch
上面例子中,file1是范围中的第一个提交,而且没有使用--root选项,因此没有针对file1的补丁,而合并分支alt是合并提交,也不生成补丁,因此才有了上面的生成顺序。
生成补丁后,就是要将之发送到另一个开发人员或开发人员列表进行代码审查,最终目标是希望其能够被另一个开发人员接收或上游仓库管理者接收,以应用到另一个版本库中。
格式化的补丁一般是要通过电子邮件发送的,可以直接导入本地用户的电子邮件用户代理(MUA),或者使用Git的git send-email命令。
如果要将一个已生成的补丁文件发送给另一个开发人员,有以下方式可以选择:
使用git send-email的示例为:
$ git send-email -to example@gmail.com 0001-commit-file2.patch
不过在使用该命令之前需要知道本地SMTP服务器机器端口号。
git send-email命令有很多配置选项,都记录在用户手册文档中。可以使用如下命令设置相关属性:
- git config --global sendemail.smtpserver smtp.my-isp.com
- git config --global sendemail.smtpserverport 465
这里不做示例,详细可查该命令的帮助文档。
Git有两条基础命令来应用补丁。高层命令git am的一部分是由底层命令git apply实现的。
git apply命令接受git diff风格的输出,然后将其应用到当前工作目录的文件中。而由于diff格式只包含逐行的编辑而没有其它信息,因此不能够用于提交,也不能记录版本库中的变更,因此,当git aply结束时,工作目录下的文件就留在修改的状态。
与之不同的是,无论是在发送前还是发送后,只要通过git format-patch格式化后的补丁,都包含额外的信息以记录版本库中的提交。
这里看个示例,首先是执行下面的命令生成全部的补丁:
- $ git format-patch --root master
- 0001-commit-file1.patch
- 0002-commit-file2.patch
- 0003-commit-file3.patch
- 0004-commit-file4.patch
- 0005-commit-file5.patch
- 0006-commit-file6.patch
- 0007-commit-file7.patch
- 0008-commit-file8.patch
再初始化新的版本库,然后将上面生成的补丁拷贝到新版本库的目录下:
- $ git init
- Initialized empty Git repository in C:/Users/wood/Desktop/GIT/tmp_file/.git/
然后执行如下代码:
- $ git am 0001-commit-file1.patch
- applying to an empty history
- Applying: commit file1
-
- $ git am 0002-commit-file2.patch
- Applying: commit file2
-
- $ git am 0003-commit-file3.patch
- Applying: commit file3
-
- $ git am 0004-commit-file4.patch
- Applying: commit file4
-
- $ git am 0005-commit-file5.patch
- Applying: commit file5
-
- $ git am 0006-commit-file6.patch
- Applying: commit file6
-
- $ git am 0007-commit-file7.patch
- Applying: commit file7
-
- $ git am 0008-commit-file8.patch
- Applying: commit file8
-
- $ ls
- 0001-commit-file1.patch 0005-commit-file5.patch file1 file5
- 0002-commit-file2.patch 0006-commit-file6.patch file2 file6
- 0003-commit-file3.patch 0007-commit-file7.patch file3 file7
- 0004-commit-file4.patch 0008-commit-file8.patch file4 file8
从上面结果来看,确实是将对应的补丁应用到了新的版本库中。
但上面的命令在某些情况下执行并不是总是能够顺序执行的,尤其是当出现冲突的情况下。上面的例子中,各个提交都是基于新的文件,各个修改之间并没有产生冲突,因此顺序应用patch可能并不会有问题。而在文件修改产生冲突,并且还涉及到多个分支的补丁,由于其可能存在的拓扑结构,应用patch可能需要用户更加小心谨慎才不会出现错误。
而要清理一个失败的的git am,并恢复原始分支,可以简单地执行git am --abort。