用 JGit 初始化 Git 库

jopen 10年前

最近,我被问及如何用 JGit 来初始化一个新的 Git 库,比如实现初始化一个库 /path/to/repodoes。

当我用 JGit 来创建库时其实并不难,这里有些细节值得提一提。因为在网上几乎少有关于这个主题的资料,而且有些还是错误的,因此本文就总结了如何使用 JGit API 来初始化一个 Git 库的方法。

本地库

为了用 JGit 来初始化一个库,那就要使用初始化命令。factoryGit 命令拥有一个静态方法 init() 来创建初始化命令。

Git git = Git.init().setDirectory( directory ).call();

在上述代码执行后,一个新的Git库就已经创建完成了。这个库的存储位置是通过setDirectory()来给定的,这个库构成了工作目录,它包含了已签出的文件及文件夹。若这个目录并不存在,那么通过上述方法会一直创建这个工作目录。

用 JGit 初始化 Git 库

一个名为 .git 的子目录在整个工作目录中处于最顶层。这里面包含了本地库的历史日志,配置参数,分支指针,索引(又名中转区)等等。

上面的截图中展示了.git 目录的内部结构。refs 目录维护了分支和标签信息。其实际内容将会存储在对象目录中。在 logs 目录中,有关对分区的修改都会被记录。举个例子,一个提交和签出操作都将创建一个日志记录,这个记录可以用 gitreflog 命令来查看。多年前我写的这个帖子 Explore Git Internals with the JGit API 里面详细的写明了如何使用 Git 来管理这个库里内容。

为了确保命令事实上成功执行,状态命令常用于查询库的状态,很像git status做的一样。但不幸的是JGit的status实现不同于原生Git,因为如果没有库,它不会报错。这使得检查一个已存在HEAD ref变得更为必要,它表示事实上有一个库。

assertNotNull( git.getRepository().getRef( Constants.HEAD ) );  assertTrue( git.status().call().isClean() );

对于最近初始化的库来说,isClean()返回true,是因为在工作目录里边没有任何改动或者有未跟踪文件。

断言使用theInitCommandscall()方法返回的GIT实例。这个类充当工厂的作用,并且常用于创建在库上执行的Git命令(例如:add,commit,checkout)。

新初始化的库比较特殊,由于还没有分支被创建。尽管有一个引用分支的HEAD(指向当前分支),(默认叫做master)这个特有的分支并不存在。

就第一个提交来说通常没有什么需要担心的,缺少的分支将会被创建。不管怎样,像git branch这样的操作可以创建分支并会在最初的提交被提交之前,会伴随着略微令人误导的错误信息'Ref HEAD cannot be resolved'而失败。

确定库是否包含一个提交,检查HEAD ref如下:

Ref headRef = git.getRepository().getRef( Constants.HEAD );  if( headRef == null || headRef.getObjectId() == null ) {    // no commit yet  }

将目录变成库

如上所示,在必要的情况下,InitCommand会创建缺少的目录。但是这个命令也常用于已存在的目录,从而将其变成一个git库。

下面的代码片段在一个包含一个文件的已存在目录里边初始化库,并保留其内容。

F‌ile f‌ile = new F‌ile( "/path/to/existing/directory/readme.txt" );  f‌ile.createNewF‌ile();  Git git = Git.init().setDirectory( f‌ile.getParentF‌ile() ).call();  assertTrue( git.status().call().getUntracked().contains( f‌ile.getName() ) );

由于文件未被跟踪,status命令会提示这个文件。直到将这个文件加入索引才能够被提交到刚刚创建的库里边。

如果指定的目录已经持有一个Git库,那就没有必要担心。既然这样,JGit将不做任何事并返回一个指向已存在库的实例。

分离工作和.git目录

默认情况下,尽管一个库有一个工作目录,它的.git目录直接位于工作目录下面,但这不是必须的。一个库完全可以没有工作目录(后续讨论)或者工作目录可以位于一个除.git目录之外完全不同的位置。

下面的初始化命令创建了这样一个库

Git git = Git.init().setDirectory( workDir ).setGitDir( gitDir ).call();

最终的库将会位于gitDir目录(储存历史记录,分支,标签等的目录),但这将会使它的工作目录在workDir上面。

对于一个已存在的库来说,工作目录配置也是可以改变的。要么手动编辑配置文件.git,或者通过JGit API配置。

StoredConfig config = git.getRepository().getConfig();  config.setString( "core", null, "worktree", workDir.getCanonicalPath() );  config.save();

没有必要把工作目录内容手动从旧的位置移动到新的位置。

空仓库

刚刚创建的库是在本地处理的,也叫非空仓库。另一种Git库叫做空仓库。

这些目的在于用作中心仓库,将被其他用户共享。没有直接的提交可以提交到空仓库中。一个空仓库收到提交,是由于它们从用户本地仓库被推送。团队成员从这个仓库中拿下其他人提交的提交。

下面的代码片段将创建一个空仓库。

Git git = Git.init().setDirectory( directory ).setBare( true ).call();

一个空仓库没有工作目录。反之,它的目录结构可以在一个非空仓库中的.git目录下面找到并在指定的目录中直接被创建。

因为status命令需要一个工作目录,所以它不能够验证上述代码成功执行。而是仓库实例应该为空并想下面验证指向所需目录。

assertTrue( git.getRepository().isBare() );  assertEquals( directory, git.getRepository().getDirectory() );

值得注意的是为这个工作目录查询仓库(也就是callinggetWorkTree())将会对空仓库抛出NoWorkTreeException。

可选API: Repository.create()

一个可选的方法来初始化仓库就是使用Repository的create方法。

Repository repository = new F‌ileRepositoryBuilder().setGitDir( directory ).build();  repository.create();    assertNotNull( git.getRepository().getRef( Constants.HEAD ) );  assertTrue( Git.wrap( repository ).status().call().isClean() );

在FileRepositoryBuilder帮助下,Repository实例被创建,代表没有现有的库。Callingcreate()方法具体化仓库。结果和InitCommand一样。

为了创建空仓库,还有一个重载的方法:create(boolean bare)。

结论

我希望这篇文章能够帮助理解怎样用JGit创建一个新仓库。这里使用的代码收集于学习测试,可以在这里发现它的全部内容:
https://gist.github.com/rherrmann/4bacb68b23be1f12c73d

它详细说明了对InitCommand API正确和不正确的使用,或许可以作为进一步实验JGit的一个起点。

如果你有任何难点或问题,可以留下评论或在友善且乐于助人的JGit社区提问以获得帮助。