// 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.truth.Truth.assertThat; import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AcceptanceTestRequestScope; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; 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.client.Comment; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.ChangeInfo; 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.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangesCollection; import com.google.gerrit.server.change.PostReview; 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.testutil.FakeEmailSender; import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.inject.Inject; import com.google.inject.Provider; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; 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.eclipse.jgit.revwalk.RevWalk; import org.junit.Before; import org.junit.Test; @NoHttpd public class CommentsIT extends AbstractDaemonTest { @Inject private Provider<ChangesCollection> changes; @Inject private Provider<PostReview> postReview; @Inject private FakeEmailSender email; @Inject private ChangeNoteUtil noteUtil; private final Integer[] lines = {0, 1}; @Before public void setUp() { setApiUser(user); } @Test public void getNonExistingComment() throws Exception { PushOneCommit.Result r = createChange(); String changeId = r.getChangeId(); String revId = r.getCommit().getName(); exception.expect(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 = 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)); } } @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 = newDraft(path, Side.REVISION, line, "ps-1"); DraftInput c2 = newDraft(path, Side.PARENT, line, "auto-merge of ps-1"); DraftInput c3 = newDraftOnParent(path, 1, line, "parent-1 of ps-1"); DraftInput c4 = 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(Lists.transform(result.get(path), 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(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", 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 postCommentWithReply() 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", 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 = 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(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); 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 = 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<>(); 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(Lists.transform(result.get(file), 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 = 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<>(); 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(Lists.transform(result.get(file), 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 = 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); exception.expectMessage("cannot comment on " + Patch.COMMIT_MSG + " on auto-merge"); revision(r).review(input); } @Test public void listComments() throws Exception { String file = "file"; 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(); assertThat(getPublishedComments(changeId, revId)).isEmpty(); List<CommentInput> expectedComments = new ArrayList<>(); for (Integer line : lines) { ReviewInput input = new ReviewInput(); 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)); revision(r).review(input); } Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId); assertThat(result).isNotEmpty(); List<CommentInfo> actualComments = result.get(file); assertThat(Lists.transform(actualComments, infoToInput(file))) .containsExactlyElementsIn(expectedComments); } @Test public void putDraft() throws Exception { for (Integer line : lines) { PushOneCommit.Result r = createChange(); Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn(); String changeId = r.getChangeId(); String revId = r.getCommit().getName(); String path = "file1"; DraftInput comment = 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 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 = 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(Lists.transform(actualComments, 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 = 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(); Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn(); String changeId = r.getChangeId(); String revId = r.getCommit().getName(); DraftInput draft = 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 { Timestamp timestamp = new Timestamp(0); 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(); Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn(); ReviewInput input = new ReviewInput(); 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)); ChangeResource changeRsrc = changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId)); RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId)); postReview.get().apply(batchUpdateFactory, 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(); addComment(r1, "nit: trailing whitespace"); addComment(r1, "nit: trailing whitespace"); Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId); assertThat(result.get(FILE_NAME)).hasSize(2); addComment(r1, "nit: trailing whitespace", true, false, null); result = getPublishedComments(changeId, revId); assertThat(result.get(FILE_NAME)).hasSize(2); PushOneCommit.Result r2 = pushFactory .create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content") .to("refs/for/master"); changeId = r2.getChangeId(); revId = r2.getCommit().getName(); addComment(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( db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId()) .to("refs/for/master"); setApiUser(admin); addDraft( r1.getChangeId(), r1.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace")); addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 1, "typo: content")); setApiUser(user); addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix")); setApiUser(admin); 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 listChangeComments() throws Exception { PushOneCommit.Result r1 = createChange(); PushOneCommit.Result r2 = pushFactory .create( db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent", r1.getChangeId()) .to("refs/for/master"); addComment(r1, "nit: trailing whitespace"); addComment(r2, "typo: content"); Map<String, List<CommentInfo>> actual = gApi.changes().id(r2.getChangeId()).comments(); 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._accountId).isEqualTo(user.getId().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.getId().get()); assertThat(c2.patchSet).isEqualTo(2); assertThat(c2.message).isEqualTo("typo: content"); assertThat(c2.side).isNull(); assertThat(c2.line).isEqualTo(1); } @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 = 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 r1 = createChange(); PushOneCommit.Result r2 = pushFactory .create( db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new\ncntent\n", r1.getChangeId()) .to("refs/for/master"); addDraft( r1.getChangeId(), r1.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace")); addDraft( r1.getChangeId(), r1.getCommit().getName(), newDraft(FILE_NAME, Side.PARENT, 2, "what happened to this?")); addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 1, "join lines")); addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 2, "typo: content")); addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.PARENT, 1, "comment 1 on base")); addDraft( r2.getChangeId(), r2.getCommit().getName(), 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(), newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment")); setApiUser(admin); // Drafts by other users aren't returned. addDraft( r2.getChangeId(), r2.getCommit().getName(), newDraft(FILE_NAME, Side.REVISION, 2, "oops")); setApiUser(user); 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("nit: trailing whitespace"); 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("join lines"); 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" + url + "#/c/" + c + "/1/a.txt\n" + "File a.txt:\n" + "\n" + url + "#/c/" + c + "/1/a.txt@a2\n" + "PS1, Line 2: \n" + "what happened to this?\n" + "\n" + "\n" + url + "#/c/" + c + "/1/a.txt@1\n" + "PS1, Line 1: ew\n" + "nit: trailing whitespace\n" + "\n" + "\n" + url + "#/c/" + c + "/2/a.txt\n" + "File a.txt:\n" + "\n" + url + "#/c/" + c + "/2/a.txt@a1\n" + "PS2, Line 1: \n" + "comment 1 on base\n" + "\n" + "\n" + url + "#/c/" + c + "/2/a.txt@a2\n" + "PS2, Line 2: \n" + "comment 2 on base\n" + "\n" + "\n" + url + "#/c/" + c + "/2/a.txt@1\n" + "PS2, Line 1: ew\n" + "join lines\n" + "\n" + "\n" + url + "#/c/" + c + "/2/a.txt@2\n" + "PS2, Line 2: nten\n" + "typo: content\n" + "\n" + "\n"); } @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 = 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 queryChangesWithUnresolvedCommentCount() 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(); addComment(result, "comment 1", false, true, null); addComment(result, "comment 2", false, null, null); addComment(result, "comment 3", false, false, null); PushOneCommit.Result result2 = amendChange(changeId1); addComment(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(); addComment(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); addComment(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(); addComment(result, "comment 1", false, false, null); comments = getPublishedComments(result.getChangeId(), result.getCommit().name()); assertThat(comments).hasSize(1); assertThat(comments.get(FILE_NAME)).hasSize(1); addComment(result, "comment 2", false, true, comments.get(FILE_NAME).get(0).id); AcceptanceTestRequestScope.Context ctx = disableDb(); try { ChangeInfo changeInfo1 = Iterables.getOnlyElement(query(changeId1)); ChangeInfo changeInfo2 = Iterables.getOnlyElement(query(changeId2)); ChangeInfo changeInfo3 = Iterables.getOnlyElement(query(changeId3)); assertThat(changeInfo1.unresolvedCommentCount).isEqualTo(2); assertThat(changeInfo2.unresolvedCommentCount).isEqualTo(0); assertThat(changeInfo3.unresolvedCommentCount).isEqualTo(1); } finally { enableDb(ctx); } } @Test public void deleteCommentCannotBeAppliedByUser() throws Exception { PushOneCommit.Result result = createChange(); CommentInput targetComment = addComment(result.getChangeId(), "My password: abc123"); Map<String, List<CommentInfo>> commentsMap = getPublishedComments(result.getChangeId(), result.getCommit().name()); assertThat(commentsMap.size()).isEqualTo(1); assertThat(commentsMap.get(FILE_NAME)).hasSize(1); String uuid = commentsMap.get(targetComment.path).get(0).id; DeleteCommentInput input = new DeleteCommentInput("contains confidential information"); setApiUser(user); exception.expect(AuthException.class); gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input); } @Test public void deleteCommentByRewritingCommitHistory() throws Exception { // Create change (the 1st commit on the change's meta branch). PushOneCommit.Result result = createChange(); String changeId = result.getChangeId(); Change.Id id = result.getChange().getId(); // Add two comments in patch set 1 (the 2nd commit on the change's meta branch). ReviewInput reviewInput = new ReviewInput(); CommentInput comment1 = newComment(FILE_NAME, Side.REVISION, 0, "My password: abc123", false); CommentInput comment2 = newComment(FILE_NAME, Side.REVISION, 1, "nit: long line", false); reviewInput.comments = ImmutableMap.of(FILE_NAME, Lists.newArrayList(comment1, comment2)); reviewInput.label("Code-Review", 1); gApi.changes().id(changeId).current().review(reviewInput); // Create patch set 2 (the 3rd commit on the change's meta branch). amendChange(changeId); // Add 'comment3' in patch set 2 (the 4th commit on the change's meta branch). CommentInput comment3 = addComment(changeId, "typo"); Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId); assertThat(commentsMap).hasSize(1); assertThat(commentsMap.get(FILE_NAME)).hasSize(3); Optional<CommentInfo> targetCommentInfo = commentsMap .get(FILE_NAME) .stream() .filter(c -> c.message.equals("My password: abc123")) .findFirst(); assertThat(targetCommentInfo.isPresent()).isTrue(); List<RevCommit> commitsBeforeDelete = new ArrayList<>(); if (notesMigration.commitChangeWrites()) { commitsBeforeDelete = getCommits(id); } String uuid = targetCommentInfo.get().id; // Get the target comment. CommentInfo oldComment = gApi.changes().id(changeId).revision(result.getCommit().getName()).comment(uuid).get(); // Delete the target comment. DeleteCommentInput input = new DeleteCommentInput("contains confidential information"); setApiUser(admin); CommentInfo updatedComment = gApi.changes() .id(changeId) .revision(result.getCommit().getName()) .comment(uuid) .delete(input); String expectedMsg = String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason); assertThat(updatedComment.message).isEqualTo(expectedMsg); updatedComment.message = oldComment.message; assertThat(updatedComment).isEqualTo(oldComment); // Check the comment's message has been replaced in NoteDb. if (notesMigration.commitChangeWrites()) { assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg); } // Make sure that comments can still be added correctly. CommentInput comment4 = addComment(changeId, "too much space"); commentsMap = getPublishedComments(changeId); assertThat(commentsMap).hasSize(1); List<CommentInput> comments = Lists.transform(commentsMap.get(FILE_NAME), infoToInput(FILE_NAME)); // Change comment1's message to the expected message. comment1.message = expectedMsg; assertThat(comments).containsExactly(comment1, comment2, comment3, comment4); } private CommentInput addComment(String changeId, String message) throws Exception { ReviewInput input = new ReviewInput(); CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, message, false); input.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment)); gApi.changes().id(changeId).current().review(input); return comment; } private List<RevCommit> getCommits(Change.Id changeId) throws IOException { try (Repository repo = repoManager.openRepository(project); RevWalk revWalk = new RevWalk(repo)) { Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId)); revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId())); return Lists.newArrayList(revWalk); } } /** * 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 = getCommits(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, com.google.gerrit.reviewdb.client.Comment> commentMapBefore = DeleteCommentRewriter.getPublishedComments( noteUtil, changeId, reader, NoteMap.read(reader, commitBefore)); Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapAfter = DeleteCommentRewriter.getPublishedComments( noteUtil, changeId, reader, NoteMap.read(reader, commitAfter)); if (commentMapBefore.containsKey(targetCommentUuid)) { assertThat(commentMapAfter).containsKey(targetCommentUuid); com.google.gerrit.reviewdb.client.Comment 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 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 ReviewInput newInput(CommentInput c) { ReviewInput in = new ReviewInput(); in.comments = new HashMap<>(); in.comments.put(c.path, Lists.newArrayList(c)); return in; } private void addComment(PushOneCommit.Result r, String message) throws Exception { addComment(r, message, false, false, null); } private void addComment( PushOneCommit.Result r, String message, boolean omitDuplicateComments, Boolean unresolved, String inReplyTo) throws Exception { CommentInput c = new CommentInput(); c.line = 1; c.message = message; c.path = FILE_NAME; c.unresolved = unresolved; c.inReplyTo = inReplyTo; ReviewInput in = newInput(c); in.omitDuplicateComments = omitDuplicateComments; gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); } private CommentInfo addDraft(String changeId, String revId, DraftInput in) throws Exception { return gApi.changes().id(changeId).revision(revId).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 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 Map<String, List<CommentInfo>> getDraftComments(String changeId, String revId) throws Exception { return gApi.changes().id(changeId).revision(revId).drafts(); } private Map<String, List<CommentInfo>> getPublishedComments(String changeId) throws Exception { return gApi.changes().id(changeId).comments(); } private CommentInfo getDraftComment(String changeId, String revId, String uuid) throws Exception { return gApi.changes().id(changeId).revision(revId).draft(uuid).get(); } private static CommentInput newComment( String path, Side side, int line, String message, Boolean unresolved) { CommentInput c = new CommentInput(); 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, false); } private DraftInput newDraft(String path, Side side, int line, String message) { DraftInput d = new DraftInput(); 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, false); } private static <C extends Comment> C populate( C c, String path, Side side, 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; range.startCharacter = 1; range.endLine = line; range.endCharacter = 5; c.range = range; } return c; } private static Function<CommentInfo, CommentInput> infoToInput(String path) { return infoToInput(path, CommentInput::new); } private static Function<CommentInfo, DraftInput> infoToDraft(String path) { return infoToInput(path, DraftInput::new); } private static <I extends Comment> Function<CommentInfo, I> infoToInput( String path, Supplier<I> supplier) { return info -> { I i = supplier.get(); i.path = path; copy(info, i); return i; }; } 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.unresolved = from.unresolved; to.inReplyTo = from.inReplyTo; } }