文章目录
  1. 1. 背景
  2. 2. 方案
    1. 2.1. api command
    2. 2.2. Patch
    3. 2.3. DiffFormatter / DiffEntry
    4. 2.4. diff
  3. 3. 动手时间
  4. 4. patch & diff

上篇博文介绍了JGit,之后就开始做项目了。遇到的第一个问题是如何用JGit生成patch文件。

背景

我希望在项目中能够实现这样的功能:用户发送一个request,服务器就帮用户生成代码并生成一个commit到用户本地的git中,但是这不太可能,因为用户的环境并不是服务器的环境。进一步的方案是直接在服务器端clone git仓库(或是维持一份最新代码),服务器本地生成commit并push,这样做会有一些安全方面需要考虑的因素。我采用的是退一步的方案,即让服务器生成一个patch文件并上传到S3,以便用户稍后下载并apply到本地。

方案

api command

打开JGit的api包一看,各种git命令应有尽有,如apply、cherry-pick等。但惟独没有format-patch命令。网上一搜,甚少有人有这样的需求或问题,只有这篇文章比较靠谱,但是它介绍的侧重于diff而非生成patch。

Patch

还是得找找patch相关的代码。源代码搜遍也就这个Patch.java应该是patch文件的JGit模型,但是读完后发现,它只能把patch文件映射成这个模型,并不能反向从模型序列化为patch文件。

DiffFormatter / DiffEntry

DiffCommand其实上还是调用的DiffFormatter和DiffEntry,所以看看这俩是否能够支持什么样的参数,来生成patch文件呢?可惜还是无果。DiffFormatter的API也不太直观,不容易理解。但是它能够做一些diff commit这样的事情。

diff

走投无路之际,在git-format-patch上看到,这个命令其实是用来生成用邮件发送的patch文件。难怪patch文件的前几行看起来有From,有Subject什么的,也许它们不是必须的?那就可以试试把diff的结果当作patch直接写入文件。

动手时间

首先创建一个git环境,a、b、c三个文件用来测试改删增:

1
2
3
4
5
6
7
8
9
10
11
12
mkdir -p /tmp/ggg
cd /tmp/ggg
git init
echo line a1 > a.txt
echo line a2 >> a.txt
echo line b1 > b.txt
git add .
git commit -m "first commit wiht a.txt and b.txt"
echo line a3 >> a.txt
echo line c1 > c.txt
rm b.txt
git diff

运行完成后就能看到diff文件的内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
diff --git a/a.txt b/a.txt
index 7a3d45f..c723fac 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
line a1
line a2
+line a3
diff --git a/b.txt b/b.txt
deleted file mode 100644
index b45d9fa..0000000
--- a/b.txt
+++ /dev/null
@@ -1 +0,0 @@
-line b1

在程序中,如此这般运行git diff命令:

1
2
Git git = Git.init().setDirectory(new File("/tmp/ggg")).call();
git.diff().setOutputStream(System.out).call();

发现JGit的diff和Git的diff还是不太一样的。JGit的diff包含了新增文件的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff --git a/a.txt b/a.txt
index 7a3d45f..c723fac 100644
--- a/a.txt
+++ b/a.txt
@@ -1,2 +1,3 @@
line a1
line a2
+line a3
diff --git a/b.txt b/b.txt
deleted file mode 100644
index b45d9fa..0000000
--- a/b.txt
+++ /dev/null
@@ -1 +0,0 @@
-line b1
diff --git a/c.txt b/c.txt
new file mode 100644
index 0000000..8e37e08
--- /dev/null
+++ b/c.txt
@@ -0,0 +1 @@
+line c1

这就非常合适了。只要证明它能够被作为patch导入到git中即可。首先修改代码输出到文件:

1
2
3
4
5
File patch = new File("/tmp/jgit.patch");
Git git = Git.init().setDirectory(new File("/tmp/ggg")).call();
try (OutputStream outputStream = new FileOutputStream(patch)) {
git.diff().setOutputStream(outputStream).call();
}

然后清空修改过的文件:

1
2
3
4
cd /tmp/ggg
git checkout .
git clean -df
git status

现在尝试apply patch:

1
2
git apply /tmp/jgit.patch
git status

果然成功了。在不考虑冲突的情况下,看起来这一招还是管用的。但是由于缺失了commit的信息,所以运行git am /tmp/jgit.patch就会报错:Patch format detection failed.有没有办法解决这个问题呢?当然了。我们现在知道了patch只不过是多了一些邮件信息罢了,那我们自己就可以生成。在try内增加如下代码,模拟git format-patch

1
2
3
4
5
outputStream.write("From: GGG <ggg@somewhere.com>\n".getBytes());
outputStream.write("Date: Tue, 12 Sep 2017 20:16:10 +0800\n".getBytes());
outputStream.write("Subject: [PATCH] ggg is not here. Turn left and ask JGit\n".getBytes());
outputStream.write("\n---\n\n".getBytes());
}

运行一下,然后尝试使用git am

1
2
3
git am /tmp/jgit.patch
git log
git show HEAD

果然可以直接生成commit。Mission Complete!

patch & diff

其实Linux已经提供了一个patch命令,无需git即可直接应用patch文件:

1
2
3
git reset HEAD^ --hard
ls
patch < /tmp/jgit.patch

而且还支持回滚(git apply也支持):

1
2
patch -R < /tmp/jgit.patch
ls

实际上patch文件一般是使用diff命令来生成的:

1
2
diff -u a.txt c.txt > diff.patch
cat diff.patch

这两个命令网上的教程不少,有兴趣的话可以自行搜索阅读。最后还是把环境恢复:

1
rm -rf /tmp/ggg /tmp/jgit.patch

文章目录
  1. 1. 背景
  2. 2. 方案
    1. 2.1. api command
    2. 2.2. Patch
    3. 2.3. DiffFormatter / DiffEntry
    4. 2.4. diff
  3. 3. 动手时间
  4. 4. patch & diff