Ability to reject commits from entering repository

If a bad commit was once in the repository and we never want it
to enter the repository after, e.g., a filter-branch operation
to remove it, we can use a git-notes branch which references bad
commits to keep it from entering the repository again.

This adds a check of the notes branch refs/meta/reject-commits to
check whether a commit should be rejected because it is bad or an
ancestor it's going to merge in is bad.

Change-Id: Ia75e4aa33435bd04faafff7d1a753127063790bb
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 8b6f397..10efeb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -148,6 +149,7 @@
   private final Project project;
   private final Repository repo;
   private final ReceivePack rp;
+  private final NoteMap rejectCommits;
 
   private ReceiveCommand newChange;
   private Branch.NameKey destBranch;
@@ -177,7 +179,7 @@
       final TrackingFooters trackingFooters,
 
       @Assisted final ProjectControl projectControl,
-      @Assisted final Repository repo) {
+      @Assisted final Repository repo) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.approvalTypes = approvalTypes;
@@ -196,6 +198,7 @@
     this.project = projectControl.getProject();
     this.repo = repo;
     this.rp = new ReceivePack(repo);
+    this.rejectCommits = loadRejectCommitsMap();
 
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
@@ -718,6 +721,28 @@
     }
   }
 
+  /**
+   * Loads a list of commits to reject from {@code refs/meta/reject-commits}.
+   *
+   * @return NoteMap of commits to be rejected, null if there are none.
+   * @throws IOException the map cannot be loaded.
+   */
+  private NoteMap loadRejectCommitsMap() throws IOException {
+    String rejectNotes = "refs/meta/reject-commits";
+    try {
+      Ref ref = repo.getRef(rejectNotes);
+      if (ref == null) {
+        return null;
+      }
+
+      RevWalk rw = rp.getRevWalk();
+      RevCommit map = rw.parseCommit(ref.getObjectId());
+      return NoteMap.read(rw.getObjectReader(), map);
+    } catch (IOException badMap) {
+      throw new IOException("Cannot load " + rejectNotes, badMap);
+    }
+  }
+
   private void parseReplaceCommand(final ReceiveCommand cmd,
       final Change.Id changeId) {
     if (cmd.getType() != ReceiveCommand.Type.CREATE) {
@@ -1481,6 +1506,12 @@
       }
     }
 
+    // Check for banned commits to prevent them from entering the tree again.
+    if (rejectCommits != null && rejectCommits.contains(c)) {
+      reject(newChange, "contains banned commit " + c.getName());
+      return false;
+    }
+
     return true;
   }