ExportReviewNotes: Dump submitted changes to refs/notes/review

This program allows site administrators to dump their existing notes
out to the refs/notes/review branch, making the prior data available
to Git clients.

Change-Id: Iebaf1e4b2fb4620443e80d2a8f840cb30ae1e389
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
new file mode 100644
index 0000000..b43989f
--- /dev/null
+++ b/Documentation/pgm-ExportReviewNotes.txt
@@ -0,0 +1,51 @@
+ExportReviewNotes
+=================
+
+NAME
+----
+ExportReviewNotes - Export successful reviews to refs/notes/review
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'ExportReviewNotes' -d <SITE_PATH>
+
+DESCRIPTION
+-----------
+Scans every submitted change and creates an initial notes
+branch detailing the previous submission information for
+each merged changed.
+
+This task can take quite some time, but can run in the background
+concurrently to the server if the database is MySQL or PostgreSQL.
+If the database is H2, this task must be run by itself.
+
+OPTIONS
+-------
+
+-d::
+\--site-path::
+	Location of the gerrit.config file, and all other per-site
+	configuration data, supporting libaries and log files.
+
+\--threads::
+	Number of threads to perform the scan work with.  Defaults to
+	twice the number of CPUs available.
+
+CONTEXT
+-------
+This command can only be run on a server which has direct
+connectivity to the metadata database, and local access to the
+managed Git repositories.
+
+EXAMPLES
+--------
+To generate all review information:
+
+====
+	$ java -jar gerrit.war ExportReviewNotes -d site_path --threads 16
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index c6430ad..ce9de71 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -18,6 +18,9 @@
 link:pgm-gsql.html[gsql]::
 	Administrative interface to idle database.
 
+link:pgm-ExportReviewNotes.html[ExportReviewNotes]::
+	Export submitted review information to refs/notes/review.
+
 link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
 	Rescan all changes after configuring trackingids.
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
new file mode 100644
index 0000000..b8e4160
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -0,0 +1,264 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.GroupCacheImpl;
+import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.config.ApprovalTypesProvider;
+import com.google.gerrit.server.config.AuthConfigModule;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.CodeReviewNoteCreationException;
+import com.google.gerrit.server.git.CreateCodeReviewNotes;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Scopes;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
+import org.eclipse.jgit.util.BlockList;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Export review notes for all submitted changes in all projects. */
+public class ExportReviewNotes extends SiteProgram {
+  @Option(name = "--threads", usage = "Number of concurrent threads to run")
+  private int threads = 2 * Runtime.getRuntime().availableProcessors();
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private final TextProgressMonitor textMonitor = new TextProgressMonitor();
+  private final ThreadSafeProgressMonitor monitor =
+      new ThreadSafeProgressMonitor(textMonitor);
+
+  private Injector dbInjector;
+  private Injector gitInjector;
+
+  @Inject
+  private GitRepositoryManager gitManager;
+
+  @Inject
+  private SchemaFactory<ReviewDb> database;
+
+  @Inject
+  private CreateCodeReviewNotes.Factory codeReviewNotesFactory;
+
+  private Map<Project.NameKey, List<Change>> changes;
+
+  @Override
+  public int run() throws Exception {
+    if (threads <= 0) {
+      threads = 1;
+    }
+
+    dbInjector = createDbInjector(MULTI_USER);
+    gitInjector = dbInjector.createChildInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
+        bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
+            Scopes.SINGLETON);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
+        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
+            .toProvider(GerritPersonIdentProvider.class).in(Scopes.SINGLETON);
+        bind(CachePool.class);
+
+        install(AccountCacheImpl.module());
+        install(GroupCacheImpl.module());
+        install(new AuthConfigModule());
+        install(new FactoryModule() {
+          @Override
+          protected void configure() {
+            factory(CreateCodeReviewNotes.Factory.class);
+          }
+        });
+        install(new LifecycleModule() {
+          @Override
+          protected void configure() {
+            listener().to(CachePool.Lifecycle.class);
+            listener().to(LocalDiskRepositoryManager.Lifecycle.class);
+          }
+        });
+      }
+    });
+
+    manager.add(dbInjector, gitInjector);
+    manager.start();
+    gitInjector.injectMembers(this);
+
+    List<Change> allChangeList = allChanges();
+    monitor.beginTask("Scanning changes", allChangeList.size());
+    changes = cluster(allChangeList);
+    allChangeList = null;
+
+    monitor.startWorkers(threads);
+    for (int tid = 0; tid < threads; tid++) {
+      new Worker().start();
+    }
+    monitor.waitForCompletion();
+    monitor.endTask();
+    manager.stop();
+    return 0;
+  }
+
+  private List<Change> allChanges() throws OrmException {
+    final ReviewDb db = database.open();
+    try {
+      return db.changes().all().toList();
+    } finally {
+      db.close();
+    }
+  }
+
+  private Map<Project.NameKey, List<Change>> cluster(List<Change> changes) {
+    HashMap<Project.NameKey, List<Change>> m =
+        new HashMap<Project.NameKey, List<Change>>();
+    for (Change change : changes) {
+      if (change.getStatus() == Change.Status.MERGED) {
+        List<Change> l = m.get(change.getProject());
+        if (l == null) {
+          l = new BlockList<Change>();
+          m.put(change.getProject(), l);
+        }
+        l.add(change);
+      } else {
+        monitor.update(1);
+      }
+    }
+    return m;
+  }
+
+  private void export(ReviewDb db, Project.NameKey project, List<Change> changes)
+      throws IOException, OrmException, CodeReviewNoteCreationException,
+      InterruptedException {
+    final Repository git;
+    try {
+      git = gitManager.openRepository(project);
+    } catch (RepositoryNotFoundException e) {
+      return;
+    }
+    try {
+      CreateCodeReviewNotes notes = codeReviewNotesFactory.create(db, git);
+      try {
+        notes.loadBase();
+        for (Change change : changes) {
+          monitor.update(1);
+          PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+          if (ps == null) {
+            continue;
+          }
+          notes.add(change, ObjectId.fromString(ps.getRevision().get()));
+        }
+        notes.commit("Exported prior reviews from Gerrit Code Review\n");
+        notes.updateRef();
+      } finally {
+        notes.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private Map.Entry<Project.NameKey, List<Change>> next() {
+    synchronized (changes) {
+      if (changes.isEmpty()) {
+        return null;
+      }
+
+      final Project.NameKey name = changes.keySet().iterator().next();
+      final List<Change> list = changes.remove(name);
+      return new Map.Entry<Project.NameKey, List<Change>>() {
+        @Override
+        public Project.NameKey getKey() {
+          return name;
+        }
+
+        @Override
+        public List<Change> getValue() {
+          return list;
+        }
+
+        @Override
+        public List<Change> setValue(List<Change> value) {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+  }
+
+  private class Worker extends Thread {
+    @Override
+    public void run() {
+      ReviewDb db;
+      try {
+        db = database.open();
+      } catch (OrmException e) {
+        e.printStackTrace();
+        return;
+      }
+      try {
+        for (;;) {
+          Entry<Project.NameKey, List<Change>> next = next();
+          if (next != null) {
+            try {
+              export(db, next.getKey(), next.getValue());
+            } catch (IOException e) {
+              e.printStackTrace();
+            } catch (OrmException e) {
+              e.printStackTrace();
+            } catch (CodeReviewNoteCreationException e) {
+              e.printStackTrace();
+            } catch (InterruptedException e) {
+              e.printStackTrace();
+            }
+          } else {
+            break;
+          }
+        }
+      } finally {
+        monitor.endWorker();
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
index 25ab239..db5bceb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
@@ -29,7 +29,7 @@
 import java.util.Collections;
 import java.util.List;
 
-class ApprovalTypesProvider implements Provider<ApprovalTypes> {
+public class ApprovalTypesProvider implements Provider<ApprovalTypes> {
   private final SchemaFactory<ReviewDb> schema;
 
   @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java
index 90de785..367ed56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.google.gerrit.server.git;
 
+import org.eclipse.jgit.revwalk.RevCommit;
+
 /**
  * Thrown when creation of a code review note fails.
  */
@@ -27,9 +29,9 @@
     super(why);
   }
 
-  public CodeReviewNoteCreationException(final CodeReviewCommit commit,
+  public CodeReviewNoteCreationException(final RevCommit commit,
       final Throwable cause) {
     super("Couldn't create code review note for the following commit: "
-        + commit, cause);
+        + commit.name(), cause);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
index 67705e4..976ec2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.gerrit.server.git.GitRepositoryManager.REFS_NOTES_REVIEW;
+
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -26,6 +29,9 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+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;
@@ -36,10 +42,10 @@
 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.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -47,6 +53,8 @@
 import java.io.IOException;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * This class create code review notes for given {@link CodeReviewCommit}s.
  * <p>
@@ -55,10 +63,9 @@
  */
 public class CreateCodeReviewNotes {
   public interface Factory {
-    CreateCodeReviewNotes create(Repository db);
+    CreateCodeReviewNotes create(ReviewDb reviewDb, Repository db);
   }
 
-  static final String REFS_NOTES_REVIEW = "refs/notes/review";
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
@@ -83,13 +90,15 @@
   private PersonIdent author;
 
   @Inject
-  CreateCodeReviewNotes(final ReviewDb reviewDb,
+  CreateCodeReviewNotes(
       @GerritPersonIdent final PersonIdent gerritIdent,
       final AccountCache accountCache,
       final ApprovalTypes approvalTypes,
-      @CanonicalWebUrl final String canonicalWebUrl,
+      @Nullable @CanonicalWebUrl final String canonicalWebUrl,
+      @Assisted  ReviewDb reviewDb,
       @Assisted final Repository db) {
     schema = reviewDb;
+    this.author = gerritIdent;
     this.gerritIdent = gerritIdent;
     this.accountCache = accountCache;
     this.approvalTypes = approvalTypes;
@@ -106,110 +115,86 @@
     try {
       this.commits = commits;
       this.author = author;
-      setBase();
-      setOurs();
-
-      int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-      RefUpdate refUpdate = createRefUpdate(oursCommit, baseCommit);
-
-      for (;;) {
-        Result result = refUpdate.update();
-
-        if (result == Result.LOCK_FAILURE) {
-          if (--remainingLockFailureCalls > 0) {
-            Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
-          } else {
-            throw new CodeReviewNoteCreationException(
-                "Failed to lock the ref: " + REFS_NOTES_REVIEW);
-          }
-
-        } else if (result == Result.REJECTED) {
-          RevCommit theirsCommit =
-              revWalk.parseCommit(refUpdate.getOldObjectId());
-          NoteMap theirs =
-              NoteMap.read(revWalk.getObjectReader(), theirsCommit);
-          NoteMapMerger merger = new NoteMapMerger(db);
-          NoteMap merged = merger.merge(base, ours, theirs);
-          RevCommit mergeCommit =
-              createCommit(merged, gerritIdent, "Merged note commits\n",
-                  theirsCommit, oursCommit);
-          refUpdate = createRefUpdate(mergeCommit, theirsCommit);
-          remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-
-        } else if (result == Result.IO_FAILURE) {
-          throw new CodeReviewNoteCreationException(
-              "Couldn't create code review notes because of IO_FAILURE");
-        } else {
-          break;
-        }
-      }
-
+      loadBase();
+      applyNotes();
+      updateRef();
     } catch (IOException e) {
       throw new CodeReviewNoteCreationException(e);
     } catch (InterruptedException e) {
       throw new CodeReviewNoteCreationException(e);
     } finally {
-      reader.release();
-      inserter.release();
-      revWalk.release();
+      release();
     }
   }
 
-  private void setBase() throws IOException {
+  public void loadBase() throws IOException {
     Ref notesBranch = db.getRef(REFS_NOTES_REVIEW);
     if (notesBranch != null) {
       baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
       base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
     }
-  }
-
-  private void setOurs() throws IOException, CodeReviewNoteCreationException {
     if (baseCommit != null) {
       ours = NoteMap.read(db.newObjectReader(), baseCommit);
     } else {
       ours = NoteMap.newEmptyMap();
     }
+  }
 
+  private void applyNotes() throws IOException, CodeReviewNoteCreationException {
     StringBuilder message =
         new StringBuilder("Update notes for submitted changes\n\n");
     for (CodeReviewCommit c : commits) {
-      ObjectId noteContent = createNoteContent(c);
-      if (ours.contains(c)) {
-        // 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
-        DefaultNoteMerger noteMerger = new DefaultNoteMerger();
-        Note newNote = new Note(c, noteContent);
-        noteContent =
-            noteMerger.merge(null, newNote, base.getNote(c), reader, inserter)
-                .getData();
-      }
-      ours.set(c, noteContent);
-
+      add(c.change, c);
       message.append("* ").append(c.getShortMessage()).append("\n");
     }
+    commit(message.toString());
+  }
 
+  public void commit(String message) throws IOException {
     if (baseCommit != null) {
-      oursCommit = createCommit(ours, author, message.toString(), baseCommit);
+      oursCommit = createCommit(ours, author, message, baseCommit);
     } else {
-      oursCommit = createCommit(ours, author, message.toString());
+      oursCommit = createCommit(ours, author, message);
     }
   }
 
-  private ObjectId createNoteContent(CodeReviewCommit commit)
+  public void add(Change change, ObjectId commit)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      CodeReviewNoteCreationException {
+    if (!(commit instanceof RevCommit)) {
+      commit = revWalk.parseCommit(commit);
+    }
+
+    RevCommit c = (RevCommit) commit;
+    ObjectId noteContent = createNoteContent(change, c);
+    if (ours.contains(c)) {
+      // 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 ReviewNoteMerger();
+      Note newNote = new Note(c, noteContent);
+      noteContent = noteMerger.merge(null, newNote, ours.getNote(c),
+          reader, inserter).getData();
+    }
+    ours.set(c, noteContent);
+  }
+
+  private ObjectId createNoteContent(Change change, RevCommit commit)
       throws CodeReviewNoteCreationException, IOException {
     try {
       ReviewNoteHeaderFormatter formatter =
         new ReviewNoteHeaderFormatter(author.getTimeZone());
       final List<String> idList = commit.getFooterLines(CHANGE_ID);
       if (idList.isEmpty())
-        formatter.appendChangeId(commit.change.getKey());
+        formatter.appendChangeId(change.getKey());
       ResultSet<PatchSetApproval> approvals =
-        schema.patchSetApprovals().byPatchSet(commit.patchsetId);
+        schema.patchSetApprovals().byPatchSet(change.currentPatchSetId());
       PatchSetApproval submit = null;
       for (PatchSetApproval a : approvals) {
-        if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+        if (a.getValue() == 0) {
+          // Ignore 0 values.
+        } else if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
           submit = a;
         } else {
           formatter.appendApproval(
@@ -219,23 +204,77 @@
         }
       }
 
-      formatter.appendSubmittedBy(accountCache.get(submit.getAccountId()).getAccount());
-      formatter.appendSubmittedAt(submit.getGranted());
-
-      formatter.appendReviewedOn(canonicalWebUrl, commit.change.getId());
-      formatter.appendProject(commit.change.getProject().get());
-      formatter.appendBranch(commit.change.getDest());
+      if (submit != null) {
+        formatter.appendSubmittedBy(accountCache.get(submit.getAccountId()).getAccount());
+        formatter.appendSubmittedAt(submit.getGranted());
+      }
+      if (canonicalWebUrl != null) {
+        formatter.appendReviewedOn(canonicalWebUrl, change.getId());
+      }
+      formatter.appendProject(change.getProject().get());
+      formatter.appendBranch(change.getDest());
       return inserter.insert(Constants.OBJ_BLOB, formatter.toString().getBytes("UTF-8"));
     } catch (OrmException e) {
       throw new CodeReviewNoteCreationException(commit, e);
     }
   }
 
+  public void updateRef() throws IOException, InterruptedException,
+      CodeReviewNoteCreationException, 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 = MAX_LOCK_FAILURE_CALLS;
+    RefUpdate refUpdate = createRefUpdate(oursCommit, baseCommit);
+
+    for (;;) {
+      Result result = refUpdate.update();
+
+      if (result == Result.LOCK_FAILURE) {
+        if (--remainingLockFailureCalls > 0) {
+          Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+        } else {
+          throw new CodeReviewNoteCreationException(
+              "Failed to lock the ref: " + REFS_NOTES_REVIEW);
+        }
+
+      } else if (result == Result.REJECTED) {
+        RevCommit theirsCommit =
+            revWalk.parseCommit(refUpdate.getOldObjectId());
+        NoteMap theirs =
+            NoteMap.read(revWalk.getObjectReader(), theirsCommit);
+        NoteMapMerger merger = new NoteMapMerger(db);
+        NoteMap merged = merger.merge(base, ours, theirs);
+        RevCommit mergeCommit =
+            createCommit(merged, gerritIdent, "Merged note commits\n",
+                theirsCommit, oursCommit);
+        refUpdate = createRefUpdate(mergeCommit, theirsCommit);
+        remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+
+      } else if (result == Result.IO_FAILURE) {
+        throw new CodeReviewNoteCreationException(
+            "Couldn't create code review notes because of IO_FAILURE");
+      } else {
+        break;
+      }
+    }
+  }
+
+  public void release() {
+    reader.release();
+    inserter.release();
+    revWalk.release();
+  }
+
   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.setAuthor(author != null ? author : gerritIdent);
     b.setCommitter(gerritIdent);
     if (parents.length > 0) {
       b.setParentIds(parents);
@@ -247,7 +286,8 @@
   }
 
 
-  private RefUpdate createRefUpdate(ObjectId newObjectId, ObjectId expectedOldObjectId) throws IOException {
+  private RefUpdate createRefUpdate(ObjectId newObjectId,
+      ObjectId expectedOldObjectId) throws IOException {
     RefUpdate refUpdate = db.updateRef(REFS_NOTES_REVIEW);
     refUpdate.setNewObjectId(newObjectId);
     if (expectedOldObjectId == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index 701716d..a19b722 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -31,6 +31,9 @@
  * environment.
  */
 public interface GitRepositoryManager {
+  /** Notes branch successful reviews are written to after being merged. */
+  public static final String REFS_NOTES_REVIEW = "refs/notes/review";
+
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
   public static final String REF_REJECT_COMMITS = "refs/meta/reject-commits";
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 60016e5..b66fcb59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -992,14 +992,15 @@
       }
     }
 
-    CreateCodeReviewNotes codeReviewNotes = codeReviewNotesFactory.create(db);
+    CreateCodeReviewNotes codeReviewNotes =
+        codeReviewNotesFactory.create(schema, db);
     try {
       codeReviewNotes.create(merged, computeAuthor(merged));
     } catch (CodeReviewNoteCreationException e) {
       log.error(e.getMessage());
     }
     replication.scheduleUpdate(destBranch.getParentKey(),
-        CreateCodeReviewNotes.REFS_NOTES_REVIEW);
+        GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
   private void dependencyError(final CodeReviewCommit commit) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java
new file mode 100644
index 0000000..60f1d0d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010, Sasa Zivkov <sasa.zivkov@sap.com> and other copyright
+ * owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v1.0 which accompanies this
+ * distribution, is reproduced below, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.gerrit.server.git;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMerger;
+import org.eclipse.jgit.util.io.UnionInputStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+class ReviewNoteMerger implements NoteMerger {
+  public Note merge(Note base, Note ours, Note theirs, ObjectReader reader,
+      ObjectInserter inserter) throws IOException {
+    if (ours == null) {
+      return theirs;
+    }
+    if (theirs == null) {
+      return ours;
+    }
+    if (ours.getData().equals(theirs.getData())) {
+      return ours;
+    }
+
+    ObjectLoader lo = reader.open(ours.getData());
+    byte[] sep = new byte[] {'\n'};
+    ObjectLoader lt = reader.open(theirs.getData());
+    UnionInputStream union = new UnionInputStream(
+        lo.openStream(),
+        new ByteArrayInputStream(sep),
+        lt.openStream());
+    ObjectId noteData = inserter.insert(Constants.OBJ_BLOB,
+        lo.getSize() + sep.length + lt.getSize(), union);
+    return new Note(ours, noteData);
+  }
+}