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:
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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() {
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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>(){
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user