summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/change/ChangeInserter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/change/ChangeInserter.java')
-rw-r--r--java/com/google/gerrit/server/change/ChangeInserter.java596
1 files changed, 596 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
new file mode 100644
index 0000000000..80d05dff71
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -0,0 +1,596 @@
+// 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.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
+import static com.google.gerrit.server.change.ReviewerAdder.newAddReviewerInputFromCommitIdentity;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+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.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAdditionList;
+import com.google.gerrit.server.config.SendEmailExecutor;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.extensions.events.CommentAdded;
+import com.google.gerrit.server.extensions.events.RevisionCreated;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.send.CreateChangeSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.InsertChangeOp;
+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 java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.ChangeIdUtil;
+
+public class ChangeInserter implements InsertChangeOp {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ ChangeInserter create(Change.Id cid, ObjectId commitId, String refName);
+ }
+
+ private final PermissionBackend permissionBackend;
+ private final ProjectCache projectCache;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final PatchSetUtil psUtil;
+ private final ApprovalsUtil approvalsUtil;
+ private final ChangeMessagesUtil cmUtil;
+ private final CreateChangeSender.Factory createChangeSenderFactory;
+ private final ExecutorService sendEmailExecutor;
+ private final CommitValidators.Factory commitValidatorsFactory;
+ private final RevisionCreated revisionCreated;
+ private final CommentAdded commentAdded;
+ private final ReviewerAdder reviewerAdder;
+
+ private final Change.Id changeId;
+ private final PatchSet.Id psId;
+ private final ObjectId commitId;
+ private final String refName;
+
+ // Fields exposed as setters.
+ private Change.Status status;
+ private String topic;
+ private String message;
+ private String patchSetDescription;
+ private boolean isPrivate;
+ private boolean workInProgress;
+ private List<String> groups = Collections.emptyList();
+ private boolean validate = true;
+ private NotifyHandling notify = NotifyHandling.ALL;
+ private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
+ private Map<String, Short> approvals;
+ private RequestScopePropagator requestScopePropagator;
+ private boolean fireRevisionCreated;
+ private boolean sendMail;
+ private boolean updateRef;
+ private Change.Id revertOf;
+ private ImmutableList<InternalAddReviewerInput> reviewerInputs;
+
+ // Fields set during the insertion process.
+ private ReceiveCommand cmd;
+ private Change change;
+ private ChangeMessage changeMessage;
+ private PatchSetInfo patchSetInfo;
+ private PatchSet patchSet;
+ private String pushCert;
+ private ProjectState projectState;
+ private ReviewerAdditionList reviewerAdditions;
+
+ @Inject
+ ChangeInserter(
+ PermissionBackend permissionBackend,
+ ProjectCache projectCache,
+ PatchSetInfoFactory patchSetInfoFactory,
+ PatchSetUtil psUtil,
+ ApprovalsUtil approvalsUtil,
+ ChangeMessagesUtil cmUtil,
+ CreateChangeSender.Factory createChangeSenderFactory,
+ @SendEmailExecutor ExecutorService sendEmailExecutor,
+ CommitValidators.Factory commitValidatorsFactory,
+ CommentAdded commentAdded,
+ RevisionCreated revisionCreated,
+ ReviewerAdder reviewerAdder,
+ @Assisted Change.Id changeId,
+ @Assisted ObjectId commitId,
+ @Assisted String refName) {
+ this.permissionBackend = permissionBackend;
+ this.projectCache = projectCache;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.psUtil = psUtil;
+ this.approvalsUtil = approvalsUtil;
+ this.cmUtil = cmUtil;
+ this.createChangeSenderFactory = createChangeSenderFactory;
+ this.sendEmailExecutor = sendEmailExecutor;
+ this.commitValidatorsFactory = commitValidatorsFactory;
+ this.revisionCreated = revisionCreated;
+ this.commentAdded = commentAdded;
+ this.reviewerAdder = reviewerAdder;
+
+ this.changeId = changeId;
+ this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID);
+ this.commitId = commitId.copy();
+ this.refName = refName;
+ this.reviewerInputs = ImmutableList.of();
+ this.approvals = Collections.emptyMap();
+ this.fireRevisionCreated = true;
+ this.sendMail = true;
+ this.updateRef = true;
+ }
+
+ @Override
+ public Change createChange(Context ctx) throws IOException {
+ change =
+ new Change(
+ getChangeKey(ctx.getRevWalk(), commitId),
+ changeId,
+ ctx.getAccountId(),
+ new Branch.NameKey(ctx.getProject(), refName),
+ ctx.getWhen());
+ change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
+ change.setTopic(topic);
+ change.setPrivate(isPrivate);
+ change.setWorkInProgress(workInProgress);
+ change.setReviewStarted(!workInProgress);
+ change.setRevertOf(revertOf);
+ return change;
+ }
+
+ private static Change.Key getChangeKey(RevWalk rw, ObjectId id) throws IOException {
+ RevCommit commit = rw.parseCommit(id);
+ rw.parseBody(commit);
+ List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
+ if (!idList.isEmpty()) {
+ return new Change.Key(idList.get(idList.size() - 1).trim());
+ }
+ ObjectId changeId =
+ ChangeIdUtil.computeChangeId(
+ commit.getTree(),
+ commit,
+ commit.getAuthorIdent(),
+ commit.getCommitterIdent(),
+ commit.getShortMessage());
+ StringBuilder changeIdStr = new StringBuilder();
+ changeIdStr.append("I").append(ObjectId.toString(changeId));
+ return new Change.Key(changeIdStr.toString());
+ }
+
+ public PatchSet.Id getPatchSetId() {
+ return psId;
+ }
+
+ public ObjectId getCommitId() {
+ return commitId;
+ }
+
+ public Change getChange() {
+ checkState(change != null, "getChange() only valid after creating change");
+ return change;
+ }
+
+ public ChangeInserter setTopic(String topic) {
+ checkState(change == null, "setTopic(String) only valid before creating change");
+ this.topic = topic;
+ return this;
+ }
+
+ public ChangeInserter setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public ChangeInserter setPatchSetDescription(String patchSetDescription) {
+ this.patchSetDescription = patchSetDescription;
+ return this;
+ }
+
+ public ChangeInserter setValidate(boolean validate) {
+ this.validate = validate;
+ return this;
+ }
+
+ public ChangeInserter setNotify(NotifyHandling notify) {
+ this.notify = notify;
+ return this;
+ }
+
+ public ChangeInserter setAccountsToNotify(
+ ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ this.accountsToNotify = requireNonNull(accountsToNotify);
+ return this;
+ }
+
+ public ChangeInserter setReviewersAndCcs(
+ Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
+ return setReviewersAndCcsAsStrings(
+ Iterables.transform(reviewers, Account.Id::toString),
+ Iterables.transform(ccs, Account.Id::toString));
+ }
+
+ public ChangeInserter setReviewersAndCcsAsStrings(
+ Iterable<String> reviewers, Iterable<String> ccs) {
+ reviewerInputs =
+ Streams.concat(
+ Streams.stream(reviewers)
+ .distinct()
+ .map(id -> newAddReviewerInput(id, ReviewerState.REVIEWER)),
+ Streams.stream(ccs).distinct().map(id -> newAddReviewerInput(id, ReviewerState.CC)))
+ .collect(toImmutableList());
+ return this;
+ }
+
+ public ChangeInserter setPrivate(boolean isPrivate) {
+ checkState(change == null, "setPrivate(boolean) only valid before creating change");
+ this.isPrivate = isPrivate;
+ return this;
+ }
+
+ public ChangeInserter setWorkInProgress(boolean workInProgress) {
+ this.workInProgress = workInProgress;
+ return this;
+ }
+
+ public ChangeInserter setStatus(Change.Status status) {
+ checkState(change == null, "setStatus(Change.Status) only valid before creating change");
+ this.status = status;
+ return this;
+ }
+
+ public ChangeInserter setGroups(List<String> groups) {
+ requireNonNull(groups, "groups may not be empty");
+ checkState(patchSet == null, "setGroups(Iterable<String>) only valid before creating change");
+ this.groups = groups;
+ return this;
+ }
+
+ public ChangeInserter setFireRevisionCreated(boolean fireRevisionCreated) {
+ this.fireRevisionCreated = fireRevisionCreated;
+ return this;
+ }
+
+ public ChangeInserter setSendMail(boolean sendMail) {
+ this.sendMail = sendMail;
+ return this;
+ }
+
+ public ChangeInserter setRequestScopePropagator(RequestScopePropagator r) {
+ this.requestScopePropagator = r;
+ return this;
+ }
+
+ public ChangeInserter setRevertOf(Change.Id revertOf) {
+ this.revertOf = revertOf;
+ return this;
+ }
+
+ public void setPushCertificate(String cert) {
+ pushCert = cert;
+ }
+
+ public PatchSet getPatchSet() {
+ checkState(patchSet != null, "getPatchSet() only valid after creating change");
+ return patchSet;
+ }
+
+ public ChangeInserter setApprovals(Map<String, Short> approvals) {
+ this.approvals = approvals;
+ return this;
+ }
+
+ /**
+ * Set whether to include the new patch set ref update in this update.
+ *
+ * <p>If false, the caller is responsible for creating the patch set ref <strong>before</strong>
+ * executing the containing {@code BatchUpdate}.
+ *
+ * <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for
+ * code and NoteDb meta refs.
+ *
+ * @param updateRef whether to update the ref during {@code updateRepo}.
+ */
+ @Deprecated
+ public ChangeInserter setUpdateRef(boolean updateRef) {
+ this.updateRef = updateRef;
+ return this;
+ }
+
+ public ChangeMessage getChangeMessage() {
+ if (message == null) {
+ return null;
+ }
+ checkState(changeMessage != null, "getChangeMessage() only valid after inserting change");
+ return changeMessage;
+ }
+
+ public ReceiveCommand getCommand() {
+ return cmd;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) throws ResourceConflictException, IOException {
+ cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName());
+ projectState = projectCache.checkedGet(ctx.getProject());
+ validate(ctx);
+ if (!updateRef) {
+ return;
+ }
+ ctx.addRefUpdate(cmd);
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException, IOException, PermissionBackendException,
+ ConfigInvalidException {
+ change = ctx.getChange(); // Use defensive copy created by ChangeControl.
+ ReviewDb db = ctx.getDb();
+ patchSetInfo =
+ patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
+ ctx.getChange().setCurrentPatchSet(patchSetInfo);
+
+ ChangeUpdate update = ctx.getUpdate(psId);
+ update.setChangeId(change.getKey().get());
+ update.setSubjectForCommit("Create change");
+ update.setBranch(change.getDest().get());
+ update.setTopic(change.getTopic());
+ update.setPsDescription(patchSetDescription);
+ update.setPrivate(isPrivate);
+ update.setWorkInProgress(workInProgress);
+ if (revertOf != null) {
+ update.setRevertOf(revertOf.get());
+ }
+
+ List<String> newGroups = groups;
+ if (newGroups.isEmpty()) {
+ newGroups = GroupCollector.getDefaultGroups(commitId);
+ }
+ patchSet =
+ psUtil.insert(
+ ctx.getDb(),
+ ctx.getRevWalk(),
+ update,
+ psId,
+ commitId,
+ newGroups,
+ pushCert,
+ patchSetDescription);
+
+ /* TODO: fixStatus is used here because the tests
+ * (byStatusClosed() in AbstractQueryChangesTest)
+ * insert changes that are already merged,
+ * and setStatus may not be used to set the Status to merged
+ *
+ * is it possible to make the tests use the merge code path,
+ * instead of setting the status directly?
+ */
+ update.fixStatus(change.getStatus());
+
+ reviewerAdditions =
+ reviewerAdder.prepare(
+ ctx.getDb(), ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
+ Optional<ReviewerAddition> reviewerError = reviewerAdditions.getFailures().stream().findFirst();
+ if (reviewerError.isPresent()) {
+ throw new UnprocessableEntityException(reviewerError.get().result.error);
+ }
+ reviewerAdditions.updateChange(ctx, patchSet);
+
+ LabelTypes labelTypes = projectState.getLabelTypes();
+ approvalsUtil.addApprovalsForNewPatchSet(
+ db, update, labelTypes, patchSet, ctx.getUser(), approvals);
+
+ // 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.
+ // TODO(dborowitz): Still necessary?
+ if (!approvals.isEmpty()) {
+ update.putReviewer(ctx.getAccountId(), REVIEWER);
+ }
+ if (message != null) {
+ changeMessage =
+ ChangeMessagesUtil.newMessage(
+ patchSet.getId(),
+ ctx.getUser(),
+ patchSet.getCreatedOn(),
+ message,
+ ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
+ cmUtil.addChangeMessage(db, update, changeMessage);
+ }
+ return true;
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws Exception {
+ reviewerAdditions.postUpdate(ctx);
+ if (sendMail && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) {
+ Runnable sender =
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ CreateChangeSender cm =
+ createChangeSenderFactory.create(change.getProject(), change.getId());
+ cm.setFrom(change.getOwner());
+ cm.setPatchSet(patchSet, patchSetInfo);
+ cm.setNotify(notify);
+ cm.setAccountsToNotify(accountsToNotify);
+ cm.addReviewers(
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
+ .map(PatchSetApproval::getAccountId)
+ .collect(toImmutableSet()));
+ cm.addReviewersByEmail(
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewersByEmail));
+ cm.addExtraCC(reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
+ cm.addExtraCCByEmail(
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
+ cm.send();
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log(
+ "Cannot send email for new change %s", change.getId());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "send-email newchange";
+ }
+ };
+ if (requestScopePropagator != null) {
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError =
+ sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
+ } else {
+ sender.run();
+ }
+ }
+
+ /* 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.
+ */
+ if (fireRevisionCreated) {
+ revisionCreated.fire(change, patchSet, ctx.getAccount(), ctx.getWhen(), notify);
+ if (approvals != null && !approvals.isEmpty()) {
+ List<LabelType> labels = projectState.getLabelTypes(change.getDest()).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(
+ change, patchSet, ctx.getAccount(), null, allApprovals, oldApprovals, ctx.getWhen());
+ }
+ }
+ }
+
+ private void validate(RepoContext ctx) throws IOException, ResourceConflictException {
+ if (!validate) {
+ return;
+ }
+
+ try {
+ try (CommitReceivedEvent event =
+ new CommitReceivedEvent(
+ cmd,
+ projectState.getProject(),
+ change.getDest().get(),
+ ctx.getRevWalk().getObjectReader(),
+ commitId,
+ ctx.getIdentifiedUser())) {
+ commitValidatorsFactory
+ .forGerritCommits(
+ permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
+ new Branch.NameKey(ctx.getProject(), refName),
+ ctx.getIdentifiedUser(),
+ new NoSshInfo(),
+ ctx.getRevWalk(),
+ change)
+ .validate(event);
+ }
+ } catch (CommitValidationException e) {
+ throw new ResourceConflictException(e.getFullMessage());
+ }
+ }
+
+ private static InternalAddReviewerInput newAddReviewerInput(
+ String reviewer, ReviewerState state) {
+ // Disable individual emails when adding reviewers, as all reviewers will receive the single
+ // bulk new change email.
+ InternalAddReviewerInput input =
+ ReviewerAdder.newAddReviewerInput(reviewer, state, NotifyHandling.NONE);
+
+ // Ignore failures for reasons like the reviewer being inactive or being unable to see the
+ // change. This is required for the push path, where it automatically sets reviewers from
+ // certain commit footers: putting a nonexistent user in a footer should not cause an error. In
+ // theory we could provide finer control to do this for some reviewers and not others, but it's
+ // not worth complicating the ChangeInserter interface further at this time.
+ input.otherFailureBehavior = ReviewerAdder.FailureBehavior.IGNORE;
+
+ return input;
+ }
+
+ private ImmutableList<InternalAddReviewerInput> getReviewerInputs() {
+ return Streams.concat(
+ reviewerInputs.stream(),
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, patchSetInfo.getAuthor().getAccount(), NotifyHandling.NONE)),
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, patchSetInfo.getCommitter().getAccount(), NotifyHandling.NONE)))
+ .collect(toImmutableList());
+ }
+}