blob: 2123ac2ab5350b0b33b7929214417ee582d6bdc1 [file] [log] [blame]
// Copyright (C) 2014 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.acceptance.server.change;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.MapSubject.assertThatMap;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.TestHumanComment;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
import com.google.gerrit.extensions.api.changes.DraftInput;
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.api.changes.ReviewerInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.DeleteCommentRewriter;
import com.google.gerrit.server.restapi.change.ChangesCollection;
import com.google.gerrit.server.restapi.change.PostReview;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class CommentsIT extends AbstractDaemonTest {
@Inject private ChangeNoteUtil noteUtil;
@Inject private FakeEmailSender email;
@Inject private ProjectOperations projectOperations;
@Inject private Provider<ChangesCollection> changes;
@Inject private Provider<PostReview> postReview;
@Inject private ChangeOperations changeOperations;
@Inject private AccountOperations accountOperations;
@Inject private RequestScopeOperations requestScopeOperations;
private final Integer[] lines = {0, 1};
@Before
public void setUp() {
requestScopeOperations.setApiUser(user.id());
}
@Test
public void getNonExistingComment() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
assertThrows(
ResourceNotFoundException.class,
() -> getPublishedComment(changeId, revId, "non-existing"));
}
@Test
public void createDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String path = "file1";
DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, line, "comment 1");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
assertThat(result).hasSize(1);
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
List<CommentInfo> list = getDraftCommentsAsList(changeId);
assertThat(list).hasSize(1);
actual = list.get(0);
assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
}
}
@Test
public void createDraftOnMergeCommitChange() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String path = "file1";
DraftInput c1 = CommentsUtil.newDraft(path, Side.REVISION, line, "ps-1");
DraftInput c2 = CommentsUtil.newDraft(path, Side.PARENT, line, "auto-merge of ps-1");
DraftInput c3 = CommentsUtil.newDraftOnParent(path, 1, line, "parent-1 of ps-1");
DraftInput c4 = CommentsUtil.newDraftOnParent(path, 2, line, "parent-2 of ps-1");
addDraft(changeId, revId, c1);
addDraft(changeId, revId, c2);
addDraft(changeId, revId, c3);
addDraft(changeId, revId, c4);
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
assertThat(result).hasSize(1);
assertThat(result.get(path).stream().map(infoToDraft(path))).containsExactly(c1, c2, c3, c4);
List<CommentInfo> list = getDraftCommentsAsList(changeId);
assertThat(list).hasSize(4);
assertThat(list.stream().map(infoToDraft(path))).containsExactly(c1, c2, c3, c4);
}
}
@Test
public void postComment() throws Exception {
for (Integer line : lines) {
String file = "file";
String contents = "contents " + line;
PushOneCommit push =
pushFactory.create(admin.newIdent(), 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 = CommentsUtil.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 commentWithRangeAndLine_lineIsIgnored() throws Exception {
String file = "file";
String contents = "contents";
PushOneCommit push =
pushFactory.create(admin.newIdent(), 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 = CommentsUtil.newComment(file, Side.REVISION, 1, "comment 1", false);
int rangeEndLine = 3;
comment.range = createRange(1, 1, rangeEndLine, 3);
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(actual.line).isEqualTo(rangeEndLine);
input = new ReviewInput();
comment = CommentsUtil.newComment(file, Side.REVISION, 1, "comment 1 reply", false);
comment.range = createRange(1, 1, rangeEndLine, 3);
// Post another comment in reply, and the line is still fixed to the range.endLine
comment.inReplyTo = actual.id;
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
revision(r).review(input);
result = getPublishedComments(changeId, revId);
assertThat(result.get(comment.path)).hasSize(2);
assertThat(result.get(comment.path).stream().allMatch(c -> c.line == rangeEndLine)).isTrue();
}
@Test
public void patchsetLevelCommentCanBeAddedAndRetrieved() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment =
CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
CommentsUtil.addComments(gApi, changeId, ps1, comment);
Map<String, List<CommentInfo>> results = getPublishedComments(changeId, ps1);
assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
}
@Test
public void deletePatchsetLevelComment() throws Exception {
requestScopeOperations.setApiUser(admin.id());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String commentMessage = "to be deleted";
CommentInput comment =
CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, commentMessage);
CommentsUtil.addComments(gApi, changeId, revId, comment);
Map<String, List<CommentInfo>> results = getPublishedComments(changeId, revId);
CommentInfo oldComment = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL));
DeleteCommentInput input = new DeleteCommentInput("reason");
gApi.changes().id(changeId).revision(revId).comment(oldComment.id).delete(input);
CommentInfo updatedComment =
Iterables.getOnlyElement(getPublishedComments(changeId, revId).get(PATCHSET_LEVEL));
assertThat(updatedComment.message).doesNotContain(commentMessage);
}
@Test
public void deletedCommentsAreResolved() throws Exception {
requestScopeOperations.setApiUser(admin.id());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String commentMessage = "to be deleted";
CommentInput comment =
CommentsUtil.newComment(
COMMIT_MSG, Side.REVISION, /*line= */ 0, commentMessage, /*unresolved= */ true);
CommentsUtil.addComments(gApi, changeId, revId, comment);
Map<String, List<CommentInfo>> results = getPublishedComments(changeId, revId);
CommentInfo oldComment = Iterables.getOnlyElement(results.get(COMMIT_MSG));
DeleteCommentInput input = new DeleteCommentInput("reason");
gApi.changes().id(changeId).revision(revId).comment(oldComment.id).delete(input);
CommentInfo updatedComment =
Iterables.getOnlyElement(getPublishedComments(changeId, revId).get(COMMIT_MSG));
assertThat(updatedComment.message).doesNotContain(commentMessage);
assertThat(updatedComment.unresolved).isFalse();
}
@Test
public void patchsetLevelCommentEmailNotification() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment =
CommentsUtil.newCommentWithOnlyMandatoryFields(
PATCHSET_LEVEL, "The change looks good, LGTM");
CommentsUtil.addComments(gApi, changeId, ps1, comment);
String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
assertThat(emailBody).contains("Patchset");
assertThat(emailBody).doesNotContain("/PATCHSET_LEVEL");
assertThat(emailBody).contains("The change looks good, LGTM");
}
@Test
public void patchsetLevelCommentCantHaveLine() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput input = CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
input.line = 1;
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> CommentsUtil.addComments(gApi, changeId, ps1, input));
assertThat(ex.getMessage()).contains("line");
}
@Test
public void patchsetLevelCommentCantHaveRange() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput input = CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
input.range = createLineRange(1, 3);
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> CommentsUtil.addComments(gApi, changeId, ps1, input));
assertThat(ex.getMessage()).contains("range");
}
@Test
public void patchsetLevelCommentCantHaveSide() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput input = CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
input.side = Side.REVISION;
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> CommentsUtil.addComments(gApi, changeId, ps1, input));
assertThat(ex.getMessage()).contains("side");
}
@Test
public void patchsetLevelDraftCommentCanBeAddedAndRetrieved() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
}
@Test
public void deletePatchsetLevelDraft() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput draft = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment 1");
CommentInfo returned = addDraft(changeId, revId, draft);
deleteDraft(changeId, revId, returned.id);
Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
assertThat(drafts).isEmpty();
}
@Test
public void patchsetLevelDraftCommentCantHaveLine() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
comment.line = 1;
BadRequestException ex =
assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
assertThat(ex.getMessage()).contains("line");
}
@Test
public void patchsetLevelDraftCommentCantHaveRange() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
comment.range = createLineRange(1, 3);
BadRequestException ex =
assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
assertThat(ex.getMessage()).contains("range");
}
@Test
public void patchsetLevelDraftCommentCantHaveSide() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
comment.side = Side.REVISION;
BadRequestException ex =
assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
assertThat(ex.getMessage()).contains("range");
}
@Test
public void patchsetLevelDraftCommentCantBeUpdatedToHaveLine() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
DraftInput update = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
update.line = 1;
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
assertThat(ex.getMessage()).contains("line");
}
@Test
public void patchsetLevelDraftCommentCantBeUpdatedToHaveRange() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
DraftInput update = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
update.range = createLineRange(1, 3);
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
assertThat(ex.getMessage()).contains("range");
}
@Test
public void patchsetLevelDraftCommentCantBeUpdatedToHaveSide() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
DraftInput update = CommentsUtil.newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
update.side = Side.REVISION;
BadRequestException ex =
assertThrows(
BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
assertThat(ex.getMessage()).contains("side");
}
@Test
public void postCommentWithReply() throws Exception {
for (Integer line : lines) {
String file = "file";
String contents = "contents " + line;
PushOneCommit push =
pushFactory.create(admin.newIdent(), 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 = CommentsUtil.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);
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
input = new ReviewInput();
comment = CommentsUtil.newComment(file, Side.REVISION, line, "comment 1 reply", false);
comment.inReplyTo = actual.id;
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
revision(r).review(input);
result = getPublishedComments(changeId, revId);
actual = result.get(comment.path).get(1);
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(admin.newIdent(), 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 = CommentsUtil.newComment(file, Side.REVISION, line, "comment 1", true);
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 postCommentOnMergeCommitChange() throws Exception {
for (Integer line : lines) {
String file = "foo";
PushOneCommit.Result r = createMergeCommitChange("refs/for/master", file);
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput c1 = CommentsUtil.newComment(file, Side.REVISION, line, "ps-1", false);
CommentInput c2 =
CommentsUtil.newComment(file, Side.PARENT, line, "auto-merge of ps-1", false);
CommentInput c3 = CommentsUtil.newCommentOnParent(file, 1, line, "parent-1 of ps-1");
CommentInput c4 = CommentsUtil.newCommentOnParent(file, 2, line, "parent-2 of ps-1");
input.comments = new HashMap<>();
input.comments.put(file, ImmutableList.of(c1, c2, c3, c4));
revision(r).review(input);
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
assertThat(result.get(file).stream().map(infoToInput(file))).containsExactly(c1, c2, c3, c4);
List<CommentInfo> list = getPublishedCommentsAsList(changeId);
assertThat(list.stream().map(infoToInput(file))).containsExactly(c1, c2, c3, c4);
}
// for the commit message comments on the auto-merge are not possible
for (Integer line : lines) {
String file = Patch.COMMIT_MSG;
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
CommentInput c1 = CommentsUtil.newComment(file, Side.REVISION, line, "ps-1", false);
CommentInput c2 = CommentsUtil.newCommentOnParent(file, 1, line, "parent-1 of ps-1");
CommentInput c3 = CommentsUtil.newCommentOnParent(file, 2, line, "parent-2 of ps-1");
input.comments = new HashMap<>();
input.comments.put(file, ImmutableList.of(c1, c2, c3));
revision(r).review(input);
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
assertThat(result.get(file).stream().map(infoToInput(file))).containsExactly(c1, c2, c3);
List<CommentInfo> list = getPublishedCommentsAsList(changeId);
assertThat(list.stream().map(infoToInput(file))).containsExactly(c1, c2, c3);
}
}
@Test
public void postCommentOnCommitMessageOnAutoMerge() throws Exception {
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
ReviewInput input = new ReviewInput();
CommentInput c =
CommentsUtil.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));
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> revision(r).review(input));
assertThat(thrown)
.hasMessageThat()
.contains("cannot comment on " + Patch.COMMIT_MSG + " on auto-merge");
}
@Test
public void postCommentsReplacingDrafts() throws Exception {
String file = "file";
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "contents");
PushOneCommit.Result r = push.to("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput draft = CommentsUtil.newDraft(file, Side.REVISION, 0, "comment");
addDraft(changeId, revId, draft);
Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
CommentInfo draftInfo = Iterables.getOnlyElement(drafts.get(draft.path));
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.KEEP;
reviewInput.message = "foo";
CommentInput comment = CommentsUtil.newComment(file, Side.REVISION, 0, "comment", false);
// Replace the existing draft.
comment.id = draftInfo.id;
reviewInput.comments = new HashMap<>();
reviewInput.comments.put(comment.path, ImmutableList.of(comment));
revision(r).review(reviewInput);
// DraftHandling.KEEP is ignored on publishing a comment.
drafts = getDraftComments(changeId, revId);
assertThat(drafts).isEmpty();
}
@Test
public void postCommentsUnreachableData() throws Exception {
String file = "file";
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "l1\nl2\n");
String dest = "refs/for/master";
PushOneCommit.Result r1 = push.to(dest);
r1.assertOkStatus();
String changeId = r1.getChangeId();
String revId = r1.getCommit().getName();
PushOneCommit.Result r2 = amendChange(r1.getChangeId());
r2.assertOkStatus();
String draftRefName = RefNames.refsDraftComments(r1.getChange().getId(), admin.id());
DraftInput draft = CommentsUtil.newDraft(file, Side.REVISION, 1, "comment");
addDraft(changeId, "1", draft);
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.PUBLISH;
reviewInput.message = "foo";
gApi.changes().id(r1.getChangeId()).revision(1).review(reviewInput);
addDraft(changeId, "2", CommentsUtil.newDraft(file, Side.REVISION, 2, "comment2"));
reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
reviewInput.message = "bar";
gApi.changes().id(r1.getChangeId()).revision(2).review(reviewInput);
Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
assertThat(drafts.isEmpty()).isTrue();
try (Repository repo = repoManager.openRepository(allUsers)) {
Ref ref = repo.exactRef(draftRefName);
assertThat(ref).isNull();
}
}
@Test
public void listComments() throws Exception {
String file = "file";
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "contents");
PushOneCommit.Result r = push.to("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
assertThat(getPublishedComments(changeId, revId)).isEmpty();
assertThat(getPublishedCommentsAsList(changeId)).isEmpty();
List<CommentInput> expectedComments = new ArrayList<>();
for (Integer line : lines) {
ReviewInput input = new ReviewInput();
CommentInput comment =
CommentsUtil.newComment(file, Side.REVISION, line, "comment " + line, false);
expectedComments.add(comment);
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();
List<CommentInfo> actualComments = result.get(file);
assertThat(actualComments.stream().map(infoToInput(file)))
.containsExactlyElementsIn(expectedComments);
List<CommentInfo> list = getPublishedCommentsAsList(changeId);
assertThat(list.stream().map(infoToInput(file))).containsExactlyElementsIn(expectedComments);
}
/**
* This test makes sure that the commits in the refs/draft-comments ref in NoteDb have no parent
* commits. This is important so that each new draft update (add, modify, delete) does not keep
* track of previous history.
*/
@Test
public void commitsInDraftCommentsRefHaveNoParent() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String draftRefName = RefNames.refsDraftComments(r.getChange().getId(), user.id());
DraftInput comment1 = CommentsUtil.newDraft("file_1", Side.REVISION, 1, "comment 1");
CommentInfo commentInfo1 = addDraft(changeId, revId, comment1);
assertThat(getHeadOfDraftCommentsRef(draftRefName).getParentCount()).isEqualTo(0);
DraftInput comment2 = CommentsUtil.newDraft("file_2", Side.REVISION, 2, "comment 2");
CommentInfo commentInfo2 = addDraft(changeId, revId, comment2);
assertThat(getHeadOfDraftCommentsRef(draftRefName).getParentCount()).isEqualTo(0);
deleteDraft(changeId, revId, commentInfo1.id);
assertThat(getHeadOfDraftCommentsRef(draftRefName).getParentCount()).isEqualTo(0);
assertThat(
getDraftComments(changeId, revId).values().stream()
.flatMap(List::stream)
.map(commentInfo -> commentInfo.message))
.containsExactly("comment 2");
deleteDraft(changeId, revId, commentInfo2.id);
assertThat(projectOperations.project(allUsers).hasHead(draftRefName)).isFalse();
assertThat(getDraftComments(changeId, revId).values().stream().flatMap(List::stream)).isEmpty();
}
@Test
public void putDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String path = "file1";
DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, line, "comment 1");
addDraft(changeId, revId, comment);
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
String uuid = actual.id;
comment.message = "updated comment 1";
updateDraft(changeId, revId, comment, uuid);
result = getDraftComments(changeId, revId);
actual = Iterables.getOnlyElement(result.get(comment.path));
assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
// Posting a draft comment doesn't cause lastUpdatedOn to change.
assertThat(r.getChange().change().getLastUpdatedOn()).isEqualTo(origLastUpdated);
}
}
@Test
public void putDraft_humanInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentCommentUuid =
changeOperations.change(changeId).currentPatchset().newComment().create();
DraftInput draft = CommentsUtil.newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
draft.inReplyTo = parentCommentUuid;
String createdDraftUuid = addDraft(changeId, draft).id;
TestHumanComment actual =
changeOperations.change(changeId).draftComment(createdDraftUuid).get();
assertThat(actual.parentUuid()).hasValue(parentCommentUuid);
}
@Test
public void putDraft_robotInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentRobotCommentUuid =
changeOperations.change(changeId).currentPatchset().newRobotComment().create();
DraftInput draft = CommentsUtil.newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
draft.inReplyTo = parentRobotCommentUuid;
String createdDraftUuid = addDraft(changeId, draft).id;
TestHumanComment actual =
changeOperations.change(changeId).draftComment(createdDraftUuid).get();
assertThat(actual.parentUuid()).hasValue(parentRobotCommentUuid);
}
@Test
public void putDraft_idMismatch() throws Exception {
String file = "file";
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraft(file, Side.REVISION, 0, "foo");
CommentInfo commentInfo = addDraft(changeId, revId, comment);
DraftInput draftInput = CommentsUtil.newDraft(file, Side.REVISION, 0, "bar");
draftInput.id = "anything_but_" + commentInfo.id;
BadRequestException e =
assertThrows(
BadRequestException.class,
() -> updateDraft(changeId, revId, draftInput, commentInfo.id));
assertThat(e).hasMessageThat().contains("id must match URL");
}
@Test
public void putDraft_negativeLine() throws Exception {
String file = "file";
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraft(file, Side.REVISION, -666, "foo");
BadRequestException e =
assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
assertThat(e).hasMessageThat().contains("line must be >= 0");
}
@Test
public void putDraft_invalidRange() throws Exception {
String file = "file";
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput draftInput =
CommentsUtil.newDraft(file, Side.REVISION, createLineRange(2, 3), "bar");
draftInput.line = 666;
BadRequestException e =
assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, draftInput));
assertThat(e)
.hasMessageThat()
.contains("range endLine must be on the same line as the comment");
}
@Test
public void putDraft_invalidInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
DraftInput draft = CommentsUtil.newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
draft.inReplyTo = "invalid";
BadRequestException exception =
assertThrows(BadRequestException.class, () -> addDraft(changeId, draft));
assertThat(exception.getMessage()).contains(String.format("%s not found", draft.inReplyTo));
}
@Test
public void putDraft_updatePath() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraft("file_foo", Side.REVISION, 0, "foo");
CommentInfo commentInfo = addDraft(changeId, revId, comment);
assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_foo");
DraftInput draftInput = CommentsUtil.newDraft("file_bar", Side.REVISION, 0, "bar");
updateDraft(changeId, revId, draftInput, commentInfo.id);
assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_bar");
}
@Test
public void putDraft_updateInvalidInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
DraftInput updatedDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
updatedDraftInput.inReplyTo = "invalid";
BadRequestException exception =
assertThrows(
BadRequestException.class,
() -> updateDraft(changeId, updatedDraftInput, originalDraft.id));
assertThat(exception.getMessage()).contains(String.format("Invalid inReplyTo"));
}
@Test
public void putDraft_updateHumanInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentCommentUuid =
changeOperations.change(changeId).currentPatchset().newComment().create();
DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
DraftInput updateDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
updateDraftInput.inReplyTo = parentCommentUuid;
updateDraft(changeId, updateDraftInput, originalDraft.id);
assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().parentUuid())
.hasValue(parentCommentUuid);
}
@Test
public void putDraft_updateRobotInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentRobotCommentUuid =
changeOperations.change(changeId).currentPatchset().newRobotComment().create();
DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
DraftInput updateDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
updateDraftInput.inReplyTo = parentRobotCommentUuid;
updateDraft(changeId, updateDraftInput, originalDraft.id);
assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().parentUuid())
.hasValue(parentRobotCommentUuid);
}
@Test
public void putDraft_updateTag() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
DraftInput updateDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
String tag = "täg";
updateDraftInput.tag = tag;
updateDraft(changeId, updateDraftInput, originalDraft.id);
assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().tag())
.hasValue(tag);
}
@Test
public void updatedDraftStillPointsToParentComment() throws Exception {
Account.Id accountId = accountOperations.newAccount().create();
Change.Id changeId = changeOperations.newChange().create();
PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
String parentCommentUuid =
changeOperations.change(changeId).patchset(patchsetId).newComment().create();
String draftCommentUuid =
changeOperations
.change(changeId)
.patchset(patchsetId)
.newDraftComment()
.parentUuid(parentCommentUuid)
.author(accountId)
.create();
// Each user can only see their own drafts.
requestScopeOperations.setApiUser(accountId);
DraftInput draftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
draftInput.message = "Another comment text.";
gApi.changes()
.id(changeId.get())
.revision(patchsetId.get())
.draft(draftCommentUuid)
.update(draftInput);
TestHumanComment comment =
changeOperations.change(changeId).draftComment(draftCommentUuid).get();
assertThat(comment.parentUuid()).hasValue(parentCommentUuid);
}
@Test
public void listDrafts() throws Exception {
String file = "file";
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
assertThat(getDraftComments(changeId, revId)).isEmpty();
List<DraftInput> expectedDrafts = new ArrayList<>();
for (Integer line : lines) {
DraftInput comment = CommentsUtil.newDraft(file, Side.REVISION, line, "comment " + line);
expectedDrafts.add(comment);
addDraft(changeId, revId, comment);
}
Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
assertThat(result).isNotEmpty();
List<CommentInfo> actualComments = result.get(file);
assertThat(actualComments.stream().map(infoToDraft(file)))
.containsExactlyElementsIn(expectedDrafts);
}
@Test
public void getDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String path = "file1";
DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, line, "comment 1");
CommentInfo returned = addDraft(changeId, revId, comment);
CommentInfo actual = getDraftComment(changeId, revId, returned.id);
assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
}
}
@Test
public void deleteDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput draft = CommentsUtil.newDraft("file1", Side.REVISION, line, "comment 1");
CommentInfo returned = addDraft(changeId, revId, draft);
deleteDraft(changeId, revId, returned.id);
Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
assertThat(drafts).isEmpty();
// Deleting a draft comment doesn't cause lastUpdatedOn to change.
assertThat(r.getChange().change().getLastUpdatedOn()).isEqualTo(origLastUpdated);
}
}
@Test
public void insertCommentsWithHistoricTimestamp() throws Exception {
Instant timestamp = Instant.EPOCH;
for (Integer line : lines) {
String file = "file";
String contents = "contents " + line;
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
PushOneCommit.Result r = push.to("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
ReviewInput input = new ReviewInput();
CommentInput comment = CommentsUtil.newComment(file, Side.REVISION, line, "comment 1", false);
comment.setUpdated(timestamp);
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
ChangeResource changeRsrc =
changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId));
RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId));
postReview.get().apply(revRsrc, input, timestamp);
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
CommentInput ci = infoToInput(file).apply(actual);
ci.updated = comment.updated;
assertThat(comment).isEqualTo(ci);
assertThat(actual.updated).isEqualTo(gApi.changes().id(r.getChangeId()).info().created);
// Updating historic comments doesn't cause lastUpdatedOn to regress.
assertThat(r.getChange().change().getLastUpdatedOn()).isEqualTo(origLastUpdated);
}
}
@Test
public void addDuplicateComments() throws Exception {
PushOneCommit.Result r1 = createChange();
String changeId = r1.getChangeId();
String revId = r1.getCommit().getName();
CommentsUtil.addComment(gApi, r1, "nit: trailing whitespace");
CommentsUtil.addComment(gApi, r1, "nit: trailing whitespace");
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(2);
CommentsUtil.addComment(gApi, r1, "nit: trailing whitespace", true, false, null);
result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(2);
PushOneCommit.Result r2 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "content")
.to("refs/for/master");
changeId = r2.getChangeId();
revId = r2.getCommit().getName();
CommentsUtil.addComment(gApi, r2, "nit: trailing whitespace", true, false, null);
result = getPublishedComments(changeId, revId);
assertThat(result.get(FILE_NAME)).hasSize(1);
}
@Test
public void listChangeDrafts() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
.to("refs/for/master");
requestScopeOperations.setApiUser(admin.id());
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
requestScopeOperations.setApiUser(user.id());
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
requestScopeOperations.setApiUser(admin.id());
Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts();
assertThat(actual.keySet()).containsExactly(FILE_NAME);
List<CommentInfo> comments = actual.get(FILE_NAME);
assertThat(comments).hasSize(2);
CommentInfo c1 = comments.get(0);
assertThat(c1.author).isNull();
assertThat(c1.patchSet).isEqualTo(1);
assertThat(c1.message).isEqualTo("nit: trailing whitespace");
assertThat(c1.side).isNull();
assertThat(c1.line).isEqualTo(1);
CommentInfo c2 = comments.get(1);
assertThat(c2.author).isNull();
assertThat(c2.patchSet).isEqualTo(2);
assertThat(c2.message).isEqualTo("typo: content");
assertThat(c2.side).isNull();
assertThat(c2.line).isEqualTo(1);
}
@Test
public void listChangeDraftsAnonymousThrowsAuthException() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
requestScopeOperations.setApiUserAnonymous();
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).draftsAsList());
}
@Test
public void listChangeComments() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
.to("refs/for/master");
CommentsUtil.addComment(gApi, r1, "nit: trailing whitespace");
CommentsUtil.addComment(gApi, r2, "typo: content");
Map<String, List<CommentInfo>> actual =
gApi.changes().id(r2.getChangeId()).commentsRequest().get();
assertThat(actual.keySet()).containsExactly(FILE_NAME);
List<CommentInfo> comments = actual.get(FILE_NAME);
assertThat(comments).hasSize(2);
// Comment context is disabled by default
assertThat(comments.stream().filter(c -> c.contextLines != null)).isEmpty();
CommentInfo c1 = comments.get(0);
assertThat(c1.author._accountId).isEqualTo(user.id().get());
assertThat(c1.patchSet).isEqualTo(1);
assertThat(c1.message).isEqualTo("nit: trailing whitespace");
assertThat(c1.side).isNull();
assertThat(c1.line).isEqualTo(1);
CommentInfo c2 = comments.get(1);
assertThat(c2.author._accountId).isEqualTo(user.id().get());
assertThat(c2.patchSet).isEqualTo(2);
assertThat(c2.message).isEqualTo("typo: content");
assertThat(c2.side).isNull();
assertThat(c2.line).isEqualTo(1);
}
@Test
public void listChangeCommentsAnonymousDoesNotRequireAuth() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId())
.to("refs/for/master");
CommentsUtil.addComment(gApi, r1, "nit: trailing whitespace");
CommentsUtil.addComment(gApi, r2, "typo: content");
List<CommentInfo> comments = gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList();
assertThat(comments.stream().map(c -> c.message).collect(toList()))
.containsExactly("nit: trailing whitespace", "typo: content");
requestScopeOperations.setApiUserAnonymous();
comments = gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList();
assertThat(comments.stream().map(c -> c.message).collect(toList()))
.containsExactly("nit: trailing whitespace", "typo: content");
}
@Test
public void listChangeWithDrafts() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput comment = CommentsUtil.newDraft("file1", Side.REVISION, line, "comment 1");
addDraft(changeId, revId, comment);
assertThat(gApi.changes().query("change:" + changeId + " has:draft").get()).hasSize(1);
}
}
@Test
public void publishCommentsAllRevisions() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n", changeId)
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
.to("refs/for/master");
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
SUBJECT,
FILE_NAME,
"new interesting\ncntent\n",
r1.getChangeId())
.to("refs/for/master");
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "Is it that bad?"));
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(
FILE_NAME, Side.PARENT, createLineRange(0, 7), "what happened to this?"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 15), "better now"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 2, "typo: content"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.PARENT, 1, "comment 1 on base"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.PARENT, 2, "comment 2 on base"));
PushOneCommit.Result other = createChange();
// Drafts on other changes aren't returned.
addDraft(
other.getChangeId(),
other.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
requestScopeOperations.setApiUser(admin.id());
// Drafts by other users aren't returned.
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
requestScopeOperations.setApiUser(user.id());
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
reviewInput.message = "comments";
gApi.changes().id(r2.getChangeId()).current().review(reviewInput);
assertThat(gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).drafts())
.isEmpty();
Map<String, List<CommentInfo>> ps1Map =
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).comments();
assertThat(ps1Map.keySet()).containsExactly(FILE_NAME);
List<CommentInfo> ps1List = ps1Map.get(FILE_NAME);
assertThat(ps1List).hasSize(2);
assertThat(ps1List.get(0).message).isEqualTo("what happened to this?");
assertThat(ps1List.get(0).side).isEqualTo(Side.PARENT);
assertThat(ps1List.get(1).message).isEqualTo("Is it that bad?");
assertThat(ps1List.get(1).side).isNull();
assertThat(gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).drafts())
.isEmpty();
Map<String, List<CommentInfo>> ps2Map =
gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).comments();
assertThat(ps2Map.keySet()).containsExactly(FILE_NAME);
List<CommentInfo> ps2List = ps2Map.get(FILE_NAME);
assertThat(ps2List).hasSize(4);
assertThat(ps2List.get(0).message).isEqualTo("comment 1 on base");
assertThat(ps2List.get(1).message).isEqualTo("comment 2 on base");
assertThat(ps2List.get(2).message).isEqualTo("better now");
assertThat(ps2List.get(3).message).isEqualTo("typo: content");
List<Message> messages = email.getMessages(r2.getChangeId(), "comment");
assertThat(messages).hasSize(1);
String url = canonicalWebUrl.get();
int c = r1.getChange().getId().get();
assertThat(extractComments(messages.get(0).body()))
.isEqualTo(
"Patch Set 2:\n"
+ "\n"
+ "(6 comments)\n"
+ "\n"
+ "comments\n"
+ "\n"
+ "File a.txt:\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps1List.get(0).id
+ " \n"
+ "PS1, Line 1: initial\n"
+ "what happened to this?\n"
+ "\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps1List.get(1).id
+ " \n"
+ "PS1, Line 1: boring\n"
+ "Is it that bad?\n"
+ "\n"
+ "\n"
+ "File a.txt:\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps2List.get(0).id
+ " \n"
+ "PS2, Line 1: initial content\n"
+ "comment 1 on base\n"
+ "\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps2List.get(1).id
+ " \n"
+ "PS2, Line 2: \n"
+ "comment 2 on base\n"
+ "\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps2List.get(2).id
+ " \n"
+ "PS2, Line 1: interesting\n"
+ "better now\n"
+ "\n"
+ "\n"
+ url
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/comment/"
+ ps2List.get(3).id
+ " \n"
+ "PS2, Line 2: cntent\n"
+ "typo: content\n"
+ "\n"
+ "\n");
}
@Test
public void publishPartialDraftsAllRevisions() throws Exception {
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n")
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "File content in PS1\n")
.to("refs/for/master");
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
SUBJECT,
FILE_NAME,
"File content in PS2\n",
r1.getChangeId())
.to("refs/for/master");
CommentInfo draftOnePs1 =
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "comment 1"));
CommentInfo draftTwoPs1 =
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 15), "comment 2"));
CommentInfo draftThreePs2 =
addDraft(
r1.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(3, 12), "comment 3"));
ReviewInput reviewInput =
createReviewInput(
DraftHandling.PUBLISH_ALL_REVISIONS,
"review message",
/* draftIdsToPublish= */ ImmutableList.of(draftOnePs1.id, draftThreePs2.id));
gApi.changes().id(r1.getChangeId()).current().review(reviewInput);
assertThat(
gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList().stream()
.map(c -> c.id))
.containsExactly(draftOnePs1.id, draftThreePs2.id);
assertThat(
gApi.changes().id(r1.getChangeId()).draftsRequest().getAsList().stream().map(c -> c.id))
.containsExactly(draftTwoPs1.id);
}
@Test
public void publishPartialDrafts_whenDraftHandlingIsKeep_doesNotPublishDrafts() throws Exception {
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n")
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "File content in PS1\n")
.to("refs/for/master");
CommentInfo draftPs1 =
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "comment 1"));
ReviewInput reviewInput =
createReviewInput(
DraftHandling.KEEP,
"review message",
/* draftIdsToPublish= */ ImmutableList.of(draftPs1.id));
gApi.changes().id(r1.getChangeId()).current().review(reviewInput);
assertThat(gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList()).isEmpty();
assertThat(
gApi.changes().id(r1.getChangeId()).draftsRequest().getAsList().stream().map(c -> c.id))
.containsExactly(draftPs1.id);
}
@Test
public void publishPartialDrafts_whenDraftHandlingIsPublish_isNotAllowedForOtherRevisions()
throws Exception {
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n")
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "File content in PS1\n")
.to("refs/for/master");
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
SUBJECT,
FILE_NAME,
"File content in PS2\n",
r1.getChangeId())
.to("refs/for/master");
CommentInfo draftPs1 =
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "comment 1"));
addDraft(
r1.getChangeId(),
r2.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(3, 12), "comment 3"));
ReviewInput reviewInput =
createReviewInput(
DraftHandling.PUBLISH,
"review message",
/* draftIdsToPublish= */ ImmutableList.of(draftPs1.id));
// Request to publish draft of PS1, while sending review for PS2 with DraftHandling=PUBLISH
Exception error =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(r1.getChangeId()).current().review(reviewInput));
assertThat(error)
.hasMessageThat()
.isEqualTo(
String.format(
"Draft comments for other revisions cannot be published when DraftHandling = PUBLISH."
+ " (draft IDs: [%s])",
draftPs1.id));
}
@Test
public void publishPartialDraftsWithInvalidDraftIdsIsNotAllowed() throws Exception {
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n")
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "File content in PS1\n")
.to("refs/for/master");
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "comment 1"));
ReviewInput reviewInput =
createReviewInput(
DraftHandling.PUBLISH_ALL_REVISIONS,
"review message",
/* draftIdsToPublish= */ ImmutableList.of("1234"));
Exception error =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(r1.getChangeId()).current().review(reviewInput));
assertThat(error).hasMessageThat().isEqualTo("Non-existing draft IDs: [1234]");
}
@Test
public void publishPartialDraftsForAnotherUserIsNotAllowed() throws Exception {
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n")
.to("refs/heads/master");
PushOneCommit.Result r1 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "File content in PS1\n")
.to("refs/for/master");
// Add drafts with user scope
requestScopeOperations.setApiUser(accountCreator.user1().id());
CommentInfo draft =
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
CommentsUtil.newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "comment 1"));
ReviewInput reviewInput =
createReviewInput(
DraftHandling.PUBLISH_ALL_REVISIONS,
"review message",
/* draftIdsToPublish= */ ImmutableList.of(draft.id));
// Try to publish the drafts using user2 scope
requestScopeOperations.setApiUser(accountCreator.user2().id());
Exception error =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(r1.getChangeId()).current().review(reviewInput));
assertThat(error)
.hasMessageThat()
.isEqualTo(String.format("Non-existing draft IDs: [%s]", draft.id));
// Request will succeed if done by user
requestScopeOperations.setApiUser(accountCreator.user1().id());
gApi.changes().id(r1.getChangeId()).current().review(reviewInput);
assertThat(
gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList().stream()
.map(c -> c.id))
.containsExactly(draft.id);
assertThat(gApi.changes().id(r1.getChangeId()).draftsRequest().getAsList()).isEmpty();
}
@Test
public void commentTags() throws Exception {
PushOneCommit.Result r = createChange();
CommentInput pub = new CommentInput();
pub.line = 1;
pub.message = "published comment";
pub.path = FILE_NAME;
ReviewInput rin = CommentsUtil.newInput(pub);
rin.tag = "tag1";
gApi.changes().id(r.getChangeId()).current().review(rin);
List<CommentInfo> comments = gApi.changes().id(r.getChangeId()).current().commentsAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).tag).isEqualTo("tag1");
DraftInput draft = new DraftInput();
draft.line = 2;
draft.message = "draft comment";
draft.path = FILE_NAME;
draft.tag = "tag2";
addDraft(r.getChangeId(), r.getCommit().name(), draft);
List<CommentInfo> drafts = gApi.changes().id(r.getChangeId()).current().draftsAsList();
assertThat(drafts).hasSize(1);
assertThat(drafts.get(0).tag).isEqualTo("tag2");
}
@Test
public void draftCommentsWithTagPublishPatchset() throws Exception {
PushOneCommit.Result result = createChange();
DraftInput draft = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 2, "draft");
draft.tag = "old_tag";
addDraft(result.getChangeId(), result.getCommit().name(), draft);
ReviewInput reviewInput = new ReviewInput();
reviewInput.tag = "new_tag";
reviewInput.drafts = DraftHandling.PUBLISH;
gApi.changes().id(result.getChangeId()).current().review(reviewInput);
assertThat(
Iterables.getOnlyElement(
gApi.changes().id(result.getChangeId()).current().commentsAsList())
.tag)
.isEqualTo("new_tag");
}
@Test
public void draftCommentsWithTagPublishAllRevisions() throws Exception {
PushOneCommit.Result result = createChange();
DraftInput draft = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 2, "draft");
draft.tag = "old_tag";
addDraft(result.getChangeId(), result.getCommit().name(), draft);
amendChange(result.getChangeId());
ReviewInput reviewInput = new ReviewInput();
reviewInput.tag = "new_tag";
reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
gApi.changes().id(result.getChangeId()).current().review(reviewInput);
assertThat(
Iterables.getOnlyElement(
gApi.changes()
.id(result.getChangeId())
.revision(result.getCommit().name())
.commentsAsList())
.tag)
.isEqualTo("new_tag");
}
@Test
public void queryChangesWithCommentCount() throws Exception {
// PS1 has three comments in three different threads, PS2 has one comment in one thread.
PushOneCommit.Result result = createChange("change 1", FILE_NAME, "content 1");
String changeId1 = result.getChangeId();
CommentsUtil.addComment(gApi, result, "comment 1", false, true, null);
CommentsUtil.addComment(gApi, result, "comment 2", false, null, null);
CommentsUtil.addComment(gApi, result, "comment 3", false, false, null);
PushOneCommit.Result result2 = amendChange(changeId1);
CommentsUtil.addComment(gApi, result2, "comment4", false, true, null);
// Change2 has two comments in one thread, the first is unresolved and the second is resolved.
result = createChange("change 2", FILE_NAME, "content 2");
String changeId2 = result.getChangeId();
CommentsUtil.addComment(gApi, result, "comment 1", false, true, null);
Map<String, List<CommentInfo>> comments =
getPublishedComments(changeId2, result.getCommit().name());
assertThat(comments).hasSize(1);
assertThat(comments.get(FILE_NAME)).hasSize(1);
CommentsUtil.addComment(
gApi, result, "comment 2", false, false, comments.get(FILE_NAME).get(0).id);
// Change3 has two comments in one thread, the first is resolved, the second is unresolved.
result = createChange("change 3", FILE_NAME, "content 3");
String changeId3 = result.getChangeId();
CommentsUtil.addComment(gApi, result, "comment 1", false, false, null);
comments = getPublishedComments(result.getChangeId(), result.getCommit().name());
assertThat(comments).hasSize(1);
assertThat(comments.get(FILE_NAME)).hasSize(1);
CommentsUtil.addComment(
gApi, result, "comment 2", false, true, comments.get(FILE_NAME).get(0).id);
try (AutoCloseable ignored = disableNoteDb()) {
ChangeInfo changeInfo1 = Iterables.getOnlyElement(query(changeId1));
ChangeInfo changeInfo2 = Iterables.getOnlyElement(query(changeId2));
ChangeInfo changeInfo3 = Iterables.getOnlyElement(query(changeId3));
assertThat(changeInfo1.unresolvedCommentCount).isEqualTo(2);
assertThat(changeInfo1.totalCommentCount).isEqualTo(4);
assertThat(changeInfo2.unresolvedCommentCount).isEqualTo(0);
assertThat(changeInfo2.totalCommentCount).isEqualTo(2);
assertThat(changeInfo3.unresolvedCommentCount).isEqualTo(1);
assertThat(changeInfo3.totalCommentCount).isEqualTo(2);
}
}
@Test
public void deleteCommentCannotBeAppliedByUser() throws Exception {
PushOneCommit.Result result = createChange();
CommentInput targetComment = CommentsUtil.addComment(gApi, result.getChangeId());
Map<String, List<CommentInfo>> commentsMap =
getPublishedComments(result.getChangeId(), result.getCommit().name());
assertThat(commentsMap).hasSize(1);
assertThat(commentsMap.get(FILE_NAME)).hasSize(1);
String uuid = commentsMap.get(targetComment.path).get(0).id;
DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
requestScopeOperations.setApiUser(user.id());
assertThrows(
AuthException.class,
() -> gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input));
}
@Test
public void deleteCommentByRewritingCommitHistory() throws Exception {
// Creates the following commit history on the meta branch of the test change. Then tries to
// delete the comments one by one, which will rewrite most of the commits on the 'meta' branch.
// Commits will be rewritten N times for N added comments. After each deletion, the meta branch
// should keep its previous state except that the target comment's message should be updated.
// 1st commit: Create PS1.
PushOneCommit.Result result1 = createChange(SUBJECT, "a.txt", "a");
Change.Id id = result1.getChange().getId();
String changeId = result1.getChangeId();
String ps1 = result1.getCommit().name();
// 2nd commit: Add (c1) to PS1.
CommentInput c1 = CommentsUtil.newComment("a.txt", "comment 1");
CommentsUtil.addComments(gApi, changeId, ps1, c1);
// 3rd commit: Add (c2, c3) to PS1.
CommentInput c2 = CommentsUtil.newComment("a.txt", "comment 2");
CommentInput c3 = CommentsUtil.newComment("a.txt", "comment 3");
CommentsUtil.addComments(gApi, changeId, ps1, c2, c3);
// 4th commit: Add (c4) to PS1.
CommentInput c4 = CommentsUtil.newComment("a.txt", "comment 4");
CommentsUtil.addComments(gApi, changeId, ps1, c4);
// 5th commit: Create PS2.
PushOneCommit.Result result2 = amendChange(changeId, "refs/for/master", "b.txt", "b");
String ps2 = result2.getCommit().name();
// 6th commit: Add (c5) to PS1.
CommentInput c5 = CommentsUtil.newComment("a.txt", "comment 5");
CommentsUtil.addComments(gApi, changeId, ps1, c5);
// 7th commit: Add (c6) to PS2.
CommentInput c6 = CommentsUtil.newComment("b.txt", "comment 6");
CommentsUtil.addComments(gApi, changeId, ps2, c6);
// 8th commit: Create PS3.
PushOneCommit.Result result3 = amendChange(changeId);
String ps3 = result3.getCommit().name();
// 9th commit: Create PS4.
PushOneCommit.Result result4 = amendChange(changeId, "refs/for/master", "c.txt", "c");
String ps4 = result4.getCommit().name();
// 10th commit: Add (c7, c8) to PS4.
CommentInput c7 = CommentsUtil.newComment("c.txt", "comment 7");
CommentInput c8 = CommentsUtil.newComment("b.txt", "comment 8");
CommentsUtil.addComments(gApi, changeId, ps4, c7, c8);
// 11th commit: Add (c9) to PS2.
CommentInput c9 = CommentsUtil.newCommentWithOnlyMandatoryFields("b.txt", "comment 9");
CommentsUtil.addComments(gApi, changeId, ps2, c9);
List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
assertThat(commentsBeforeDelete).hasSize(9);
// PS1 has comments [c1, c2, c3, c4, c5].
assertThat(getRevisionComments(changeId, ps1)).hasSize(5);
// PS2 has comments [c6, c9].
assertThat(getRevisionComments(changeId, ps2)).hasSize(2);
// PS3 has no comment.
assertThat(getRevisionComments(changeId, ps3)).isEmpty();
// PS4 has comments [c7, c8].
assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
requestScopeOperations.setApiUser(admin.id());
for (int i = 0; i < commentsBeforeDelete.size(); i++) {
List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
CommentInfo comment = commentsBeforeDelete.get(i);
String uuid = comment.id;
int patchSet = comment.patchSet;
// 'oldComment' has some fields unset compared with 'comment'.
CommentInfo oldComment = gApi.changes().id(changeId).revision(patchSet).comment(uuid).get();
DeleteCommentInput input = new DeleteCommentInput("delete comment " + uuid);
CommentInfo updatedComment =
gApi.changes().id(changeId).revision(patchSet).comment(uuid).delete(input);
String expectedMsg =
String.format("Comment removed by: %s; Reason: %s", admin.fullName(), input.reason);
assertThat(updatedComment.message).isEqualTo(expectedMsg);
oldComment.message = expectedMsg;
assertThat(updatedComment).isEqualTo(oldComment);
// Check the NoteDb state after the deletion.
assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
comment.message = expectedMsg;
commentsBeforeDelete.set(i, comment);
List<CommentInfo> commentsAfterDelete = getChangeSortedComments(id.get());
assertThat(commentsAfterDelete).isEqualTo(commentsBeforeDelete);
}
// Make sure that comments can still be added correctly.
CommentInput c10 = CommentsUtil.newComment("a.txt", "comment 10");
CommentInput c11 = CommentsUtil.newComment("b.txt", "comment 11");
CommentInput c12 = CommentsUtil.newComment("a.txt", "comment 12");
CommentInput c13 = CommentsUtil.newComment("c.txt", "comment 13");
CommentsUtil.addComments(gApi, changeId, ps1, c10);
CommentsUtil.addComments(gApi, changeId, ps2, c11);
CommentsUtil.addComments(gApi, changeId, ps3, c12);
CommentsUtil.addComments(gApi, changeId, ps4, c13);
assertThat(getChangeSortedComments(id.get())).hasSize(13);
assertThat(getRevisionComments(changeId, ps1)).hasSize(6);
assertThat(getRevisionComments(changeId, ps2)).hasSize(3);
assertThat(getRevisionComments(changeId, ps3)).hasSize(1);
assertThat(getRevisionComments(changeId, ps4)).hasSize(3);
}
@Test
public void deleteOneCommentMultipleTimes() throws Exception {
PushOneCommit.Result result = createChange();
Change.Id id = result.getChange().getId();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput c1 = CommentsUtil.newComment(FILE_NAME, "comment 1");
CommentInput c2 = CommentsUtil.newComment(FILE_NAME, "comment 2");
CommentInput c3 = CommentsUtil.newComment(FILE_NAME, "comment 3");
CommentsUtil.addComments(gApi, changeId, ps1, c1);
CommentsUtil.addComments(gApi, changeId, ps1, c2);
CommentsUtil.addComments(gApi, changeId, ps1, c3);
List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
assertThat(commentsBeforeDelete).hasSize(3);
Optional<CommentInfo> targetComment =
commentsBeforeDelete.stream().filter(c -> c.message.equals("comment 2")).findFirst();
assertThat(targetComment).isPresent();
String uuid = targetComment.get().id;
CommentInfo oldComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get();
List<RevCommit> commitsBeforeDelete = getChangeMetaCommitsInReverseOrder(id);
requestScopeOperations.setApiUser(admin.id());
for (int i = 0; i < 3; i++) {
DeleteCommentInput input = new DeleteCommentInput("delete comment 2, iteration: " + i);
gApi.changes().id(changeId).revision(ps1).comment(uuid).delete(input);
}
CommentInfo updatedComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get();
String expectedMsg =
String.format(
"Comment removed by: %s; Reason: %s",
admin.fullName(), "delete comment 2, iteration: 2");
assertThat(updatedComment.message).isEqualTo(expectedMsg);
oldComment.message = expectedMsg;
assertThat(updatedComment).isEqualTo(oldComment);
assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
assertThat(getChangeSortedComments(id.get())).hasSize(3);
}
@Test
public void canCreateHumanCommentWithRobotCommentAsParentAndUnsetUnresolved() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentRobotCommentUuid =
changeOperations.change(changeId).currentPatchset().newRobotComment().create();
CommentInput createdCommentInput = CommentsUtil.newComment(COMMIT_MSG, "comment reply");
createdCommentInput.inReplyTo = parentRobotCommentUuid;
createdCommentInput.unresolved = null;
CommentsUtil.addComments(gApi, changeId, createdCommentInput);
CommentInfo resultNewComment =
Iterables.getOnlyElement(
getPublishedCommentsAsList(changeId).stream()
.filter(c -> c.message.equals("comment reply"))
.collect(toImmutableSet()));
assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
// Default unresolved is false.
assertThat(resultNewComment.unresolved).isFalse();
}
@Test
public void canCreateHumanCommentWithHumanCommentAsParent() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentCommentUuid =
changeOperations.change(changeId).currentPatchset().newComment().create();
CommentInput createdCommentInput = CommentsUtil.newComment(COMMIT_MSG, "comment reply");
createdCommentInput.inReplyTo = parentCommentUuid;
CommentsUtil.addComments(gApi, changeId, createdCommentInput);
CommentInfo resultNewComment =
Iterables.getOnlyElement(
getPublishedCommentsAsList(changeId).stream()
.filter(c -> c.message.equals("comment reply"))
.collect(toImmutableSet()));
assertThat(resultNewComment.inReplyTo).isEqualTo(parentCommentUuid);
}
@Test
public void canCreateHumanCommentWithRobotCommentAsParent() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
String parentRobotCommentUuid =
changeOperations.change(changeId).currentPatchset().newRobotComment().create();
CommentInput createdCommentInput = CommentsUtil.newComment(COMMIT_MSG, "comment reply");
createdCommentInput.inReplyTo = parentRobotCommentUuid;
CommentsUtil.addComments(gApi, changeId, createdCommentInput);
CommentInfo resultNewComment =
Iterables.getOnlyElement(
getPublishedCommentsAsList(changeId).stream()
.filter(c -> c.message.equals("comment reply"))
.collect(toImmutableSet()));
assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
}
@Test
public void cannotCreateCommentWithInvalidInReplyTo() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, "comment 1 reply");
comment.inReplyTo = "invalid";
BadRequestException exception =
assertThrows(
BadRequestException.class, () -> CommentsUtil.addComments(gApi, changeId, comment));
assertThat(exception.getMessage()).contains(String.format("%s not found", comment.inReplyTo));
}
@Test
public void commentsOnRootCommitsAreIncludedInEmails() throws Exception {
// Create a change in a new branch, making the patch-set commit a root commit.
ChangeInfo changeInfo = createChangeInNewBranch("newBranch");
Change.Id changeId = Change.Id.tryParse(Integer.toString(changeInfo._number)).get();
// Add a file.
gApi.changes().id(changeId.get()).edit().modifyFile("f1.txt", RawInputUtil.create("content"));
gApi.changes().id(changeId.get()).edit().publish();
email.clear();
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.reviewer = admin.email();
gApi.changes().id(changeId.get()).addReviewer(reviewerInput);
changeInfo = gApi.changes().id(changeId.get()).get();
assertThat(email.getMessages()).hasSize(1);
Message message = email.getMessages().get(0);
assertThat(message.body()).contains("f1.txt");
email.clear();
// Send a comment. Make sure the email that is sent includes the comment text.
CommentInput c1 =
CommentsUtil.newComment(
"f1.txt",
Side.REVISION,
/* line= */ 1,
/* message= */ "Comment text",
/* unresolved= */ false);
CommentsUtil.addComments(gApi, changeId.toString(), changeInfo.currentRevision, c1);
assertThat(email.getMessages()).hasSize(1);
Message commentMessage = email.getMessages().get(0);
assertThat(commentMessage.body())
.contains("Patch Set 2:\n" + "\n" + "(1 comment)\n" + "\n" + "File f1.txt:");
assertThat(commentMessage.body()).contains("PS2, Line 1: content\n" + "Comment text");
}
@Test
public void commentsOnDeletedFileIsIncludedInEmails() throws Exception {
// Create a change with a file.
createChange("subject", "f1.txt", "content");
// Stack a second change that deletes the file.
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
gApi.changes().id(changeId).edit().deleteFile("f1.txt");
gApi.changes().id(changeId).edit().publish();
String currentRevision = gApi.changes().id(changeId).get().currentRevision;
// Add a comment on the deleted file on the parent side.
email.clear();
CommentInput commentInput =
CommentsUtil.newComment(
"f1.txt",
Side.PARENT,
/* line= */ 1,
/* message= */ "Comment text",
/* unresolved= */ false);
CommentsUtil.addComments(gApi, changeId, currentRevision, commentInput);
// Assert email contains the comment text.
assertThat(email.getMessages()).hasSize(1);
Message commentMessage = email.getMessages().get(0);
assertThat(commentMessage.body()).contains("Patch Set 2:\n\n(1 comment)\n\nFile f1.txt:");
assertThat(commentMessage.body()).contains("PS2, Line 1: content\nComment text");
}
private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
return getPublishedComments(changeId, revId).values().stream()
.flatMap(List::stream)
.collect(toList());
}
/**
* All the commits, which contain the target comment before, should still contain the comment with
* the updated message. All the other metas of the commits should be exactly the same.
*/
private void assertMetaBranchCommitsAfterRewriting(
List<RevCommit> beforeDelete,
Change.Id changeId,
String targetCommentUuid,
String expectedMessage)
throws Exception {
List<RevCommit> afterDelete = getChangeMetaCommitsInReverseOrder(changeId);
assertThat(afterDelete).hasSize(beforeDelete.size());
try (Repository repo = repoManager.openRepository(project);
ObjectReader reader = repo.newObjectReader()) {
for (int i = 0; i < beforeDelete.size(); i++) {
RevCommit commitBefore = beforeDelete.get(i);
RevCommit commitAfter = afterDelete.get(i);
Map<String, HumanComment> commentMapBefore =
DeleteCommentRewriter.getPublishedComments(
noteUtil, reader, NoteMap.read(reader, commitBefore));
Map<String, HumanComment> commentMapAfter =
DeleteCommentRewriter.getPublishedComments(
noteUtil, reader, NoteMap.read(reader, commitAfter));
if (commentMapBefore.containsKey(targetCommentUuid)) {
assertThat(commentMapAfter).containsKey(targetCommentUuid);
HumanComment comment = commentMapAfter.get(targetCommentUuid);
assertThat(comment.message).isEqualTo(expectedMessage);
comment.message = commentMapBefore.get(targetCommentUuid).message;
commentMapAfter.put(targetCommentUuid, comment);
assertThat(commentMapAfter).isEqualTo(commentMapBefore);
} else {
assertThat(commentMapAfter).doesNotContainKey(targetCommentUuid);
}
// Other metas should be exactly the same.
assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage());
assertThat(commitAfter.getCommitterIdent()).isEqualTo(commitBefore.getCommitterIdent());
assertThat(commitAfter.getAuthorIdent()).isEqualTo(commitBefore.getAuthorIdent());
assertThat(commitAfter.getEncoding()).isEqualTo(commitBefore.getEncoding());
assertThat(commitAfter.getEncodingName()).isEqualTo(commitBefore.getEncodingName());
}
}
}
private RevCommit getHeadOfDraftCommentsRef(String refName) throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
return getHead(repo, refName);
}
}
private static String extractComments(String msg) {
// Extract lines between start "....." and end "-- ".
Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
Matcher m = p.matcher(msg);
return m.matches() ? m.group(1) : msg;
}
private CommentInfo addDraft(String changeId, String revId, DraftInput in) throws Exception {
return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
}
private CommentInfo addDraft(Change.Id changeId, DraftInput in) throws Exception {
return gApi.changes().id(changeId.get()).current().createDraft(in).get();
}
private void updateDraft(String changeId, String revId, DraftInput in, String uuid)
throws Exception {
gApi.changes().id(changeId).revision(revId).draft(uuid).update(in);
}
private void updateDraft(Change.Id changeId, DraftInput in, String uuid) throws Exception {
gApi.changes().id(changeId.get()).current().draft(uuid).update(in);
}
private void deleteDraft(String changeId, String revId, String uuid) throws Exception {
gApi.changes().id(changeId).revision(revId).draft(uuid).delete();
}
private CommentInfo getPublishedComment(String changeId, String revId, String uuid)
throws Exception {
return gApi.changes().id(changeId).revision(revId).comment(uuid).get();
}
private Map<String, List<CommentInfo>> getPublishedComments(String changeId, String revId)
throws Exception {
return gApi.changes().id(changeId).revision(revId).comments();
}
private List<CommentInfo> getPublishedCommentsAsList(String changeId) throws Exception {
return gApi.changes().id(changeId).commentsRequest().getAsList();
}
private List<CommentInfo> getPublishedCommentsAsList(Change.Id changeId) throws Exception {
return gApi.changes().id(changeId.get()).commentsRequest().getAsList();
}
private Map<String, List<CommentInfo>> getDraftComments(String changeId, String revId)
throws Exception {
return gApi.changes().id(changeId).revision(revId).drafts();
}
private List<CommentInfo> getDraftCommentsAsList(String changeId) throws Exception {
return gApi.changes().id(changeId).draftsAsList();
}
private CommentInfo getDraftComment(String changeId, String revId, String uuid) throws Exception {
return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
}
private static Comment.Range createLineRange(int startChar, int endChar) {
Comment.Range range = new Comment.Range();
range.startLine = 1;
range.startCharacter = startChar;
range.endLine = 1;
range.endCharacter = endChar;
return range;
}
private static Comment.Range createRange(
int startLine, int startCharacter, int endLine, int endCharacter) {
Comment.Range range = new Comment.Range();
range.startLine = startLine;
range.startCharacter = startCharacter;
range.endLine = endLine;
range.endCharacter = endCharacter;
return range;
}
private static Function<CommentInfo, CommentInput> infoToInput(String path) {
return info -> {
CommentInput commentInput = new CommentInput();
commentInput.path = path;
commentInput.unresolved = info.unresolved;
copy(info, commentInput);
return commentInput;
};
}
private static Function<CommentInfo, DraftInput> infoToDraft(String path) {
return info -> {
DraftInput draftInput = new DraftInput();
draftInput.path = path;
draftInput.unresolved = info.unresolved;
copy(info, draftInput);
return draftInput;
};
}
private static void copy(Comment from, Comment to) {
to.side = from.side == null ? Side.REVISION : from.side;
to.parent = from.parent;
to.line = from.line;
to.message = from.message;
to.range = from.range;
to.inReplyTo = from.inReplyTo;
}
private ReviewInput createReviewInput(
DraftHandling handling, String message, List<String> draftIdsToPublish) {
ReviewInput reviewInput = new ReviewInput();
reviewInput.drafts = handling;
reviewInput.message = message;
reviewInput.draftIdsToPublish = draftIdsToPublish;
return reviewInput;
}
private ChangeInfo createChangeInNewBranch(String branchName) throws Exception {
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = branchName;
in.newBranch = true;
in.subject = "New changes";
return gApi.changes().create(in).get();
}
}