JGit是一个由Eclipse基金会开发、用于操作git的纯Java库。它本身也是Eclispe的一部分,实际上Eclipse的插件EGit便是基于JGit的。如果你像我这样有使用代码来操作git的需求,那就准备好拥抱JGit吧。目前来看别的竞品没它靠谱。
概念
从用户指南的概念一节中可以看到,JGit的基本概念如下:
Git对象(Git Objects):就是git的对象。它们在git中用SHA-1来表示。在JGit中用
AnyObjectId和ObjectId表示。而它又包含了四种类型:
二进制大对象(blob):文件数据树(tree):指向其它的tree和blob
提交(commit):指向某一棵tree
标签(tag):把一个commit标记为一个标签
引用(Ref):对某一个git对象的引用。
仓库(Repository):顾名思义,就是用于存储所有git对象和Ref的仓库。
RevWalk:该类用于从commit的关系图(graph)中遍历commit。晦涩难懂?看到范例就清楚了。
RevCommit:表示一个git的commit
RevTag:表示一个git的tag
RevTree:表示一个git的tree
TreeWalk:类似RevWalk,但是用于遍历一棵tree
准备环境
让我们从一个最典型的用例开始吧。首先在/tmp/jgit/repo中创建一个git仓库:
1 | mkdir -p /tmp/jgit/repo |
再创建一个clone该仓库的客户端:
1 | cd /tmp/jgit/ |
输入git status应该能够看到Initial commit,这样环境就没有问题了。然后提交一个文件,给仓库里来点库存:
1 | echo hello > hello.txt |
动手
获取仓库
动手时间。新建Maven工程,往pom.xml中增加dependency,最后的pom.xml看起来就像这样:
1 |
|
让我们先尝试clone一下这个仓库。因为client分为已经存在以及重新clone的两种,所以我们在src/main/java中新增一个RepositoryProvider接口,用两种不同实现以示区分:
1 | public interface RepositoryProvider { |
并实现之:
1 | public class RepositoryProviderCloneImpl implements RepositoryProvider { |
新增一个HelloJGit主程序类:
1 | public class HelloJGit { |
直接运行HelloJGit的main函数,ls /tmp/jgit/应该就能看到新clone出来的clientJava文件夹了。
1 | cd /tmp/jgit/clientJava |
我们当然不希望总是在使用的时候才重新clone一个仓库,因为当仓库很大的时候可能会非常耗时。让我们在client中再提交一个commit:
1 | echo hello2 > hello2.txt |
然后尝试直接从刚刚clone下来的clientJava中创建Repository:
1 | public class RepositoryProviderExistingClientImpl implements RepositoryProvider { |
然后把HelloJGit的repoProvider实例替换为RepositoryProviderExistingClientImpl:
1 | private static RepositoryProvider repoProvider = new RepositoryProviderExistingClientImpl("/tmp/jgit/clientJava/.git"); |
注意这次的路径中需要加上.git才行。再次运行HelloJGit的main函数,便可以通过ls /tmp/jgit/clientJava看到新提交的hello2.txt文件了。
常用操作
接下来尝试git add、git commit和git push这几个最常用的命令。让我们往clientJava中添加一个hello3.txt文件并提交。如下修改HelloJGit:
1 | public static void main(String[] args) throws Exception { |
虽然操作多了,但是有了Repository和Git对象之后,看起来它们的实现都非常直观。运行main函数之后,可以到client文件夹中校验一下:
1 | cd /tmp/jgit/client |
在我的机器上运行git log,可以得到:commit 7841b8b80a77918f2ec45bcedb934e2723b16b5c (HEAD -> master, origin/master),以及另外两个commit。有兴趣的读者们可以自行尝试其它的git命令。
其它对象
虽然上面两小节的内容对于普通需求来说已经大致上够用了,但是在概念一节中介绍到的其它概念,如Git对象、引用等还没有出场呢。我们再新建一个WalkJGit的类,在main函数中编写如下代码:
1 | try (Repository repo = repoProvider.get()) { |
这回,Ref和ObjectId都出现了。在我的机器上,运行以上程序打印出来了AnyObjectId[7841b8b80a77918f2ec45bcedb934e2723b16b5c]。我们可以看到,取得HEAD的Ref,其ObjectId其实就是在client文件夹中运行git log之后结果。除了HEAD以外,repo.getAllRefs()返回的Map实例中还有refs/heads/master和refs/remotes/origin/master,在目前的情况下,它们的ObjectId完全相同。那么如何获取其它的commit呢?那就是RevWalk出场的时候。把main函数中的内容替换为如下代码:
1 | try (Repository repo = repoProvider.get()) { |
可以看到RevWalk本身是实现了Iterable接口的。通过对该对象进行循环,就可以获取所有的commit的RevCommit对象。可以到client文件夹确认一下,这些SHA-1字符串应该跟刚才git log命令的结果相同。RevCommit对象本身含有这个commit的所有信息,所以可以如下打印出来:
1 | revWalk.forEach(c -> { |
这样看起来是不是很有git log的感觉呢?需要注意的是,RevWalk线程不安全,并且像Stream那样,只能使用一次。如果想要再来一次,就需要重新创建RevWalk对象或是调用其reset方法(还得重新markStart!)。
要想看到每个commit中有什么内容,那就需要用到TreeWalk了,它的思路和RevWalk类似。尝试如下代码:
1 | for (RevCommit commit : revWalk) { |
这样便可以显示仓库在每个commit时候的状态了。如果需要diff,那么还将需要用到DiffEntry等类,本文就不再赘述了,有兴趣的读者可以参考这个类。
最后将环境还原:
1 | rm -rf /tmp/jgit |