Merge "Factor out code for updating notes branches to NotesBranchUtil."

This commit is contained in:
Edwin Kempin
2012-06-28 06:50:52 -07:00
committed by gerrit code review
6 changed files with 367 additions and 318 deletions

View File

@@ -21,7 +21,6 @@ import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCacheImpl;
@@ -35,6 +34,7 @@ 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.gerrit.server.git.NotesBranchUtil;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -44,7 +44,6 @@ 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.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
@@ -104,6 +103,7 @@ public class ExportReviewNotes extends SiteProgram {
@Override
protected void configure() {
factory(CreateCodeReviewNotes.Factory.class);
factory(NotesBranchUtil.Factory.class);
}
});
install(new LifecycleModule() {
@@ -172,21 +172,8 @@ public class ExportReviewNotes extends SiteProgram {
}
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();
}
notes.create(changes, null,
"Exported prior reviews from Gerrit Code Review\n", monitor);
} finally {
git.close();
}

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.NotesBranchUtil;
import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.AddReviewerSender;
@@ -82,6 +83,7 @@ public class GerritRequestModule extends FactoryModule {
factory(SubmoduleOp.Factory.class);
factory(MergeOp.Factory.class);
factory(CreateCodeReviewNotes.Factory.class);
factory(NotesBranchUtil.Factory.class);
install(new AsyncReceiveCommits.Module());
// Not really per-request, but dammit, I don't know where else to

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.git;
import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
import com.google.common.base.Strings;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -23,34 +24,25 @@ import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
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.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.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.notes.NoteMapMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
public class BanCommit {
private static final int MAX_LOCK_FAILURE_CALLS = 10;
private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
public interface Factory {
BanCommit create();
}
@@ -58,49 +50,37 @@ public class BanCommit {
private final Provider<IdentifiedUser> currentUser;
private final GitRepositoryManager repoManager;
private final PersonIdent gerritIdent;
private NotesBranchUtil.Factory notesBranchUtilFactory;
@Inject
BanCommit(final Provider<IdentifiedUser> currentUser,
final GitRepositoryManager repoManager,
@GerritPersonIdent final PersonIdent gerritIdent) {
@GerritPersonIdent final PersonIdent gerritIdent,
final NotesBranchUtil.Factory notesBranchUtilFactory) {
this.currentUser = currentUser;
this.repoManager = repoManager;
this.gerritIdent = gerritIdent;
this.notesBranchUtilFactory = notesBranchUtilFactory;
}
public BanCommitResult ban(final ProjectControl projectControl,
final List<ObjectId> commitsToBan, final String reason)
throws PermissionDeniedException, IOException,
InterruptedException, MergeException {
InterruptedException, MergeException, ConcurrentRefUpdateException {
if (!projectControl.isOwner()) {
throw new PermissionDeniedException(
"No project owner: not permitted to ban commits");
}
final BanCommitResult result = new BanCommitResult();
final PersonIdent currentUserIdent = createPersonIdent();
NoteMap banCommitNotes = NoteMap.newEmptyMap();
// add a note for each banned commit to notes
final Repository repo =
repoManager.openRepository(projectControl.getProject().getNameKey());
try {
final RevWalk revWalk = new RevWalk(repo);
final ObjectInserter inserter = repo.newObjectInserter();
try {
NoteMap baseNoteMap = null;
RevCommit baseCommit = null;
final Ref notesBranch = repo.getRef(REF_REJECT_COMMITS);
if (notesBranch != null) {
baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
baseNoteMap = NoteMap.read(revWalk.getObjectReader(), baseCommit);
}
final NoteMap ourNoteMap;
if (baseCommit != null) {
ourNoteMap = NoteMap.read(repo.newObjectReader(), baseCommit);
} else {
ourNoteMap = NoteMap.newEmptyMap();
}
for (final ObjectId commitToBan : commitsToBan) {
try {
revWalk.parseCommit(commitToBan);
@@ -110,31 +90,22 @@ public class BanCommit {
result.notACommit(commitToBan, e.getMessage());
continue;
}
final Note note = ourNoteMap.getNote(commitToBan);
if (note != null) {
result.commitAlreadyBanned(commitToBan);
continue;
banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
}
inserter.flush();
NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(repo);
NoteMap newlyCreated =
notesBranchUtil.commitNewNotes(banCommitNotes, REF_REJECT_COMMITS,
createPersonIdent(), buildCommitMessage(commitsToBan, reason));
final String noteContent = reason != null ? reason : "";
final ObjectId noteContentId =
inserter
.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
ourNoteMap.set(commitToBan, noteContentId);
result.commitBanned(commitToBan);
for (Note n : banCommitNotes) {
if (newlyCreated.contains(n)) {
result.commitBanned(n);
} else {
result.commitAlreadyBanned(n);
}
}
if (result.getNewlyBannedCommits().isEmpty()) {
return result;
}
final ObjectId ourCommit =
commit(ourNoteMap, inserter, currentUserIdent, baseCommit, result,
reason);
updateRef(repo, revWalk, inserter, ourNoteMap, ourCommit, baseNoteMap,
baseCommit);
} finally {
revWalk.release();
inserter.release();
@@ -142,8 +113,15 @@ public class BanCommit {
} finally {
repo.close();
}
}
return result;
private ObjectId createNoteContent(String reason, ObjectInserter inserter)
throws UnsupportedEncodingException, IOException {
String noteContent = reason != null ? reason : "";
if (noteContent.length() > 0 && !noteContent.endsWith("\n")) {
noteContent = noteContent + "\n";
}
return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
}
private PersonIdent createPersonIdent() {
@@ -152,35 +130,6 @@ public class BanCommit {
return currentUser.get().newCommitterIdent(now, tz);
}
private static ObjectId commit(final NoteMap noteMap,
final ObjectInserter inserter, final PersonIdent personIdent,
final ObjectId baseCommit, final BanCommitResult result,
final String reason) throws IOException {
final String commitMsg =
buildCommitMessage(result.getNewlyBannedCommits(), reason);
if (baseCommit != null) {
return createCommit(noteMap, inserter, personIdent, commitMsg, baseCommit);
} else {
return createCommit(noteMap, inserter, personIdent, commitMsg);
}
}
private static ObjectId createCommit(final NoteMap noteMap,
final ObjectInserter inserter, final PersonIdent personIdent,
final String message, final ObjectId... parents) throws IOException {
final CommitBuilder b = new CommitBuilder();
b.setTreeId(noteMap.writeTree(inserter));
b.setAuthor(personIdent);
b.setCommitter(personIdent);
if (parents.length > 0) {
b.setParentIds(parents);
}
b.setMessage(message);
final ObjectId commitId = inserter.insert(b);
inserter.flush();
return commitId;
}
private static String buildCommitMessage(final List<ObjectId> bannedCommits,
final String reason) {
final StringBuilder commitMsg = new StringBuilder();
@@ -205,61 +154,4 @@ public class BanCommit {
commitMsg.append(commitList);
return commitMsg.toString();
}
public void updateRef(final Repository repo, final RevWalk revWalk,
final ObjectInserter inserter, final NoteMap ourNoteMap,
final ObjectId oursCommit, final NoteMap baseNoteMap,
final ObjectId baseCommit) throws IOException, InterruptedException,
MissingObjectException, IncorrectObjectTypeException,
CorruptObjectException, MergeException {
int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
RefUpdate refUpdate = createRefUpdate(repo, oursCommit, baseCommit);
for (;;) {
final Result result = refUpdate.update();
if (result == Result.LOCK_FAILURE) {
if (--remainingLockFailureCalls > 0) {
Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
} else {
throw new MergeException("Failed to lock the ref: "
+ REF_REJECT_COMMITS);
}
} else if (result == Result.REJECTED) {
final RevCommit theirsCommit =
revWalk.parseCommit(refUpdate.getOldObjectId());
final NoteMap theirNoteMap =
NoteMap.read(revWalk.getObjectReader(), theirsCommit);
final NoteMapMerger merger = new NoteMapMerger(repo);
final NoteMap merged =
merger.merge(baseNoteMap, ourNoteMap, theirNoteMap);
final ObjectId mergeCommit =
createCommit(merged, inserter, gerritIdent,
"Merged note commits\n", oursCommit, theirsCommit);
refUpdate = createRefUpdate(repo, mergeCommit, theirsCommit);
remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
} else if (result == Result.IO_FAILURE) {
throw new IOException(
"Couldn't create commit reject notes because of IO_FAILURE");
} else {
break;
}
}
}
private static RefUpdate createRefUpdate(final Repository repo,
final ObjectId newObjectId, final ObjectId expectedOldObjectId)
throws IOException {
RefUpdate refUpdate = repo.updateRef(REF_REJECT_COMMITS);
refUpdate.setNewObjectId(newObjectId);
if (expectedOldObjectId == null) {
refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
} else {
refUpdate.setExpectedOldObjectId(expectedOldObjectId);
}
return refUpdate;
}
}

View File

@@ -20,6 +20,7 @@ import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
@@ -31,23 +32,15 @@ import com.google.gwtorm.server.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.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
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.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
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;
@@ -68,30 +61,22 @@ public class CreateCodeReviewNotes {
CreateCodeReviewNotes create(ReviewDb reviewDb, Repository db);
}
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");
private final ReviewDb schema;
private final PersonIdent gerritIdent;
private final AccountCache accountCache;
private final ApprovalTypes approvalTypes;
private final String canonicalWebUrl;
private final String anonymousCowardName;
private final ReviewDb schema;
private final Repository db;
private final RevWalk revWalk;
private final ObjectInserter inserter;
private final ObjectReader reader;
private RevCommit baseCommit;
private NoteMap base;
private RevCommit oursCommit;
private NoteMap ours;
private List<CodeReviewCommit> commits;
private PersonIdent author;
private RevWalk revWalk;
private ObjectInserter inserter;
private final NotesBranchUtil.Factory notesBranchUtilFactory;
@Inject
CreateCodeReviewNotes(
@GerritPersonIdent final PersonIdent gerritIdent,
@@ -99,90 +84,87 @@ public class CreateCodeReviewNotes {
final ApprovalTypes approvalTypes,
final @Nullable @CanonicalWebUrl String canonicalWebUrl,
final @AnonymousCowardName String anonymousCowardName,
final NotesBranchUtil.Factory notesBranchUtilFactory,
final @Assisted ReviewDb reviewDb,
final @Assisted Repository db) {
schema = reviewDb;
this.author = gerritIdent;
this.gerritIdent = gerritIdent;
this.accountCache = accountCache;
this.approvalTypes = approvalTypes;
this.canonicalWebUrl = canonicalWebUrl;
this.anonymousCowardName = anonymousCowardName;
this.notesBranchUtilFactory = notesBranchUtilFactory;
schema = reviewDb;
this.db = db;
revWalk = new RevWalk(db);
inserter = db.newObjectInserter();
reader = db.newObjectReader();
}
public void create(List<CodeReviewCommit> commits, PersonIdent author)
throws CodeReviewNoteCreationException {
try {
this.commits = commits;
revWalk = new RevWalk(db);
inserter = db.newObjectInserter();
if (author != null) {
this.author = author;
loadBase();
applyNotes();
updateRef();
} catch (IOException e) {
throw new CodeReviewNoteCreationException(e);
} catch (InterruptedException e) {
throw new CodeReviewNoteCreationException(e);
} finally {
release();
}
}
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);
}
if (baseCommit != null) {
ours = NoteMap.read(db.newObjectReader(), baseCommit);
} else {
ours = NoteMap.newEmptyMap();
}
}
private void applyNotes() throws IOException, CodeReviewNoteCreationException {
NoteMap notes = NoteMap.newEmptyMap();
StringBuilder message =
new StringBuilder("Update notes for submitted changes\n\n");
for (CodeReviewCommit c : commits) {
add(c.change, c);
notes.set(c, createNoteContent(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, baseCommit);
} else {
oursCommit = createCommit(ours, author, message);
NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
message.toString());
} catch (IOException e) {
throw new CodeReviewNoteCreationException(e);
} catch (ConcurrentRefUpdateException e) {
throw new CodeReviewNoteCreationException(e);
} finally {
revWalk.release();
inserter.release();
}
}
public void add(Change change, ObjectId commit)
throws MissingObjectException, IncorrectObjectTypeException, IOException,
CodeReviewNoteCreationException {
public void create(List<Change> changes, PersonIdent author,
String commitMessage, ProgressMonitor monitor) throws OrmException,
IOException, CodeReviewNoteCreationException {
try {
revWalk = new RevWalk(db);
inserter = db.newObjectInserter();
if (author != null) {
this.author = author;
}
if (monitor == null) {
monitor = NullProgressMonitor.INSTANCE;
}
NoteMap notes = NoteMap.newEmptyMap();
for (Change c : changes) {
monitor.update(1);
PatchSet ps = schema.patchSets().get(c.currentPatchSetId());
ObjectId commitId = ObjectId.fromString(ps.getRevision().get());
notes.set(commitId, createNoteContent(c, commitId));
}
NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
commitMessage);
} catch (ConcurrentRefUpdateException e) {
throw new CodeReviewNoteCreationException(e);
} finally {
revWalk.release();
inserter.release();
}
}
private ObjectId createNoteContent(Change change, ObjectId commit)
throws CodeReviewNoteCreationException, IOException {
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);
return createNoteContent(change, (RevCommit) commit);
}
private ObjectId createNoteContent(Change change, RevCommit commit)
@@ -227,83 +209,4 @@ public class CreateCodeReviewNotes {
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 != null ? author : gerritIdent);
b.setCommitter(gerritIdent);
if (parents.length > 0) {
b.setParentIds(parents);
}
b.setMessage(message);
ObjectId commitId = inserter.insert(b);
inserter.flush();
return revWalk.parseCommit(commitId);
}
private RefUpdate createRefUpdate(ObjectId newObjectId,
ObjectId expectedOldObjectId) throws IOException {
RefUpdate refUpdate = db.updateRef(REFS_NOTES_REVIEW);
refUpdate.setNewObjectId(newObjectId);
if (expectedOldObjectId == null) {
refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
} else {
refUpdate.setExpectedOldObjectId(expectedOldObjectId);
}
return refUpdate;
}
}

View File

@@ -0,0 +1,262 @@
// Copyright (C) 2012 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.server.git;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
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.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.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.IOException;
/**
* A utility class for updating a notes branch with automatic merge of note
* trees.
*/
public class NotesBranchUtil {
public interface Factory {
NotesBranchUtil create(Repository db);
}
private static final int MAX_LOCK_FAILURE_CALLS = 10;
private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
private PersonIdent gerritIdent;
private final Repository db;
private RevCommit baseCommit;
private NoteMap base;
private RevCommit oursCommit;
private NoteMap ours;
private RevWalk revWalk;
private ObjectInserter inserter;
private ObjectReader reader;
private boolean overwrite;
@Inject
public NotesBranchUtil(@GerritPersonIdent final PersonIdent gerritIdent,
@Assisted Repository db) {
this.gerritIdent = gerritIdent;
this.db = db;
}
/**
* Create a new commit in the <code>notesBranch</code> by updating existing
* or creating new notes from the <code>notes</code> map.
*
* @param notes map of notes
* @param notesBranch notes branch to update
* @param commitAuthor author of the commit in the notes branch
* @param commitMessage for the commit in the notes branch
* @throws IOException
* @throws ConcurrentRefUpdateException
*/
public final void commitAllNotes(NoteMap notes, String notesBranch,
PersonIdent commitAuthor, String commitMessage) throws IOException,
ConcurrentRefUpdateException {
this.overwrite = true;
commitNotes(notes, notesBranch, commitAuthor, commitMessage);
}
/**
* Create a new commit in the <code>notesBranch</code> by creating not yet
* existing notes from the <code>notes</code> map. The notes from the
* <code>notes</code> map which already exist in the note-tree of the
* tip of the <code>notesBranch</code> will not be updated.
*
* @param notes map of notes
* @param notesBranch notes branch to update
* @param commitAuthor author of the commit in the notes branch
* @param commitMessage for the commit in the notes branch
* @return map with those notes from the <code>notes</code> that were newly
* created
* @throws IOException
* @throws ConcurrentRefUpdateException
*/
public final NoteMap commitNewNotes(NoteMap notes, String notesBranch,
PersonIdent commitAuthor, String commitMessage) throws IOException,
ConcurrentRefUpdateException {
this.overwrite = false;
commitNotes(notes, notesBranch, commitAuthor, commitMessage);
NoteMap newlyCreated = NoteMap.newEmptyMap();
for (Note n : notes) {
if (base == null || !base.contains(n)) {
newlyCreated.set(n, n.getData());
}
}
return newlyCreated;
}
private void commitNotes(NoteMap notes, String notesBranch,
PersonIdent commitAuthor, String commitMessage) throws IOException,
ConcurrentRefUpdateException {
try {
revWalk = new RevWalk(db);
inserter = db.newObjectInserter();
reader = db.newObjectReader();
loadBase(notesBranch);
if (overwrite) {
addAllNotes(notes);
} else {
addNewNotes(notes);
}
if (base != null) {
oursCommit = createCommit(ours, commitAuthor, commitMessage, baseCommit);
} else {
oursCommit = createCommit(ours, commitAuthor, commitMessage);
}
updateRef(notesBranch);
} finally {
revWalk.release();
inserter.release();
reader.release();
}
}
private void addNewNotes(NoteMap notes) throws IOException {
for (Note n : notes) {
if (! ours.contains(n)) {
ours.set(n, n.getData());
}
}
}
private void addAllNotes(NoteMap notes) throws IOException {
for (Note n : notes) {
if (ours.contains(n)) {
// Merge the existing and the new note as if they are both new,
// means: base == null
// There is no really a common ancestry for these two note revisions
NoteMerger noteMerger = new ReviewNoteMerger();
ObjectId noteContent = noteMerger.merge(null, n, ours.getNote(n),
reader, inserter).getData();
ours.set(n, noteContent);
} else {
ours.set(n, n.getData());
}
}
}
private void loadBase(String notesBranch) throws IOException {
Ref branch = db.getRef(notesBranch);
if (branch != null) {
baseCommit = revWalk.parseCommit(branch.getObjectId());
base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
}
if (baseCommit != null) {
ours = NoteMap.read(revWalk.getObjectReader(), baseCommit);
} else {
ours = NoteMap.newEmptyMap();
}
}
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 != null ? author : gerritIdent);
b.setCommitter(gerritIdent);
if (parents.length > 0) {
b.setParentIds(parents);
}
b.setMessage(message);
ObjectId commitId = inserter.insert(b);
inserter.flush();
return revWalk.parseCommit(commitId);
}
private void updateRef(String notesBranch) throws IOException,
MissingObjectException, IncorrectObjectTypeException,
CorruptObjectException, ConcurrentRefUpdateException {
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(notesBranch, oursCommit, baseCommit);
for (;;) {
Result result = refUpdate.update();
if (result == Result.LOCK_FAILURE) {
if (--remainingLockFailureCalls > 0) {
try {
Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
} catch (InterruptedException e) {
// ignore
}
} else {
throw new ConcurrentRefUpdateException("Failed to lock the ref: "
+ notesBranch, db.getRef(notesBranch), result);
}
} 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(notesBranch, mergeCommit, theirsCommit);
remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
} else if (result == Result.IO_FAILURE) {
throw new IOException("Couldn't update " + notesBranch + ". "
+ result.name());
} else {
break;
}
}
}
private RefUpdate createRefUpdate(String notesBranch, ObjectId newObjectId,
ObjectId expectedOldObjectId) throws IOException {
RefUpdate refUpdate = db.updateRef(notesBranch);
refUpdate.setNewObjectId(newObjectId);
if (expectedOldObjectId == null) {
refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
} else {
refUpdate.setExpectedOldObjectId(expectedOldObjectId);
}
return refUpdate;
}
}

View File

@@ -22,6 +22,7 @@ import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -80,6 +81,8 @@ public class BanCommitCommand extends SshCommand {
throw die(e);
} catch (InterruptedException e) {
throw die(e);
} catch (ConcurrentRefUpdateException e) {
throw die(e);
}
}