summaryrefslogtreecommitdiffstats
path: root/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java')
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java318
1 files changed, 318 insertions, 0 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
new file mode 100644
index 0000000000..c63bf5d3a7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -0,0 +1,318 @@
+// Copyright (C) 2012 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.gerrit.common.data.SubmitRecord.Status.OK;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.change.Submit.Input;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class Submit implements RestModifyView<RevisionResource, Input> {
+ public static class Input {
+ public boolean waitForMerge;
+ }
+
+ public enum Status {
+ SUBMITTED, MERGED;
+ }
+
+ public static class Output {
+ public Status status;
+ transient Change change;
+
+ private Output(Status s, Change c) {
+ status = s;
+ change = c;
+ }
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final GitRepositoryManager repoManager;
+ private final MergeQueue mergeQueue;
+
+ @Inject
+ Submit(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager repoManager,
+ MergeQueue mergeQueue) {
+ this.dbProvider = dbProvider;
+ this.repoManager = repoManager;
+ this.mergeQueue = mergeQueue;
+ }
+
+ @Override
+ public Output apply(RevisionResource rsrc, Input input) throws AuthException,
+ ResourceConflictException, RepositoryNotFoundException, IOException,
+ OrmException {
+ ChangeControl control = rsrc.getControl();
+ IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+ Change change = rsrc.getChange();
+ if (!control.canSubmit()) {
+ throw new AuthException("submit not permitted");
+ } else if (!change.getStatus().isOpen()) {
+ throw new ResourceConflictException("change is " + status(change));
+ } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
+ throw new ResourceConflictException(String.format(
+ "destination branch \"%s\" not found.",
+ change.getDest().get()));
+ } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) {
+ // TODO Allow submitting non-current revision by changing the current.
+ throw new ResourceConflictException(String.format(
+ "revision %s is not current revision",
+ rsrc.getPatchSet().getRevision().get()));
+ }
+
+ checkSubmitRule(rsrc);
+ change = submit(rsrc, caller);
+
+ if (input.waitForMerge) {
+ mergeQueue.merge(change.getDest());
+ change = dbProvider.get().changes().get(change.getId());
+ } else {
+ mergeQueue.schedule(change.getDest());
+ }
+
+ if (change == null) {
+ throw new ResourceConflictException("change is deleted");
+ }
+ switch (change.getStatus()) {
+ case SUBMITTED:
+ return new Output(Status.SUBMITTED, change);
+ case MERGED:
+ return new Output(Status.MERGED, change);
+ case NEW:
+ // If the merge was attempted and it failed the system usually
+ // writes a comment as a ChangeMessage and sets status to NEW.
+ // Find the relevant message and report that as the conflict.
+ final Timestamp before = rsrc.getChange().getLastUpdatedOn();
+ ChangeMessage msg = Iterables.getFirst(Iterables.filter(
+ Lists.reverse(dbProvider.get().changeMessages()
+ .byChange(change.getId())
+ .toList()),
+ new Predicate<ChangeMessage>() {
+ @Override
+ public boolean apply(ChangeMessage input) {
+ return input.getAuthor() == null
+ && input.getWrittenOn().getTime() >= before.getTime();
+ }
+ }), null);
+ if (msg != null) {
+ throw new ResourceConflictException(msg.getMessage());
+ }
+ default:
+ throw new ResourceConflictException("change is " + status(change));
+ }
+ }
+
+ private Change submit(RevisionResource rsrc, IdentifiedUser caller)
+ throws OrmException, ResourceConflictException {
+ final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+ Change change = rsrc.getChange();
+ ReviewDb db = dbProvider.get();
+ db.changes().beginTransaction(change.getId());
+ try {
+ approve(rsrc.getPatchSet(), caller, timestamp);
+ change = db.changes().atomicUpdate(
+ change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen()) {
+ change.setStatus(Change.Status.SUBMITTED);
+ change.setLastUpdatedOn(timestamp);
+ ChangeUtil.computeSortKey(change);
+ return change;
+ }
+ return null;
+ }
+ });
+ if (change == null) {
+ throw new ResourceConflictException("change is "
+ + status(db.changes().get(rsrc.getChange().getId())));
+ }
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+ return change;
+ }
+
+ private void approve(PatchSet rev, IdentifiedUser caller, Timestamp timestamp)
+ throws OrmException {
+ PatchSetApproval submit = Iterables.getFirst(Iterables.filter(
+ dbProvider.get().patchSetApprovals()
+ .byPatchSetUser(rev.getId(), caller.getAccountId()),
+ new Predicate<PatchSetApproval>() {
+ @Override
+ public boolean apply(PatchSetApproval input) {
+ return input.isSubmit();
+ }
+ }), null);
+ if (submit == null) {
+ submit = new PatchSetApproval(
+ new PatchSetApproval.Key(
+ rev.getId(),
+ caller.getAccountId(),
+ LabelId.SUBMIT),
+ (short) 1);
+ }
+ submit.setValue((short) 1);
+ submit.setGranted(timestamp);
+ dbProvider.get().patchSetApprovals().upsert(Collections.singleton(submit));
+ }
+
+ private void checkSubmitRule(RevisionResource rsrc)
+ throws ResourceConflictException {
+ List<SubmitRecord> results = rsrc.getControl().canSubmit(
+ dbProvider.get(),
+ rsrc.getPatchSet());
+ Optional<SubmitRecord> ok = findOkRecord(results);
+ if (ok.isPresent()) {
+ // Rules supplied a valid solution.
+ return;
+ } else if (results.isEmpty()) {
+ throw new IllegalStateException(String.format(
+ "ChangeControl.canSubmit returned empty list for %s in %s",
+ rsrc.getPatchSet().getId(),
+ rsrc.getChange().getProject().get()));
+ }
+
+ for (SubmitRecord record : results) {
+ switch (record.status) {
+ case CLOSED:
+ throw new ResourceConflictException("change is closed");
+
+ case RULE_ERROR:
+ throw new ResourceConflictException(String.format(
+ "rule error: %s",
+ record.errorMessage));
+
+ case NOT_READY:
+ StringBuilder msg = new StringBuilder();
+ for (SubmitRecord.Label lbl : record.labels) {
+ switch (lbl.status) {
+ case OK:
+ case MAY:
+ continue;
+
+ case REJECT:
+ if (msg.length() > 0) msg.append("; ");
+ msg.append("blocked by " + lbl.label);
+ continue;
+
+ case NEED:
+ if (msg.length() > 0) msg.append("; ");
+ msg.append("needs " + lbl.label);
+ continue;
+
+ case IMPOSSIBLE:
+ if (msg.length() > 0) msg.append("; ");
+ msg.append("needs " + lbl.label + " (check project access)");
+ continue;
+
+ default:
+ throw new IllegalStateException(String.format(
+ "Unsupported SubmitRecord.Label %s for %s in %s",
+ lbl.toString(),
+ rsrc.getPatchSet().getId(),
+ rsrc.getChange().getProject().get()));
+ }
+ }
+ throw new ResourceConflictException(msg.toString());
+
+ default:
+ throw new IllegalStateException(String.format(
+ "Unsupported SubmitRecord %s for %s in %s",
+ record,
+ rsrc.getPatchSet().getId(),
+ rsrc.getChange().getProject().get()));
+ }
+ }
+ }
+
+ private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
+ return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
+ @Override
+ public boolean apply(SubmitRecord input) {
+ return input.status == OK;
+ }
+ });
+ }
+
+ private static String status(Change change) {
+ return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+ }
+
+ public static class CurrentRevision implements
+ RestModifyView<ChangeResource, Input> {
+ private final Provider<ReviewDb> dbProvider;
+ private final Submit submit;
+ private final ChangeJson json;
+
+ @Inject
+ CurrentRevision(Provider<ReviewDb> dbProvider,
+ Submit submit,
+ ChangeJson json) {
+ this.dbProvider = dbProvider;
+ this.submit = submit;
+ this.json = json;
+ }
+
+ @Override
+ public Object apply(ChangeResource rsrc, Input input) throws AuthException,
+ ResourceConflictException, RepositoryNotFoundException, IOException,
+ OrmException {
+ PatchSet ps = dbProvider.get().patchSets()
+ .get(rsrc.getChange().currentPatchSetId());
+ if (ps == null) {
+ throw new ResourceConflictException("current revision is missing");
+ } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+ throw new AuthException("current revision not accessible");
+ }
+ Output out = submit.apply(new RevisionResource(rsrc, ps), input);
+ return json.format(out.change);
+ }
+ }
+}