com.github.checkstyle.regression.git.DiffParser.java Source code

Java tutorial

Introduction

Here is the source code for com.github.checkstyle.regression.git.DiffParser.java

Source

////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2019 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.github.checkstyle.regression.git;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang.math.IntRange;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.io.DisabledOutputStream;

import com.github.checkstyle.regression.data.GitChange;
import com.github.checkstyle.regression.data.ImmutableGitChange;

/**
 * Parses git diff between PR branch and master for the further use.
 * @author LuoLiangchen
 */
// -@cs[ClassDataAbstractionCoupling] We have to import many classes from JGit
public final class DiffParser {
    /** Prevents instantiation. */
    private DiffParser() {
    }

    /**
     * Parses the diff between a given branch and the master in the give repository path.
     * @param repositoryPath the path of checkstyle repository
     * @param branchName     the name of the branch to be compared with master
     * @return a list of {@link GitChange} to represent the changes
     * @throws IOException     JGit library exception
     * @throws GitAPIException JGit library exception
     */
    public static List<GitChange> parse(String repositoryPath, String branchName)
            throws IOException, GitAPIException {
        final List<GitChange> returnValue = new LinkedList<>();
        final File gitDir = new File(repositoryPath, ".git");
        final Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).readEnvironment().findGitDir()
                .build();

        try {
            final TreeParserPair pair = getTreeParserPair(repository, branchName);
            final Git git = new Git(repository);
            final DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE);
            formatter.setRepository(repository);

            try {
                final List<DiffEntry> diffs = git.diff().setOldTree(pair.commonAncestorTreeParser)
                        .setNewTree(pair.prTreeParser).call().stream()
                        .filter(entry -> entry.getChangeType() != DiffEntry.ChangeType.DELETE)
                        .collect(Collectors.toList());
                for (DiffEntry diff : diffs) {
                    returnValue.add(convertDiffEntryToGitChange(diff, formatter));
                }
            } finally {
                git.close();
            }
        } finally {
            repository.close();
        }

        return returnValue;
    }

    /**
     * Gets a TreeParserPair for the further use.
     * @param repository the repository to parse
     * @param branch     the name of the PR branch
     * @return the TreeParserPair prepared for the further use
     * @throws IOException JGit library exception
     */
    private static TreeParserPair getTreeParserPair(Repository repository, String branch) throws IOException {
        final TreeParserPair returnValue;
        final RevWalk walk = new RevWalk(repository);

        try {
            final RevCommit prCommit = walk
                    .parseCommit(repository.exactRef(Constants.R_HEADS + branch).getObjectId());
            final RevCommit masterCommit = walk
                    .parseCommit(repository.exactRef(Constants.R_HEADS + "master").getObjectId());
            final RevCommit commonAncestorCommit = getMergeBaseCommit(walk, prCommit, masterCommit);
            walk.dispose();

            returnValue = new TreeParserPair(prepareTreeParser(walk, prCommit),
                    prepareTreeParser(walk, commonAncestorCommit));
        } finally {
            walk.close();
        }

        return returnValue;
    }

    /**
     * Gets the merge-base of two commits.
     * A merge-base is a best common ancestor between two commits. One common ancestor is
     * better than another common ancestor if the latter is an ancestor of the former.
     * A common ancestor that does not have any better common ancestor is a best common ancestor.
     * @param walk    the {@link RevWalk} for computing merge bases
     * @param commitA the first commit to start the walk with
     * @param commitB the second commit to start the walk with
     * @return the merge-base of two commits
     * @throws IOException JGit library exception
     */
    private static RevCommit getMergeBaseCommit(RevWalk walk, RevCommit commitA, RevCommit commitB)
            throws IOException {
        walk.reset();
        walk.setRevFilter(RevFilter.MERGE_BASE);
        walk.markStart(commitA);
        walk.markStart(commitB);
        return walk.next();
    }

    /**
     * Creates a tree parser from a commit, to be used by diff command.
     * @param walk   the {@link RevWalk} to parse the tree
     * @param commit the commit to create tree parser from
     * @return the tree parser
     * @throws IOException JGit library exception
     */
    private static AbstractTreeIterator prepareTreeParser(RevWalk walk, RevCommit commit) throws IOException {
        final RevTree tree = walk.parseTree(commit.getTree().getId());
        final CanonicalTreeParser returnValue;
        returnValue = new CanonicalTreeParser();
        returnValue.reset(walk.getObjectReader(), tree.getId());
        return returnValue;
    }

    /**
     * Converts a {@link DiffEntry} to {@link GitChange} for the further use.
     * @param diffEntry the {@link DiffEntry} instance to be converted
     * @param formatter the diff formatter to provide the line changes information
     * @return the {@link GitChange} instance converted from the given {@link DiffEntry}
     * @throws IOException JGit library exception
     */
    private static GitChange convertDiffEntryToGitChange(DiffEntry diffEntry, DiffFormatter formatter)
            throws IOException {
        final List<Integer> addedLines = formatter.toFileHeader(diffEntry).toEditList().stream()
                .filter(edit -> edit.getBeginB() < edit.getEndB())
                .flatMapToInt(edit -> Arrays.stream(new IntRange(edit.getBeginB(), edit.getEndB() - 1).toArray()))
                .boxed().collect(Collectors.toList());
        final List<Integer> deletedLines = formatter.toFileHeader(diffEntry).toEditList().stream()
                .filter(edit -> edit.getBeginA() < edit.getEndA())
                .flatMapToInt(edit -> Arrays.stream(new IntRange(edit.getBeginA(), edit.getEndA() - 1).toArray()))
                .boxed().collect(Collectors.toList());
        return ImmutableGitChange.builder().path(diffEntry.getNewPath()).addAllAddedLines(addedLines)
                .addAllDeletedLines(deletedLines).build();
    }

    /** A pair of tree parsers: PR branch tree parser and common ancestor tree parser. */
    private static class TreeParserPair {
        /** The tree parser of the head commit of PR branch. */
        private final AbstractTreeIterator prTreeParser;

        /** The tree parser of the merge-base commit between PR branch and master. */
        private final AbstractTreeIterator commonAncestorTreeParser;

        /**
         * Creates a new TreeParserPair instance.
         * @param prTreeParser             the tree parser of the head commit of PR branch
         * @param commonAncestorTreeParser the tree parser of the merge-base commit between
         *                                 PR branch and master
         */
        TreeParserPair(AbstractTreeIterator prTreeParser, AbstractTreeIterator commonAncestorTreeParser) {
            this.prTreeParser = prTreeParser;
            this.commonAncestorTreeParser = commonAncestorTreeParser;
        }
    }
}