Use /changes/{id}/revisions/{sha1}/submit to submit changes

Replace the legacy JSON-RPC invocation with a new REST style API that
names the patch set being submitted in the URI. Callers that don't
care can use /changes/{id}/submit to submit whatever the current patch
set is at the time the call starts at the server.

The input is trivial, an optional boolean indicating if the caller
wants to wait for the merge operation to execute now, or just have
it schedule in the background to complete some time later. Web UI
and SSH both set wait_for_merge true to match the old behavior.

The logic to identify an error message written by the server is now
part of the server rather than being buried in the client UI and is
also now reported over SSH if there is an error.

Change-Id: Ibade3bda3e716c789522da7ce14b284e07df08bc
This commit is contained in:
Shawn O. Pearce
2012-11-26 17:00:33 -08:00
parent 327048b612
commit 230343b94d
18 changed files with 448 additions and 349 deletions

View File

@@ -25,10 +25,6 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0)
public interface ChangeManageService extends RemoteJsonService {
@Audit
@SignInRequired
void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
@Audit
@SignInRequired
void createNewPatchSet(final PatchSet.Id patchSetId, final String newCommitMessage,

View File

@@ -60,23 +60,46 @@ public class ChangeApi {
}
}
/** Submit a specific revision of a change. */
public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
SubmitInput in = SubmitInput.create();
in.wait_for_merge(true);
api(id, commit, "submit").data(in).post(cb);
}
private static class Input extends JavaScriptObject {
final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
final native void message(String m) /*-{ if(m)this.message=m; }-*/;
static Input create() {
return (Input) JavaScriptObject.createObject();
return (Input) createObject();
}
protected Input() {
}
}
private static class SubmitInput extends JavaScriptObject {
final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
static SubmitInput create() {
return (SubmitInput) createObject();
}
protected SubmitInput() {
}
}
private static RestApi api(int id, String action) {
// TODO Switch to triplet project~branch~id format in URI.
return new RestApi("/changes/" + id + "/" + action);
}
private static RestApi api(int id, String commit, String action) {
// TODO Switch to triplet project~branch~id format in URI.
return new RestApi("/changes/" + id + "/revisions/" + commit + "/" + action);
}
public static String emptyToNull(String str) {
return str == null || str.isEmpty() ? null : str;
}

View File

@@ -30,13 +30,12 @@ import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.PatchSetDetail;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
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.PatchSetInfo;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
@@ -308,11 +307,29 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
@Override
public void onClick(final ClickEvent event) {
b.setEnabled(false);
Util.MANAGE_SVC.submit(patchSet.getId(),
new ChangeDetailCache.GerritWidgetCallback(b) {
public void onSuccess(ChangeDetail result) {
onSubmitResult(result);
}
ChangeApi.submit(
patchSet.getId().getParentKey().get(),
patchSet.getRevision().get(),
new GerritCallback<SubmitInfo>() {
public void onSuccess(SubmitInfo result) {
redisplay();
}
public void onFailure(Throwable err) {
if (SubmitFailureDialog.isConflict(err)) {
new SubmitFailureDialog(err.getMessage()).center();
redisplay();
} else {
b.setEnabled(true);
super.onFailure(err);
}
}
private void redisplay() {
Gerrit.display(
PageLinks.toChange(patchSet.getId().getParentKey()),
new ChangeScreen(patchSet.getId().getParentKey()));
}
});
}
});
@@ -588,28 +605,6 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
Gerrit.RESOURCES.css().header());
}
private void onSubmitResult(final ChangeDetail result) {
if (result.getChange().getStatus() == Change.Status.NEW) {
// The submit failed. Try to locate the message and display
// it to the user, it should be the last one created by Gerrit.
//
ChangeMessage msg = null;
if (result.getMessages() != null && result.getMessages().size() > 0) {
for (int i = result.getMessages().size() - 1; i >= 0; i--) {
if (result.getMessages().get(i).getAuthor() == null) {
msg = result.getMessages().get(i);
break;
}
}
}
if (msg != null) {
new SubmitFailureDialog(result, msg).center();
}
}
detailCache.set(result);
}
public PatchSet getPatchSet() {
return patchSet;
}

View File

@@ -26,7 +26,6 @@ import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.PatchSetPublishDetail;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
@@ -399,17 +398,21 @@ public class PublishCommentScreen extends AccountScreen implements
}
private void submit() {
Util.MANAGE_SVC.submit(patchSetId,
new GerritCallback<ChangeDetail>() {
public void onSuccess(ChangeDetail result) {
ChangeApi.submit(patchSetId.getParentKey().get(), revision,
new GerritCallback<SubmitInfo>() {
public void onSuccess(SubmitInfo result) {
saveStateOnUnload = false;
goChange();
}
@Override
public void onFailure(Throwable caught) {
public void onFailure(Throwable err) {
if (SubmitFailureDialog.isConflict(err)) {
new SubmitFailureDialog(err.getMessage()).center();
} else {
super.onFailure(err);
}
goChange();
super.onFailure(caught);
}
});
}

View File

@@ -15,13 +15,17 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.client.RemoteJsonException;
class SubmitFailureDialog extends ErrorDialog {
SubmitFailureDialog(final ChangeDetail result, final ChangeMessage msg) {
super(new SafeHtmlBuilder().append(msg.getMessage().trim()).wikify());
static boolean isConflict(Throwable err) {
return err instanceof RemoteJsonException
&& 409 == ((RemoteJsonException) err).getCode();
}
SubmitFailureDialog(String msg) {
super(new SafeHtmlBuilder().append(msg.trim()).wikify());
setText(Util.C.submitFailed());
}
}

View File

@@ -0,0 +1,29 @@
// 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.client.changes;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject;
class SubmitInfo extends JavaScriptObject {
final Change.Status status() {
return Change.Status.valueOf(statusRaw());
}
private final native String statusRaw() /*-{ return this.status; }-*/;
protected SubmitInfo() {
}
}

View File

@@ -23,30 +23,23 @@ import com.google.gwtjsonrpc.common.VoidResult;
import com.google.inject.Inject;
class ChangeManageServiceImpl implements ChangeManageService {
private final SubmitAction.Factory submitAction;
private final RebaseChangeHandler.Factory rebaseChangeFactory;
private final PublishAction.Factory publishAction;
private final DeleteDraftChange.Factory deleteDraftChangeFactory;
private final EditCommitMessageHandler.Factory editCommitMessageHandlerFactory;
@Inject
ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
ChangeManageServiceImpl(
final RebaseChangeHandler.Factory rebaseChangeFactory,
final PublishAction.Factory publishAction,
final DeleteDraftChange.Factory deleteDraftChangeFactory,
final EditCommitMessageHandler.Factory editCommitMessageHandler) {
this.submitAction = patchSetAction;
this.rebaseChangeFactory = rebaseChangeFactory;
this.publishAction = publishAction;
this.deleteDraftChangeFactory = deleteDraftChangeFactory;
this.editCommitMessageHandlerFactory = editCommitMessageHandler;
}
public void submit(final PatchSet.Id patchSetId,
final AsyncCallback<ChangeDetail> cb) {
submitAction.create(patchSetId).to(cb);
}
public void rebaseChange(final PatchSet.Id patchSetId,
final AsyncCallback<ChangeDetail> callback) {
rebaseChangeFactory.create(patchSetId).to(callback);

View File

@@ -34,7 +34,6 @@ public class ChangeModule extends RpcServletModule {
factory(IncludedInDetailFactory.Factory.class);
factory(PatchSetDetailFactory.Factory.class);
factory(PatchSetPublishDetailFactory.Factory.class);
factory(SubmitAction.Factory.class);
factory(PublishAction.Factory.class);
factory(DeleteDraftChange.Factory.class);
}

View File

@@ -1,67 +0,0 @@
// Copyright (C) 2009 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.httpd.rpc.changedetail;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ReviewResult;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import java.io.IOException;
class SubmitAction extends Handler<ChangeDetail> {
interface Factory {
SubmitAction create(PatchSet.Id patchSetId);
}
private final Submit.Factory submitFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
private final PatchSet.Id patchSetId;
@Inject
SubmitAction(final Submit.Factory submitFactory,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted final PatchSet.Id patchSetId) {
this.submitFactory = submitFactory;
this.changeDetailFactory = changeDetailFactory;
this.patchSetId = patchSetId;
}
@Override
public ChangeDetail call() throws OrmException, NoSuchEntityException,
IllegalStateException, InvalidChangeOperationException,
PatchSetInfoNotAvailableException, NoSuchChangeException,
RepositoryNotFoundException, IOException {
final ReviewResult result =
submitFactory.create(patchSetId).call();
if (result.getErrors().size() > 0) {
throw new IllegalStateException(
"Cannot submit " + result.getErrors().get(0).getMessageOrType());
}
return changeDetailFactory.create(result.getChangeId()).call();
}
}

View File

@@ -428,6 +428,10 @@ public final class Change {
return lastUpdatedOn;
}
public void setLastUpdatedOn(Timestamp now) {
lastUpdatedOn = now;
}
public void resetLastUpdatedOn() {
lastUpdatedOn = new Timestamp(System.currentTimeMillis());
}

View File

@@ -43,11 +43,13 @@ public class Module extends RestApiModule {
post(CHANGE_KIND, "restore").to(Restore.class);
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
post(CHANGE_KIND, "revert").to(Revert.class);
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
get(REVIEWER_KIND).to(GetReviewer.class);
child(CHANGE_KIND, "revisions").to(Revisions.class);
post(REVISION_KIND, "review").to(PostReview.class);
post(REVISION_KIND, "submit").to(Submit.class);
child(REVISION_KIND, "drafts").to(Drafts.class);
put(REVISION_KIND, "drafts").to(CreateDraft.class);

View File

@@ -0,0 +1,328 @@
// 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.ApprovalCategory;
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.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 Class<Input> inputType() {
return Input.class;
}
@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 ApprovalCategory.SUBMIT.equals(input.getCategoryId());
}
}), null);
if (submit == null) {
submit = new PatchSetApproval(
new PatchSetApproval.Key(
rev.getId(),
caller.getAccountId(),
ApprovalCategory.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 Class<Input> inputType() {
return Input.class;
}
@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);
}
}
}

View File

@@ -1,207 +0,0 @@
// 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.changedetail;
import static com.google.gerrit.reviewdb.client.ApprovalCategory.SUBMIT;
import com.google.gerrit.common.data.ReviewResult;
import com.google.gerrit.common.data.SubmitRecord;
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.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.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
public class Submit implements Callable<ReviewResult> {
public interface Factory {
Submit create(PatchSet.Id patchSetId);
}
private final ChangeControl.Factory changeControlFactory;
private final MergeOp.Factory opFactory;
private final MergeQueue merger;
private final ReviewDb db;
private final GitRepositoryManager repoManager;
private final IdentifiedUser currentUser;
private final PatchSet.Id patchSetId;
@Inject
Submit(final ChangeControl.Factory changeControlFactory,
final MergeOp.Factory opFactory, final MergeQueue merger,
final ReviewDb db, final GitRepositoryManager repoManager,
final IdentifiedUser currentUser, @Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.opFactory = opFactory;
this.merger = merger;
this.db = db;
this.repoManager = repoManager;
this.currentUser = currentUser;
this.patchSetId = patchSetId;
}
@Override
public ReviewResult call() throws IllegalStateException,
InvalidChangeOperationException, NoSuchChangeException, OrmException,
IOException {
final ReviewResult result = new ReviewResult();
final PatchSet patch = db.patchSets().get(patchSetId);
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl control = changeControlFactory.validateFor(changeId);
result.setChangeId(changeId);
if (patch == null) {
throw new NoSuchChangeException(changeId);
}
List<SubmitRecord> submitResult = control.canSubmit(db, patch);
if (submitResult.isEmpty()) {
throw new IllegalStateException(
"ChangeControl.canSubmit returned empty list");
}
for (SubmitRecord submitRecord : submitResult) {
switch (submitRecord.status) {
case OK:
if (!control.getRefControl().canSubmit()) {
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.SUBMIT_NOT_PERMITTED));
}
break;
case NOT_READY:
StringBuilder errMsg = new StringBuilder();
for (SubmitRecord.Label lbl : submitRecord.labels) {
switch (lbl.status) {
case OK:
break;
case REJECT:
if (errMsg.length() > 0) errMsg.append("; ");
errMsg.append("change " + changeId + ": blocked by "
+ lbl.label);
break;
case NEED:
if (errMsg.length() > 0) errMsg.append("; ");
errMsg.append("change " + changeId + ": needs " + lbl.label);
break;
case MAY:
// The MAY label didn't cause the NOT_READY status
break;
case IMPOSSIBLE:
if (errMsg.length() > 0) errMsg.append("; ");
errMsg.append("change " + changeId + ": needs " + lbl.label
+ " (check project access)");
break;
default:
throw new IllegalArgumentException(
"Unsupported SubmitRecord.Label.status (" + lbl.status
+ ")");
}
}
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.SUBMIT_NOT_READY, errMsg.toString()));
break;
case CLOSED:
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.CHANGE_IS_CLOSED));
break;
case RULE_ERROR:
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.RULE_ERROR,
submitResult.get(0).errorMessage));
break;
default:
throw new IllegalStateException(
"Unsupported SubmitRecord.status + (" + submitRecord.status
+ ")");
}
}
if (!ProjectUtil.branchExists(repoManager, control.getChange().getDest())) {
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND,
"Destination branch \"" + control.getChange().getDest().get()
+ "\" not found."));
return result;
}
// Submit the change if we can
if (result.getErrors().isEmpty()) {
final List<PatchSetApproval> allApprovals =
new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
patchSetId).toList());
final PatchSetApproval.Key akey =
new PatchSetApproval.Key(patchSetId, currentUser.getAccountId(),
SUBMIT);
PatchSetApproval approval = new PatchSetApproval(akey, (short) 1);
for (final PatchSetApproval candidateApproval : allApprovals) {
if (akey.equals(candidateApproval.getKey())) {
candidateApproval.setValue((short) 1);
candidateApproval.setGranted();
approval = candidateApproval;
break;
}
}
db.patchSetApprovals().upsert(Collections.singleton(approval));
final Change updatedChange = db.changes().atomicUpdate(changeId,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus() == Change.Status.NEW) {
change.setStatus(Change.Status.SUBMITTED);
ChangeUtil.updated(change);
}
return change;
}
});
if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
merger.merge(opFactory, updatedChange.getDest());
}
}
return result;
}
}

View File

@@ -28,7 +28,6 @@ import com.google.gerrit.server.account.VisibleGroups;
import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
@@ -92,7 +91,6 @@ public class GerritRequestModule extends FactoryModule {
factory(GroupDetailFactory.Factory.class);
factory(GroupMembers.Factory.class);
factory(CreateProject.Factory.class);
factory(Submit.Factory.class);
factory(SuggestParentCandidates.Factory.class);
factory(BanCommit.Factory.class);
}

View File

@@ -119,9 +119,9 @@ public class ChangeMergeQueue implements MergeQueue {
}
@Override
public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
public void merge(Branch.NameKey branch) {
if (start(branch)) {
mergeImpl(mof, branch);
mergeImpl(branch);
}
}
@@ -199,16 +199,6 @@ public class ChangeMergeQueue implements MergeQueue {
e.needMerge = false;
}
private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) {
try {
opFactory.create(branch).merge();
} catch (Throwable e) {
log.error("Merge attempt for " + branch + " failed", e);
} finally {
finish(branch);
}
}
private void mergeImpl(final Branch.NameKey branch) {
try {
threadScoper.scope(new Callable<Void>(){

View File

@@ -19,9 +19,7 @@ import com.google.gerrit.reviewdb.client.Branch;
import java.util.concurrent.TimeUnit;
public interface MergeQueue {
void merge(MergeOp.Factory mof, Branch.NameKey branch);
void merge(Branch.NameKey branch);
void schedule(Branch.NameKey branch);
void recheckAfter(Branch.NameKey branch, long delay, TimeUnit delayUnit);
}

View File

@@ -307,6 +307,10 @@ public class ChangeControl {
return canSubmit(db, patchSet, null, false, true);
}
public boolean canSubmit() {
return getRefControl().canSubmit();
}
public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
return canSubmit(db, patchSet, null, false, false);
}

View File

@@ -36,7 +36,7 @@ import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -137,7 +137,7 @@ public class ReviewCommand extends SshCommand {
private Provider<Restore> restoreProvider;
@Inject
private Submit.Factory submitFactory;
private Provider<Submit> submitProvider;
private List<ApproveOption> optionList;
@@ -242,8 +242,13 @@ public class ReviewCommand extends SshCommand {
}
}
if (submitChange) {
final ReviewResult result = submitFactory.create(patchSetId).call();
handleReviewResultErrors(result);
Submit submit = submitProvider.get();
Submit.Input input = new Submit.Input();
input.waitForMerge = true;
submit.apply(new RevisionResource(
new ChangeResource(ctl),
db.patchSets().get(patchSetId)),
input);
}
} catch (InvalidChangeOperationException e) {
throw error(e.getMessage());
@@ -253,6 +258,8 @@ public class ReviewCommand extends SshCommand {
throw error(e.getMessage());
} catch (BadRequestException e) {
throw error(e.getMessage());
} catch (ResourceConflictException e) {
throw error(e.getMessage());
}
if (publishPatchSet) {