// 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.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIXED; import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIX_FAILED; import static com.google.gerrit.testutil.TestChanges.newPatchSet; import static java.util.Collections.singleton; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ProblemInfo; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; 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.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ConsistencyChecker; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; 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.RepoContext; import com.google.gerrit.testutil.InMemoryRepositoryManager; import com.google.gerrit.testutil.TestChanges; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; @NoHttpd public class ConsistencyCheckerIT extends AbstractDaemonTest { @Inject private ChangeControl.GenericFactory changeControlFactory; @Inject private Provider<ConsistencyChecker> checkerProvider; @Inject private IdentifiedUser.GenericFactory userFactory; @Inject private ChangeInserter.Factory changeInserterFactory; @Inject private PatchSetInserter.Factory patchSetInserterFactory; @Inject private ChangeNoteUtil noteUtil; @Inject @AnonymousCowardName private String anonymousCowardName; @Inject private Sequences sequences; @Inject private AccountsUpdate.Server accountsUpdate; private RevCommit tip; private Account.Id adminId; private ConsistencyChecker checker; @Before public void setUp() throws Exception { // Ignore client clone of project; repurpose as server-side TestRepository. testRepo = new TestRepository<>((InMemoryRepository) repoManager.openRepository(project)); tip = testRepo.getRevWalk().parseCommit(testRepo.getRepository().exactRef("HEAD").getObjectId()); adminId = admin.getId(); checker = checkerProvider.get(); } @Test public void validNewChange() throws Exception { assertNoProblems(insertChange(), null); } @Test public void validMergedChange() throws Exception { ChangeControl ctl = mergeChange(incrementPatchSet(insertChange())); assertNoProblems(ctl, null); } @Test public void missingOwner() throws Exception { TestAccount owner = accounts.create("missing"); ChangeControl ctl = insertChange(owner); accountsUpdate.create().deleteByKey(db, owner.getId()); assertProblems(ctl, null, problem("Missing change owner: " + owner.getId())); } @Test public void missingRepo() throws Exception { // NoteDb can't have a change without a repo. assume().that(notesMigration.enabled()).isFalse(); ChangeControl ctl = insertChange(); Project.NameKey name = ctl.getProject().getNameKey(); ((InMemoryRepositoryManager) repoManager).deleteRepository(name); assertProblems(ctl, null, problem("Destination repository not found: " + name)); } @Test public void invalidRevision() throws Exception { // NoteDb always parses the revision when inserting a patch set, so we can't // create an invalid patch set. assume().that(notesMigration.enabled()).isFalse(); ChangeControl ctl = insertChange(); PatchSet ps = newPatchSet( ctl.getChange().currentPatchSetId(), "fooooooooooooooooooooooooooooooooooooooo", adminId); db.patchSets().update(singleton(ps)); assertProblems( ctl, null, problem("Invalid revision on patch set 1: fooooooooooooooooooooooooooooooooooooooo")); } // No test for ref existing but object missing; InMemoryRepository won't let // us do such a thing. @Test public void patchSetObjectAndRefMissing() throws Exception { String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; ChangeControl ctl = insertChange(); PatchSet ps = insertMissingPatchSet(ctl, rev); ctl = reload(ctl); assertProblems( ctl, null, problem("Ref missing: " + ps.getId().toRefName()), problem("Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); } @Test public void patchSetObjectAndRefMissingWithFix() throws Exception { String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; ChangeControl ctl = insertChange(); PatchSet ps = insertMissingPatchSet(ctl, rev); ctl = reload(ctl); String refName = ps.getId().toRefName(); assertProblems( ctl, new FixInput(), problem("Ref missing: " + refName), problem("Object missing: patch set 2: " + rev)); } @Test public void patchSetRefMissing() throws Exception { ChangeControl ctl = insertChange(); testRepo.update( "refs/other/foo", ObjectId.fromString(psUtil.current(db, ctl.getNotes()).getRevision().get())); String refName = ctl.getChange().currentPatchSetId().toRefName(); deleteRef(refName); assertProblems(ctl, null, problem("Ref missing: " + refName)); } @Test public void patchSetRefMissingWithFix() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); testRepo.update("refs/other/foo", ObjectId.fromString(rev)); String refName = ctl.getChange().currentPatchSetId().toRefName(); deleteRef(refName); assertProblems( ctl, new FixInput(), problem("Ref missing: " + refName, FIXED, "Repaired patch set ref")); assertThat(testRepo.getRepository().exactRef(refName).getObjectId().name()).isEqualTo(rev); } @Test public void patchSetObjectAndRefMissingWithDeletingPatchSet() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; PatchSet ps2 = insertMissingPatchSet(ctl, rev2); ctl = reload(ctl); FixInput fix = new FixInput(); fix.deletePatchSetIfCommitMissing = true; assertProblems( ctl, fix, problem("Ref missing: " + ps2.getId().toRefName()), problem("Object missing: patch set 2: " + rev2, FIXED, "Deleted patch set")); ctl = reload(ctl); assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1); assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull(); assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull(); } @Test public void patchSetMultipleObjectsMissingWithDeletingPatchSets() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); String rev2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; PatchSet ps2 = insertMissingPatchSet(ctl, rev2); ctl = incrementPatchSet(reload(ctl)); PatchSet ps3 = psUtil.current(db, ctl.getNotes()); String rev4 = "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee"; PatchSet ps4 = insertMissingPatchSet(ctl, rev4); ctl = reload(ctl); FixInput fix = new FixInput(); fix.deletePatchSetIfCommitMissing = true; assertProblems( ctl, fix, problem("Ref missing: " + ps2.getId().toRefName()), problem("Object missing: patch set 2: " + rev2, FIXED, "Deleted patch set"), problem("Ref missing: " + ps4.getId().toRefName()), problem("Object missing: patch set 4: " + rev4, FIXED, "Deleted patch set")); ctl = reload(ctl); assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(3); assertThat(psUtil.get(db, ctl.getNotes(), ps1.getId())).isNotNull(); assertThat(psUtil.get(db, ctl.getNotes(), ps2.getId())).isNull(); assertThat(psUtil.get(db, ctl.getNotes(), ps3.getId())).isNotNull(); assertThat(psUtil.get(db, ctl.getNotes(), ps4.getId())).isNull(); } @Test public void onlyPatchSetObjectMissingWithFix() throws Exception { Change c = TestChanges.newChange(project, admin.getId(), sequences.nextChangeId()); PatchSet.Id psId = c.currentPatchSetId(); String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; PatchSet ps = newPatchSet(psId, rev, adminId); if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) { db.changes().insert(singleton(c)); db.patchSets().insert(singleton(ps)); } addNoteDbCommit( c.getId(), "Create change\n" + "\n" + "Patch-set: 1\n" + "Branch: " + c.getDest().get() + "\n" + "Change-id: " + c.getKey().get() + "\n" + "Subject: Bogus subject\n" + "Commit: " + rev + "\n" + "Groups: " + rev + "\n"); indexer.index(db, c.getProject(), c.getId()); IdentifiedUser user = userFactory.create(admin.getId()); ChangeControl ctl = changeControlFactory.controlFor(db, c.getProject(), c.getId(), user); FixInput fix = new FixInput(); fix.deletePatchSetIfCommitMissing = true; assertProblems( ctl, fix, problem("Ref missing: " + ps.getId().toRefName()), problem( "Object missing: patch set 1: " + rev, FIX_FAILED, "Cannot delete patch set; no patch sets would remain")); ctl = reload(ctl); assertThat(ctl.getChange().currentPatchSetId().get()).isEqualTo(1); assertThat(psUtil.current(db, ctl.getNotes())).isNotNull(); } @Test public void currentPatchSetMissing() throws Exception { // NoteDb can't create a change without a patch set. assume().that(notesMigration.enabled()).isFalse(); ChangeControl ctl = insertChange(); db.patchSets().deleteKeys(singleton(ctl.getChange().currentPatchSetId())); assertProblems(ctl, null, problem("Current patch set 1 not found")); } @Test public void duplicatePatchSetRevisions() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); String rev = ps1.getRevision().get(); ctl = incrementPatchSet(ctl, testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev))); assertProblems(ctl, null, problem("Multiple patch sets pointing to " + rev + ": [1, 2]")); } @Test public void missingDestRef() throws Exception { ChangeControl ctl = insertChange(); String ref = "refs/heads/master"; // Detach head so we're allowed to delete ref. testRepo.reset(testRepo.getRepository().exactRef(ref).getObjectId()); RefUpdate ru = testRepo.getRepository().updateRef(ref); ru.setForceUpdate(true); assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); assertProblems(ctl, null, problem("Destination ref not found (may be new branch): " + ref)); } @Test public void mergedChangeIsNotMerged() throws Exception { ChangeControl ctl = insertChange(); try (BatchUpdate bu = newUpdate(adminId)) { bu.addOp( ctl.getId(), new BatchUpdateOp() { @Override public boolean updateChange(ChangeContext ctx) throws OrmException { ctx.getChange().setStatus(Change.Status.MERGED); ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); return true; } }); bu.execute(); } ctl = reload(ctl); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); ObjectId tip = getDestRef(ctl); assertProblems( ctl, null, problem( "Patch set 1 (" + rev + ") is not merged into destination ref" + " refs/heads/master (" + tip.name() + "), but change status is MERGED")); } @Test public void newChangeIsMerged() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev))); assertProblems( ctl, null, problem( "Patch set 1 (" + rev + ") is merged into destination ref" + " refs/heads/master (" + rev + "), but change status is NEW")); } @Test public void newChangeIsMergedWithFix() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev))); assertProblems( ctl, new FixInput(), problem( "Patch set 1 (" + rev + ") is merged into destination ref" + " refs/heads/master (" + rev + "), but change status is NEW", FIXED, "Marked change as merged")); ctl = reload(ctl); assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED); assertNoProblems(ctl, null); } @Test public void extensionApiReturnsUpdatedValueAfterFix() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev))); ChangeInfo info = gApi.changes().id(ctl.getId().get()).info(); assertThat(info.status).isEqualTo(ChangeStatus.NEW); info = gApi.changes().id(ctl.getId().get()).check(new FixInput()); assertThat(info.status).isEqualTo(ChangeStatus.MERGED); } @Test public void expectedMergedCommitIsLatestPatchSet() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev))); FixInput fix = new FixInput(); fix.expectMergedAs = rev; assertProblems( ctl, fix, problem( "Patch set 1 (" + rev + ") is merged into destination ref" + " refs/heads/master (" + rev + "), but change status is NEW", FIXED, "Marked change as merged")); ctl = reload(ctl); assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED); assertNoProblems(ctl, null); } @Test public void expectedMergedCommitNotMergedIntoDestination() throws Exception { ChangeControl ctl = insertChange(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); RevCommit commit = testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)); testRepo.branch(ctl.getChange().getDest().get()).update(commit); FixInput fix = new FixInput(); RevCommit other = testRepo.commit().message(commit.getFullMessage()).create(); fix.expectMergedAs = other.name(); assertProblems( ctl, fix, problem( "Expected merged commit " + other.name() + " is not merged into destination ref refs/heads/master" + " (" + commit.name() + ")")); } @Test public void createNewPatchSetForExpectedMergeCommitWithNoChangeId() throws Exception { ChangeControl ctl = insertChange(); String dest = ctl.getChange().getDest().get(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); RevCommit commit = testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)); RevCommit mergedAs = testRepo.commit().parent(commit.getParent(0)).message(commit.getShortMessage()).create(); testRepo.getRevWalk().parseBody(mergedAs); assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).isEmpty(); testRepo.update(dest, mergedAs); assertNoProblems(ctl, null); FixInput fix = new FixInput(); fix.expectMergedAs = mergedAs.name(); assertProblems( ctl, fix, problem( "No patch set found for merged commit " + mergedAs.name(), FIXED, "Marked change as merged"), problem( "Expected merged commit " + mergedAs.name() + " has no associated patch set", FIXED, "Inserted as patch set 2")); ctl = reload(ctl); PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2); assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2); assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get()) .isEqualTo(mergedAs.name()); assertNoProblems(ctl, null); } @Test public void createNewPatchSetForExpectedMergeCommitWithChangeId() throws Exception { ChangeControl ctl = insertChange(); String dest = ctl.getChange().getDest().get(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); RevCommit commit = testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)); RevCommit mergedAs = testRepo .commit() .parent(commit.getParent(0)) .message( commit.getShortMessage() + "\n" + "\n" + "Change-Id: " + ctl.getChange().getKey().get() + "\n") .create(); testRepo.getRevWalk().parseBody(mergedAs); assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)) .containsExactly(ctl.getChange().getKey().get()); testRepo.update(dest, mergedAs); assertNoProblems(ctl, null); FixInput fix = new FixInput(); fix.expectMergedAs = mergedAs.name(); assertProblems( ctl, fix, problem( "No patch set found for merged commit " + mergedAs.name(), FIXED, "Marked change as merged"), problem( "Expected merged commit " + mergedAs.name() + " has no associated patch set", FIXED, "Inserted as patch set 2")); ctl = reload(ctl); PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2); assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2); assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get()) .isEqualTo(mergedAs.name()); assertNoProblems(ctl, null); } @Test public void expectedMergedCommitIsOldPatchSetOfSameChange() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); String rev1 = ps1.getRevision().get(); ctl = incrementPatchSet(ctl); PatchSet ps2 = psUtil.current(db, ctl.getNotes()); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev1))); FixInput fix = new FixInput(); fix.expectMergedAs = rev1; assertProblems( ctl, fix, problem("No patch set found for merged commit " + rev1, FIXED, "Marked change as merged"), problem( "Expected merge commit " + rev1 + " corresponds to patch set 1," + " not the current patch set 2", FIXED, "Deleted patch set"), problem( "Expected merge commit " + rev1 + " corresponds to patch set 1," + " not the current patch set 2", FIXED, "Inserted as patch set 3")); ctl = reload(ctl); PatchSet.Id psId3 = new PatchSet.Id(ctl.getId(), 3); assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId3); assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED); assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet()) .containsExactly(ps2.getId(), psId3); assertThat(psUtil.get(db, ctl.getNotes(), psId3).getRevision().get()).isEqualTo(rev1); } @Test public void expectedMergedCommitIsDanglingPatchSetOlderThanCurrent() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); // Create dangling ref so next ID in the database becomes 3. PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2); RevCommit commit2 = patchSetCommit(psId2); String rev2 = commit2.name(); testRepo.branch(psId2.toRefName()).update(commit2); ctl = incrementPatchSet(ctl); PatchSet ps3 = psUtil.current(db, ctl.getNotes()); assertThat(ps3.getId().get()).isEqualTo(3); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2))); FixInput fix = new FixInput(); fix.expectMergedAs = rev2; assertProblems( ctl, fix, problem("No patch set found for merged commit " + rev2, FIXED, "Marked change as merged"), problem( "Expected merge commit " + rev2 + " corresponds to patch set 2," + " not the current patch set 3", FIXED, "Deleted patch set"), problem( "Expected merge commit " + rev2 + " corresponds to patch set 2," + " not the current patch set 3", FIXED, "Inserted as patch set 4")); ctl = reload(ctl); PatchSet.Id psId4 = new PatchSet.Id(ctl.getId(), 4); assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId4); assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED); assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet()) .containsExactly(ps1.getId(), ps3.getId(), psId4); assertThat(psUtil.get(db, ctl.getNotes(), psId4).getRevision().get()).isEqualTo(rev2); } @Test public void expectedMergedCommitIsDanglingPatchSetNewerThanCurrent() throws Exception { ChangeControl ctl = insertChange(); PatchSet ps1 = psUtil.current(db, ctl.getNotes()); // Create dangling ref with no patch set. PatchSet.Id psId2 = new PatchSet.Id(ctl.getId(), 2); RevCommit commit2 = patchSetCommit(psId2); String rev2 = commit2.name(); testRepo.branch(psId2.toRefName()).update(commit2); testRepo .branch(ctl.getChange().getDest().get()) .update(testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev2))); FixInput fix = new FixInput(); fix.expectMergedAs = rev2; assertProblems( ctl, fix, problem("No patch set found for merged commit " + rev2, FIXED, "Marked change as merged"), problem( "Expected merge commit " + rev2 + " corresponds to patch set 2," + " not the current patch set 1", FIXED, "Inserted as patch set 2")); ctl = reload(ctl); assertThat(ctl.getChange().currentPatchSetId()).isEqualTo(psId2); assertThat(ctl.getChange().getStatus()).isEqualTo(Change.Status.MERGED); assertThat(psUtil.byChangeAsMap(db, ctl.getNotes()).keySet()) .containsExactly(ps1.getId(), psId2); assertThat(psUtil.get(db, ctl.getNotes(), psId2).getRevision().get()).isEqualTo(rev2); } @Test public void expectedMergedCommitWithMismatchedChangeId() throws Exception { ChangeControl ctl = insertChange(); String dest = ctl.getChange().getDest().get(); RevCommit parent = testRepo.branch(dest).commit().message("parent").create(); String rev = psUtil.current(db, ctl.getNotes()).getRevision().get(); RevCommit commit = testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)); testRepo.branch(dest).update(commit); String badId = "I0000000000000000000000000000000000000000"; RevCommit mergedAs = testRepo .commit() .parent(parent) .message(commit.getShortMessage() + "\n\nChange-Id: " + badId + "\n") .create(); testRepo.getRevWalk().parseBody(mergedAs); assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).containsExactly(badId); testRepo.update(dest, mergedAs); assertNoProblems(ctl, null); FixInput fix = new FixInput(); fix.expectMergedAs = mergedAs.name(); assertProblems( ctl, fix, problem( "Expected merged commit " + mergedAs.name() + " has Change-Id: " + badId + ", but expected " + ctl.getChange().getKey().get())); } @Test public void expectedMergedCommitMatchesMultiplePatchSets() throws Exception { ChangeControl ctl1 = insertChange(); PatchSet.Id psId1 = psUtil.current(db, ctl1.getNotes()).getId(); String dest = ctl1.getChange().getDest().get(); String rev = psUtil.current(db, ctl1.getNotes()).getRevision().get(); RevCommit commit = testRepo.getRevWalk().parseCommit(ObjectId.fromString(rev)); testRepo.branch(dest).update(commit); ChangeControl ctl2 = insertChange(); ctl2 = incrementPatchSet(ctl2, commit); PatchSet.Id psId2 = psUtil.current(db, ctl2.getNotes()).getId(); ChangeControl ctl3 = insertChange(); ctl3 = incrementPatchSet(ctl3, commit); PatchSet.Id psId3 = psUtil.current(db, ctl3.getNotes()).getId(); FixInput fix = new FixInput(); fix.expectMergedAs = commit.name(); assertProblems( ctl1, fix, problem( "Multiple patch sets for expected merged commit " + commit.name() + ": [" + psId1 + ", " + psId2 + ", " + psId3 + "]")); } private BatchUpdate newUpdate(Account.Id owner) { return batchUpdateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs()); } private ChangeControl insertChange() throws Exception { return insertChange(admin); } private ChangeControl insertChange(TestAccount owner) throws Exception { return insertChange(owner, "refs/heads/master"); } private ChangeControl insertChange(TestAccount owner, String dest) throws Exception { Change.Id id = new Change.Id(sequences.nextChangeId()); ChangeInserter ins; try (BatchUpdate bu = newUpdate(owner.getId())) { RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1)); ins = changeInserterFactory .create(id, commit, dest) .setValidate(false) .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(false) .setSendMail(false); bu.insertChange(ins).execute(); } // Return control for admin regardless of owner. return changeControlFactory.controlFor(db, ins.getChange(), userFactory.create(adminId)); } private PatchSet.Id nextPatchSetId(ChangeControl ctl) throws Exception { return ChangeUtil.nextPatchSetId(testRepo.getRepository(), ctl.getChange().currentPatchSetId()); } private ChangeControl incrementPatchSet(ChangeControl ctl) throws Exception { return incrementPatchSet(ctl, patchSetCommit(nextPatchSetId(ctl))); } private ChangeControl incrementPatchSet(ChangeControl ctl, RevCommit commit) throws Exception { PatchSetInserter ins; try (BatchUpdate bu = newUpdate(ctl.getChange().getOwner())) { ins = patchSetInserterFactory .create(ctl, nextPatchSetId(ctl), commit) .setValidate(false) .setFireRevisionCreated(false) .setNotify(NotifyHandling.NONE); bu.addOp(ctl.getId(), ins).execute(); } return reload(ctl); } private ChangeControl reload(ChangeControl ctl) throws Exception { return changeControlFactory.controlFor( db, ctl.getChange().getProject(), ctl.getId(), ctl.getUser()); } private RevCommit patchSetCommit(PatchSet.Id psId) throws Exception { RevCommit c = testRepo.commit().parent(tip).message("Change " + psId).create(); return testRepo.parseBody(c); } private PatchSet insertMissingPatchSet(ChangeControl ctl, String rev) throws Exception { // Don't use BatchUpdate since we're manually updating the meta ref rather // than using ChangeUpdate. String subject = "Subject for missing commit"; Change c = new Change(ctl.getChange()); PatchSet.Id psId = nextPatchSetId(ctl); c.setCurrentPatchSet(psId, subject, c.getOriginalSubject()); PatchSet ps = newPatchSet(psId, rev, adminId); if (PrimaryStorage.of(c) == PrimaryStorage.REVIEW_DB) { db.patchSets().insert(singleton(ps)); db.changes().update(singleton(c)); } addNoteDbCommit( c.getId(), "Update patch set " + psId.get() + "\n" + "\n" + "Patch-set: " + psId.get() + "\n" + "Commit: " + rev + "\n" + "Subject: " + subject + "\n"); indexer.index(db, c.getProject(), c.getId()); return ps; } private void deleteRef(String refName) throws Exception { RefUpdate ru = testRepo.getRepository().updateRef(refName, true); ru.setForceUpdate(true); assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); } private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception { if (!notesMigration.commitChangeWrites()) { return; } PersonIdent committer = serverIdent.get(); PersonIdent author = noteUtil.newIdent( accountCache.get(admin.getId()).getAccount(), committer.getWhen(), committer, anonymousCowardName); testRepo .branch(RefNames.changeMetaRef(id)) .commit() .author(author) .committer(committer) .message(commitMessage) .create(); } private ObjectId getDestRef(ChangeControl ctl) throws Exception { return testRepo.getRepository().exactRef(ctl.getChange().getDest().get()).getObjectId(); } private ChangeControl mergeChange(ChangeControl ctl) throws Exception { final ObjectId oldId = getDestRef(ctl); final ObjectId newId = ObjectId.fromString(psUtil.current(db, ctl.getNotes()).getRevision().get()); final String dest = ctl.getChange().getDest().get(); try (BatchUpdate bu = newUpdate(adminId)) { bu.addOp( ctl.getId(), new BatchUpdateOp() { @Override public void updateRepo(RepoContext ctx) throws IOException { ctx.addRefUpdate(oldId, newId, dest); } @Override public boolean updateChange(ChangeContext ctx) throws OrmException { ctx.getChange().setStatus(Change.Status.MERGED); ctx.getUpdate(ctx.getChange().currentPatchSetId()).fixStatus(Change.Status.MERGED); return true; } }); bu.execute(); } return reload(ctl); } private static ProblemInfo problem(String message) { ProblemInfo p = new ProblemInfo(); p.message = message; return p; } private static ProblemInfo problem(String message, ProblemInfo.Status status, String outcome) { ProblemInfo p = problem(message); p.status = checkNotNull(status); p.outcome = checkNotNull(outcome); return p; } private void assertProblems( ChangeControl ctl, @Nullable FixInput fix, ProblemInfo first, ProblemInfo... rest) { List<ProblemInfo> expected = new ArrayList<>(1 + rest.length); expected.add(first); expected.addAll(Arrays.asList(rest)); assertThat(checker.check(ctl, fix).problems()).containsExactlyElementsIn(expected).inOrder(); } private void assertNoProblems(ChangeControl ctl, @Nullable FixInput fix) { assertThat(checker.check(ctl, fix).problems()).isEmpty(); } }