Merge "Add ability to modify workInProgress to PostReview"

This commit is contained in:
Logan Hanks
2017-07-07 16:52:48 +00:00
committed by Gerrit Code Review
7 changed files with 192 additions and 9 deletions

View File

@@ -3603,7 +3603,7 @@ describing the related changes.
-- --
Sets a review on a revision, optionally also publishing draft comments, setting Sets a review on a revision, optionally also publishing draft comments, setting
labels, and adding reviewers or CCs. labels, adding reviewers or CCs, and modifying the work in progress property.
The review must be provided in the request body as a The review must be provided in the request body as a
link:#review-input[ReviewInput] entity. link:#review-input[ReviewInput] entity.
@@ -6713,6 +6713,12 @@ have been granted `labelAs-NAME` permission for all keys of labels.
|`reviewers` |optional| |`reviewers` |optional|
A list of link:rest-api-changes.html#reviewer-input[ReviewerInput] A list of link:rest-api-changes.html#reviewer-input[ReviewerInput]
representing reviewers that should be added to the change. representing reviewers that should be added to the change.
|`ready` |optional|
If true, and if the change is work in progress, then start review.
It is an error for both `ready` and `work_in_progress` to be true.
|`work_in_progress` |optional|
If true, mark the change as work in progress. It is an error for both
`ready` and `work_in_progress` to be true.
|============================ |============================
[[review-result]] [[review-result]]

View File

@@ -105,6 +105,7 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.ProjectConfig;
@@ -501,6 +502,70 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
} }
@Test
public void reviewAndStartReview() throws Exception {
PushOneCommit.Result r = createWorkInProgressChange();
r.assertOkStatus();
assertThat(r.getChange().change().isWorkInProgress()).isTrue();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(false);
gApi.changes().id(r.getChangeId()).revision("current").review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isNull();
}
@Test
public void reviewAndMoveToWorkInProgress() throws Exception {
PushOneCommit.Result r = createChange();
r.assertOkStatus();
assertThat(r.getChange().change().isWorkInProgress()).isFalse();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
gApi.changes().id(r.getChangeId()).revision("current").review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
}
@Test
public void reviewAndSetWorkInProgressAndAddReviewerAndVote() throws Exception {
PushOneCommit.Result r = createChange();
r.assertOkStatus();
assertThat(r.getChange().change().isWorkInProgress()).isFalse();
ReviewInput in =
ReviewInput.approve().reviewer(user.email).label("Code-Review", 1).setWorkInProgress(true);
gApi.changes().id(r.getChangeId()).revision("current").review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
.containsExactly(admin.id.get(), user.id.get());
assertThat(info.labels.get("Code-Review").recommended._accountId).isEqualTo(admin.id.get());
}
@Test
public void reviewWithWorkInProgressAndReadyReturnsError() throws Exception {
PushOneCommit.Result r = createChange();
r.assertOkStatus();
ReviewInput in = ReviewInput.noScore();
in.ready = true;
in.workInProgress = true;
ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE);
}
@Test
public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
PushOneCommit.Result r = createChange();
r.assertOkStatus();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
setApiUser(user);
ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in);
assertThat(result.error).isEqualTo(PostReview.ERROR_ONLY_OWNER_CAN_MODIFY_WORK_IN_PROGRESS);
}
@Test @Test
public void getAmbiguous() throws Exception { public void getAmbiguous() throws Exception {
PushOneCommit.Result r1 = createChange(); PushOneCommit.Result r1 = createChange();

View File

@@ -825,6 +825,44 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.noOneElse(); .noOneElse();
} }
@Test
public void noCommentAndSetWorkInProgress() throws Exception {
StagedChange sc = stageReviewableChange();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
gApi.changes().id(sc.changeId).revision("current").review(in);
assertThat(sender).notSent();
}
@Test
public void commentAndSetWorkInProgress() throws Exception {
StagedChange sc = stageReviewableChange();
ReviewInput in = ReviewInput.noScore().message("ok").setWorkInProgress(true);
gApi.changes().id(sc.changeId).revision("current").review(in);
assertThat(sender)
.sent("comment", sc)
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
assertThat(sender).notSent();
}
@Test
public void commentOnWipChangeAndStartReview() throws Exception {
StagedChange sc = stageWipChange();
ReviewInput in = ReviewInput.noScore().message("ok").setWorkInProgress(false);
gApi.changes().id(sc.changeId).revision("current").review(in);
assertThat(sender)
.sent("comment", sc)
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
assertThat(sender).notSent();
}
private void review(TestAccount account, String changeId, EmailStrategy strategy) private void review(TestAccount account, String changeId, EmailStrategy strategy)
throws Exception { throws Exception {
review(account, changeId, strategy, null); review(account, changeId, strategy, null);

View File

@@ -74,6 +74,18 @@ public class ReviewInput {
/** Reviewers that should be added to this change. */ /** Reviewers that should be added to this change. */
public List<AddReviewerInput> reviewers; public List<AddReviewerInput> reviewers;
/**
* If true mark the change as work in progress. It is an error for both {@link #workInProgress}
* and {@link #ready} to be true.
*/
public boolean workInProgress;
/**
* If true mark the change as ready for review. It is an error for both {@link #workInProgress}
* and {@link #ready} to be true.
*/
public boolean ready;
public enum DraftHandling { public enum DraftHandling {
/** Delete pending drafts on this revision only. */ /** Delete pending drafts on this revision only. */
DELETE, DELETE,
@@ -141,6 +153,18 @@ public class ReviewInput {
return this; return this;
} }
public ReviewInput setWorkInProgress(boolean workInProgress) {
this.workInProgress = workInProgress;
ready = !workInProgress;
return this;
}
public ReviewInput setReady(boolean ready) {
this.ready = ready;
workInProgress = !ready;
return this;
}
public static ReviewInput recommend() { public static ReviewInput recommend() {
return new ReviewInput().label("Code-Review", 1); return new ReviewInput().label("Code-Review", 1);
} }

View File

@@ -30,4 +30,7 @@ public class ReviewResult {
* additions were requested. * additions were requested.
*/ */
@Nullable public Map<String, AddReviewerResult> reviewers; @Nullable public Map<String, AddReviewerResult> reviewers;
/** Error message for non-200 responses. */
@Nullable public String error;
} }

View File

@@ -132,6 +132,12 @@ import org.eclipse.jgit.lib.Config;
@Singleton @Singleton
public class PostReview public class PostReview
extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> { extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> {
public static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
public static final String ERROR_ONLY_OWNER_CAN_MODIFY_WORK_IN_PROGRESS =
"only change owner can specify work_in_progress or ready";
public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
"work_in_progress and ready are mutually exclusive";
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson(); private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024; private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
@@ -150,6 +156,7 @@ public class PostReview
private final NotesMigration migration; private final NotesMigration migration;
private final NotifyUtil notifyUtil; private final NotifyUtil notifyUtil;
private final Config gerritConfig; private final Config gerritConfig;
private final WorkInProgressOp.Factory workInProgressOpFactory;
@Inject @Inject
PostReview( PostReview(
@@ -168,7 +175,8 @@ public class PostReview
PostReviewers postReviewers, PostReviewers postReviewers,
NotesMigration migration, NotesMigration migration,
NotifyUtil notifyUtil, NotifyUtil notifyUtil,
@GerritServerConfig Config gerritConfig) { @GerritServerConfig Config gerritConfig,
WorkInProgressOp.Factory workInProgressOpFactory) {
super(retryHelper); super(retryHelper);
this.db = db; this.db = db;
this.changes = changes; this.changes = changes;
@@ -185,6 +193,7 @@ public class PostReview
this.migration = migration; this.migration = migration;
this.notifyUtil = notifyUtil; this.notifyUtil = notifyUtil;
this.gerritConfig = gerritConfig; this.gerritConfig = gerritConfig;
this.workInProgressOpFactory = workInProgressOpFactory;
} }
@Override @Override
@@ -259,6 +268,7 @@ public class PostReview
ReviewResult output = new ReviewResult(); ReviewResult output = new ReviewResult();
output.reviewers = reviewerJsonResults; output.reviewers = reviewerJsonResults;
if (hasError || confirm) { if (hasError || confirm) {
output.error = ERROR_ADDING_REVIEWER;
return Response.withStatusCode(SC_BAD_REQUEST, output); return Response.withStatusCode(SC_BAD_REQUEST, output);
} }
output.labels = input.labels; output.labels = input.labels;
@@ -310,9 +320,30 @@ public class PostReview
bu.addOp(revision.getChange().getId(), selfAddition.op); bu.addOp(revision.getChange().getId(), selfAddition.op);
} }
// Add WorkInProgressOp if requested.
if (input.ready || input.workInProgress) {
if (input.ready && input.workInProgress) {
output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
return Response.withStatusCode(SC_BAD_REQUEST, output);
}
if (!revision.getChange().getOwner().equals(revision.getUser().getAccountId())) {
output.error = ERROR_ONLY_OWNER_CAN_MODIFY_WORK_IN_PROGRESS;
return Response.withStatusCode(SC_BAD_REQUEST, output);
}
// Suppress notifications in WorkInProgressOp, we'll take care of
// them in this endpoint.
WorkInProgressOp.Input wipIn = new WorkInProgressOp.Input();
wipIn.notify = NotifyHandling.NONE;
bu.addOp(
revision.getChange().getId(),
workInProgressOpFactory.create(input.workInProgress, wipIn));
}
// Add the review op.
bu.addOp( bu.addOp(
revision.getChange().getId(), revision.getChange().getId(),
new Op(revision.getPatchSet().getId(), input, accountsToNotify)); new Op(revision.getPatchSet().getId(), input, accountsToNotify));
bu.execute(); bu.execute();
for (PostReviewers.Addition reviewerResult : reviewerResults) { for (PostReviewers.Addition reviewerResult : reviewerResults) {
@@ -326,12 +357,20 @@ public class PostReview
} }
private NotifyHandling defaultNotify(Change c, ReviewInput in) { private NotifyHandling defaultNotify(Change c, ReviewInput in) {
if (ChangeMessagesUtil.isAutogenerated(in.tag)) { boolean workInProgress = c.isWorkInProgress();
// Autogenerated comments default to lower notify levels. if (in.workInProgress) {
return c.isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.OWNER_REVIEWERS; workInProgress = true;
}
if (in.ready) {
workInProgress = false;
} }
if (c.isWorkInProgress() && !c.hasReviewStarted()) { if (ChangeMessagesUtil.isAutogenerated(in.tag)) {
// Autogenerated comments default to lower notify levels.
return workInProgress ? NotifyHandling.OWNER : NotifyHandling.OWNER_REVIEWERS;
}
if (workInProgress && !c.hasReviewStarted()) {
// If review hasn't started we want to minimize recipients, no matter who // If review hasn't started we want to minimize recipients, no matter who
// the author is. // the author is.
return NotifyHandling.OWNER; return NotifyHandling.OWNER;

View File

@@ -14,9 +14,11 @@
package com.google.gerrit.server.change; package com.google.gerrit.server.change;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -35,7 +37,9 @@ import com.google.inject.assistedinject.Assisted;
/* Set work in progress or ready for review state on a change */ /* Set work in progress or ready for review state on a change */
public class WorkInProgressOp implements BatchUpdateOp { public class WorkInProgressOp implements BatchUpdateOp {
public static class Input { public static class Input {
String message; @Nullable String message;
@Nullable NotifyHandling notify;
public Input() {} public Input() {}
@@ -53,6 +57,7 @@ public class WorkInProgressOp implements BatchUpdateOp {
private final PatchSetUtil psUtil; private final PatchSetUtil psUtil;
private final boolean workInProgress; private final boolean workInProgress;
private final Input in; private final Input in;
private final NotifyHandling notify;
private Change change; private Change change;
private ChangeNotes notes; private ChangeNotes notes;
@@ -71,6 +76,9 @@ public class WorkInProgressOp implements BatchUpdateOp {
this.psUtil = psUtil; this.psUtil = psUtil;
this.workInProgress = workInProgress; this.workInProgress = workInProgress;
this.in = in; this.in = in;
notify =
MoreObjects.firstNonNull(
in.notify, workInProgress ? NotifyHandling.NONE : NotifyHandling.ALL);
} }
@Override @Override
@@ -113,12 +121,12 @@ public class WorkInProgressOp implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) { public void postUpdate(Context ctx) {
if (workInProgress) { if (workInProgress || notify.ordinal() < NotifyHandling.OWNER_REVIEWERS.ordinal()) {
return; return;
} }
email email
.create( .create(
NotifyHandling.ALL, notify,
ImmutableListMultimap.of(), ImmutableListMultimap.of(),
notes, notes,
ps, ps,