| // 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.google.gerrit.server.notedb; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL; |
| import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET; |
| import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS; |
| import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH; |
| import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Multimap; |
| import com.google.gerrit.common.data.SubmitRecord; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.PatchLineComment; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MetaDataUpdate; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.util.LabelVote; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.assistedinject.Assisted; |
| import com.google.inject.assistedinject.AssistedInject; |
| |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.notes.NoteMap; |
| import org.eclipse.jgit.revwalk.FooterKey; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * A single delta to apply atomically to a change. |
| * <p> |
| * This delta becomes a single commit on the notes branch, so there are |
| * limitations on the set of modifications that can be handled in a single |
| * update. In particular, there is a single author and timestamp for each |
| * update. |
| * <p> |
| * This class is not thread-safe. |
| */ |
| public class ChangeUpdate extends AbstractChangeUpdate { |
| public interface Factory { |
| ChangeUpdate create(ChangeControl ctl); |
| ChangeUpdate create(ChangeControl ctl, Date when); |
| @VisibleForTesting |
| ChangeUpdate create(ChangeControl ctl, Date when, |
| Comparator<String> labelNameComparator); |
| } |
| |
| private final AccountCache accountCache; |
| private final Map<String, Optional<Short>> approvals; |
| private final Map<Account.Id, ReviewerState> reviewers; |
| private Change.Status status; |
| private String subject; |
| private List<SubmitRecord> submitRecords; |
| private final CommentsInNotesUtil commentsUtil; |
| private List<PatchLineComment> commentsForBase; |
| private List<PatchLineComment> commentsForPs; |
| private String changeMessage; |
| |
| @AssistedInject |
| private ChangeUpdate( |
| @GerritPersonIdent PersonIdent serverIdent, |
| GitRepositoryManager repoManager, |
| NotesMigration migration, |
| AccountCache accountCache, |
| MetaDataUpdate.User updateFactory, |
| ProjectCache projectCache, |
| IdentifiedUser user, |
| @Assisted ChangeControl ctl, |
| CommentsInNotesUtil commentsUtil) { |
| this(serverIdent, repoManager, migration, accountCache, updateFactory, |
| projectCache, ctl, serverIdent.getWhen(), commentsUtil); |
| } |
| |
| @AssistedInject |
| private ChangeUpdate( |
| @GerritPersonIdent PersonIdent serverIdent, |
| GitRepositoryManager repoManager, |
| NotesMigration migration, |
| AccountCache accountCache, |
| MetaDataUpdate.User updateFactory, |
| ProjectCache projectCache, |
| @Assisted ChangeControl ctl, |
| @Assisted Date when, |
| CommentsInNotesUtil commentsUtil) { |
| this(serverIdent, repoManager, migration, accountCache, updateFactory, ctl, |
| when, |
| projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(), |
| commentsUtil); |
| } |
| |
| private static Project.NameKey getProjectName(ChangeControl ctl) { |
| return ctl.getChange().getDest().getParentKey(); |
| } |
| |
| @AssistedInject |
| private ChangeUpdate( |
| @GerritPersonIdent PersonIdent serverIdent, |
| GitRepositoryManager repoManager, |
| NotesMigration migration, |
| AccountCache accountCache, |
| MetaDataUpdate.User updateFactory, |
| @Assisted ChangeControl ctl, |
| @Assisted Date when, |
| @Assisted Comparator<String> labelNameComparator, |
| CommentsInNotesUtil commentsUtil) { |
| super(migration, repoManager, updateFactory, ctl, serverIdent, when); |
| this.accountCache = accountCache; |
| this.commentsUtil = commentsUtil; |
| this.approvals = Maps.newTreeMap(labelNameComparator); |
| this.reviewers = Maps.newLinkedHashMap(); |
| this.commentsForPs = Lists.newArrayList(); |
| this.commentsForBase = Lists.newArrayList(); |
| } |
| |
| public void setStatus(Change.Status status) { |
| checkArgument(status != Change.Status.SUBMITTED, |
| "use submit(Iterable<PatchSetApproval>)"); |
| this.status = status; |
| } |
| |
| public void putApproval(String label, short value) { |
| approvals.put(label, Optional.of(value)); |
| } |
| |
| public void removeApproval(String label) { |
| approvals.put(label, Optional.<Short> absent()); |
| } |
| |
| public void submit(Iterable<SubmitRecord> submitRecords) { |
| status = Change.Status.SUBMITTED; |
| this.submitRecords = ImmutableList.copyOf(submitRecords); |
| checkArgument(!this.submitRecords.isEmpty(), |
| "no submit records specified at submit time"); |
| } |
| |
| public void setSubject(String subject) { |
| this.subject = subject; |
| } |
| |
| public void setChangeMessage(String changeMessage) { |
| this.changeMessage = changeMessage; |
| } |
| |
| public void putComment(PatchLineComment comment) { |
| checkArgument(psId != null, |
| "setPatchSetId must be called before putComment"); |
| checkArgument(getCommentPsId(comment).equals(psId), |
| "Comment on %s doesn't match previous patch set %s", |
| getCommentPsId(comment), psId); |
| checkArgument(comment.getRevId() != null); |
| if (comment.getSide() == 0) { |
| commentsForBase.add(comment); |
| } else { |
| commentsForPs.add(comment); |
| } |
| } |
| |
| public void putReviewer(Account.Id reviewer, ReviewerState type) { |
| checkArgument(type != ReviewerState.REMOVED, "invalid ReviewerType"); |
| reviewers.put(reviewer, type); |
| } |
| |
| public void removeReviewer(Account.Id reviewer) { |
| reviewers.put(reviewer, ReviewerState.REMOVED); |
| } |
| |
| /** @return the tree id for the updated tree */ |
| private ObjectId storeCommentsInNotes() throws OrmException, IOException { |
| ChangeNotes notes = ctl.getNotes().load(); |
| NoteMap noteMap = notes.getNoteMap(); |
| if (noteMap == null) { |
| noteMap = NoteMap.newEmptyMap(); |
| } |
| if (commentsForPs.isEmpty() && commentsForBase.isEmpty()) { |
| return null; |
| } |
| |
| Multimap<PatchSet.Id, PatchLineComment> allCommentsOnBases = |
| notes.getBaseComments(); |
| Multimap<PatchSet.Id, PatchLineComment> allCommentsOnPs = |
| notes.getPatchSetComments(); |
| |
| // This writes all comments for the base of this PS to the note map. |
| if (!commentsForBase.isEmpty()) { |
| List<PatchLineComment> baseCommentsForThisPs = |
| new ArrayList<>(allCommentsOnBases.get(psId)); |
| baseCommentsForThisPs.addAll(commentsForBase); |
| commentsUtil.writeCommentsToNoteMap(noteMap, baseCommentsForThisPs, |
| inserter); |
| } |
| |
| // This write all comments for this PS to the note map. |
| if (!commentsForPs.isEmpty()) { |
| List<PatchLineComment> commentsForThisPs = |
| new ArrayList<>(allCommentsOnPs.get(psId)); |
| commentsForThisPs.addAll(commentsForPs); |
| commentsUtil.writeCommentsToNoteMap(noteMap, commentsForThisPs, inserter); |
| } |
| return noteMap.writeTree(inserter); |
| } |
| |
| public RevCommit commit() throws IOException { |
| BatchMetaDataUpdate batch = openUpdate(); |
| try { |
| CommitBuilder builder = new CommitBuilder(); |
| if (migration.write()) { |
| ObjectId treeId = storeCommentsInNotes(); |
| if (treeId != null) { |
| builder.setTreeId(treeId); |
| } |
| } |
| batch.write(builder); |
| RevCommit c = batch.commit(); |
| return c; |
| } catch (OrmException e) { |
| throw new IOException(e); |
| } finally { |
| batch.close(); |
| } |
| } |
| |
| @Override |
| protected String getRefName() { |
| return ChangeNoteUtil.changeRefName(getChange().getId()); |
| } |
| |
| @Override |
| protected boolean onSave(CommitBuilder commit) { |
| if (isEmpty()) { |
| return false; |
| } |
| commit.setAuthor(newIdent(getUser().getAccount(), when)); |
| commit.setCommitter(new PersonIdent(serverIdent, when)); |
| |
| int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get(); |
| StringBuilder msg = new StringBuilder(); |
| if (subject != null) { |
| msg.append(subject); |
| } else { |
| msg.append("Update patch set ").append(ps); |
| } |
| msg.append("\n\n"); |
| |
| if (changeMessage != null) { |
| msg.append(changeMessage); |
| msg.append("\n\n"); |
| } |
| |
| |
| addFooter(msg, FOOTER_PATCH_SET, ps); |
| if (status != null) { |
| addFooter(msg, FOOTER_STATUS, status.name().toLowerCase()); |
| } |
| |
| for (Map.Entry<Account.Id, ReviewerState> e : reviewers.entrySet()) { |
| Account account = accountCache.get(e.getKey()).getAccount(); |
| PersonIdent ident = newIdent(account, when); |
| addFooter(msg, e.getValue().getFooterKey()) |
| .append(ident.getName()) |
| .append(" <").append(ident.getEmailAddress()).append(">\n"); |
| } |
| |
| for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) { |
| if (!e.getValue().isPresent()) { |
| addFooter(msg, FOOTER_LABEL, '-', e.getKey()); |
| } else { |
| addFooter(msg, FOOTER_LABEL, |
| new LabelVote(e.getKey(), e.getValue().get()).formatWithEquals()); |
| } |
| } |
| |
| if (submitRecords != null) { |
| for (SubmitRecord rec : submitRecords) { |
| addFooter(msg, FOOTER_SUBMITTED_WITH) |
| .append(rec.status); |
| if (rec.errorMessage != null) { |
| msg.append(' ').append(sanitizeFooter(rec.errorMessage)); |
| } |
| msg.append('\n'); |
| |
| if (rec.labels != null) { |
| for (SubmitRecord.Label label : rec.labels) { |
| addFooter(msg, FOOTER_SUBMITTED_WITH) |
| .append(label.status).append(": ").append(label.label); |
| if (label.appliedBy != null) { |
| PersonIdent ident = |
| newIdent(accountCache.get(label.appliedBy).getAccount(), when); |
| msg.append(": ").append(ident.getName()) |
| .append(" <").append(ident.getEmailAddress()).append('>'); |
| } |
| msg.append('\n'); |
| } |
| } |
| } |
| } |
| |
| commit.setMessage(msg.toString()); |
| return true; |
| } |
| |
| @Override |
| protected Project.NameKey getProjectName() { |
| return getProjectName(ctl); |
| } |
| |
| private boolean isEmpty() { |
| return approvals.isEmpty() |
| && reviewers.isEmpty() |
| && commentsForBase.isEmpty() |
| && commentsForPs.isEmpty() |
| && status == null |
| && submitRecords == null |
| && changeMessage == null; |
| } |
| |
| private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) { |
| return sb.append(footer.getName()).append(": "); |
| } |
| |
| private static void addFooter(StringBuilder sb, FooterKey footer, |
| Object... values) { |
| addFooter(sb, footer); |
| for (Object value : values) { |
| sb.append(value); |
| } |
| sb.append('\n'); |
| } |
| |
| private static String sanitizeFooter(String value) { |
| return value.replace('\n', ' ').replace('\0', ' '); |
| } |
| } |