| // Copyright (C) 2013 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.googlesource.gerrit.plugins.reviewnotes; |
| |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.LabelType; |
| import com.google.gerrit.common.data.LabelTypes; |
| import com.google.gerrit.reviewdb.client.Branch; |
| 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.client.Project; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.ApprovalsUtil; |
| 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.gerrit.server.git.LabelNormalizer; |
| import com.google.gerrit.server.git.NotesBranchUtil; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| |
| import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| 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.PersonIdent; |
| import org.eclipse.jgit.lib.ProgressMonitor; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.notes.NoteMap; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| |
| class CreateReviewNotes { |
| |
| private static final Logger log = |
| LoggerFactory.getLogger(CreateReviewNotes.class); |
| |
| interface Factory { |
| CreateReviewNotes create(ReviewDb reviewDb, Project.NameKey project, |
| Repository git); |
| } |
| |
| private static final String REFS_NOTES_REVIEW = "refs/notes/review"; |
| |
| private final PersonIdent gerritServerIdent; |
| private final AccountCache accountCache; |
| private final String anonymousCowardName; |
| private final LabelTypes labelTypes; |
| private final ApprovalsUtil approvalsUtil; |
| private final ChangeNotes.Factory changeNotesFactory; |
| private final LabelNormalizer labelNormalizer; |
| private final NotesBranchUtil.Factory notesBranchUtilFactory; |
| private final String canonicalWebUrl; |
| private final ReviewDb reviewDb; |
| private final Project.NameKey project; |
| private final Repository git; |
| |
| private ObjectInserter inserter; |
| private NoteMap reviewNotes; |
| private StringBuilder message; |
| |
| @Inject |
| CreateReviewNotes(@GerritPersonIdent final PersonIdent gerritIdent, |
| final AccountCache accountCache, |
| final @AnonymousCowardName String anonymousCowardName, |
| final ProjectCache projectCache, |
| final ApprovalsUtil approvalsUtil, |
| final LabelNormalizer labelNormalizer, |
| final ChangeNotes.Factory changeNotesFactory, |
| final NotesBranchUtil.Factory notesBranchUtilFactory, |
| final @Nullable @CanonicalWebUrl String canonicalWebUrl, |
| final @Assisted ReviewDb reviewDb, |
| final @Assisted Project.NameKey project, |
| final @Assisted Repository git) { |
| this.gerritServerIdent = gerritIdent; |
| this.accountCache = accountCache; |
| this.anonymousCowardName = anonymousCowardName; |
| ProjectState projectState = projectCache.get(project); |
| if (projectState == null) { |
| log.error("Could not obtain available labels for project " |
| + project.get() + ". Expect missing labels in its review notes."); |
| this.labelTypes = new LabelTypes(Collections.<LabelType> emptyList()); |
| } else { |
| this.labelTypes = projectState.getLabelTypes(); |
| } |
| this.approvalsUtil = approvalsUtil; |
| this.labelNormalizer = labelNormalizer; |
| this.changeNotesFactory = changeNotesFactory; |
| this.notesBranchUtilFactory = notesBranchUtilFactory; |
| this.canonicalWebUrl = canonicalWebUrl; |
| this.reviewDb = reviewDb; |
| this.project = project; |
| this.git = git; |
| } |
| |
| void createNotes(String branch, ObjectId oldObjectId, ObjectId newObjectId, |
| ProgressMonitor monitor) throws OrmException, IOException { |
| if (ObjectId.zeroId().equals(newObjectId)) { |
| return; |
| } |
| |
| RevWalk rw = new RevWalk(git); |
| try { |
| RevCommit n = rw.parseCommit(newObjectId); |
| rw.markStart(n); |
| if (n.getParentCount() == 1 && n.getParent(0).equals(oldObjectId)) { |
| rw.markUninteresting(rw.parseCommit(oldObjectId)); |
| } else { |
| markUninteresting(git, branch, rw, oldObjectId); |
| } |
| } catch (Exception e) { |
| log.error(e.getMessage(), e); |
| return; |
| } |
| |
| if (monitor == null) { |
| monitor = NullProgressMonitor.INSTANCE; |
| } |
| |
| try { |
| for (RevCommit c : rw) { |
| ObjectId content = createNoteContent(loadPatchSet(c, branch)); |
| if (content != null) { |
| monitor.update(1); |
| getNotes().set(c, content); |
| getMessage().append("* ").append(c.getShortMessage()).append("\n"); |
| } |
| } |
| } finally { |
| rw.release(); |
| } |
| } |
| |
| void createNotes(List<Change> changes, ProgressMonitor monitor) |
| throws OrmException, IOException { |
| RevWalk rw = new RevWalk(git); |
| try { |
| if (monitor == null) { |
| monitor = NullProgressMonitor.INSTANCE; |
| } |
| |
| for (Change c : changes) { |
| monitor.update(1); |
| PatchSet ps = reviewDb.patchSets().get(c.currentPatchSetId()); |
| ObjectId commitId = ObjectId.fromString(ps.getRevision().get()); |
| RevCommit commit = rw.parseCommit(commitId); |
| getNotes().set(commitId, createNoteContent(ps)); |
| getMessage().append("* ").append(commit.getShortMessage()).append("\n"); |
| } |
| } finally { |
| rw.release(); |
| } |
| } |
| |
| void commitNotes() throws IOException, ConcurrentRefUpdateException { |
| try { |
| if (reviewNotes == null) { |
| return; |
| } |
| |
| message.insert(0, "Update notes for submitted changes\n\n"); |
| notesBranchUtilFactory.create(project, git, inserter) |
| .commitAllNotes(reviewNotes, REFS_NOTES_REVIEW, gerritServerIdent, |
| message.toString()); |
| } finally { |
| if (inserter != null) { |
| inserter.release(); |
| } |
| } |
| } |
| |
| private void markUninteresting(Repository git, String branch, RevWalk rw, |
| ObjectId oldObjectId) { |
| for (final Ref r : git.getAllRefs().values()) { |
| try { |
| if (r.getName().equals(branch)) { |
| if (!ObjectId.zeroId().equals(oldObjectId)) { |
| // For the updated branch the oldObjectId is the tip of uninteresting |
| // commit history |
| rw.markUninteresting(rw.parseCommit(oldObjectId)); |
| } |
| } else if (r.getName().startsWith(Constants.R_HEADS) |
| || r.getName().startsWith(Constants.R_TAGS)) { |
| rw.markUninteresting(rw.parseCommit(r.getObjectId())); |
| } |
| } catch (IncorrectObjectTypeException e) { |
| // skip if not parseable as a commit |
| } catch (MissingObjectException e) { |
| // skip if not parseable as a commit |
| } catch (IOException e) { |
| // skip if not parseable as a commit |
| } |
| } |
| } |
| |
| private ObjectId createNoteContent(PatchSet ps) |
| throws OrmException, IOException { |
| HeaderFormatter fmt = |
| new HeaderFormatter(gerritServerIdent.getTimeZone(), anonymousCowardName); |
| if (ps != null) { |
| try { |
| createCodeReviewNote(ps, fmt); |
| return getInserter().insert(Constants.OBJ_BLOB, |
| fmt.toString().getBytes("UTF-8")); |
| } catch (NoSuchChangeException e) { |
| throw new IOException(e); |
| } |
| } |
| return null; |
| } |
| |
| private PatchSet loadPatchSet(RevCommit c, String destBranch) |
| throws OrmException { |
| List<PatchSet> patches = reviewDb.patchSets().byRevision(new RevId(c.name())) |
| .toList(); |
| if (patches.isEmpty()) { |
| return null; // TODO: createNoCodeReviewNote(branch, c, fmt); |
| } else if (patches.size() == 1) { |
| return patches.get(0); |
| } else { |
| Branch.NameKey dest = new Branch.NameKey(project, destBranch); |
| for (PatchSet ps : patches) { |
| Change.Id cid = ps.getId().getParentKey(); |
| Change change = reviewDb.changes().get(cid); |
| if (change.getDest().equals(dest)) { |
| return ps; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private void createCodeReviewNote(PatchSet ps, HeaderFormatter fmt) |
| throws OrmException, NoSuchChangeException { |
| createCodeReviewNote( |
| reviewDb.changes().get(ps.getId().getParentKey()), ps, fmt); |
| } |
| |
| private void createCodeReviewNote(Change change, PatchSet ps, |
| HeaderFormatter fmt) throws OrmException, NoSuchChangeException { |
| // This races with the label normalization/writeback done by MergeOp. It may |
| // repeat some work, but results should be identical except in the case of |
| // an additional race with a permissions change. |
| // TODO(dborowitz): These will eventually be stamped in the ChangeNotes at |
| // commit time so we will be able to skip this normalization step. |
| ChangeNotes changeNotes = changeNotesFactory.create(change); |
| List<PatchSetApproval> curr = |
| approvalsUtil.byPatchSet(reviewDb, changeNotes, ps.getId()); |
| PatchSetApproval submit = null; |
| for (PatchSetApproval a : |
| labelNormalizer.normalize(change, curr).getNormalized()) { |
| if (a.getValue() == 0) { |
| // Ignore 0 values. |
| } else if (a.isSubmit()) { |
| submit = a; |
| } else { |
| LabelType type = labelTypes.byLabel(a.getLabelId()); |
| if (type != null) { |
| fmt.appendApproval(type, a.getValue(), |
| accountCache.get(a.getAccountId()).getAccount()); |
| } |
| } |
| } |
| if (submit != null) { |
| fmt.appendSubmittedBy(accountCache.get(submit.getAccountId()).getAccount()); |
| fmt.appendSubmittedAt(submit.getGranted()); |
| } |
| if (canonicalWebUrl != null) { |
| fmt.appendReviewedOn(canonicalWebUrl, ps.getId().getParentKey()); |
| } |
| fmt.appendProject(project.get()); |
| fmt.appendBranch(change.getDest().get()); |
| } |
| |
| private ObjectInserter getInserter() { |
| if (inserter == null) { |
| inserter = git.newObjectInserter(); |
| } |
| return inserter; |
| } |
| |
| private NoteMap getNotes() { |
| if (reviewNotes == null) { |
| reviewNotes = NoteMap.newEmptyMap(); |
| } |
| return reviewNotes; |
| } |
| |
| private StringBuilder getMessage() { |
| if (message == null) { |
| message = new StringBuilder(); |
| } |
| return message; |
| } |
| } |