From 1b8810f411387f4eb2e314c54be4b69c9e6dc08e Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Fri, 20 May 2011 13:10:13 -0700 Subject: [PATCH] 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 --- Documentation/pgm-ExportReviewNotes.txt | 51 ++++ Documentation/pgm-index.txt | 3 + .../google/gerrit/pgm/ExportReviewNotes.java | 264 ++++++++++++++++++ .../server/config/ApprovalTypesProvider.java | 2 +- .../git/CodeReviewNoteCreationException.java | 6 +- .../server/git/CreateCodeReviewNotes.java | 196 +++++++------ .../server/git/GitRepositoryManager.java | 3 + .../com/google/gerrit/server/git/MergeOp.java | 5 +- .../gerrit/server/git/ReviewNoteMerger.java | 77 +++++ 9 files changed, 524 insertions(+), 83 deletions(-) create mode 100644 Documentation/pgm-ExportReviewNotes.txt create mode 100644 gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt new file mode 100644 index 0000000000..b43989fa57 --- /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 + +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 c6430adc35..ce9de71e06 100644 --- a/Documentation/pgm-index.txt +++ b/Documentation/pgm-index.txt @@ -18,6 +18,9 @@ link:pgm-daemon.html[daemon]:: 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 0000000000..b8e4160c17 --- /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 database; + + @Inject + private CreateCodeReviewNotes.Factory codeReviewNotesFactory; + + private Map> 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 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 allChanges() throws OrmException { + final ReviewDb db = database.open(); + try { + return db.changes().all().toList(); + } finally { + db.close(); + } + } + + private Map> cluster(List changes) { + HashMap> m = + new HashMap>(); + for (Change change : changes) { + if (change.getStatus() == Change.Status.MERGED) { + List l = m.get(change.getProject()); + if (l == null) { + l = new BlockList(); + m.put(change.getProject(), l); + } + l.add(change); + } else { + monitor.update(1); + } + } + return m; + } + + private void export(ReviewDb db, Project.NameKey project, List 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> next() { + synchronized (changes) { + if (changes.isEmpty()) { + return null; + } + + final Project.NameKey name = changes.keySet().iterator().next(); + final List list = changes.remove(name); + return new Map.Entry>() { + @Override + public Project.NameKey getKey() { + return name; + } + + @Override + public List getValue() { + return list; + } + + @Override + public List setValue(List 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> 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 25ab239fe9..db5bceb2a7 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.ArrayList; import java.util.Collections; import java.util.List; -class ApprovalTypesProvider implements Provider { +public class ApprovalTypesProvider implements Provider { private final SchemaFactory 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 90de785871..367ed56820 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 @@ public class CodeReviewNoteCreationException extends Exception { 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 67705e407d..976ec2e54a 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.gwtorm.client.ResultSet; 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.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.FooterKey; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -47,6 +53,8 @@ import org.eclipse.jgit.revwalk.RevWalk; import java.io.IOException; import java.util.List; +import javax.annotation.Nullable; + /** * This class create code review notes for given {@link CodeReviewCommit}s. *

@@ -55,10 +63,9 @@ import java.util.List; */ 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 @@ public class CreateCodeReviewNotes { 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 @@ public class CreateCodeReviewNotes { 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 idList = commit.getFooterLines(CHANGE_ID); if (idList.isEmpty()) - formatter.appendChangeId(commit.change.getKey()); + formatter.appendChangeId(change.getKey()); ResultSet 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 @@ public class CreateCodeReviewNotes { } } - 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 @@ public class CreateCodeReviewNotes { } - 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 701716dcdd..a19b72207f 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 @@ import java.io.IOException; * 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 60016e5a03..b66fcb5932 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 @@ public class MergeOp { } } - 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 0000000000..60f1d0dc98 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2010, Sasa Zivkov 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); + } +}