| // 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.server.git; |
| |
| import static com.google.gerrit.server.git.GitRepositoryManager.REFS_NOTES_REVIEW; |
| |
| 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.PatchSetApproval; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.config.AnonymousCowardName; |
| import com.google.gerrit.server.config.CanonicalWebUrl; |
| import com.google.gwtorm.server.OrmException; |
| 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.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.RefUpdate.Result; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.notes.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; |
| |
| import java.io.IOException; |
| import java.util.List; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * This class create code review notes for given {@link CodeReviewCommit}s. |
| * <p> |
| * After the {@link #create(List, PersonIdent)} method is invoked once this |
| * instance must not be reused. Create a new instance of this class if needed. |
| */ |
| public class CreateCodeReviewNotes { |
| public interface Factory { |
| 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 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; |
| |
| @Inject |
| CreateCodeReviewNotes( |
| @GerritPersonIdent final PersonIdent gerritIdent, |
| final AccountCache accountCache, |
| final ApprovalTypes approvalTypes, |
| final @Nullable @CanonicalWebUrl String canonicalWebUrl, |
| final @AnonymousCowardName String anonymousCowardName, |
| 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.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; |
| 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 { |
| StringBuilder message = |
| new StringBuilder("Update notes for submitted changes\n\n"); |
| for (CodeReviewCommit c : commits) { |
| 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, baseCommit); |
| } else { |
| oursCommit = createCommit(ours, author, message); |
| } |
| } |
| |
| 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(), |
| anonymousCowardName); |
| final List<String> idList = commit.getFooterLines(CHANGE_ID); |
| if (idList.isEmpty()) |
| formatter.appendChangeId(change.getKey()); |
| ResultSet<PatchSetApproval> approvals = |
| schema.patchSetApprovals().byPatchSet(change.currentPatchSetId()); |
| PatchSetApproval submit = null; |
| for (PatchSetApproval a : approvals) { |
| if (a.getValue() == 0) { |
| // Ignore 0 values. |
| } else if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { |
| submit = a; |
| } else { |
| ApprovalType type = approvalTypes.byId(a.getCategoryId()); |
| if (type != null) { |
| formatter.appendApproval( |
| type.getCategory(), |
| a.getValue(), |
| accountCache.get(a.getAccountId()).getAccount()); |
| } |
| } |
| } |
| |
| 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 != 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; |
| } |
| } |