用 JGit 初始化 Git 库
最近,我被问及如何用 JGit 来初始化一个新的 Git 库,比如实现初始化一个库 /path/to/repodoes。
当我用 JGit 来创建库时其实并不难,这里有些细节值得提一提。因为在网上几乎少有关于这个主题的资料,而且有些还是错误的,因此本文就总结了如何使用 JGit API 来初始化一个 Git 库的方法。
本地库
为了用 JGit 来初始化一个库,那就要使用初始化命令。factoryGit 命令拥有一个静态方法 init() 来创建初始化命令。
Git git = Git.init().setDirectory( directory ).call();
在上述代码执行后,一个新的Git库就已经创建完成了。这个库的存储位置是通过setDirectory()来给定的,这个库构成了工作目录,它包含了已签出的文件及文件夹。若这个目录并不存在,那么通过上述方法会一直创建这个工作目录。
一个名为 .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库。
下面的代码片段在一个包含一个文件的已存在目录里边初始化库,并保留其内容。
File file = new File( "/path/to/existing/directory/readme.txt" ); file.createNewFile(); Git git = Git.init().setDirectory( file.getParentFile() ).call(); assertTrue( git.status().call().getUntracked().contains( file.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 FileRepositoryBuilder().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社区提问以获得帮助。