summaryrefslogtreecommitdiffstats
path: root/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java')
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java604
1 files changed, 604 insertions, 0 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
new file mode 100644
index 0000000000..4455aedd4f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -0,0 +1,604 @@
+// Copyright (C) 2016 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.receive;
+
+import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.server.ApprovalCopier;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.extensions.events.CommentAdded;
+import com.google.gerrit.server.extensions.events.RevisionCreated;
+import com.google.gerrit.server.git.MergedByPushOp;
+import com.google.gerrit.server.git.SendEmailExecutor;
+import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.util.Providers;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ReplaceOp implements BatchUpdateOp {
+ public interface Factory {
+ ReplaceOp create(
+ ProjectControl projectControl,
+ Branch.NameKey dest,
+ boolean checkMergedInto,
+ @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
+ @Assisted("priorCommitId") ObjectId priorCommit,
+ @Assisted("patchSetId") PatchSet.Id patchSetId,
+ @Assisted("commitId") ObjectId commitId,
+ PatchSetInfo info,
+ List<String> groups,
+ @Nullable MagicBranchInput magicBranch,
+ @Nullable PushCertificate pushCertificate);
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(ReplaceOp.class);
+
+ private static final String CHANGE_IS_CLOSED = "change is closed";
+
+ private final AccountResolver accountResolver;
+ private final ApprovalCopier approvalCopier;
+ private final ApprovalsUtil approvalsUtil;
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeKindCache changeKindCache;
+ private final ChangeMessagesUtil cmUtil;
+ private final CommentsUtil commentsUtil;
+ private final EmailReviewComments.Factory emailCommentsFactory;
+ private final ExecutorService sendEmailExecutor;
+ private final RevisionCreated revisionCreated;
+ private final CommentAdded commentAdded;
+ private final MergedByPushOp.Factory mergedByPushOpFactory;
+ private final PatchSetUtil psUtil;
+ private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+ private final ProjectCache projectCache;
+
+ private final ProjectControl projectControl;
+ private final Branch.NameKey dest;
+ private final boolean checkMergedInto;
+ private final PatchSet.Id priorPatchSetId;
+ private final ObjectId priorCommitId;
+ private final PatchSet.Id patchSetId;
+ private final ObjectId commitId;
+ private final PatchSetInfo info;
+ private final MagicBranchInput magicBranch;
+ private final PushCertificate pushCertificate;
+ private List<String> groups = ImmutableList.of();
+
+ private final Map<String, Short> approvals = new HashMap<>();
+ private final MailRecipients recipients = new MailRecipients();
+ private RevCommit commit;
+ private ReceiveCommand cmd;
+ private ChangeNotes notes;
+ private PatchSet newPatchSet;
+ private ChangeKind changeKind;
+ private ChangeMessage msg;
+ private List<Comment> comments = ImmutableList.of();
+ private String rejectMessage;
+ private MergedByPushOp mergedByPushOp;
+ private RequestScopePropagator requestScopePropagator;
+
+ @Inject
+ ReplaceOp(
+ AccountResolver accountResolver,
+ ApprovalCopier approvalCopier,
+ ApprovalsUtil approvalsUtil,
+ ChangeData.Factory changeDataFactory,
+ ChangeKindCache changeKindCache,
+ ChangeMessagesUtil cmUtil,
+ CommentsUtil commentsUtil,
+ EmailReviewComments.Factory emailCommentsFactory,
+ RevisionCreated revisionCreated,
+ CommentAdded commentAdded,
+ MergedByPushOp.Factory mergedByPushOpFactory,
+ PatchSetUtil psUtil,
+ ReplacePatchSetSender.Factory replacePatchSetFactory,
+ ProjectCache projectCache,
+ @SendEmailExecutor ExecutorService sendEmailExecutor,
+ @Assisted ProjectControl projectControl,
+ @Assisted Branch.NameKey dest,
+ @Assisted boolean checkMergedInto,
+ @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
+ @Assisted("priorCommitId") ObjectId priorCommitId,
+ @Assisted("patchSetId") PatchSet.Id patchSetId,
+ @Assisted("commitId") ObjectId commitId,
+ @Assisted PatchSetInfo info,
+ @Assisted List<String> groups,
+ @Assisted @Nullable MagicBranchInput magicBranch,
+ @Assisted @Nullable PushCertificate pushCertificate) {
+ this.accountResolver = accountResolver;
+ this.approvalCopier = approvalCopier;
+ this.approvalsUtil = approvalsUtil;
+ this.changeDataFactory = changeDataFactory;
+ this.changeKindCache = changeKindCache;
+ this.cmUtil = cmUtil;
+ this.commentsUtil = commentsUtil;
+ this.emailCommentsFactory = emailCommentsFactory;
+ this.revisionCreated = revisionCreated;
+ this.commentAdded = commentAdded;
+ this.mergedByPushOpFactory = mergedByPushOpFactory;
+ this.psUtil = psUtil;
+ this.replacePatchSetFactory = replacePatchSetFactory;
+ this.projectCache = projectCache;
+ this.sendEmailExecutor = sendEmailExecutor;
+
+ this.projectControl = projectControl;
+ this.dest = dest;
+ this.checkMergedInto = checkMergedInto;
+ this.priorPatchSetId = priorPatchSetId;
+ this.priorCommitId = priorCommitId.copy();
+ this.patchSetId = patchSetId;
+ this.commitId = commitId.copy();
+ this.info = info;
+ this.groups = groups;
+ this.magicBranch = magicBranch;
+ this.pushCertificate = pushCertificate;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) throws Exception {
+ commit = ctx.getRevWalk().parseCommit(commitId);
+ ctx.getRevWalk().parseBody(commit);
+ changeKind =
+ changeKindCache.getChangeKind(
+ projectControl.getProject().getNameKey(),
+ ctx.getRevWalk(),
+ ctx.getRepoView().getConfig(),
+ priorCommitId,
+ commitId);
+
+ if (checkMergedInto) {
+ String mergedInto = findMergedInto(ctx, dest.get(), commit);
+ if (mergedInto != null) {
+ mergedByPushOp =
+ mergedByPushOpFactory.create(requestScopePropagator, patchSetId, mergedInto);
+ }
+ }
+
+ cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName());
+ ctx.addRefUpdate(cmd);
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException, IOException, PermissionBackendException {
+ notes = ctx.getNotes();
+ Change change = notes.getChange();
+ if (change == null || change.getStatus().isClosed()) {
+ rejectMessage = CHANGE_IS_CLOSED;
+ return false;
+ }
+ if (groups.isEmpty()) {
+ PatchSet prevPs = psUtil.current(ctx.getDb(), notes);
+ groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of();
+ }
+
+ ChangeUpdate update = ctx.getUpdate(patchSetId);
+ update.setSubjectForCommit("Create patch set " + patchSetId.get());
+
+ String reviewMessage = null;
+ String psDescription = null;
+ if (magicBranch != null) {
+ recipients.add(magicBranch.getMailRecipients());
+ reviewMessage = magicBranch.message;
+ psDescription = magicBranch.message;
+ approvals.putAll(magicBranch.labels);
+ Set<String> hashtags = magicBranch.hashtags;
+ if (hashtags != null && !hashtags.isEmpty()) {
+ hashtags.addAll(notes.getHashtags());
+ update.setHashtags(hashtags);
+ }
+ if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
+ update.setTopic(magicBranch.topic);
+ }
+ if (magicBranch.removePrivate) {
+ change.setPrivate(false);
+ update.setPrivate(false);
+ } else if (magicBranch.isPrivate) {
+ change.setPrivate(true);
+ update.setPrivate(true);
+ }
+ if (magicBranch.ready) {
+ change.setWorkInProgress(false);
+ change.setReviewStarted(true);
+ update.setWorkInProgress(false);
+ } else if (magicBranch.workInProgress) {
+ change.setWorkInProgress(true);
+ update.setWorkInProgress(true);
+ }
+ if (shouldPublishComments()) {
+ boolean workInProgress = change.isWorkInProgress();
+ if (magicBranch != null && magicBranch.workInProgress) {
+ workInProgress = true;
+ }
+ comments = publishComments(ctx, workInProgress);
+ }
+ }
+
+ newPatchSet =
+ psUtil.insert(
+ ctx.getDb(),
+ ctx.getRevWalk(),
+ update,
+ patchSetId,
+ commitId,
+ groups,
+ pushCertificate != null ? pushCertificate.toTextWithSignature() : null,
+ psDescription);
+
+ update.setPsDescription(psDescription);
+ recipients.add(getRecipientsFromFooters(accountResolver, commit.getFooterLines()));
+ recipients.remove(ctx.getAccountId());
+ ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
+ MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
+ Iterable<PatchSetApproval> newApprovals =
+ approvalsUtil.addApprovalsForNewPatchSet(
+ ctx.getDb(),
+ update,
+ projectControl.getProjectState().getLabelTypes(),
+ newPatchSet,
+ ctx.getUser(),
+ approvals);
+ approvalCopier.copyInReviewDb(
+ ctx.getDb(),
+ ctx.getNotes(),
+ ctx.getUser(),
+ newPatchSet,
+ ctx.getRevWalk(),
+ ctx.getRepoView().getConfig(),
+ newApprovals);
+ approvalsUtil.addReviewers(
+ ctx.getDb(),
+ update,
+ projectControl.getProjectState().getLabelTypes(),
+ change,
+ newPatchSet,
+ info,
+ recipients.getReviewers(),
+ oldRecipients.getAll());
+
+ // Check if approvals are changing in with this update. If so, add current user to reviewers.
+ // Note that this is done separately as addReviewers is filtering out the change owner as
+ // reviewer which is needed in several other code paths.
+ if (magicBranch != null && !magicBranch.labels.isEmpty()) {
+ update.putReviewer(ctx.getAccountId(), REVIEWER);
+ }
+
+ recipients.add(oldRecipients);
+
+ msg = createChangeMessage(ctx, reviewMessage);
+ cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+
+ if (mergedByPushOp == null) {
+ resetChange(ctx);
+ } else {
+ mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
+ }
+
+ return true;
+ }
+
+ private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
+ throws OrmException, IOException {
+ String approvalMessage =
+ ApprovalsUtil.renderMessageWithApprovals(
+ patchSetId.get(), approvals, scanLabels(ctx, approvals));
+ String kindMessage = changeKindMessage(changeKind);
+ StringBuilder message = new StringBuilder(approvalMessage);
+ if (!Strings.isNullOrEmpty(kindMessage)) {
+ message.append(kindMessage);
+ } else {
+ message.append('.');
+ }
+ if (comments.size() == 1) {
+ message.append("\n\n(1 comment)");
+ } else if (comments.size() > 1) {
+ message.append(String.format("\n\n(%d comments)", comments.size()));
+ }
+ if (!Strings.isNullOrEmpty(reviewMessage)) {
+ message.append("\n\n").append(reviewMessage);
+ }
+ boolean workInProgress = ctx.getChange().isWorkInProgress();
+ if (magicBranch != null && magicBranch.workInProgress) {
+ workInProgress = true;
+ }
+ return ChangeMessagesUtil.newMessage(
+ patchSetId,
+ ctx.getUser(),
+ ctx.getWhen(),
+ message.toString(),
+ ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
+ }
+
+ private String changeKindMessage(ChangeKind changeKind) {
+ switch (changeKind) {
+ case MERGE_FIRST_PARENT_UPDATE:
+ case TRIVIAL_REBASE:
+ case NO_CHANGE:
+ return ": Patch Set " + priorPatchSetId.get() + " was rebased.";
+ case NO_CODE_CHANGE:
+ return ": Commit message was updated.";
+ case REWORK:
+ default:
+ return null;
+ }
+ }
+
+ private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals)
+ throws OrmException, IOException {
+ Map<String, PatchSetApproval> current = new HashMap<>();
+ // We optimize here and only retrieve current when approvals provided
+ if (!approvals.isEmpty()) {
+ for (PatchSetApproval a :
+ approvalsUtil.byPatchSetUser(
+ ctx.getDb(),
+ ctx.getNotes(),
+ ctx.getUser(),
+ priorPatchSetId,
+ ctx.getAccountId(),
+ ctx.getRevWalk(),
+ ctx.getRepoView().getConfig())) {
+ if (a.isLegacySubmit()) {
+ continue;
+ }
+
+ LabelType lt = projectControl.getProjectState().getLabelTypes().byLabel(a.getLabelId());
+ if (lt != null) {
+ current.put(lt.getName(), a);
+ }
+ }
+ }
+ return current;
+ }
+
+ private void resetChange(ChangeContext ctx) {
+ Change change = ctx.getChange();
+ if (!change.currentPatchSetId().equals(priorPatchSetId)) {
+ return;
+ }
+
+ if (magicBranch != null && magicBranch.topic != null) {
+ change.setTopic(magicBranch.topic);
+ }
+ change.setStatus(Change.Status.NEW);
+ change.setCurrentPatchSet(info);
+
+ List<String> idList = commit.getFooterLines(CHANGE_ID);
+ if (idList.isEmpty()) {
+ change.setKey(new Change.Key("I" + commitId.name()));
+ } else {
+ change.setKey(new Change.Key(idList.get(idList.size() - 1).trim()));
+ }
+ }
+
+ private List<Comment> publishComments(ChangeContext ctx, boolean workInProgress)
+ throws OrmException {
+ List<Comment> comments =
+ commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), ctx.getUser().getAccountId());
+ commentsUtil.publish(
+ ctx, patchSetId, comments, ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
+ return comments;
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws Exception {
+ if (changeKind != ChangeKind.TRIVIAL_REBASE) {
+ // TODO(dborowitz): Merge email templates so we only have to send one.
+ Runnable e = new ReplaceEmailTask(ctx);
+ if (requestScopePropagator != null) {
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError = sendEmailExecutor.submit(requestScopePropagator.wrap(e));
+ } else {
+ e.run();
+ }
+ }
+
+ NotifyHandling notify = magicBranch != null ? magicBranch.getNotify(notes) : NotifyHandling.ALL;
+
+ if (shouldPublishComments()) {
+ emailCommentsFactory
+ .create(
+ notify,
+ magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(),
+ notes,
+ newPatchSet,
+ ctx.getUser().asIdentifiedUser(),
+ msg,
+ comments,
+ msg.getMessage(),
+ ImmutableList.of()) // TODO(dborowitz): Include labels.
+ .sendAsync();
+ }
+
+ revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
+ try {
+ fireCommentAddedEvent(ctx);
+ } catch (Exception e) {
+ log.warn("comment-added event invocation failed", e);
+ }
+ if (mergedByPushOp != null) {
+ mergedByPushOp.postUpdate(ctx);
+ }
+ }
+
+ private class ReplaceEmailTask implements Runnable {
+ private final Context ctx;
+
+ private ReplaceEmailTask(Context ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void run() {
+ try {
+ ReplacePatchSetSender cm =
+ replacePatchSetFactory.create(
+ projectControl.getProject().getNameKey(), notes.getChangeId());
+ cm.setFrom(ctx.getAccount().getId());
+ cm.setPatchSet(newPatchSet, info);
+ cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
+ if (magicBranch != null) {
+ cm.setNotify(magicBranch.getNotify(notes));
+ cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
+ }
+ cm.addReviewers(recipients.getReviewers());
+ cm.addExtraCC(recipients.getCcOnly());
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "send-email newpatchset";
+ }
+ }
+
+ private void fireCommentAddedEvent(Context ctx) throws IOException {
+ if (approvals.isEmpty()) {
+ return;
+ }
+
+ /* For labels that are not set in this operation, show the "current" value
+ * of 0, and no oldValue as the value was not modified by this operation.
+ * For labels that are set in this operation, the value was modified, so
+ * show a transition from an oldValue of 0 to the new value.
+ */
+ List<LabelType> labels =
+ projectCache
+ .checkedGet(ctx.getProject())
+ .getLabelTypes(notes, ctx.getUser())
+ .getLabelTypes();
+ Map<String, Short> allApprovals = new HashMap<>();
+ Map<String, Short> oldApprovals = new HashMap<>();
+ for (LabelType lt : labels) {
+ allApprovals.put(lt.getName(), (short) 0);
+ oldApprovals.put(lt.getName(), null);
+ }
+ for (Map.Entry<String, Short> entry : approvals.entrySet()) {
+ if (entry.getValue() != 0) {
+ allApprovals.put(entry.getKey(), entry.getValue());
+ oldApprovals.put(entry.getKey(), (short) 0);
+ }
+ }
+
+ commentAdded.fire(
+ notes.getChange(),
+ newPatchSet,
+ ctx.getAccount(),
+ null,
+ allApprovals,
+ oldApprovals,
+ ctx.getWhen());
+ }
+
+ public PatchSet getPatchSet() {
+ return newPatchSet;
+ }
+
+ public Change getChange() {
+ return notes.getChange();
+ }
+
+ public String getRejectMessage() {
+ return rejectMessage;
+ }
+
+ public ReceiveCommand getCommand() {
+ return cmd;
+ }
+
+ public ReplaceOp setRequestScopePropagator(RequestScopePropagator requestScopePropagator) {
+ this.requestScopePropagator = requestScopePropagator;
+ return this;
+ }
+
+ private static String findMergedInto(Context ctx, String first, RevCommit commit) {
+ try {
+ RevWalk rw = ctx.getRevWalk();
+ Optional<ObjectId> firstId = ctx.getRepoView().getRef(first);
+ if (firstId.isPresent() && rw.isMergedInto(commit, rw.parseCommit(firstId.get()))) {
+ return first;
+ }
+
+ for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(R_HEADS).entrySet()) {
+ if (rw.isMergedInto(commit, rw.parseCommit(e.getValue()))) {
+ return R_HEADS + e.getKey();
+ }
+ }
+ return null;
+ } catch (IOException e) {
+ log.warn("Can't check for already submitted change", e);
+ return null;
+ }
+ }
+
+ private boolean shouldPublishComments() {
+ return magicBranch != null && magicBranch.shouldPublishComments();
+ }
+}