com.google.appraise.eclipse.core.client.git.GitNoteWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appraise.eclipse.core.client.git.GitNoteWriter.java

Source

/*******************************************************************************
 * Copyright (c) 2015 Google and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Scott McMaster - initial implementation
 *******************************************************************************/
package com.google.appraise.eclipse.core.client.git;

import com.google.gson.Gson;

import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.DefaultNoteMerger;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.notes.NoteMapMerger;
import org.eclipse.jgit.notes.NoteMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Writes git notes in the format that Appraise expects.
 * This is based off an old version of Gerrit's CreateCodeReviewNotes
 * (https://code.google.com/p/gerrit/source/browse/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java),
 * which is about the only non-trivial git-notes Jgit example on the web.
 *
 * @param <T> The type of note to write, which will be serialized as a JSON
 * string and appended with a newline.
 */
public class GitNoteWriter<T> implements Closeable {
    private static final Logger logger = Logger.getLogger(GitNoteWriter.class.getName());

    /**
     * The ref where the notes are to be written.
     */
    private final String ref;

    // Some Jgit objects.
    private final Repository repo;
    private final RevWalk revWalk;
    private final ObjectInserter inserter;
    private final ObjectReader reader;

    // The base commit that we need to merge into.
    private RevCommit baseCommit;
    private NoteMap base;

    // The thing we need to merge.
    private RevCommit oursCommit;
    private NoteMap ours;

    private List<T> noteRecords;

    /**
     * Who is writing the notes out, which is assumed to be the same person making
     * the reviews/comments in the first place.
     */
    private PersonIdent author;

    /**
     * The commit that we want to attach to.
     */
    private final RevCommit reviewCommit;

    /**
     * Creates a writer to write comments to a given review.
     */
    public static <T> GitNoteWriter<T> createNoteWriter(String reviewCommitHash, final Repository db,
            PersonIdent author, String ref) {
        return new GitNoteWriter<T>(reviewCommitHash, db, ref, author);
    }

    /**
     * Private ctor. Use the static factory methods.
     */
    private GitNoteWriter(String reviewHash, final Repository repo, String ref, PersonIdent author) {
        this.ref = ref;
        this.repo = repo;
        this.author = author;

        revWalk = new RevWalk(repo);
        inserter = repo.newObjectInserter();
        reader = repo.newObjectReader();

        try {
            ObjectId reviewRefObjId = repo.resolve(reviewHash);
            this.reviewCommit = revWalk.parseCommit(reviewRefObjId);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Failed to init note writer for commit " + reviewHash, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates the given notes in the pre-configured review.
     */
    public void create(String message, List<T> noteRecords) {
        try {
            this.noteRecords = noteRecords;
            loadBase();
            applyNotes(message);
            updateRef();
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Failed to write notes for commit " + this.reviewCommit.getId(), e);
        }
    }

    private void loadBase() throws IOException {
        Ref notesBranch = repo.getRef(ref);
        if (notesBranch != null) {
            baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
            base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
        }
        if (baseCommit != null) {
            ours = NoteMap.read(repo.newObjectReader(), baseCommit);
        } else {
            ours = NoteMap.newEmptyMap();
        }
    }

    private void applyNotes(String message) throws IOException {
        for (T c : noteRecords) {
            add(c);
        }
        commit(message.toString());
    }

    private void commit(String message) throws IOException {
        if (baseCommit != null) {
            oursCommit = createCommit(ours, author, message, baseCommit);
        } else {
            oursCommit = createCommit(ours, author, message);
        }
    }

    private void add(T noteRecord)
            throws MissingObjectException, IncorrectObjectTypeException, IOException, RuntimeException {
        ObjectId noteContent = createNoteContent(noteRecord);
        if (ours.contains(reviewCommit)) {
            // merge the existing and the new note as if they are both new
            // means: base == null
            // there is not really a common ancestry for these two note revisions
            // use the same NoteMerger that is used from the NoteMapMerger
            NoteMerger noteMerger = new DefaultNoteMerger();
            Note newNote = new Note(reviewCommit, noteContent);
            noteContent = noteMerger.merge(null, newNote, ours.getNote(reviewCommit), reader, inserter).getData();
        }
        ours.set(reviewCommit, noteContent);
    }

    private ObjectId createNoteContent(T noteRecord) throws RuntimeException {
        try {
            return inserter.insert(Constants.OBJ_BLOB, (new Gson().toJson(noteRecord) + '\n').getBytes("UTF-8"));
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Failed create note content for commit " + this.reviewCommit.getId(), e);
            throw new RuntimeException(e);
        }
    }

    private void updateRef() throws IOException, InterruptedException, RuntimeException, MissingObjectException,
            IncorrectObjectTypeException, CorruptObjectException {
        if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
            // If the trees are identical, there is no change in the notes.
            // Avoid saving this commit as it has no new information.
            return;
        }

        int remainingLockFailureCalls = JgitUtils.MAX_LOCK_FAILURE_CALLS;
        RefUpdate refUpdate = JgitUtils.updateRef(repo, oursCommit, baseCommit, ref);

        for (;;) {
            Result result = refUpdate.update();

            if (result == Result.LOCK_FAILURE) {
                if (--remainingLockFailureCalls > 0) {
                    Thread.sleep(JgitUtils.SLEEP_ON_LOCK_FAILURE_MS);
                } else {
                    throw new RuntimeException("Failed to lock the ref: " + ref);
                }

            } else if (result == Result.REJECTED) {
                RevCommit theirsCommit = revWalk.parseCommit(refUpdate.getOldObjectId());
                NoteMap theirs = NoteMap.read(revWalk.getObjectReader(), theirsCommit);
                NoteMapMerger merger = new NoteMapMerger(repo);
                NoteMap merged = merger.merge(base, ours, theirs);
                RevCommit mergeCommit = createCommit(merged, author, "Merged note records\n", theirsCommit,
                        oursCommit);
                refUpdate = JgitUtils.updateRef(repo, mergeCommit, theirsCommit, ref);
                remainingLockFailureCalls = JgitUtils.MAX_LOCK_FAILURE_CALLS;

            } else if (result == Result.IO_FAILURE) {
                throw new RuntimeException("Couldn't create notes because of IO_FAILURE");
            } else {
                break;
            }
        }
    }

    @Override
    public void close() {
        reader.close();
        inserter.close();
        revWalk.close();
    }

    private RevCommit createCommit(NoteMap map, PersonIdent author, String message, RevCommit... parents)
            throws IOException {
        CommitBuilder b = new CommitBuilder();
        b.setTreeId(map.writeTree(inserter));
        b.setAuthor(author);
        b.setCommitter(author);
        if (parents.length > 0) {
            b.setParentIds(parents);
        }
        b.setMessage(message);
        ObjectId commitId = inserter.insert(b);
        inserter.flush();
        return revWalk.parseCommit(commitId);
    }
}