Merge "Add api portion of resolvable comments"

This commit is contained in:
Dave Borowitz
2017-01-05 21:19:01 +00:00
committed by Gerrit Code Review
11 changed files with 156 additions and 52 deletions

View File

@@ -5315,6 +5315,10 @@ Value of the `tag` field from link:#review-input[ReviewInput] set
while posting the review.
NOTE: To apply different tags on on different votes/comments multiple
invocations of the REST call are required.
|`unresolved` |optional|
Whether or not the comment must be addressed by the user. The state of
resolution of a comment thread is stored in the last comment in that thread
chronologically.
|===========================
[[comment-input]]
@@ -5357,6 +5361,10 @@ comment is deleted.
Value of the `tag` field. Only allowed on link:#create-draft[draft comment] +
inputs; for published comments, use the `tag` field in +
link#review-input[ReviewInput]
|`unresolved` |optional|
Whether or not the comment must be addressed by the user. This value will
default to false if the comment is an orphan, or the value of the `in_reply_to`
comment if it is supplied.
|===========================
[[comment-range]]

View File

@@ -135,7 +135,33 @@ public class CommentsIT extends AbstractDaemonTest {
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
CommentInput comment =
newComment(file, Side.REVISION, line, "comment 1", false);
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
revision(r).review(input);
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
assertThat(comment).isEqualTo(infoToInput(file).apply(actual));
assertThat(comment).isEqualTo(infoToInput(file).apply(
getPublishedComment(changeId, revId, actual.id)));
}
}
@Test
public void postCommentWithUnresolved() throws Exception {
for (Integer line : lines) {
String file = "file";
String contents = "contents " + line;
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
"first subject", file, contents);
PushOneCommit.Result r = push.to("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput comment =
newComment(file, Side.REVISION, line, "comment 1", true);
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
revision(r).review(input);
@@ -156,8 +182,9 @@ public class CommentsIT extends AbstractDaemonTest {
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1");
CommentInput c2 = newComment(file, Side.PARENT, line, "auto-merge of ps-1");
CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1", false);
CommentInput c2 =
newComment(file, Side.PARENT, line, "auto-merge of ps-1", false);
CommentInput c3 = newCommentOnParent(file, 1, line, "parent-1 of ps-1");
CommentInput c4 = newCommentOnParent(file, 2, line, "parent-2 of ps-1");
input.comments = new HashMap<>();
@@ -176,7 +203,7 @@ public class CommentsIT extends AbstractDaemonTest {
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1");
CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1", false);
CommentInput c2 = newCommentOnParent(file, 1, line, "parent-1 of ps-1");
CommentInput c3 = newCommentOnParent(file, 2, line, "parent-2 of ps-1");
input.comments = new HashMap<>();
@@ -193,8 +220,8 @@ public class CommentsIT extends AbstractDaemonTest {
public void postCommentOnCommitMessageOnAutoMerge() throws Exception {
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
ReviewInput input = new ReviewInput();
CommentInput c =
newComment(Patch.COMMIT_MSG, Side.PARENT, 0, "comment on auto-merge");
CommentInput c = newComment(Patch.COMMIT_MSG, Side.PARENT, 0,
"comment on auto-merge", false);
input.comments = new HashMap<>();
input.comments.put(Patch.COMMIT_MSG, ImmutableList.of(c));
exception.expect(BadRequestException.class);
@@ -216,7 +243,8 @@ public class CommentsIT extends AbstractDaemonTest {
List<CommentInput> expectedComments = new ArrayList<>();
for (Integer line : lines) {
ReviewInput input = new ReviewInput();
CommentInput comment = newComment(file, Side.REVISION, line, "comment " + line);
CommentInput comment =
newComment(file, Side.REVISION, line, "comment " + line, false);
expectedComments.add(comment);
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
@@ -326,7 +354,8 @@ public class CommentsIT extends AbstractDaemonTest {
Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
ReviewInput input = new ReviewInput();
CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
CommentInput comment =
newComment(file, Side.REVISION, line, "comment 1", false);
comment.updated = timestamp;
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
@@ -554,12 +583,12 @@ public class CommentsIT extends AbstractDaemonTest {
+ url + "#/c/" + c + "/1/a.txt\n"
+ "File a.txt:\n"
+ "\n"
+ url + "#/c/12/1/a.txt@a2\n"
+ url + "#/c/" + c + "/1/a.txt@a2\n"
+ "PS1, Line 2: \n"
+ "what happened to this?\n"
+ "\n"
+ "\n"
+ url + "#/c/12/1/a.txt@1\n"
+ url + "#/c/" + c + "/1/a.txt@1\n"
+ "PS1, Line 1: ew\n"
+ "nit: trailing whitespace\n"
+ "\n"
@@ -567,22 +596,22 @@ public class CommentsIT extends AbstractDaemonTest {
+ url + "#/c/" + c + "/2/a.txt\n"
+ "File a.txt:\n"
+ "\n"
+ url + "#/c/12/2/a.txt@a1\n"
+ url + "#/c/" + c + "/2/a.txt@a1\n"
+ "PS2, Line 1: \n"
+ "comment 1 on base\n"
+ "\n"
+ "\n"
+ url + "#/c/12/2/a.txt@a2\n"
+ url + "#/c/" + c + "/2/a.txt@a2\n"
+ "PS2, Line 2: \n"
+ "comment 2 on base\n"
+ "\n"
+ "\n"
+ url + "#/c/12/2/a.txt@1\n"
+ url + "#/c/" + c + "/2/a.txt@1\n"
+ "PS2, Line 1: ew\n"
+ "join lines\n"
+ "\n"
+ "\n"
+ url + "#/c/12/2/a.txt@2\n"
+ url + "#/c/" + c + "/2/a.txt@2\n"
+ "PS2, Line 2: nten\n"
+ "typo: content\n"
+ "\n"
@@ -688,36 +717,39 @@ public class CommentsIT extends AbstractDaemonTest {
}
private static CommentInput newComment(String path, Side side, int line,
String message) {
String message, Boolean unresolved) {
CommentInput c = new CommentInput();
return populate(c, path, side, null, line, message);
return populate(c, path, side, null, line, message, unresolved);
}
private static CommentInput newCommentOnParent(String path, int parent,
int line, String message) {
CommentInput c = new CommentInput();
return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message);
return populate(c, path, Side.PARENT, Integer.valueOf(parent), line,
message, false);
}
private DraftInput newDraft(String path, Side side, int line,
String message) {
DraftInput d = new DraftInput();
return populate(d, path, side, null, line, message);
return populate(d, path, side, null, line, message, false);
}
private DraftInput newDraftOnParent(String path, int parent, int line,
String message) {
DraftInput d = new DraftInput();
return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message);
return populate(d, path, Side.PARENT, Integer.valueOf(parent), line,
message, false);
}
private static <C extends Comment> C populate(C c, String path, Side side,
Integer parent, int line, String message) {
Integer parent, int line, String message, Boolean unresolved) {
c.path = path;
c.side = side;
c.parent = parent;
c.line = line != 0 ? line : null;
c.message = message;
c.unresolved = unresolved;
if (line != 0) {
Comment.Range range = new Comment.Range();
range.startLine = line;
@@ -753,5 +785,6 @@ public class CommentsIT extends AbstractDaemonTest {
to.line = from.line;
to.message = from.message;
to.range = from.range;
to.unresolved = from.unresolved;
}
}

View File

@@ -38,6 +38,7 @@ import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
@@ -227,7 +228,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
public void draftComment() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment");
putDraft(user, id, 1, "comment", null);
checker.rebuildAndCheckChanges(id);
}
@@ -235,7 +236,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
public void draftAndPublishedComment() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "draft comment");
putDraft(user, id, 1, "draft comment", null);
putComment(user, id, 1, "published comment");
checker.rebuildAndCheckChanges(id);
}
@@ -244,7 +245,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
public void publishDraftComment() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "draft comment");
putDraft(user, id, 1, "draft comment", null);
publishDrafts(user, id);
checker.rebuildAndCheckChanges(id);
}
@@ -371,14 +372,14 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
changeMetaId.name());
putDraft(user, id, 1, "comment by user");
putDraft(user, id, 1, "comment by user", null);
ObjectId userDraftsId = getMetaRef(
allUsers, refsDraftComments(id, user.getId()));
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
changeMetaId.name()
+ "," + user.getId() + "=" + userDraftsId.name());
putDraft(admin, id, 2, "comment by admin");
putDraft(admin, id, 2, "comment by admin", null);
ObjectId adminDraftsId = getMetaRef(
allUsers, refsDraftComments(id, admin.getId()));
assertThat(admin.getId().get()).isLessThan(user.getId().get());
@@ -387,7 +388,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
+ "," + admin.getId() + "=" + adminDraftsId.name()
+ "," + user.getId() + "=" + userDraftsId.name());
putDraft(admin, id, 2, "revised comment by admin");
putDraft(admin, id, 2, "revised comment by admin", null);
adminDraftsId = getMetaRef(
allUsers, refsDraftComments(id, admin.getId()));
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
@@ -565,7 +566,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment by user");
putDraft(user, id, 1, "comment by user", null);
assertChangeUpToDate(true, id);
ObjectId oldMetaId =
@@ -573,7 +574,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
// Add a draft behind NoteDb's back.
setNotesMigration(false, false);
putDraft(user, id, 1, "second comment by user");
putDraft(user, id, 1, "second comment by user", null);
setInvalidNoteDbState(id);
assertDraftsUpToDate(false, id, user);
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId())))
@@ -608,7 +609,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment by user");
putDraft(user, id, 1, "comment by user", null);
assertChangeUpToDate(true, id);
ObjectId oldMetaId =
@@ -616,7 +617,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
// Add a draft behind NoteDb's back.
setNotesMigration(false, false);
putDraft(user, id, 1, "second comment by user");
putDraft(user, id, 1, "second comment by user", null);
ReviewDb db = getUnwrappedDb();
Change c = db.changes().get(id);
@@ -668,12 +669,12 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment");
putDraft(user, id, 1, "comment", null);
assertDraftsUpToDate(true, id, user);
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
setNotesMigration(false, false);
putDraft(user, id, 1, "comment");
putDraft(user, id, 1, "comment", null);
setInvalidNoteDbState(id);
assertDraftsUpToDate(false, id, user);
@@ -900,7 +901,7 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
public void rebuildDeletesOldDraftRefs() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment");
putDraft(user, id, 1, "comment", null);
Account.Id otherAccountId = new Account.Id(user.getId().get() + 1234);
String otherDraftRef = refsDraftComments(id, otherAccountId);
@@ -1162,6 +1163,23 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1);
}
@Test
public void resolveCommentsInheritsValueFromParentWhenUnspecified()
throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "comment", true);
putDraft(user, id, 1, "newComment", null);
Map<String, List<CommentInfo>> comments =
gApi.changes().id(id.get()).current().drafts();
for (List<CommentInfo> cList: comments.values()) {
for (CommentInfo ci: cList) {
assertThat(ci.unresolved).isEqualTo(true);
}
}
}
private void assertChangesReadOnly(RestApiException e) throws Exception {
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(UpdateException.class);
@@ -1213,12 +1231,13 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
}
}
private void putDraft(TestAccount account, Change.Id id, int line, String msg)
throws Exception {
private void putDraft(TestAccount account, Change.Id id, int line, String msg,
Boolean unresolved) throws Exception {
DraftInput in = new DraftInput();
in.line = line;
in.message = msg;
in.path = PushOneCommit.FILE_NAME;
in.unresolved = unresolved;
AcceptanceTestRequestScope.Context old = setApiUser(account);
try {
gApi.changes().id(id.get()).current().createDraft(in);

View File

@@ -34,7 +34,7 @@ public abstract class Comment {
public String inReplyTo;
public Timestamp updated;
public String message;
public boolean unresolved;
public Boolean unresolved;
public static class Range {
public int startLine;

View File

@@ -124,6 +124,7 @@ public final class PatchLineComment {
plc.setRevId(new RevId(c.revId));
plc.setStatus(status);
plc.setRealAuthor(c.getRealAuthor().getId());
plc.setUnresolved(c.unresolved);
return plc;
}
@@ -323,6 +324,14 @@ public final class PatchLineComment {
return tag;
}
public void setUnresolved(Boolean unresolved) {
this.unresolved = unresolved;
}
public Boolean getUnresolved() {
return unresolved;
}
public Comment asComment(String serverId) {
Comment c = new Comment(key.asCommentKey(), author, writtenOn, side,
message, serverId, unresolved);
@@ -349,7 +358,8 @@ public final class PatchLineComment {
&& Objects.equals(parentUuid, c.getParentUuid())
&& Objects.equals(range, c.getRange())
&& Objects.equals(revId, c.getRevId())
&& Objects.equals(tag, c.getTag());
&& Objects.equals(tag, c.getTag())
&& Objects.equals(unresolved, c.getUnresolved());
}
return false;
}

View File

@@ -27,6 +27,7 @@ import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Comment;
@@ -141,12 +142,33 @@ public class CommentsUtil {
this.serverId = serverId;
}
public Comment newComment(ChangeContext ctx, String path, PatchSet.Id psId,
short side, String message) throws OrmException {
public Comment newComment(ChangeContext ctx,
String path,
PatchSet.Id psId,
short side,
String message,
@Nullable Boolean unresolved,
@Nullable String parentUuid)
throws OrmException, UnprocessableEntityException {
if (unresolved == null) {
if (parentUuid == null) {
// Default to false if comment is not descended from another.
unresolved = false;
} else {
// Inherit unresolved value from inReplyTo comment if not specified.
Comment.Key key = new Comment.Key(parentUuid, path, psId.patchSetId);
Optional<Comment> parent = get(ctx.getDb(), ctx.getNotes(), key);
if (!parent.isPresent()) {
throw new UnprocessableEntityException(
"Invalid parentUuid supplied for comment");
}
unresolved = parent.get().unresolved;
}
}
Comment c = new Comment(
new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path, psId.get()),
ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId,
false);
unresolved);
ctx.getUser().updateRealAccountId(c::setRealAuthor);
return c;
}

View File

@@ -25,6 +25,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
@@ -103,16 +104,20 @@ public class CreateDraftComment implements RestModifyView<RevisionResource, Draf
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceNotFoundException, OrmException {
throws ResourceNotFoundException, OrmException,
UnprocessableEntityException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (ps == null) {
throw new ResourceNotFoundException("patch set not found: " + psId);
}
String parentUuid = Url.decode(in.inReplyTo);
comment = commentsUtil.newComment(
ctx, in.path, ps.getId(), in.side(), in.message.trim());
comment.parentUuid = Url.decode(in.inReplyTo);
ctx, in.path, ps.getId(), in.side(), in.message.trim(),
in.unresolved, parentUuid);
comment.setLineNbrAndRange(in.line, in.range);
comment.tag = in.tag;
setCommentRevId(
comment, patchListCache, ctx.getChange(), ps);

View File

@@ -694,7 +694,8 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
@Override
public boolean updateChange(ChangeContext ctx)
throws OrmException, ResourceConflictException {
throws OrmException, ResourceConflictException,
UnprocessableEntityException {
user = ctx.getIdentifiedUser();
notes = ctx.getNotes();
ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
@@ -729,7 +730,8 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
approvals, oldApprovals, ctx.getWhen());
}
private boolean insertComments(ChangeContext ctx) throws OrmException {
private boolean insertComments(ChangeContext ctx)
throws OrmException, UnprocessableEntityException {
Map<String, List<CommentInput>> map = in.comments;
if (map == null) {
map = Collections.emptyMap();
@@ -757,16 +759,14 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
String parent = Url.decode(c.inReplyTo);
Comment e = drafts.remove(Url.decode(c.id));
if (e == null) {
e = commentsUtil.newComment(ctx, path, psId, c.side(), c.message);
e = commentsUtil.newComment(ctx, path, psId, c.side(), c.message,
c.unresolved, parent);
} else {
e.writtenOn = ctx.getWhen();
e.side = c.side();
e.message = c.message;
}
if (parent != null) {
e.parentUuid = parent;
}
setCommentRevId(e, patchListCache, ctx.getChange(), ps);
e.setLineNbrAndRange(c.line, c.range);
e.tag = in.tag;

View File

@@ -164,6 +164,9 @@ public class PutDraftComment implements RestModifyView<DraftCommentResource, Dra
// TODO(dborowitz): Can we support changing tags via PUT?
e.tag = in.tag;
}
if (in.unresolved != null) {
e.unresolved = in.unresolved;
}
return e;
}
}

View File

@@ -18,6 +18,7 @@ import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -179,7 +180,8 @@ public class MailProcessor {
}
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
public boolean updateChange(ChangeContext ctx) throws OrmException,
UnprocessableEntityException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (ps == null) {
throw new OrmException("patch set not found: " + psId);
@@ -224,12 +226,14 @@ public class MailProcessor {
}
Comment comment = commentsUtil.newComment(ctx, fileName,
psForComment.getId(), (short) side.ordinal(), c.message);
psForComment.getId(), (short) side.ordinal(), c.message,
false, null);
comment.tag = tag;
if (c.inReplyTo != null) {
comment.parentUuid = c.inReplyTo.key.uuid;
comment.lineNbr = c.inReplyTo.lineNbr;
comment.range = c.inReplyTo.range;
comment.unresolved = c.inReplyTo.unresolved;
}
CommentsUtil.setCommentRevId(comment, patchListCache,
ctx.getChange(), psForComment);

View File

@@ -434,7 +434,7 @@ public class CommentSender extends ReplyToChangeSender {
}
/**
* Retrieve the file lines refered to by a comment.
* Retrieve the file lines referred to by a comment.
* @param comment The comment that refers to some file contents. The comment
* may be a line comment or a ranged comment.
* @param fileData The file on which the comment appears.