blob: eb5f52b0fd382de0ceb790b2266a17f40cac1a72 [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
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.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;
public class CommentJsonMigratorTest extends AbstractChangeNotesTest {
private CommentJsonMigrator migrator;
@Inject private ChangeNoteUtil noteUtil;
@Inject private CommentsUtil commentsUtil;
@Inject private LegacyChangeNoteWrite legacyChangeNoteWrite;
private AtomicInteger uuidCounter;
@Before
public void setUpCounter() {
uuidCounter = new AtomicInteger();
migrator = new CommentJsonMigrator(new ChangeNoteJson(), "gerrit");
}
@Test
public void noOpIfAllCommentsAreJson() throws Exception {
Change c = newChange();
incrementPatchSet(c);
ChangeNotes notes = newNotes(c);
ChangeUpdate update = newUpdate(c, changeOwner);
Comment ps1Comment = newComment(notes, 1, "comment on ps1");
update.putComment(Status.PUBLISHED, ps1Comment);
update.commit();
notes = newNotes(c);
update = newUpdate(c, changeOwner);
Comment ps2Comment = newComment(notes, 2, "comment on ps2");
update.putComment(Status.PUBLISHED, ps2Comment);
update.commit();
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getComments()))
.containsExactly(
getRevId(notes, 1), ps1Comment.toString(),
getRevId(notes, 2), ps2Comment.toString());
ChangeNotes oldNotes = notes;
migrate(project, migrator::migrateChanges, 0);
assertNoDifferences(notes, oldNotes);
assertThat(notes.getMetaId()).isEqualTo(oldNotes.getMetaId());
}
@Test
public void migratePublishedComments() throws Exception {
Change c = newChange();
incrementPatchSet(c);
ChangeNotes notes = newNotes(c);
Comment ps1Comment1 = newComment(notes, 1, "first comment on ps1");
Comment ps2Comment1 = newComment(notes, 2, "first comment on ps2");
Comment ps1Comment2 = newComment(notes, 1, "second comment on ps1");
// Construct legacy format 'by hand'.
ByteArrayOutputStream out1 = new ByteArrayOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment1).build(), out1);
ByteArrayOutputStream out2 = new ByteArrayOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(2, ps2Comment1).build(), out2);
ByteArrayOutputStream out3 = new ByteArrayOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder()
.put(1, ps1Comment2)
.put(1, ps1Comment1)
.build(),
out3);
TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
String metaRefName = RefNames.changeMetaRef(c.getId());
testRepository
.branch(metaRefName)
.commit()
.message("Review ps 1\n\nPatch-set: 1")
.add(ps1Comment1.revId, out1.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
testRepository
.branch(metaRefName)
.commit()
.message("Review ps 2\n\nPatch-set: 2")
.add(ps2Comment1.revId, out2.toString())
.add(ps1Comment1.revId, out3.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getComments()))
.containsExactly(
getRevId(notes, 1), ps1Comment1.toString(),
getRevId(notes, 1), ps1Comment2.toString(),
getRevId(notes, 2), ps2Comment1.toString());
// Comments at each commit all have legacy format.
ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
assertThat(oldLog).hasSize(4);
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2)))
.containsExactly(ps1Comment1.key, true);
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
.containsExactly(ps1Comment1.key, true, ps1Comment2.key, true, ps2Comment1.key, true);
ChangeNotes oldNotes = notes;
migrate(project, migrator::migrateChanges, 1);
// Comment content is the same.
notes = newNotes(c);
assertNoDifferences(notes, oldNotes);
assertThat(getToStringRepresentations(notes.getComments()))
.containsExactly(
getRevId(notes, 1), ps1Comment1.toString(),
getRevId(notes, 1), ps1Comment2.toString(),
getRevId(notes, 2), ps2Comment1.toString());
// Comments at each commit all have JSON format.
ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
assertLogEqualExceptTrees(newLog, oldLog);
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2)))
.containsExactly(ps1Comment1.key, false);
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
.containsExactly(ps1Comment1.key, false, ps1Comment2.key, false, ps2Comment1.key, false);
}
@Test
public void migrateDraftComments() throws Exception {
Change c = newChange();
incrementPatchSet(c);
ChangeNotes notes = newNotes(c);
ObjectId origMetaId = notes.getMetaId();
Comment ownerCommentPs1 = newComment(notes, 1, "owner comment on ps1", changeOwner);
Comment ownerCommentPs2 = newComment(notes, 2, "owner comment on ps2", changeOwner);
Comment otherCommentPs1 = newComment(notes, 1, "other user comment on ps1", otherUser);
ByteOutputStream out1 = new ByteOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(1, ownerCommentPs1).build(), out1);
ByteOutputStream out2 = new ByteOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(2, ownerCommentPs2).build(), out2);
ByteOutputStream out3 = new ByteOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(1, otherCommentPs1).build(), out3);
try (Repository allUsersRepo = repoManager.openRepository(allUsers);
RevWalk allUsersRw = new RevWalk(allUsersRepo)) {
TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, allUsersRw);
testRepository
.branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
.commit()
.message("Review ps 1\n\nPatch-set: 1")
.add(ownerCommentPs1.revId, out1.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
testRepository
.branch(RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()))
.commit()
.message("Review ps 1\n\nPatch-set: 2")
.add(ownerCommentPs2.revId, out2.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
testRepository
.branch(RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()))
.commit()
.message("Review ps 2\n\nPatch-set: 2")
.add(otherCommentPs1.revId, out3.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
}
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
.containsExactly(
getRevId(notes, 1), ownerCommentPs1.toString(),
getRevId(notes, 2), ownerCommentPs2.toString());
assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
.containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
// Comments at each commit all have legacy format.
ImmutableList<RevCommit> oldOwnerLog =
log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
assertThat(oldOwnerLog).hasSize(2);
assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(0)))
.containsExactly(ownerCommentPs1.key, true);
assertThat(getLegacyFormatMapForDraftComments(notes, oldOwnerLog.get(1)))
.containsExactly(ownerCommentPs1.key, true, ownerCommentPs2.key, true);
ImmutableList<RevCommit> oldOtherLog =
log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
assertThat(oldOtherLog).hasSize(1);
assertThat(getLegacyFormatMapForDraftComments(notes, oldOtherLog.get(0)))
.containsExactly(otherCommentPs1.key, true);
ChangeNotes oldNotes = notes;
migrate(allUsers, migrator::migrateDrafts, 2);
assertNoDifferences(notes, oldNotes);
// Migration doesn't touch change ref.
assertThat(repo.exactRef(RefNames.changeMetaRef(c.getId())).getObjectId())
.isEqualTo(origMetaId);
// Comment content is the same.
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getDraftComments(changeOwner.getAccountId())))
.containsExactly(
getRevId(notes, 1), ownerCommentPs1.toString(),
getRevId(notes, 2), ownerCommentPs2.toString());
assertThat(getToStringRepresentations(notes.getDraftComments(otherUser.getAccountId())))
.containsExactly(getRevId(notes, 1), otherCommentPs1.toString());
// Comments at each commit all have JSON format.
ImmutableList<RevCommit> newOwnerLog =
log(allUsers, RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()));
assertLogEqualExceptTrees(newOwnerLog, oldOwnerLog);
assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(0)))
.containsExactly(ownerCommentPs1.key, false);
assertThat(getLegacyFormatMapForDraftComments(notes, newOwnerLog.get(1)))
.containsExactly(ownerCommentPs1.key, false, ownerCommentPs2.key, false);
ImmutableList<RevCommit> newOtherLog =
log(allUsers, RefNames.refsDraftComments(c.getId(), otherUser.getAccountId()));
assertLogEqualExceptTrees(newOtherLog, oldOtherLog);
assertThat(getLegacyFormatMapForDraftComments(notes, newOtherLog.get(0)))
.containsExactly(otherCommentPs1.key, false);
}
@Test
public void migrateMixOfJsonAndLegacyComments() throws Exception {
// 3 comments: legacy, JSON, legacy. Because adding a comment necessarily rewrites the entire
// note, these comments need to be on separate patch sets.
Change c = newChange();
incrementPatchSet(c);
incrementPatchSet(c);
ChangeNotes notes = newNotes(c);
Comment ps1Comment = newComment(notes, 1, "comment on ps1 (legacy)");
ByteOutputStream out1 = new ByteOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(1, ps1Comment).build(), out1);
TestRepository<Repository> testRepository = new TestRepository<>(repo, rw);
String metaRefName = RefNames.changeMetaRef(c.getId());
testRepository
.branch(metaRefName)
.commit()
.message("Review ps 1\n\nPatch-set: 1")
.add(ps1Comment.revId, out1.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
notes = newNotes(c);
ChangeUpdate update = newUpdate(c, changeOwner);
Comment ps2Comment = newComment(notes, 2, "comment on ps2 (JSON)");
update.putComment(Status.PUBLISHED, ps2Comment);
update.commit();
Comment ps3Comment = newComment(notes, 3, "comment on ps3 (legacy)");
ByteOutputStream out3 = new ByteOutputStream(0);
legacyChangeNoteWrite.buildNote(
ImmutableListMultimap.<Integer, Comment>builder().put(3, ps3Comment).build(), out3);
testRepository
.branch(metaRefName)
.commit()
.message("Review ps 3\n\nPatch-set: 3")
.add(ps3Comment.revId, out3.toString())
.author(serverIdent)
.committer(serverIdent)
.create();
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getComments()))
.containsExactly(
getRevId(notes, 1), ps1Comment.toString(),
getRevId(notes, 2), ps2Comment.toString(),
getRevId(notes, 3), ps3Comment.toString());
// Comments at each commit match expected format.
ImmutableList<RevCommit> oldLog = log(project, RefNames.changeMetaRef(c.getId()));
assertThat(oldLog).hasSize(6);
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(0))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(1))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(2))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
.containsExactly(ps1Comment.key, true);
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(4)))
.containsExactly(ps1Comment.key, true, ps2Comment.key, false);
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(5)))
.containsExactly(ps1Comment.key, true, ps2Comment.key, false, ps3Comment.key, true);
ChangeNotes oldNotes = notes;
migrate(project, migrator::migrateChanges, 1);
assertNoDifferences(notes, oldNotes);
// Comment content is the same.
notes = newNotes(c);
assertThat(getToStringRepresentations(notes.getComments()))
.containsExactly(
getRevId(notes, 1), ps1Comment.toString(),
getRevId(notes, 2), ps2Comment.toString(),
getRevId(notes, 3), ps3Comment.toString());
// Comments at each commit all have JSON format.
ImmutableList<RevCommit> newLog = log(project, RefNames.changeMetaRef(c.getId()));
assertLogEqualExceptTrees(newLog, oldLog);
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(0))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(1))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(2))).isEmpty();
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(3)))
.containsExactly(ps1Comment.key, false);
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(4)))
.containsExactly(ps1Comment.key, false, ps2Comment.key, false);
assertThat(getLegacyFormatMapForPublishedComments(notes, newLog.get(5)))
.containsExactly(ps1Comment.key, false, ps2Comment.key, false, ps3Comment.key, false);
}
@FunctionalInterface
interface MigrateFunction {
boolean call(
Project.NameKey project,
Repository repo,
RevWalk rw,
ObjectInserter ins,
BatchRefUpdate bru)
throws Exception;
}
private void migrate(Project.NameKey project, MigrateFunction func, int expectedCommands)
throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
bru.setAllowNonFastForwards(true);
assertThat(func.call(project, repo, rw, ins, bru)).isTrue();
assertThat(bru.getCommands()).hasSize(expectedCommands);
if (!bru.getCommands().isEmpty()) {
ins.flush();
RefUpdateUtil.executeChecked(bru, rw);
}
}
}
private Comment newComment(ChangeNotes notes, int psNum, String message) {
return newComment(notes, psNum, message, changeOwner);
}
private Comment newComment(
ChangeNotes notes, int psNum, String message, IdentifiedUser commenter) {
return newComment(
new PatchSet.Id(notes.getChangeId(), psNum),
"filename",
"uuid-" + uuidCounter.getAndIncrement(),
null,
0,
commenter,
null,
TimeUtil.nowTs(),
message,
(short) 1,
getRevId(notes, psNum).get(),
false);
}
private void incrementPatchSet(Change c) throws Exception {
TestChanges.incrementPatchSet(c);
RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
ChangeUpdate update = newUpdate(c, changeOwner);
update.setCommit(rw, commit);
update.commit();
}
private static RevId getRevId(ChangeNotes notes, int psNum) {
PatchSet.Id psId = new PatchSet.Id(notes.getChangeId(), psNum);
PatchSet ps = notes.getPatchSets().get(psId);
checkArgument(ps != null, "no patch set %s: %s", psNum, notes.getPatchSets());
return ps.getRevision();
}
private static ListMultimap<RevId, String> getToStringRepresentations(
ListMultimap<RevId, Comment> comments) {
// Use string representation for equality comparison in this test, because Comment#equals only
// compares keys.
return Multimaps.transformValues(comments, Comment::toString);
}
private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForPublishedComments(
ChangeNotes notes, ObjectId metaId) throws Exception {
return getLegacyFormatMap(project, notes.getChangeId(), metaId, Status.PUBLISHED);
}
private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMapForDraftComments(
ChangeNotes notes, ObjectId metaId) throws Exception {
return getLegacyFormatMap(allUsers, notes.getChangeId(), metaId, Status.DRAFT);
}
private ImmutableMap<Comment.Key, Boolean> getLegacyFormatMap(
Project.NameKey project, Change.Id changeId, ObjectId metaId, Status status)
throws Exception {
try (Repository repo = repoManager.openRepository(project);
ObjectReader reader = repo.newObjectReader();
RevWalk rw = new RevWalk(reader)) {
NoteMap noteMap = NoteMap.read(reader, rw.parseCommit(metaId));
RevisionNoteMap<ChangeRevisionNote> revNoteMap =
RevisionNoteMap.parse(
noteUtil.getChangeNoteJson(),
noteUtil.getLegacyChangeNoteRead(),
changeId,
reader,
noteMap,
status);
return revNoteMap
.revisionNotes
.values()
.stream()
.flatMap(crn -> crn.getComments().stream())
.collect(toImmutableMap(c -> c.key, c -> c.legacyFormat));
}
}
private ImmutableList<RevCommit> log(Project.NameKey project, String refName) throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
rw.sort(RevSort.TOPO);
rw.sort(RevSort.REVERSE);
Ref ref = repo.exactRef(refName);
checkArgument(ref != null, "missing ref: %s", refName);
rw.markStart(rw.parseCommit(ref.getObjectId()));
return ImmutableList.copyOf(rw);
}
}
private static void assertLogEqualExceptTrees(
ImmutableList<RevCommit> actualLog, ImmutableList<RevCommit> expectedLog) {
assertThat(actualLog).hasSize(expectedLog.size());
for (int i = 0; i < expectedLog.size(); i++) {
RevCommit actual = actualLog.get(i);
RevCommit expected = expectedLog.get(i);
assertThat(actual.getAuthorIdent())
.named("author of entry %s", i)
.isEqualTo(expected.getAuthorIdent());
assertThat(actual.getCommitterIdent())
.named("committer of entry %s", i)
.isEqualTo(expected.getCommitterIdent());
assertThat(actual.getFullMessage()).named("message of entry %s", i).isNotNull();
assertThat(actual.getFullMessage())
.named("message of entry %s", i)
.isEqualTo(expected.getFullMessage());
}
}
private void assertNoDifferences(ChangeNotes actual, ChangeNotes expected) throws Exception {
assertThat(
ChangeBundle.fromNotes(commentsUtil, actual)
.differencesFrom(ChangeBundle.fromNotes(commentsUtil, expected)))
.isEmpty();
}
}