JGIT使用教程(含LCA、BlobId解析...)

2 阅读7分钟

大家好,我是南哥,今天给大家分享一些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。晦涩难懂?看到范例就清楚了。

  1. RevWalk由一个一个RevCommit组成
  2. markStart()
  • 用于指定应从哪开始提交历史记录,告诉RevWalk,从这些提交对象开始,往历史提交记录遍历。
  • 可多次调用,每次调用都会往"起点集合"中添加新的起点,而非覆盖原先的起点。同时从两个分支的最新提交开始,往历史遍历所有可达的提交。
  • 遍历是去重的,即使多个起点最终指向同一个提交,RevWalk也只会遍历一次该提交,不会重复处理。
  • master: A <- B <- C <- D ; feature: A <- B <- E <- F ; 遍历出来是 D、C、F、E、B、A 遍历的顺序默认是提交时间倒序,跟设置markStart顺序无关
  1. 遍历的过程中,RevWalk会从这些起点开始,递归遍历每一个提交的父提交,直到没有父提交或遇到markStop()标记的终点
  2. 可用于比对两个分支的所有提交(筛选出只在feature分支提交的部分)、查找两个分支的完整历史、查找两个分支的最近公共祖先(遍历过程中记录第一个被两个分支包含的提交)
  3. markUninteresting() 标记"无意义/不感兴趣"的提交
  • 该方法用于标注(不需要出现在遍历结果中)的提交,RevWalk遍历到这些提交时,会跳过该提交,且停止遍历该提交的所有父提交
  • 若祖先同时存在正常路径和Uninteresting路径中,会被标记为Uninteresting并跳过,这是实现分支差异对比的核心。

感谢

感谢大家观看我的文章,希望对大家有帮助,如果大家有兴趣的话,也可以点点关注。

稀土掘金:南神编码

微信公众号:大熊在树上

github:github.com/cznczai