Checkout a specific revision based on the commit date with JGit

2.6k views Asked by At

I want to use the Java JGit library in order to retrieve previously committed revisions of a file. The file gets updated on a daily basis and I need to have access to the previous versions from a git repository.

I know how to checkout a specific revision by providing the commit hash of the revision I need, as described here with this command:

git.checkout().setName( "<id-to-commit>" ).call();

Currently I clone the repository, search for the most recent commit of that file and then checkout the revision. The process I have now is cumbersome as I would need to re-checkout the repository each time. I would rather just export a single file from a specific revision (of the master branch, for instance).

Revision Log

Retrieve the revision log of the repository and get the commits of the file I am interested in.

 private static HashMap getRevisionsByLog(Repository repository, String filePath) {


    HashMap commitMap = new HashMap<String, DateTime >();


    Git git = new Git(repository);
    LogCommand logCommand = null;
    try {
        logCommand = git.log()
                .add(git.getRepository().resolve(Constants.HEAD))
                .addPath(filePath);
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        for (RevCommit revCommit : logCommand.call()) {
            DateTime commitDate = new DateTime(1000L * revCommit.getCommitTime());
            // Store commit hash and date
            commitMap.put(revCommit.getName(),commitDate);
        }
    } catch (GitAPIException e) {
        e.printStackTrace();
    }

    return commitMap;
}

Most recent Revision

This code retrieves the most recent revision of the commits, which is before the date I am interested in:

 private static String getMostRecentCommit(HashMap<String, DateTime > commitMap, DateTime execDate){
    Iterator it = commitMap.entrySet().iterator();
    Map.Entry<String, DateTime> mostRecentCommit = null;

    while (it.hasNext()) {
        Map.Entry pair = (Map.Entry)it.next();
        DateTime currentCommitDate = (DateTime) pair.getValue();

        if(mostRecentCommit==null && ((DateTime) pair.getValue()).isBefore(execDate)){

           mostRecentCommit = pair;
            }else if (currentCommitDate.isBefore(execDate)){
                System.out.println("Current date is before exec");
                if(currentCommitDate.isAfter(mostRecentCommit.getValue())){
                    System.out.println("Current date is before exec and after the most recent one");
                    mostRecentCommit=pair;
                }
            }

    }

    System.out.println("Current most recent:  " + mostRecentCommit.getKey() + " = " + mostRecentCommit.getValue());
    return mostRecentCommit.getKey();

}

A Simple Test Application

In this application, I want to revert the working copy of the file to the state which it was before 2015-06-26T14:25:00.

    public static void main(String[] args) {

    DateTime dt = new DateTime("2015-06-26T14:25:00");
    Date execDate = dt.toDate();

    String remotePath = "/media/Data/Gittest/repo/";
    String localPath="/tmp/localgit";

    // Clone repository
    try {
        Git.cloneRepository().setURI(remotePath)
                .setDirectory(new File(localPath)).call();
    } catch (GitAPIException e) {
        e.printStackTrace();
    }


    Git git = null;
    try {
        git = Git.open(new File(localPath));
        Repository repo = git.getRepository();

        HashMap map = getRevisionsByLog(repo, "versuch.txt");

        String commitID = getMostRecentCommit(map,dt);

        System.out.println("Commit hash" +commitID);
        git.checkout().setName(commitID).call();



    } catch (IOException e) {
       ...
    }


}

Questions

This is a very naive implementation, I am sure that JGit offers a more elegant way, but I could not find any hints. I would like to export a specific revision of one single file programmatically. I need to refer to a specific date, I want to fetch the revision which was valid during that date. The file it self gets updated quite often, nevertheless I need a mechanism to access the previous versions and use them as input for another process. What would be the best way of achieving this with Java and JGit?

Solution based on the answer from Rüdiger Herrmann

The solution you proposed only works with the exact date/timestamp. Thus as you said in your answer, the solution only works if there is only one commit per day. As this is not guaranteed, I use a slightly different approach.

First, I retrieve all the commits from the file and sort them by date in a Treemap. The commits are sorted in a descending order, the most recent one is therefore last.

    /*
    * Get all commits of a file before a given date
    * */
    private TreeMap<DateTime,RevCommit> getAllCommitsBefore(Date execDate, String path){
    RevWalk walk = new RevWalk( this.repo );
    TreeMap<DateTime, RevCommit> commitsByDate = new TreeMap<DateTime, RevCommit>();
    try {
        walk.markStart( walk.parseCommit( this.repo.resolve( Constants.HEAD ) ) );

        walk.sort( RevSort.COMMIT_TIME_DESC);
        walk.setTreeFilter(PathFilter.create(path));

        for( RevCommit commit : walk ) {
            if ( commit.getCommitterIdent().getWhen().before(execDate) ) {
                DateTime commitTime = new DateTime(commit.getCommitterIdent().getWhen());
                commitsByDate.put(commitTime, commit);

            }
        }
        walk.close();
        System.out.println("Number of valid commits: " + commitsByDate.size());
    } catch (IOException e) {
        e.printStackTrace();
    }
    return commitsByDate;
    }

Then, I can retrieve the most recent commit simply by retrieving the last commit in the TreeMap.

    public RevCommit getMostRecentCommit(Date execDate, String path){
    TreeMap<DateTime,RevCommit> allCommits = this.getAllCommitsBefore(execDate,path);
    RevCommit lastCommit = allCommits.lastEntry().getValue();
    System.out.println("Last entry: " + lastCommit.getName());
    return lastCommit;

    }

Then, I can retrieve the file of that revision and export it.

    public void retrieveFileFromCommit(String path, RevCommit commit, String outputPath){
    TreeWalk treeWalk  = null;
    try {
        treeWalk = TreeWalk.forPath(this.repo, path, commit.getTree());
        InputStream inputStream = this.repo.open( treeWalk.getObjectId( 0 ), Constants.OBJ_BLOB ).openStream();

        this.writeFile(inputStream,outputPath);
        treeWalk.close(); // use release() in JGit < 4.0

    } catch (IOException e) {
        e.printStackTrace();
    }

    }

    /*
    * Write the stream to the disk
    * */
    private void writeFile(InputStream inputStream, String outputPath){
    OutputStream outputStream =
            null;
    try {
        outputStream = new FileOutputStream(new File(outputPath));
        int read = 0;
        byte[] bytes = new byte[1024];

        while ((read = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, read);
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }


    System.out.println("Done!");

    }
1

There are 1 answers

0
Rüdiger Herrmann On BEST ANSWER

If I understand your question correctly, you want to find a commit within a given date range and then read the contents of a specific file form that commit.

Assuming that there is only one commit per date, you can use a RevWalk to To get get the desired commit:

try (RevWalk walk = new RevWalk(repo)) {
  walk.markStart(walk.parseCommit(repo.resolve(Constants.HEAD)));
  walk.sort(RevSort.COMMIT_TIME_DESC);
  walk.setRevFilter(myFilter);
  for (RevCommit commit : walk) {
    if (commit.getCommitter().getWhen().equals(date)) {
      // this is the commit you are looking for
      revWalk.parseCommit(commit);
      break;
    }
  }
}

I am not entirely sure if the revWalk.parseCommit(commit); is necessary - it depends on how the RevWalk is set up. Try to run the code without parsing the commit and if the file is found, leave it at that.

Now that you have the desired commit, use a TreeWalk to obtain the content of the file:

try (TreeWalk treeWalk = TreeWalk.forPath(repository, fileName, commit.getTree())) {
  InputStream inputStream = repository.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB).openStream();
  // use the inputStream
}

fileName holds the repository-relative path to the file in question.