1431 lines
54 KiB
Java
1431 lines
54 KiB
Java
// Copyright (C) 2016 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.notedb;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
import static com.google.common.truth.Truth.assert_;
|
|
import static com.google.common.truth.TruthJUnit.assume;
|
|
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
|
|
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
|
|
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
import static java.util.concurrent.TimeUnit.DAYS;
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
|
import static java.util.stream.Collectors.toList;
|
|
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
|
import static org.junit.Assert.fail;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.Ordering;
|
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
|
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
|
|
import com.google.gerrit.acceptance.PushOneCommit;
|
|
import com.google.gerrit.acceptance.TestAccount;
|
|
import com.google.gerrit.common.TimeUtil;
|
|
import com.google.gerrit.common.data.GlobalCapability;
|
|
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.Side;
|
|
import com.google.gerrit.extensions.common.CommentInfo;
|
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
|
import com.google.gerrit.reviewdb.client.Account;
|
|
import com.google.gerrit.reviewdb.client.Change;
|
|
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
|
import com.google.gerrit.reviewdb.client.Patch;
|
|
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
|
import com.google.gerrit.reviewdb.client.Project;
|
|
import com.google.gerrit.reviewdb.client.RefNames;
|
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
|
import com.google.gerrit.server.ChangeUtil;
|
|
import com.google.gerrit.server.CommentsUtil;
|
|
import com.google.gerrit.server.Sequences;
|
|
import com.google.gerrit.server.change.PostReview;
|
|
import com.google.gerrit.server.change.Rebuild;
|
|
import com.google.gerrit.server.change.RevisionResource;
|
|
import com.google.gerrit.server.config.AllUsersName;
|
|
import com.google.gerrit.server.git.ProjectConfig;
|
|
import com.google.gerrit.server.git.RepoRefCache;
|
|
import com.google.gerrit.server.notedb.ChangeBundle;
|
|
import com.google.gerrit.server.notedb.ChangeBundleReader;
|
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
|
import com.google.gerrit.server.notedb.NoteDbChangeState;
|
|
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
|
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
|
|
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
|
|
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
|
|
import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
|
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
|
|
import com.google.gerrit.server.project.Util;
|
|
import com.google.gerrit.server.query.change.ChangeData;
|
|
import com.google.gerrit.server.update.BatchUpdate;
|
|
import com.google.gerrit.server.update.BatchUpdateOp;
|
|
import com.google.gerrit.server.update.ChangeContext;
|
|
import com.google.gerrit.server.update.UpdateException;
|
|
import com.google.gerrit.testutil.ConfigSuite;
|
|
import com.google.gerrit.testutil.NoteDbChecker;
|
|
import com.google.gerrit.testutil.NoteDbMode;
|
|
import com.google.gerrit.testutil.TestChanges;
|
|
import com.google.gerrit.testutil.TestTimeUtil;
|
|
import com.google.gwtorm.server.OrmException;
|
|
import com.google.gwtorm.server.OrmRuntimeException;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Provider;
|
|
import java.sql.Timestamp;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.TimeUnit;
|
|
import org.apache.http.Header;
|
|
import org.apache.http.message.BasicHeader;
|
|
import org.eclipse.jgit.junit.TestRepository;
|
|
import org.eclipse.jgit.lib.Config;
|
|
import org.eclipse.jgit.lib.ObjectId;
|
|
import org.eclipse.jgit.lib.ObjectInserter;
|
|
import org.eclipse.jgit.lib.Ref;
|
|
import org.eclipse.jgit.lib.RefUpdate;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
import org.junit.After;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
|
|
public class ChangeRebuilderIT extends AbstractDaemonTest {
|
|
@ConfigSuite.Default
|
|
public static Config defaultConfig() {
|
|
Config cfg = new Config();
|
|
cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
|
|
|
|
// Disable async reindex-if-stale check after index update. This avoids
|
|
// unintentional auto-rebuilding of the change in NoteDb during the read
|
|
// path of the reindex-if-stale check. For the purposes of this test, we
|
|
// want precise control over when auto-rebuilding happens.
|
|
cfg.setBoolean("index", null, "autoReindexIfStale", false);
|
|
|
|
return cfg;
|
|
}
|
|
|
|
@Inject private AllUsersName allUsers;
|
|
|
|
@Inject private NoteDbChecker checker;
|
|
|
|
@Inject private Rebuild rebuildHandler;
|
|
|
|
@Inject private Provider<ReviewDb> dbProvider;
|
|
|
|
@Inject private CommentsUtil commentsUtil;
|
|
|
|
@Inject private Provider<PostReview> postReview;
|
|
|
|
@Inject private TestChangeRebuilderWrapper rebuilderWrapper;
|
|
|
|
@Inject private BatchUpdate.Factory batchUpdateFactory;
|
|
|
|
@Inject private Sequences seq;
|
|
|
|
@Inject private ChangeBundleReader bundleReader;
|
|
|
|
@Inject private PatchSetInfoFactory patchSetInfoFactory;
|
|
|
|
@Before
|
|
public void setUp() throws Exception {
|
|
assume().that(NoteDbMode.readWrite()).isFalse();
|
|
TestTimeUtil.resetWithClockStep(1, SECONDS);
|
|
setNotesMigration(false, false);
|
|
}
|
|
|
|
@After
|
|
public void tearDown() {
|
|
TestTimeUtil.useSystemTime();
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private void setNotesMigration(boolean writeChanges, boolean readChanges) throws Exception {
|
|
notesMigration.setWriteChanges(writeChanges);
|
|
notesMigration.setReadChanges(readChanges);
|
|
db = atrScope.reopenDb().getReviewDbProvider().get();
|
|
|
|
if (notesMigration.readChangeSequence()) {
|
|
// Copy next ReviewDb ID to NoteDb.
|
|
seq.getChangeIdRepoSequence().set(db.nextChangeId());
|
|
} else {
|
|
// Copy next NoteDb ID to ReviewDb.
|
|
while (db.nextChangeId() < seq.getChangeIdRepoSequence().next()) {}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void changeFields() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void patchSets() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
r = amendChange(r.getChangeId());
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void publishedComment() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putComment(user, id, 1, "comment", null);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void publishedCommentAndReply() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putComment(user, id, 1, "comment", null);
|
|
Map<String, List<CommentInfo>> comments = getPublishedComments(id);
|
|
String parentUuid = comments.get("a.txt").get(0).id;
|
|
putComment(user, id, 1, "comment", parentUuid);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void patchSetWithNullGroups() throws Exception {
|
|
Timestamp ts = TimeUtil.nowTs();
|
|
Change c = TestChanges.newChange(project, user.getId(), seq.nextChangeId());
|
|
c.setCreatedOn(ts);
|
|
c.setLastUpdatedOn(ts);
|
|
PatchSet ps =
|
|
TestChanges.newPatchSet(
|
|
c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", user.getId());
|
|
ps.setCreatedOn(ts);
|
|
db.changes().insert(Collections.singleton(c));
|
|
db.patchSets().insert(Collections.singleton(ps));
|
|
|
|
assertThat(ps.getGroups()).isEmpty();
|
|
checker.rebuildAndCheckChanges(c.getId());
|
|
}
|
|
|
|
@Test
|
|
public void draftComment() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment", null);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void draftAndPublishedComment() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "draft comment", null);
|
|
putComment(user, id, 1, "published comment", null);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void publishDraftComment() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "draft comment", null);
|
|
publishDrafts(user, id);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void nullAccountId() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
|
|
// Events need to be otherwise identical for the account ID to be compared.
|
|
ChangeMessage msg1 = insertMessage(id, psId, user.getId(), TimeUtil.nowTs(), "message 1");
|
|
insertMessage(id, psId, null, msg1.getWrittenOn(), "message 2");
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void nullPatchSetId() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId1 = r.getPatchSetId();
|
|
Change.Id id = psId1.getParentKey();
|
|
|
|
// Events need to be otherwise identical for the PatchSet.ID to be compared.
|
|
ChangeMessage msg1 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 1");
|
|
insertMessage(id, null, user.getId(), msg1.getWrittenOn(), "message 2");
|
|
|
|
PatchSet.Id psId2 = amendChange(r.getChangeId()).getPatchSetId();
|
|
|
|
ChangeMessage msg3 = insertMessage(id, null, user.getId(), TimeUtil.nowTs(), "message 3");
|
|
insertMessage(id, null, user.getId(), msg3.getWrittenOn(), "message 4");
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
Map<String, PatchSet.Id> psIds = new HashMap<>();
|
|
for (ChangeMessage msg : notes.getChangeMessages()) {
|
|
PatchSet.Id psId = msg.getPatchSetId();
|
|
assertThat(psId).named("patchset for " + msg).isNotNull();
|
|
psIds.put(msg.getMessage(), psId);
|
|
}
|
|
// Patch set IDs were replaced during conversion process.
|
|
assertThat(psIds).containsEntry("message 1", psId1);
|
|
assertThat(psIds).containsEntry("message 2", psId1);
|
|
assertThat(psIds).containsEntry("message 3", psId2);
|
|
assertThat(psIds).containsEntry("message 4", psId2);
|
|
}
|
|
|
|
@Test
|
|
public void noWriteToNewRef() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
checker.assertNoChangeRef(project, id);
|
|
|
|
setNotesMigration(true, false);
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
|
|
// First write doesn't create the ref, but rebuilding works.
|
|
checker.assertNoChangeRef(project, id);
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNull();
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
// Now that there is a ref, writes are "turned on" for this change, and
|
|
// NoteDb stays up to date without explicit rebuilding.
|
|
gApi.changes().id(id.get()).topic(name("new-topic"));
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNotNull();
|
|
checker.checkChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void restApiNotFoundWhenNoteDbDisabled() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
exception.expect(ResourceNotFoundException.class);
|
|
rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Rebuild.Input());
|
|
}
|
|
|
|
@Test
|
|
public void rebuildViaRestApi() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
setNotesMigration(true, false);
|
|
|
|
checker.assertNoChangeRef(project, id);
|
|
rebuildHandler.apply(parseChangeResource(r.getChangeId()), new Rebuild.Input());
|
|
checker.checkChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void writeToNewRefForNewChange() throws Exception {
|
|
PushOneCommit.Result r1 = createChange();
|
|
Change.Id id1 = r1.getPatchSetId().getParentKey();
|
|
|
|
setNotesMigration(true, false);
|
|
gApi.changes().id(id1.get()).topic(name("a-topic"));
|
|
PushOneCommit.Result r2 = createChange();
|
|
Change.Id id2 = r2.getPatchSetId().getParentKey();
|
|
|
|
// Second change was created after NoteDb writes were turned on, so it was
|
|
// allowed to write to a new ref.
|
|
checker.checkChanges(id2);
|
|
|
|
// First change was created before NoteDb writes were turned on, so its meta
|
|
// ref doesn't exist until a manual rebuild.
|
|
checker.assertNoChangeRef(project, id1);
|
|
checker.rebuildAndCheckChanges(id1);
|
|
}
|
|
|
|
@Test
|
|
public void noteDbChangeState() throws Exception {
|
|
setNotesMigration(true, true);
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
|
|
ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(changeMetaId.name());
|
|
|
|
putDraft(user, id, 1, "comment by user", null);
|
|
ObjectId userDraftsId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
|
|
.isEqualTo(changeMetaId.name() + "," + user.getId() + "=" + userDraftsId.name());
|
|
|
|
putDraft(admin, id, 2, "comment by admin", null);
|
|
ObjectId adminDraftsId = getMetaRef(allUsers, refsDraftComments(id, admin.getId()));
|
|
assertThat(admin.getId().get()).isLessThan(user.getId().get());
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
|
|
.isEqualTo(
|
|
changeMetaId.name()
|
|
+ ","
|
|
+ admin.getId()
|
|
+ "="
|
|
+ adminDraftsId.name()
|
|
+ ","
|
|
+ user.getId()
|
|
+ "="
|
|
+ userDraftsId.name());
|
|
|
|
putDraft(admin, id, 2, "revised comment by admin", null);
|
|
adminDraftsId = getMetaRef(allUsers, refsDraftComments(id, admin.getId()));
|
|
assertThat(getUnwrappedDb().changes().get(id).getNoteDbState())
|
|
.isEqualTo(
|
|
changeMetaId.name()
|
|
+ ","
|
|
+ admin.getId()
|
|
+ "="
|
|
+ adminDraftsId.name()
|
|
+ ","
|
|
+ user.getId()
|
|
+ "="
|
|
+ userDraftsId.name());
|
|
}
|
|
|
|
@Test
|
|
public void rebuildAutomaticallyWhenChangeOutOfDate() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
|
|
setNotesMigration(false, false);
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
setInvalidNoteDbState(id);
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// On next NoteDb read, the change is transparently rebuilt.
|
|
setNotesMigration(true, true);
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Check that the bundles are equal.
|
|
ChangeBundle actual =
|
|
ChangeBundle.fromNotes(commentsUtil, notesFactory.create(dbProvider.get(), project, id));
|
|
ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
public void rebuildAutomaticallyWithinBatchUpdate() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
final Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Update ReviewDb and NoteDb, then revert the corresponding NoteDb change
|
|
// to simulate it failing.
|
|
NoteDbChangeState oldState = NoteDbChangeState.parse(getUnwrappedDb().changes().get(id));
|
|
String topic = name("a-topic");
|
|
gApi.changes().id(id.get()).topic(topic);
|
|
try (Repository repo = repoManager.openRepository(project)) {
|
|
new TestRepository<>(repo).update(RefNames.changeMetaRef(id), oldState.getChangeMetaId());
|
|
}
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// Next NoteDb read comes inside the transaction started by BatchUpdate. In
|
|
// reality this could be caused by a failed update happening between when
|
|
// the change is parsed by ChangesCollection and when the BatchUpdate
|
|
// executes. We simulate it here by using BatchUpdate directly and not going
|
|
// through an API handler.
|
|
final String msg = "message from BatchUpdate";
|
|
try (BatchUpdate bu =
|
|
batchUpdateFactory.create(
|
|
db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) {
|
|
bu.addOp(
|
|
id,
|
|
new BatchUpdateOp() {
|
|
@Override
|
|
public boolean updateChange(ChangeContext ctx) throws OrmException {
|
|
PatchSet.Id psId = ctx.getChange().currentPatchSetId();
|
|
ChangeMessage cm =
|
|
new ChangeMessage(
|
|
new ChangeMessage.Key(id, ChangeUtil.messageUuid()),
|
|
ctx.getAccountId(),
|
|
ctx.getWhen(),
|
|
psId);
|
|
cm.setMessage(msg);
|
|
ctx.getDb().changeMessages().insert(Collections.singleton(cm));
|
|
ctx.getUpdate(psId).setChangeMessage(msg);
|
|
return true;
|
|
}
|
|
});
|
|
try {
|
|
bu.execute();
|
|
fail("expected update to fail");
|
|
} catch (UpdateException e) {
|
|
assertThat(e.getMessage()).contains("cannot copy ChangeNotesState");
|
|
}
|
|
}
|
|
|
|
// TODO(dborowitz): Re-enable these assertions once we fix auto-rebuilding
|
|
// in the BatchUpdate path.
|
|
//// As an implementation detail, change wasn't actually rebuilt inside the
|
|
//// BatchUpdate transaction, but it was rebuilt during read for the
|
|
//// subsequent reindex. Thus it's impossible to actually observe an
|
|
//// out-of-date state in the caller.
|
|
// assertChangeUpToDate(true, id);
|
|
|
|
//// Check that the bundles are equal.
|
|
// ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
|
|
// ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
|
|
// ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
// assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
// assertThat(
|
|
// Iterables.transform(
|
|
// notes.getChangeMessages(),
|
|
// ChangeMessage::getMessage))
|
|
// .contains(msg);
|
|
// assertThat(actual.getChange().getTopic()).isEqualTo(topic);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildIgnoresErrorIfChangeIsUpToDateAfter() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
|
|
setNotesMigration(false, false);
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
setInvalidNoteDbState(id);
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// Force the next rebuild attempt to fail but also rebuild the change in the
|
|
// background.
|
|
rebuilderWrapper.stealNextUpdate();
|
|
setNotesMigration(true, true);
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Check that the bundles are equal.
|
|
ChangeBundle actual =
|
|
ChangeBundle.fromNotes(commentsUtil, notesFactory.create(dbProvider.get(), project, id));
|
|
ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
public void rebuildReturnsCorrectResultEvenIfSavingToNoteDbFailed() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
ObjectId oldMetaId = getMetaRef(project, changeMetaRef(id));
|
|
|
|
// Make a ReviewDb change behind NoteDb's back.
|
|
setNotesMigration(false, false);
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
setInvalidNoteDbState(id);
|
|
assertChangeUpToDate(false, id);
|
|
assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
|
|
|
|
// Force the next rebuild attempt to fail.
|
|
rebuilderWrapper.failNextUpdate();
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
|
|
|
|
// Not up to date, but the actual returned state matches anyway.
|
|
assertChangeUpToDate(false, id);
|
|
assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
|
|
ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
|
|
ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// Another rebuild attempt succeeds
|
|
notesFactory.create(dbProvider.get(), project, id);
|
|
assertThat(getMetaRef(project, changeMetaRef(id))).isNotEqualTo(oldMetaId);
|
|
assertChangeUpToDate(true, id);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildReturnsDraftResultWhenRebuildingInChangeNotesFails() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment by user", null);
|
|
assertChangeUpToDate(true, id);
|
|
|
|
ObjectId oldMetaId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
|
|
|
|
// Add a draft behind NoteDb's back.
|
|
setNotesMigration(false, false);
|
|
putDraft(user, id, 1, "second comment by user", null);
|
|
setInvalidNoteDbState(id);
|
|
assertDraftsUpToDate(false, id, user);
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
|
|
|
|
// Force the next rebuild attempt to fail (in ChangeNotes).
|
|
rebuilderWrapper.failNextUpdate();
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
|
|
notes.getDraftComments(user.getId());
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
|
|
|
|
// Not up to date, but the actual returned state matches anyway.
|
|
assertDraftsUpToDate(false, id, user);
|
|
ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
|
|
ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
|
|
// Another rebuild attempt succeeds
|
|
notesFactory.create(dbProvider.get(), project, id);
|
|
assertChangeUpToDate(true, id);
|
|
assertDraftsUpToDate(true, id, user);
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isNotEqualTo(oldMetaId);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildReturnsDraftResultWhenRebuildingInDraftCommentNotesFails() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment by user", null);
|
|
assertChangeUpToDate(true, id);
|
|
|
|
ObjectId oldMetaId = getMetaRef(allUsers, refsDraftComments(id, user.getId()));
|
|
|
|
// Add a draft behind NoteDb's back.
|
|
setNotesMigration(false, false);
|
|
putDraft(user, id, 1, "second comment by user", null);
|
|
|
|
ReviewDb db = getUnwrappedDb();
|
|
Change c = db.changes().get(id);
|
|
// Leave change meta ID alone so DraftCommentNotes does the rebuild.
|
|
ObjectId badSha = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
|
|
NoteDbChangeState bogusState =
|
|
new NoteDbChangeState(
|
|
id,
|
|
PrimaryStorage.REVIEW_DB,
|
|
Optional.of(
|
|
NoteDbChangeState.RefState.create(
|
|
NoteDbChangeState.parse(c).getChangeMetaId(),
|
|
ImmutableMap.of(user.getId(), badSha))),
|
|
Optional.empty());
|
|
c.setNoteDbState(bogusState.toString());
|
|
db.changes().update(Collections.singleton(c));
|
|
|
|
assertDraftsUpToDate(false, id, user);
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
|
|
|
|
// Force the next rebuild attempt to fail (in DraftCommentNotes).
|
|
rebuilderWrapper.failNextUpdate();
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
|
|
notes.getDraftComments(user.getId());
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isEqualTo(oldMetaId);
|
|
|
|
// Not up to date, but the actual returned state matches anyway.
|
|
assertChangeUpToDate(true, id);
|
|
assertDraftsUpToDate(false, id, user);
|
|
ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
|
|
ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
assertThat(actual.differencesFrom(expected)).isEmpty();
|
|
|
|
// Another rebuild attempt succeeds
|
|
notesFactory.create(dbProvider.get(), project, id).getDraftComments(user.getId());
|
|
assertChangeUpToDate(true, id);
|
|
assertDraftsUpToDate(true, id, user);
|
|
assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))).isNotEqualTo(oldMetaId);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildAutomaticallyWhenDraftsOutOfDate() throws Exception {
|
|
setNotesMigration(true, true);
|
|
setApiUser(user);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment", null);
|
|
assertDraftsUpToDate(true, id, user);
|
|
|
|
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
|
|
setNotesMigration(false, false);
|
|
putDraft(user, id, 1, "comment", null);
|
|
setInvalidNoteDbState(id);
|
|
assertDraftsUpToDate(false, id, user);
|
|
|
|
// On next NoteDb read, the drafts are transparently rebuilt.
|
|
setNotesMigration(true, true);
|
|
assertThat(gApi.changes().id(id.get()).current().drafts()).containsKey(PushOneCommit.FILE_NAME);
|
|
assertDraftsUpToDate(true, id, user);
|
|
}
|
|
|
|
@Test
|
|
public void pushCert() throws Exception {
|
|
// We don't have the code in our test harness to do signed pushes, so just
|
|
// use a hard-coded cert. This cert was actually generated by C git 2.2.0
|
|
// (albeit not for sending to Gerrit).
|
|
String cert =
|
|
"certificate version 0.1\n"
|
|
+ "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
|
|
+ "pushee git://localhost/repo.git\n"
|
|
+ "nonce 1433954361-bde756572d665bba81d8\n"
|
|
+ "\n"
|
|
+ "0000000000000000000000000000000000000000"
|
|
+ "b981a177396fb47345b7df3e4d3f854c6bea7"
|
|
+ "s/heads/master\n"
|
|
+ "-----BEGIN PGP SIGNATURE-----\n"
|
|
+ "Version: GnuPG v1\n"
|
|
+ "\n"
|
|
+ "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
|
|
+ "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
|
|
+ "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
|
|
+ "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
|
|
+ "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
|
|
+ "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
|
|
+ "=XFeC\n"
|
|
+ "-----END PGP SIGNATURE-----\n";
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
|
|
PatchSet ps = db.patchSets().get(psId);
|
|
ps.setPushCertificate(cert);
|
|
db.patchSets().update(Collections.singleton(ps));
|
|
indexer.index(db, project, id);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void emptyTopic() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
Change c = db.changes().get(id);
|
|
assertThat(c.getTopic()).isNull();
|
|
c.setTopic("");
|
|
db.changes().update(Collections.singleton(c));
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
|
|
// Rebuild and check was successful, but NoteDb doesn't support storing an
|
|
// empty topic, so it comes out as null.
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
assertThat(notes.getChange().getTopic()).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void commentBeforeFirstPatchSet() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
|
|
Change c = db.changes().get(id);
|
|
c.setCreatedOn(new Timestamp(c.getCreatedOn().getTime() - 5000));
|
|
db.changes().update(Collections.singleton(c));
|
|
indexer.index(db, project, id);
|
|
|
|
ReviewInput rin = new ReviewInput();
|
|
rin.message = "comment";
|
|
|
|
Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
|
|
assertThat(ts).isGreaterThan(c.getCreatedOn());
|
|
assertThat(ts).isLessThan(db.patchSets().get(psId).getCreatedOn());
|
|
RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
|
|
postReview.get().apply(revRsrc, rin, ts);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void commentPredatingChangeBySomeoneOtherThanOwner() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
Change c = db.changes().get(id);
|
|
|
|
ReviewInput rin = new ReviewInput();
|
|
rin.message = "comment";
|
|
|
|
Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000);
|
|
RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
|
|
setApiUser(user);
|
|
postReview.get().apply(revRsrc, rin, ts);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void noteDbUsesOriginalSubjectFromPatchSetAndIgnoresChangeField() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
String orig = r.getChange().change().getSubject();
|
|
r =
|
|
pushFactory
|
|
.create(
|
|
db,
|
|
admin.getIdent(),
|
|
testRepo,
|
|
orig + " v2",
|
|
PushOneCommit.FILE_NAME,
|
|
"new contents",
|
|
r.getChangeId())
|
|
.to("refs/for/master");
|
|
r.assertOkStatus();
|
|
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
Change c = db.changes().get(id);
|
|
|
|
c.setCurrentPatchSet(psId, c.getSubject(), "Bogus original subject");
|
|
db.changes().update(Collections.singleton(c));
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
Change nc = notes.getChange();
|
|
assertThat(nc.getSubject()).isEqualTo(c.getSubject());
|
|
assertThat(nc.getSubject()).isEqualTo(orig + " v2");
|
|
assertThat(nc.getOriginalSubject()).isNotEqualTo(c.getOriginalSubject());
|
|
assertThat(nc.getOriginalSubject()).isEqualTo(orig);
|
|
}
|
|
|
|
@Test
|
|
public void deleteDraftPS1WithNoOtherEntities() throws Exception {
|
|
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
|
|
PushOneCommit.Result r = push.to("refs/drafts/master");
|
|
push =
|
|
pushFactory.create(
|
|
db,
|
|
admin.getIdent(),
|
|
testRepo,
|
|
PushOneCommit.SUBJECT,
|
|
"b.txt",
|
|
"4711",
|
|
r.getChangeId());
|
|
r = push.to("refs/drafts/master");
|
|
PatchSet.Id psId = r.getPatchSetId();
|
|
Change.Id id = psId.getParentKey();
|
|
|
|
gApi.changes().id(r.getChangeId()).revision(1).delete();
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
assertThat(notes.getPatchSets().keySet()).containsExactly(psId);
|
|
}
|
|
|
|
@Test
|
|
public void ignorePatchLineCommentsOnPatchSet0() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change change = r.getChange().change();
|
|
Change.Id id = change.getId();
|
|
|
|
PatchLineComment comment =
|
|
new PatchLineComment(
|
|
new PatchLineComment.Key(
|
|
new Patch.Key(new PatchSet.Id(id, 0), PushOneCommit.FILE_NAME), "uuid"),
|
|
0,
|
|
user.getId(),
|
|
null,
|
|
TimeUtil.nowTs());
|
|
comment.setSide((short) 1);
|
|
comment.setMessage("message");
|
|
comment.setStatus(PatchLineComment.Status.PUBLISHED);
|
|
db.patchComments().insert(Collections.singleton(comment));
|
|
indexer.index(db, change.getProject(), id);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
assertThat(notes.getComments()).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
public void leadingSpacesInSubject() throws Exception {
|
|
String subj = " " + PushOneCommit.SUBJECT;
|
|
PushOneCommit push =
|
|
pushFactory.create(
|
|
db,
|
|
admin.getIdent(),
|
|
testRepo,
|
|
subj,
|
|
PushOneCommit.FILE_NAME,
|
|
PushOneCommit.FILE_CONTENT);
|
|
PushOneCommit.Result r = push.to("refs/for/master");
|
|
r.assertOkStatus();
|
|
Change change = r.getChange().change();
|
|
assertThat(change.getSubject()).isEqualTo(subj);
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
setNotesMigration(true, true);
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
assertThat(notes.getChange().getSubject()).isNotEqualTo(subj);
|
|
assertThat(notes.getChange().getSubject()).isEqualTo(PushOneCommit.SUBJECT);
|
|
}
|
|
|
|
@Test
|
|
public void createWithAutoRebuildingDisabled() throws Exception {
|
|
ReviewDb oldDb = db;
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
ChangeNotes oldNotes = notesFactory.create(db, project, id);
|
|
|
|
// Make a ReviewDb change behind NoteDb's back.
|
|
Change c = oldDb.changes().get(id);
|
|
assertThat(c.getTopic()).isNull();
|
|
String topic = name("a-topic");
|
|
c.setTopic(topic);
|
|
oldDb.changes().update(Collections.singleton(c));
|
|
|
|
c = oldDb.changes().get(c.getId());
|
|
ChangeNotes newNotes = notesFactory.createWithAutoRebuildingDisabled(c, null);
|
|
assertThat(newNotes.getChange().getTopic()).isNotEqualTo(topic);
|
|
assertThat(newNotes.getChange().getTopic()).isEqualTo(oldNotes.getChange().getTopic());
|
|
}
|
|
|
|
@Test
|
|
public void rebuildDeletesOldDraftRefs() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment", null);
|
|
|
|
Account.Id otherAccountId = new Account.Id(user.getId().get() + 1234);
|
|
String otherDraftRef = refsDraftComments(id, otherAccountId);
|
|
|
|
try (Repository repo = repoManager.openRepository(allUsers);
|
|
ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId sha = ins.insert(OBJ_BLOB, "garbage data".getBytes(UTF_8));
|
|
ins.flush();
|
|
RefUpdate ru = repo.updateRef(otherDraftRef);
|
|
ru.setExpectedOldObjectId(ObjectId.zeroId());
|
|
ru.setNewObjectId(sha);
|
|
assertThat(ru.update()).isEqualTo(RefUpdate.Result.NEW);
|
|
}
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
|
|
try (Repository repo = repoManager.openRepository(allUsers)) {
|
|
assertThat(repo.exactRef(otherDraftRef)).isNull();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void failWhenWritesDisabled() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isNull();
|
|
|
|
// Turning off writes causes failure.
|
|
setNotesMigration(false, true);
|
|
try {
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
fail("Expected write to fail");
|
|
} catch (RestApiException e) {
|
|
assertChangesReadOnly(e);
|
|
}
|
|
|
|
// Update was not written.
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isNull();
|
|
assertChangeUpToDate(true, id);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildWhenWritesDisabledWorksButDoesNotWrite() throws Exception {
|
|
setNotesMigration(true, true);
|
|
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
assertChangeUpToDate(true, id);
|
|
|
|
// Make a ReviewDb change behind NoteDb's back and ensure it's detected.
|
|
setNotesMigration(false, false);
|
|
gApi.changes().id(id.get()).topic(name("a-topic"));
|
|
setInvalidNoteDbState(id);
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// On next NoteDb read, change is rebuilt in-memory but not stored.
|
|
setNotesMigration(false, true);
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
|
|
assertChangeUpToDate(false, id);
|
|
|
|
// Attempting to write directly causes failure.
|
|
try {
|
|
gApi.changes().id(id.get()).topic(name("other-topic"));
|
|
fail("Expected write to fail");
|
|
} catch (RestApiException e) {
|
|
assertChangesReadOnly(e);
|
|
}
|
|
|
|
// Update was not written.
|
|
assertThat(gApi.changes().id(id.get()).info().topic).isEqualTo(name("a-topic"));
|
|
assertChangeUpToDate(false, id);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildChangeWithNoPatchSets() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
db.changes().beginTransaction(id);
|
|
try {
|
|
db.patchSets().delete(db.patchSets().byChange(id));
|
|
db.commit();
|
|
} finally {
|
|
db.rollback();
|
|
}
|
|
|
|
exception.expect(NoPatchSetsException.class);
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void rebuildEntitiesCreatedByImpersonation() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
PatchSet.Id psId = new PatchSet.Id(id, 1);
|
|
String prefix = "/changes/" + id + "/revisions/current/";
|
|
|
|
// For each of the entities that have a real user field, create one entity
|
|
// without impersonation and one with.
|
|
CommentInput ci = new CommentInput();
|
|
ci.path = Patch.COMMIT_MSG;
|
|
ci.side = Side.REVISION;
|
|
ci.line = 1;
|
|
ci.message = "comment without impersonation";
|
|
ReviewInput ri = new ReviewInput();
|
|
ri.label("Code-Review", -1);
|
|
ri.message = "message without impersonation";
|
|
ri.drafts = DraftHandling.KEEP;
|
|
ri.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
|
|
userRestSession.post(prefix + "review", ri).assertOK();
|
|
|
|
DraftInput di = new DraftInput();
|
|
di.path = Patch.COMMIT_MSG;
|
|
di.side = Side.REVISION;
|
|
di.line = 1;
|
|
di.message = "draft without impersonation";
|
|
userRestSession.put(prefix + "drafts", di).assertCreated();
|
|
|
|
allowRunAs();
|
|
try {
|
|
Header runAs = new BasicHeader("X-Gerrit-RunAs", user.id.toString());
|
|
ci.message = "comment with impersonation";
|
|
ri.message = "message with impersonation";
|
|
ri.label("Code-Review", 1);
|
|
adminRestSession.postWithHeader(prefix + "review", runAs, ri).assertOK();
|
|
|
|
di.message = "draft with impersonation";
|
|
adminRestSession.putWithHeader(prefix + "drafts", runAs, di).assertCreated();
|
|
} finally {
|
|
removeRunAs();
|
|
}
|
|
|
|
List<ChangeMessage> msgs =
|
|
Ordering.natural()
|
|
.onResultOf(ChangeMessage::getWrittenOn)
|
|
.sortedCopy(db.changeMessages().byChange(id));
|
|
assertThat(msgs).hasSize(3);
|
|
assertThat(msgs.get(1).getMessage()).endsWith("message without impersonation");
|
|
assertThat(msgs.get(1).getAuthor()).isEqualTo(user.id);
|
|
assertThat(msgs.get(1).getRealAuthor()).isEqualTo(user.id);
|
|
assertThat(msgs.get(2).getMessage()).endsWith("message with impersonation");
|
|
assertThat(msgs.get(2).getAuthor()).isEqualTo(user.id);
|
|
assertThat(msgs.get(2).getRealAuthor()).isEqualTo(admin.id);
|
|
|
|
List<PatchSetApproval> psas = db.patchSetApprovals().byChange(id).toList();
|
|
assertThat(psas).hasSize(1);
|
|
assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
|
|
assertThat(psas.get(0).getValue()).isEqualTo(1);
|
|
assertThat(psas.get(0).getAccountId()).isEqualTo(user.id);
|
|
assertThat(psas.get(0).getRealAccountId()).isEqualTo(admin.id);
|
|
|
|
Ordering<PatchLineComment> commentOrder =
|
|
Ordering.natural().onResultOf(PatchLineComment::getWrittenOn);
|
|
List<PatchLineComment> drafts =
|
|
commentOrder.sortedCopy(db.patchComments().draftByPatchSetAuthor(psId, user.id));
|
|
assertThat(drafts).hasSize(2);
|
|
assertThat(drafts.get(0).getMessage()).isEqualTo("draft without impersonation");
|
|
assertThat(drafts.get(0).getAuthor()).isEqualTo(user.id);
|
|
assertThat(drafts.get(0).getRealAuthor()).isEqualTo(user.id);
|
|
assertThat(drafts.get(1).getMessage()).isEqualTo("draft with impersonation");
|
|
assertThat(drafts.get(1).getAuthor()).isEqualTo(user.id);
|
|
assertThat(drafts.get(1).getRealAuthor()).isEqualTo(admin.id);
|
|
|
|
List<PatchLineComment> pub =
|
|
commentOrder.sortedCopy(db.patchComments().publishedByPatchSet(psId));
|
|
assertThat(pub).hasSize(2);
|
|
assertThat(pub.get(0).getMessage()).isEqualTo("comment without impersonation");
|
|
assertThat(pub.get(0).getAuthor()).isEqualTo(user.id);
|
|
assertThat(pub.get(0).getRealAuthor()).isEqualTo(user.id);
|
|
assertThat(pub.get(1).getMessage()).isEqualTo("comment with impersonation");
|
|
assertThat(pub.get(1).getAuthor()).isEqualTo(user.id);
|
|
assertThat(pub.get(1).getRealAuthor()).isEqualTo(admin.id);
|
|
}
|
|
|
|
@Test
|
|
public void laterEventsDependingOnEarlierPatchSetDontIntefereWithOtherPatchSets()
|
|
throws Exception {
|
|
PushOneCommit.Result r1 = createChange();
|
|
ChangeData cd = r1.getChange();
|
|
Change.Id id = cd.getId();
|
|
amendChange(cd.change().getKey().get());
|
|
TestTimeUtil.incrementClock(90, TimeUnit.DAYS);
|
|
|
|
ReviewInput rin = ReviewInput.approve();
|
|
rin.message = "Some very late message on PS1";
|
|
gApi.changes().id(id.get()).revision(1).review(rin);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
}
|
|
|
|
@Test
|
|
public void ignoreChangeMessageBeyondCurrentPatchSet() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId1 = r.getPatchSetId();
|
|
Change.Id id = psId1.getParentKey();
|
|
gApi.changes().id(id.get()).current().review(ReviewInput.recommend());
|
|
|
|
r = amendChange(r.getChangeId());
|
|
PatchSet.Id psId2 = r.getPatchSetId();
|
|
|
|
assertThat(db.patchSets().byChange(id)).hasSize(2);
|
|
assertThat(db.changeMessages().byPatchSet(psId2)).hasSize(1);
|
|
db.patchSets().deleteKeys(Collections.singleton(psId2));
|
|
|
|
checker.rebuildAndCheckChanges(psId2.getParentKey());
|
|
setNotesMigration(true, true);
|
|
|
|
ChangeData cd = changeDataFactory.create(db, project, id);
|
|
assertThat(cd.change().currentPatchSetId()).isEqualTo(psId1);
|
|
assertThat(cd.patchSets().stream().map(ps -> ps.getId()).collect(toList()))
|
|
.containsExactly(psId1);
|
|
PatchSet ps = cd.currentPatchSet();
|
|
assertThat(ps).isNotNull();
|
|
assertThat(ps.getId()).isEqualTo(psId1);
|
|
}
|
|
|
|
@Test
|
|
public void highestNumberedPatchSetIsNotCurrent() throws Exception {
|
|
PushOneCommit.Result r1 = createChange();
|
|
PatchSet.Id psId1 = r1.getPatchSetId();
|
|
Change.Id id = psId1.getParentKey();
|
|
PushOneCommit.Result r2 = amendChange(r1.getChangeId());
|
|
PatchSet.Id psId2 = r2.getPatchSetId();
|
|
|
|
try (BatchUpdate bu =
|
|
batchUpdateFactory.create(
|
|
db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) {
|
|
bu.addOp(
|
|
id,
|
|
new BatchUpdateOp() {
|
|
@Override
|
|
public boolean updateChange(ChangeContext ctx)
|
|
throws PatchSetInfoNotAvailableException {
|
|
ctx.getChange()
|
|
.setCurrentPatchSet(patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), psId1));
|
|
return true;
|
|
}
|
|
});
|
|
bu.execute();
|
|
}
|
|
ChangeNotes notes = notesFactory.create(db, project, id);
|
|
assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(psId1, psId2);
|
|
assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1);
|
|
|
|
assertThat(db.changes().get(id).currentPatchSetId()).isEqualTo(psId1);
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
setNotesMigration(true, true);
|
|
|
|
notes = notesFactory.create(db, project, id);
|
|
assertThat(psUtil.byChangeAsMap(db, notes).keySet()).containsExactly(psId1, psId2);
|
|
assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1);
|
|
}
|
|
|
|
@Test
|
|
public void resolveCommentsInheritsValueFromParentWhenUnspecified() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getPatchSetId().getParentKey();
|
|
putDraft(user, id, 1, "comment", true);
|
|
putDraft(user, id, 1, "newComment", null);
|
|
|
|
Map<String, List<CommentInfo>> comments = gApi.changes().id(id.get()).current().drafts();
|
|
for (List<CommentInfo> cList : comments.values()) {
|
|
for (CommentInfo ci : cList) {
|
|
assertThat(ci.unresolved).isEqualTo(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void rebuilderRespectsReadOnlyInNoteDbChangeState() throws Exception {
|
|
TestTimeUtil.resetWithClockStep(1, SECONDS);
|
|
PushOneCommit.Result r = createChange();
|
|
PatchSet.Id psId1 = r.getPatchSetId();
|
|
Change.Id id = psId1.getParentKey();
|
|
|
|
checker.rebuildAndCheckChanges(id);
|
|
setNotesMigration(true, true);
|
|
|
|
ReviewDb db = getUnwrappedDb();
|
|
Change c = db.changes().get(id);
|
|
NoteDbChangeState state = NoteDbChangeState.parse(c);
|
|
Timestamp until = new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS));
|
|
state = state.withReadOnlyUntil(until);
|
|
c.setNoteDbState(state.toString());
|
|
db.changes().update(Collections.singleton(c));
|
|
|
|
try {
|
|
rebuilderWrapper.rebuild(db, id);
|
|
assert_().fail("expected rebuild to fail");
|
|
} catch (OrmRuntimeException e) {
|
|
assertThat(e.getMessage()).contains("read-only until");
|
|
}
|
|
|
|
TestTimeUtil.setClock(new Timestamp(until.getTime() + MILLISECONDS.convert(1, SECONDS)));
|
|
rebuilderWrapper.rebuild(db, id);
|
|
}
|
|
|
|
@Test
|
|
public void commitWithCrLineEndings() throws Exception {
|
|
PushOneCommit.Result r =
|
|
createChange("Subject\r\rBody\r", PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
|
|
Change c = r.getChange().change();
|
|
|
|
// This assertion demonstrates an arguable bug in JGit's commit subject
|
|
// parsing, and shows how this kind of data might have gotten into
|
|
// ReviewDb. If that bug ever gets fixed upstream, this assert may start
|
|
// failing. If that happens, this test can be rewritten to directly set the
|
|
// subject field in ReviewDb.
|
|
assertThat(c.getSubject()).isEqualTo("Subject\r\rBody");
|
|
|
|
checker.rebuildAndCheckChanges(c.getId());
|
|
}
|
|
|
|
@Test
|
|
public void patchSetsOutOfOrder() throws Exception {
|
|
String id = createChange().getChangeId();
|
|
amendChange(id);
|
|
PushOneCommit.Result r = amendChange(id);
|
|
|
|
ChangeData cd = r.getChange();
|
|
PatchSet.Id psId3 = cd.change().currentPatchSetId();
|
|
assertThat(psId3.get()).isEqualTo(3);
|
|
|
|
PatchSet ps1 = db.patchSets().get(new PatchSet.Id(cd.getId(), 1));
|
|
PatchSet ps3 = db.patchSets().get(psId3);
|
|
assertThat(ps1.getCreatedOn()).isLessThan(ps3.getCreatedOn());
|
|
|
|
// Simulate an old Gerrit bug by setting the created timestamp of the latest
|
|
// patch set ID to the timestamp of PS1.
|
|
ps3.setCreatedOn(ps1.getCreatedOn());
|
|
db.patchSets().update(Collections.singleton(ps3));
|
|
|
|
checker.rebuildAndCheckChanges(cd.getId());
|
|
|
|
setNotesMigration(true, true);
|
|
cd = changeDataFactory.create(db, project, cd.getId());
|
|
assertThat(cd.change().currentPatchSetId()).isEqualTo(psId3);
|
|
|
|
List<PatchSet> patchSets = ImmutableList.copyOf(cd.patchSets());
|
|
assertThat(patchSets).hasSize(3);
|
|
|
|
PatchSet newPs1 = patchSets.get(0);
|
|
assertThat(newPs1.getId()).isEqualTo(ps1.getId());
|
|
assertThat(newPs1.getCreatedOn()).isEqualTo(ps1.getCreatedOn());
|
|
|
|
PatchSet newPs2 = patchSets.get(1);
|
|
assertThat(newPs2.getCreatedOn()).isGreaterThan(newPs1.getCreatedOn());
|
|
|
|
PatchSet newPs3 = patchSets.get(2);
|
|
assertThat(newPs3.getId()).isEqualTo(ps3.getId());
|
|
// Migrated with a newer timestamp than the original, to preserve ordering.
|
|
assertThat(newPs3.getCreatedOn()).isAtLeast(newPs2.getCreatedOn());
|
|
assertThat(newPs3.getCreatedOn()).isGreaterThan(ps1.getCreatedOn());
|
|
}
|
|
|
|
@Test
|
|
public void ignoreNoteDbStateWithNoCorrespondingRefWhenWritesAndReadsDisabled() throws Exception {
|
|
PushOneCommit.Result r = createChange();
|
|
Change.Id id = r.getChange().getId();
|
|
ReviewDb db = getUnwrappedDb();
|
|
Change c = db.changes().get(id);
|
|
c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
|
|
db.changes().update(Collections.singleton(c));
|
|
c = db.changes().get(id);
|
|
|
|
String refName = RefNames.changeMetaRef(id);
|
|
assertThat(getMetaRef(project, refName)).isNull();
|
|
|
|
ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
|
|
assertThat(notes.getChange().getRowVersion()).isEqualTo(c.getRowVersion());
|
|
|
|
notes = notesFactory.createChecked(dbProvider.get(), project, id);
|
|
assertThat(notes.getChange().getRowVersion()).isEqualTo(c.getRowVersion());
|
|
|
|
assertThat(getMetaRef(project, refName)).isNull();
|
|
}
|
|
|
|
private void assertChangesReadOnly(RestApiException e) throws Exception {
|
|
Throwable cause = e.getCause();
|
|
assertThat(cause).isInstanceOf(UpdateException.class);
|
|
assertThat(cause.getCause()).isInstanceOf(OrmException.class);
|
|
assertThat(cause.getCause()).hasMessageThat().isEqualTo(NoteDbUpdateManager.CHANGES_READ_ONLY);
|
|
}
|
|
|
|
private void setInvalidNoteDbState(Change.Id id) throws Exception {
|
|
ReviewDb db = getUnwrappedDb();
|
|
Change c = db.changes().get(id);
|
|
// In reality we would have NoteDb writes enabled, which would write a real
|
|
// state into this field. For tests however, we turn NoteDb writes off, so
|
|
// just use a dummy state to force ChangeNotes to view the notes as
|
|
// out-of-date.
|
|
c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
|
|
db.changes().update(Collections.singleton(c));
|
|
}
|
|
|
|
private void assertChangeUpToDate(boolean expected, Change.Id id) throws Exception {
|
|
try (Repository repo = repoManager.openRepository(project)) {
|
|
Change c = getUnwrappedDb().changes().get(id);
|
|
assertThat(c).isNotNull();
|
|
assertThat(c.getNoteDbState()).isNotNull();
|
|
assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(new RepoRefCache(repo)))
|
|
.isEqualTo(expected);
|
|
}
|
|
}
|
|
|
|
private void assertDraftsUpToDate(boolean expected, Change.Id changeId, TestAccount account)
|
|
throws Exception {
|
|
try (Repository repo = repoManager.openRepository(allUsers)) {
|
|
Change c = getUnwrappedDb().changes().get(changeId);
|
|
assertThat(c).isNotNull();
|
|
assertThat(c.getNoteDbState()).isNotNull();
|
|
NoteDbChangeState state = NoteDbChangeState.parse(c);
|
|
assertThat(state.areDraftsUpToDate(new RepoRefCache(repo), account.getId()))
|
|
.isEqualTo(expected);
|
|
}
|
|
}
|
|
|
|
private ObjectId getMetaRef(Project.NameKey p, String name) throws Exception {
|
|
try (Repository repo = repoManager.openRepository(p)) {
|
|
Ref ref = repo.exactRef(name);
|
|
return ref != null ? ref.getObjectId() : null;
|
|
}
|
|
}
|
|
|
|
private void putDraft(TestAccount account, Change.Id id, int line, String msg, Boolean unresolved)
|
|
throws Exception {
|
|
DraftInput in = new DraftInput();
|
|
in.line = line;
|
|
in.message = msg;
|
|
in.path = PushOneCommit.FILE_NAME;
|
|
in.unresolved = unresolved;
|
|
AcceptanceTestRequestScope.Context old = setApiUser(account);
|
|
try {
|
|
gApi.changes().id(id.get()).current().createDraft(in);
|
|
} finally {
|
|
atrScope.set(old);
|
|
}
|
|
}
|
|
|
|
private void putComment(TestAccount account, Change.Id id, int line, String msg, String inReplyTo)
|
|
throws Exception {
|
|
CommentInput in = new CommentInput();
|
|
in.line = line;
|
|
in.message = msg;
|
|
in.inReplyTo = inReplyTo;
|
|
ReviewInput rin = new ReviewInput();
|
|
rin.comments = new HashMap<>();
|
|
rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
|
|
rin.drafts = ReviewInput.DraftHandling.KEEP;
|
|
AcceptanceTestRequestScope.Context old = setApiUser(account);
|
|
try {
|
|
gApi.changes().id(id.get()).current().review(rin);
|
|
} finally {
|
|
atrScope.set(old);
|
|
}
|
|
}
|
|
|
|
private void publishDrafts(TestAccount account, Change.Id id) throws Exception {
|
|
ReviewInput rin = new ReviewInput();
|
|
rin.drafts = ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS;
|
|
AcceptanceTestRequestScope.Context old = setApiUser(account);
|
|
try {
|
|
gApi.changes().id(id.get()).current().review(rin);
|
|
} finally {
|
|
atrScope.set(old);
|
|
}
|
|
}
|
|
|
|
private ChangeMessage insertMessage(
|
|
Change.Id id, PatchSet.Id psId, Account.Id author, Timestamp ts, String message)
|
|
throws Exception {
|
|
ChangeMessage msg =
|
|
new ChangeMessage(new ChangeMessage.Key(id, ChangeUtil.messageUuid()), author, ts, psId);
|
|
msg.setMessage(message);
|
|
db.changeMessages().insert(Collections.singleton(msg));
|
|
|
|
Change c = db.changes().get(id);
|
|
if (ts.compareTo(c.getLastUpdatedOn()) > 0) {
|
|
c.setLastUpdatedOn(ts);
|
|
db.changes().update(Collections.singleton(c));
|
|
}
|
|
|
|
return msg;
|
|
}
|
|
|
|
private ReviewDb getUnwrappedDb() {
|
|
ReviewDb db = dbProvider.get();
|
|
return ReviewDbUtil.unwrapDb(db);
|
|
}
|
|
|
|
private void allowRunAs() throws Exception {
|
|
ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
|
|
Util.allow(
|
|
cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
|
|
saveProjectConfig(allProjects, cfg);
|
|
}
|
|
|
|
private void removeRunAs() throws Exception {
|
|
ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
|
|
Util.remove(
|
|
cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
|
|
saveProjectConfig(allProjects, cfg);
|
|
}
|
|
|
|
private Map<String, List<CommentInfo>> getPublishedComments(Change.Id id) throws Exception {
|
|
return gApi.changes().id(id.get()).current().comments();
|
|
}
|
|
}
|