Question

I've managed to get to grips with the basics of jGit file in terms of connecting to a repos and adding, commiting, and even looping of the commit messages for the files.

File gitDir = new File("/Users/myname/Sites/helloworld/.git");

RepositoryBuilder builder = new RepositoryBuilder();
Repository repository;
repository = builder.setGitDir(gitDir).readEnvironment()
        .findGitDir().build();

Git git = new Git(repository);
RevWalk walk = new RevWalk(repository);
RevCommit commit = null;

// Add all files
// AddCommand add = git.add();
// add.addFilepattern(".").call();

// Commit them
// CommitCommand commit = git.commit();
// commit.setMessage("Commiting from java").call();

Iterable<RevCommit> logs = git.log().call();
Iterator<RevCommit> i = logs.iterator();

while (i.hasNext()) {
    commit = walk.parseCommit( i.next() );
    System.out.println( commit.getFullMessage() );

}

What I want to do next is be able to get all the commit message for a single file and then be able revert the single file back to a specific reference/point in time.

Was it helpful?

Solution

Here is how to find the changes of a commit based on all parent commits

        var tree = new TreeWalk(repository)
        tree.addTree(commit.getTree)
        commit.getParents foreach {
            parent => tree.addTree(parent.getTree)
        }
        tree.setFilter(TreeFilter.ANY_DIFF)

(scala code)

Note that TreeFilter.ANY_DIFF works for a single tree walker and will return all elements available in a root commit.

You would then have to iterate over the tree to see if your file is in the given delta (this is quite easy).

    while (tree.next)
            if (tree.getDepth == cleanPath.size) {
                // we are at the right level, do what you want
            } else {
                if (tree.isSubtree &&
                    name == cleanPath(tree.getDepth)) {
                    tree.enterSubtree
                }
            }
    }

(cleanPath is the pure in repo path, split by '/')

Now wrap that code into a RevWalk.next loop and you'll get the commits and files altered by the commit.

You may want to use a different filter than ANY_DIFF, because ANY_DIFF is true if one tree differs. This is a bit counter-intuitive in case of a merge where the blob didn't change compared to all parent trees. So here is the core of an ALL_DIFF, that will only show elements that differ from all parent trees:

override def include(walker: TreeWalk): Boolean = {
    val n = walker.getTreeCount();
    if (n == 1) {
        return true;
    }
    val m = walker.getRawMode(0)
    var i = 1
    while (i < n) {
        if (walker.getRawMode(i) == m && walker.idEqual(i, 0)) {
            return false
        }
        i += 1
    }
    true
}

(scala code, derived from AnyDiffFilter)

OTHER TIPS

So I tried to get charlieboy's solution to work, and it mostly did, but it failed for me in the following case (maybe something changed in jgit since that post?)

add fileA, commit as "commit 1" add fileB, commit as "commit 2"

getFileVersionDateList("fileA")

Both commit 1 and commit 2 were found, where I expected only commit 1.

My solution was as follows:

List<Commit> commits = new ArrayList<Commit>();

RevWalk revWalk = new RevWalk(repository);
revWalk.setTreeFilter(
        AndTreeFilter.create(
                PathFilterGroup.createFromStrings(<relative path in question>),
                TreeFilter.ANY_DIFF)
);

RevCommit rootCommit = revWalk.parseCommit(repository.resolve(Constants.HEAD));
revWalk.sort(RevSort.COMMIT_TIME_DESC);
revWalk.markStart(rootCommit);

for (RevCommit revCommit : revWalk) {
    commits.add(new GitCommit(getRepository(), revCommit));
}

Using the LogCommand is even simpler, and looks like this:

List<Commit> commitsList = new ArrayList<Commit>();

Git git = new Git(repository);
LogCommand logCommand = git.log()
        .add(git.getRepository().resolve(Constants.HEAD))
        .addPath(<relative path in question>);

for (RevCommit revCommit : logCommand.call()) {
    commitsList.add(new GitCommit(this, revCommit));
}

Obviously you'd also check the commit dates, etc, as needed.

The general approach with git to find the history of a specific file is to walk through the revision graph (which you are doing) and for each one, test the object referred to by the path in question (can be either a blob or a tree to find the history for an entire subtree). So essentially act as a filter on the revision set output by the revision walker.

The jgit documentation seems... sparse. But you should be able to get a RevTree corresponding to each RevCommit, and if necessarily walk through that with each path segment in turn down to a terminal object ID.

araqnid is right, this is how I got a list of Dates, each date relating to commit that included the file in question...

Then you can retrieve the file from a specific commit as you have the name of the file and the date of the commit, see the two methods below....

note: this code is in a .groovy class, so you will no doubt have to amend a little for java.

byte[] getAnyPreviousVersionFileBytes(String relativeFilePath, Date date) {

    byte[] bytes = null
    try {

        RevWalk revWalk = new RevWalk(repository)
        ObjectId headId = repository.resolve(Constants.HEAD);
        RevCommit root = revWalk.parseCommit(headId);

        revWalk.sort(RevSort.COMMIT_TIME_DESC);
        revWalk.markStart(root);

        for (RevCommit revCommit: revWalk) {

            // if date matches then walk the tree in this commit
            if (new Date(revCommit.commitTime * 1000L) == date) {

                TreeWalk treeWalk = TreeWalk.forPath(repository, relativeFilePath, revCommit.getTree())

                if (treeWalk != null) {
                    treeWalk.setRecursive(true)
                    CanonicalTreeParser canonicalTreeParser = treeWalk.getTree(0, CanonicalTreeParser)

                    while (!canonicalTreeParser.eof()) {

                        // if the filename matches, we have a match, so set teh byte array to return
                        if (canonicalTreeParser.getEntryPathString() == relativeFilePath) {
                            ObjectLoader objectLoader = repository.open(canonicalTreeParser.getEntryObjectId())
                            bytes = objectLoader.bytes
                        }
                        canonicalTreeParser.next(1)
                    }
                }
            }

        }

    }
    catch (Exception e) {
        throw new JgitException(e)
    }
    return bytes
}

List<Date> getFileVersionDateList(String relativeFilePath) {

    List<Date> versions = new LinkedList<Date>()
    try {

        RevWalk revWalk = new RevWalk(repository)
        ObjectId headId = repository.resolve(Constants.HEAD);
        RevCommit root = revWalk.parseCommit(headId);

        revWalk.sort(RevSort.COMMIT_TIME_DESC);
        revWalk.markStart(root);

        for (RevCommit revCommit: revWalk) {

            TreeWalk treeWalk = TreeWalk.forPath(repository, relativeFilePath, revCommit.getTree())

            if (treeWalk != null) {
                treeWalk.setRecursive(true)
                CanonicalTreeParser canonicalTreeParser = treeWalk.getTree(0, CanonicalTreeParser)

                while (!canonicalTreeParser.eof()) {
                    // if the filename matches, we have a match, so add the date of this commit to the list
                    if (canonicalTreeParser.getEntryPathString() == relativeFilePath) {
                        versions.add(new Date(revCommit.commitTime * 1000L))
                    }
                    canonicalTreeParser.next(1)
                }
            }
        }
    }
    catch (Exception e) {
        throw new JgitException(e)
    }

    return versions
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top