四时宝库

程序员的知识宝库

如何在自己的Java项目中集成Git实现版本控制功能

笔者最近遇到一个需求,在项目中实现对用户文档的版本管理。自己造轮子复杂且毫无意义,于是乎想到了集成Git。

版本管理工具Git

首先想到一个方案,使用CMD命令调用Git,再通过Java执行CMD语句。

如:

cmd /c start /d 文件路径

git语句

后来发现,已经有一个第三方库(JGit)把调用Git的工作完成了。在Eclipse官网下载JGit及依赖库后,导入项目中即可使用(亦可使用官网上Maven导入方法)。

JGit常用功能

1.创建仓库

Git git = Git.init()

.setDirectory(repositoryPath)

.call();

git.close();

执行成功后,会在repositoryPath(本地文件夹)下生成.git文件夹。

2.克隆仓库

Git git = Git.cloneRepository()

.setURI( uriPath )

.setDirectory( repositoryPath);

git.close();

上面的代码会将远程Git仓库克隆到本地。

3.填充仓库

创建仓库后,就可以向仓库中填充内容了。但为了提交文件首先需要将它添加到所谓的索引(又名临时区)。只有在索引中增加或者删除的文件才会被 commit 命令考虑。

DirCache index = git.add()

.addFilePattern( FilePath )

.call();

上述代码把某个文件加入到了索引中。值得注意的是文件的实际内容是被复制到了索引。这意味着后来的对这个文件的修改不会被包含在索引中,除非再次添加。

给 addfilepattern() 的路径必须相对于工作目录。如果一个路径没有指向一个现有的文件,它会被忽略。

虽然方法名称表明模式是可以接受的,但是 在 JGit中 的支持是有限的。传一个‘.’将添加工作目录中所有文件。但是 *.java 之类的用法是不支持的,尽管在原生 Git 命令中可以这么用。

通过 call() 返回的索引,在 JGit 命名为 DirCache,通过检查验证了它实际上包含了我们所期望的东西。它的 getEntryCount() 方法返回了文件总数, 而且 getEntry() 方法反回了指定位置的入口。

一切就绪后,可以使用commit命令提交更改。

RevCommit commit = git.commit()

.setMessage( "Message Info" )

.call();

提交消息必须被指定,否则 call() 将会抛出一个 NoMessageException 异常。但是一个空的消息是被允许的。作者和提交者如果没有被相应的标记的方法指出的话,将会从配置文件中取得。

返回的 RevCommit 描述了这条 commit 的信息作者,提交者,时间戳,还有一个指针指向文件树和构成此提交的目录。

新增或者修改的文件需要被增加,同样的删除的文件也需要被明确地被移除。Rm命令是相对于Add命令的,使用方式也是一样。

DirCache index = git.rm()

.addFilepattern( FilePath)

.call();

上面的指令会删除指定的文件。由于这是仓库中的唯一的文件,当查询该条目的编号时,返回的索引值将为0。

除非 setCached 被指定为 true,文件也会在工作目录被删除。因为 Git 不会跟踪目录, Rm 命令也会删除指定文件的空的父目录。

试图删除一个不存在的文件会被忽略掉。但是和 Add 命令不一样的是, Rm 命令在 addFilepattern() 命令中不接受通配符。任何要被删除的文件需要被单独指定。

这些改变将会与下一个提交一起被存储在仓库中。请注意,创建一个空的提交是完全合法的,例如一个没有文件增加或者删除执行过的仓库。

4.仓库的状态

状态命令会列出索引和目前最新的提交之间有差异的文件或者工作目录和索引之间有差异的文件或者没有被 Git 跟踪到的文件。

Status 命令以其最简单的形式,收集了所有属于库文件的状态。

Status status = git.status()

.call();

对象状态的 get 类方法应该是意义自明的。它们返回在该方法名描述的状态中的文件名的集合。例如,如前文所述中在指定文件被增加到索引后,status.getAdded()这个方法会返回一个包含刚刚添加的文件的路径的集合。

如果没有任何差别,也没有未被跟踪到的文件,那么 Status.isClean() 这个方法会返回 true。而顾名思义,如果有未提交的改变的话 Status.hasUncommittedChanges() 这个方法会返回 true。

Status 命令可以用 addPath() 方法来配置为只显示某些文件状态。给定的路径必须指明一个文件或者目录名。不存在的路径会被忽视,并且正则表达式或通配符是不被支持的。

Status status = git.status()

.addPath( "documentation" )

.call();

在以上的例子中,在 documentation 目录下递归的所有文件的状态将被计算出。

5.探索库

最简单的 git log 对应在 JGit 中的形式允许列出所有的可以从目前的 HEAD 中得到的提交。

Iterable iterable = git.log().call();

返回的迭代器可以用来循环遍历所有的用 Log 命令找到的提交。

对于更高级的使用情况,建议使用 RevWalk API,这个同样类也被 Log 命令所使用。除了提供更大的灵活性,也避免了可能的资源泄漏,因为 Log 命令内部使用的 RevWalk 是永远不会关闭。

Repository repository = git.getRepository()

try( RevWalk revWalk = new RevWalk( repository ) ) {

ObjectId commitId = repository.resolve( "refs/heads/side-branch" );

revWalk.markStart( revWalk.parseCommit( commitId ) );

for( RevCommit commit : revWalk ) {

System.out.println( commit.getFullMessage );

}

}

分支的 side-branch分支点的提交 id 被获取,然后 RevWalk 被委派去遍历历史记录。因为markStart() 方法需要 Rev 命令,RevWalk 的 parse命令被用来将提交 id 转化为实际提交。

当 RevWalk 设置完毕后,上述代码段循环打印出每条提交的信息。try-with-resource 声明确保了 RevWalk 会在结束后被关闭。需要注意的是,多次调用 markStart() 方法来包含多次的参考之间的往返移动是合法的。

一个 RevWalk 也可以被设定用来过滤提交,既可以通过匹配提交对象的属性,也可以通过匹配它代表的目录树的路径。如果提前知道的话,不感兴趣的提交和他们的祖先链可以从输出中排除。当然,输出可以进行排序,例如按日期或拓扑(所有子类在父类前)。

6.和远程仓库交互

本地库经常是从远程库中克隆来的。本地的改变最终应该发布到原始仓库中。为了完成这一操作,JGit 提供了一个 push 命令.

Iterable iterable = local.push().call();

PushResult pushResult = iterable.iterator().next();

Status status = pushResult.getRemoteUpdate( "refs/heads/master" ).getStatus();

该命令将返回一个可迭代的 PushResults。在上述例子中的迭代只有一个元素。为了验证推送是否成功,pushResult 可以被要求返回一个 RemoteRefUpdate 给指定的分支。

一个 RemoteRefUpdate 详细描述了更新了什么和如何更新的。但它同时也包含了一个状态属性,用以对结果状态进行总结性表述。如果状态返回 OK,我们可以放心的确认操作是成功的。

即使命令不给任何建议也能工作,它有很多的选项,以下只列出了最常用的。默认情况下,该命令推送到了默认的叫做 ‘origin’ 的远程库。使用 setRemote() 指定一个不同的远程存储库的 URL 或名称。 如果其他分支比当前分支更应该推送的话,refspecs 可以用 setRefSpec() 来指定。标签是否需要被转移可以用 setPushTags() 来控制。最后,如果你不确定结果是否理想,则有一个 dry-run 选项,可以模拟一个推送操作。

FetchResult fetchResult = local.fetch().call();

TrackingRefUpdate refUpdate

= fetchResult.getTrackingRefUpdate( "refs/remotes/origin/master" );

Result result = refUpdate.getResult();

FetchResult 提供了详细的操作结果的信息。每一个受到影响的分支可以获得一个 TrackingRefUpdate 实例。最有趣的可能是 getResult() 的返回值对更新操作的结果进行了总结。此外,它还包括了哪个本地 ref (getLocalBame()) 和哪个远程 ref (getRemoteName()) 更新的信息,还有本地 ref 在更新前和更新后 (getOldObjectId() 和 getNewObjectid()) 指向的对象 id 的信息。

7.子模块管理

对于一个较大的Git工程,你可能会想在多个仓库之间共享代码,不管这些代码是在多个不同产品间使用的项目共享库或是一些模板。Git通过子模块来实现这样的需求。子模块允许将其他代码仓库的克隆作为子目录放到一个父仓库(有时候也称为父项目)中。一个子模块也是一个独立的仓库,你可以像其他仓库一样执行commit,branch,rebase等等操作。

1)添加一个子模块

@Test

public void testAddSubmodule() throws Exception {

String uri

= library.getRepository().getDirectory().getCanonicalPath();

SubmoduleAddCommand addCommand = parent.submoduleAdd();

addCommand.setURI( uri );

addCommand.setPath( "modules/library" );

Repository repository = addCommand.call();

repository.close();

File workDir = parent.getRepository().getWorkTree();

File readme = new File( workDir, "modules/library/readme.txt" );

File gitmodules = new File( workDir, ".gitmodules" );

assertTrue( readme.isFile() );

assertTrue( gitmodules.isF?ile() );

}

SubmoduleAddCommand对象需要知道两件事,第一是子模块从哪里克隆而来,第二是它应该存放在哪里。URI属性表示仓库库的克隆地址,这个克隆地址将会传递给clone命令。path属性则指定了相对于parent仓库根工作目录的路径,子模块将被存放在这个路径。

2)列出子模块

@Test

public void testListSubmodules() throws Exception {

addLibrarySubmodule();

Map<String,SubmoduleStatus> submodules

= parent.submoduleStatus().call();

assertEquals( 1, submodules.size() );

SubmoduleStatus status = submodules.get( "modules/library" );

assertEquals( INITIALIZED, status.getType() );

}

SubmoduleStatus命令返回了一个子模块的Map集合,其中键是子模块的路径,值是这个模块的状态值。通过以上代码我们能够验证子模块确实已经添加进去,而且它的状态是INITIALIZED的。这个命令还允许添加一个或多个路径来限制子模块状态。

JGit的StatusCommand并非原生的Git指令。如果在执行指令时添加选项‐‐ignore-submodules=dirty,那么所有对子模块工作目录的修改都会被忽略。

3)更新子模块

@Test

public void testUpdateSubmodule() throws Exception {

addLibrarySubmodule();

ObjectId newHead = library.commit().setMessage( "msg" ).call();

File workDir = parent.getRepository().getWorkTree();

Git libModule = Git.open( new File( workDir, "modules/library" ) );

libModule.pull().call();

libModule.close();

parent.add().addF?ilepattern( "modules/library" ).call();

parent.commit().setMessage( "Update submodule" ).call();

assertEquals( newHead, getSubmoduleHead( "modules/library" ) );

}

4)在父仓库中更新对子模块的修改

将上游的提交拉取到父仓库中也会修改子模块的配置。然而子模块本身并不会自动得到更新。

SubmoduleUpdateCommand就是用来解决这个问题。使用这个命令并不需要指定其他参数,它会更新所有已注册的子模块。该命令会克隆缺失的子模块并检出其配置中指定的提交。就如其他子模块命令一样,这里也有一个addPath()方法,以保证只更新给定路径下的子模块。

5)克隆一个包含子模块的仓库

此时你可能已经掌握一个规律,所有对子模块的操作都是手动的。克隆一个包含子模块配置的仓库并不会默认克隆它的子模块。但是,CloneCommand命令有一个cloneSubmodules的属性,如果设置为true,那么将会克隆所有配置的子模块。从内部看,在对父仓库进行克隆之后,SubmoduleInitCommand和SubmoduleUpdateCommand命令会被递归地执行,并且父仓库的工作目录会被检出。

6)移除一个子模块

使用removeSubmodule()方法。

首先,各个子模块会从.gitsubmodules和.git/config配置文件中移除。其次,子模块的入口会从索引中被移除。最后,.gitsubmodules文件以及索引的修改会被提交,并且子模块的内容会从工作目录中删除。

7)遍历子模块

@Test

public void testSubmoduleWalk() throws Exception {

addLibrarySubmodule();

int submoduleCount = 0;

Repository parentRepository = parent.getRepository();

SubmoduleWalk walk = SubmoduleWalk.forIndex( parentRepository );

while( walk.next() ) {

Repository submoduleRepository = walk.getRepository();

Git.wrap( submoduleRepository ).fetch().call();

submoduleRepository.close();

submoduleCount++;

}

walk.release();

assertEquals( 1, submoduleCount );

}

通过next()方法walk对象可以指向下一个子模块,如果没有更多的子模块,该方法会返回false。使用SubmoduleWalk时,通过调用release()方法可以释放子模块相关的资源。再次强调,如果你获得一个子模块的仓库实例可别忘了关闭它。

SubmoduleWalk也可以用来获取子模块的详细信息。通过它的大部分getter方法可以访问到当前子模块的属性,诸如path,head,remote URL等等。

8)同步远程URL

从上面我们知道子模块的配置保存在父仓库根工作目录下的.gitsubmodules文件中。而至少,在.git/config文件中,我们可以重写覆盖子模块的远程URL。对于每个子模块,它们本身都有一个配置文件。那么反过来,每个子模块可以有另一个远程URL。SubmoduleSyncCommand命令可以用来将所有远程URL重置为.gitmodules中的配置。

8.分支管理

// 获取引用

Ref master = repo.getRef("master");

// 获取该引用所指向的对象

ObjectId masterTip = master.getObjectId();

// Rev-parse

ObjectId obj = repo.resolve("HEAD^{tree}");

// 装载对象原始内容

ObjectLoader loader = repo.open(masterTip);

loader.copyTo(System.out);

// 创建分支

RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");

createBranch1.setNewObjectId(masterTip);

createBranch1.update();

// 删除分支

RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");

deleteBranch1.setForceUpdate(true);

deleteBranch1.delete();

// 配置

Config cfg = repo.getConfig();

String name = cfg.getString("user", null, "name");

第一行获取一个指向 master 引用的指针。 JGit 自动抓取位于 refs/heads/master 的 真正的 master 引用,并返回一个允许你获取该引用的信息的对象。 你可以获取它的名字 (.getName()) ,或者一个直接引用的目标对象 (.getObjectId()) ,或者一个指向该引用的符号指针 (.getTarget()) 。 引用对象也经常被用来表示标签的引用和对象,所以你可以询问某个标签是否被 “削除” 了,或者说它指向一个标签对象的(也许很长的)字符串的最终目标。

第二行获得以 master 引用的目标,它返回一个 ObjectId 实例。 不管是否存在于一个 Git 对象的数据库,ObjectId 都会代表一个对象的 SHA-1 哈希。 第三行与此相似,但是它展示了 JGit 如何处理 rev-parse 语法(要了解更多,请看 分支引用 ),你可以传入任何 Git 了解的对象说明符,然后 JGit 会返回该对象的一个有效的 ObjectId ,或者 null 。

接下来两行展示了如何装载一个对象的原始内容。 在这个例子中,我们调用 ObjectLoader.copyTo() 直接向标准输出流输出对象的内容,除此之外 ObjectLoader 还带有读取对象的类型和长度并将它以字节数组返回的方法。 对于一个( .isLarge() 返回 true 的)大的对象,你可以调用 .openStream() 来获得一个类似 InputStream 的对象,它可以在没有一次性将所有数据拉到内存的前提下读取对象的原始数据。

接下来几行展现了如何创建一个新的分支。 我们创建一个 RefUpdate 实例,配置一些参数,然后调用 .update() 来确认这个更改。 删除相同分支的代码就在这行下面。 记住必须先 .setForceUpdate(true) 才能让它工作,否则调用 .delete() 只会返回 REJECTED ,然后什么都没有发生。

最后一个例子展示了如何从 Git 配置文件中获取 user.name 的值。 这个 Config 实例使用我们先前打开的仓库做本地配置,但是它也会自动地检测并读取全局和系统的配置文件。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接