大家好,我是南哥,今天给大家分享一些JGIT一些比较常用的用法,因为网上信息很少,所以写一篇文章补充这方面的知识。
总结
在这篇文章中,将详细记录自己使用JGIT的一些实战经验,同时讲述自己在应用过程中踩的坑
大部分的场景可以通过github项目"jgit-cookbook"里面的例子进行实现(含拉代码,更新代码,两个分支差异清单场景...),但有一些特殊场景github并没有提及到,我在此处进行补充!
场景一:通过LCA,更加准确的diff清单获取方式
问题:通过获取功能版本featureA,基准版本master的各自最新commit进行diff时,diff会依据两个featureA、master分支各自所生成的RevTree进行比对,然后生成diff差异清单。但这种比对会存在一个问题:如果这时候有一个新的featureB合并到master后并未同步到featureA,这时候通过featureA、master进行diff时,生成的diff差异清单就会包含featureB,数据精准度就没有那么高。
目标:featureA与master进行diff时,diff差异清单只会显示feature改动的部分,其他分支改完合并到master,不会比对出来。
解决方案:获取featureA与master版本的公共祖先节点node后,再利用featureA与node进行diff出来的差异清单,才是精准的。
// 找到两个分支的最小公共祖先
public RevCommit getLCABothRef(Repository repository, Ref targetRef, Ref baseRef) throws IOException {
RevWalk revWalk = new RevWalk(repository);
RevCommit targetHead = revWalk.parseCommit(targetRef.getObjectId());
RevCommit baseHead = revWalk.parseCommit(baseRef.getObjectId());
revWalk.markStart(baseHead);
revWalk.markStart(targetHead);
// 我们设置的 filter 是 MERGE_BASE, 它会自动查找这两个 commit 所在分支的 MERGE_BASE。其中 MERGE_BASE 可以看作是分支的分岔点,合并的时候 MERGE_BASE 会作为参照。
revWalk.setRevFilter(RevFilter.MERGE_BASE);
RevCommit next = revWalk.next();
if (next == null) {
return null;
} else {
return next;
}
}
/**
* 用于获取两个分支之间的差异,调用该方法前,请先检查是否仓库已经是最新的代码
* @param repository
* @return
* @throws IOException
*/
public List<DiffEntry> getDiffEntryOfCommit(Repository repository, RevCommit targetCommit, RevCommit baseCommit) throws IOException {
// TODO 用于获取两个分支之间的差异,只进行比对,不进行代码分支更新
// TODO 用于比较两个公共的commit,与目标分支的最新commit进行比较
Git git = null;
RevWalk revWalk = null;
List<DiffEntry> res = new ArrayList<>();
// 开始读取两个版本的差异清单
try {
logger.info("开始比对两个commit的差异清单");
git = new Git(repository);
revWalk = new RevWalk(repository);
AbstractTreeIterator targetTreeParser = prepareTreeParser(repository, targetCommit);
AbstractTreeIterator masterTreeParser = prepareTreeParser(repository, baseCommit);
// 顺序不能反了
res = git.diff().setOldTree(masterTreeParser).setNewTree(targetTreeParser).call();
logger.info("完成比对两个commit的差异清单");
} catch (Exception e) {
logger.error("比对两个commit的差异清单出现异常:{}", e.getMessage());
throw new RuntimeException("比对两个commit的差异清单出现异常");
} finally {
if (revWalk != null) {
revWalk.dispose();
}
if (git != null) {
git.close();
}
}
return res;
}
private AbstractTreeIterator prepareTreeParser(Repository repository, RevCommit revCommit) throws IOException {
// TODO 在仓库中提取对应的分支上的revCommit,将该分支进行运算
RevWalk walk = null;
try {
walk = new RevWalk(repository);
RevTree tree = walk.parseTree(revCommit.getTree().getId());
CanonicalTreeParser treeParser = new CanonicalTreeParser();
try (ObjectReader reader = repository.newObjectReader()) {
treeParser.reset(reader, tree.getId());
}
return treeParser;
} catch (Exception e) {
logger.error("解析git版本树结构时出现异常:{}", e.getMessage());
throw new RuntimeException("解析git版本树结构时出现异常");
} finally {
if (walk != null) {
walk.dispose();
}
}
}
场景二:查询本地仓库汇中,分支是否存在
问题:当克隆代码仓库到本地时,会存在一个问题:拉到本地的仓库只包含默认分支,对于其他代码分支并没有创建
目标:克隆代码仓库到本地后,存在所需要的代码分支。
解决方案:克隆项目之后,手工创建其他所需要使用的代码分支。
public int cloneGitCode(File repoPath, String gitUrl, CredentialsProvider credentialsProvider, String targetVersion, String baseVersion) {
// TODO 用于拉取目标仓库代码,并同步指定的分支信息到本地
Git git = null;
try {
if (repoPath.exists()) {
logger.info("正在清除本地仓库的文件夹{}", repoPath.getName());
// FileUtil.delete(repoPath);
logger.info("完成清除本地仓库的文件夹{}", repoPath.getName());
}
logger.info("开始拉取仓库{}的代码", gitUrl);
git = Git.cloneRepository()
.setURI(gitUrl)
.setCredentialsProvider(credentialsProvider)
.setDirectory(repoPath)
// .setFs() // 文件系统特色化设置
// TextProgressMonitor 可以打印出克隆的进度
.setProgressMonitor(new TextProgressMonitor())
// setBranchesToClone 克隆之后代码分支一直有问题,先注释掉
// .setBranchesToClone(Arrays.asList("refs/heads/" + baseVersion, "refs/heads/" + targetVersion)) // 指定要克隆的分支列表,需要标注完整的引用路径 refs
.setBranch(baseVersion) // 克隆结束后切换到指定的分支
.call();
// 利用 exactRef 可以检查分支是否存在,不过使用时需要增加"refs/heads/.."来表示本地分支,"origin/..."来表示远程分支
if (git.getRepository().exactRef("refs/heads/" + targetVersion) == null) {
git.checkout()
.setCreateBranch(true)
.setStartPoint("origin/" + targetVersion)
.setProgressMonitor(new TextProgressMonitor())
.setName(targetVersion)
.call();
}
logger.info("完成拉取仓库{}的代码", gitUrl);
} catch (Exception e) {
logger.error("拉取仓库{}代码出现异常:{}", gitUrl, e.getMessage());
throw new RuntimeException("拉取仓库" + gitUrl + "代码出现异常");
} finally {
if (git != null) {
git.close();
}
}
return 1;
}
场景三:获取当前版本feature上某个文件的内容
问题:分析文件清单在两个版本分支之间的差异(不限于代码差异,也可以是AST树差异...)时,按照传统做法是通过拉取两个代码仓库到本地,然后根据文件路径到两个本地代码仓库找到对应的文件,最后进行比对,浪费资源。
目标:通过在同一个代码仓库进行获取对应文件的两个版本具体内容,再进行比对。
解决方案:可以通过文件的blobId(用来存储文件的版本)来获取目标版本的文件内容。
public String getTargetFileContent(Repository repository, RevTree targetRevTree,String targetFilePath) throws IOException {
// TODO 获取目标文件所对应的版本,展示当前版本的完整内容
ObjectReader reader = null;
try {
reader = repository.newObjectReader();
ObjectId targetFileObjectId = findFileBlobId(repository, targetRevTree, targetFilePath);
ObjectLoader targetLoader = reader.open(targetFileObjectId);
byte[] targetContentBytes = targetLoader.getBytes();
String targetFileContent = new String(targetContentBytes, StandardCharsets.UTF_8);
return targetFileContent;
} catch (IOException e) {
throw e;
}finally {
if( reader != null){
reader.close();
}
}
}
private ObjectId findFileBlobId(Repository repository, RevTree revTree, String filePath) throws IOException {
// TODO 找到对应文件的BlobId 可以用于后续查询整个文件在该版本下的完整文件内容
TreeWalk treeWalk = null;
try{
// 专门遍历树结构的工具
treeWalk = new TreeWalk(repository);
// 通过TreeWalk进行遍历,可以遍历所有的文件
treeWalk.addTree(revTree);
// 启用递归(支持子目录文件)
treeWalk.setRecursive(true);
// 设置目标路径
treeWalk.setFilter(PathFilter.create(filePath));
ObjectId fileObjectId = null;
while(treeWalk.next()){ // 使用while的话 会把仓库整个文件都遍历一遍,造成资源浪费
// 只看文件类型 即Blob
if(treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB){
// 1. 找到目标文件的Blob Id值
fileObjectId = treeWalk.getObjectId(0);
}
}
return fileObjectId;
}catch (Exception e){
throw e;
}finally {
if(treeWalk!=null){
treeWalk.close();
}
}
}
补充知识点
RevWalk: 该类用于从commit的关系图(graph)中遍历commit。晦涩难懂?看到范例就清楚了。
- RevWalk由一个一个RevCommit组成
- markStart()
- 用于指定应从哪开始提交历史记录,告诉RevWalk,从这些提交对象开始,往历史提交记录遍历。
- 可多次调用,每次调用都会往"起点集合"中添加新的起点,而非覆盖原先的起点。同时从两个分支的最新提交开始,往历史遍历所有可达的提交。
- 遍历是去重的,即使多个起点最终指向同一个提交,RevWalk也只会遍历一次该提交,不会重复处理。
- master: A <- B <- C <- D ; feature: A <- B <- E <- F ; 遍历出来是 D、C、F、E、B、A 遍历的顺序默认是提交时间倒序,跟设置markStart顺序无关
- 遍历的过程中,RevWalk会从这些起点开始,递归遍历每一个提交的父提交,直到没有父提交或遇到markStop()标记的终点
- 可用于比对两个分支的所有提交(筛选出只在feature分支提交的部分)、查找两个分支的完整历史、查找两个分支的最近公共祖先(遍历过程中记录第一个被两个分支包含的提交)
- markUninteresting() 标记"无意义/不感兴趣"的提交
- 该方法用于标注(不需要出现在遍历结果中)的提交,RevWalk遍历到这些提交时,会跳过该提交,且停止遍历该提交的所有父提交
- 若祖先同时存在正常路径和Uninteresting路径中,会被标记为Uninteresting并跳过,这是实现分支差异对比的核心。
感谢
感谢大家观看我的文章,希望对大家有帮助,如果大家有兴趣的话,也可以点点关注。
稀土掘金:南神编码
微信公众号:大熊在树上
github:github.com/cznczai