summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGal Paikin <paiking@google.com>2021-12-23 13:42:59 +0100
committerDavid Ostrovsky <david@ostrovsky.org>2022-05-23 17:10:46 +0200
commite7b6b42dfd5ad727080c1e48fee5d79d490011f4 (patch)
tree11bdb2c928d8b16224e8648db9503f109ebf91c2
parent6bf056a620a531643dd56b34ad81ba03c39a11f1 (diff)
Create site program to persist copied votes on all changes
As an extension of If95c0f8b0, this site program makes sure all changes will have the Copied-Label. When moving to version 3.6, votes will not be copied over on demand but only computed by looking at Copied-Label in the storage. This is useful for open changes to ensure votes are not lost, but also on closed changes so that it's more clear which votes were done that enabled the submission of that change. Release-Notes: Add site program to persists copied votes on all changes Change-Id: I3fec6440c0dd675c71cff2b463f1b06bf3386530
-rw-r--r--java/com/google/gerrit/pgm/CopyApprovals.java138
-rw-r--r--java/com/google/gerrit/server/approval/ApprovalsUtil.java42
-rw-r--r--java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java108
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeUpdate.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java160
-rw-r--r--javatests/com/google/gerrit/acceptance/pgm/CopyApprovalsPgmIT.java51
6 files changed, 503 insertions, 0 deletions
diff --git a/java/com/google/gerrit/pgm/CopyApprovals.java b/java/com/google/gerrit/pgm/CopyApprovals.java
new file mode 100644
index 0000000000..73b0da63d7
--- /dev/null
+++ b/java/com/google/gerrit/pgm/CopyApprovals.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2022 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.pgm;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.index.IndexType;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.BatchProgramModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.ModuleOverloader;
+import com.google.gerrit.server.approval.RecursiveApprovalCopier;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.options.AutoFlush;
+import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.util.ReplicaUtil;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.multibindings.OptionalBinder;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class CopyApprovals extends SiteProgram {
+
+ private Injector dbInjector;
+ private Injector sysInjector;
+ private Injector cfgInjector;
+ private Config globalConfig;
+
+ @Inject private RecursiveApprovalCopier recursiveApprovalCopier;
+
+ @Override
+ public int run() throws Exception {
+ mustHaveValidSite();
+ dbInjector = createDbInjector();
+ cfgInjector = dbInjector.createChildInjector();
+ globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+ LifecycleManager dbManager = new LifecycleManager();
+ dbManager.add(dbInjector);
+ dbManager.start();
+
+ sysInjector = createSysInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
+ LifecycleManager sysManager = new LifecycleManager();
+ sysManager.add(sysInjector);
+ sysManager.start();
+
+ sysInjector.injectMembers(this);
+
+ try {
+ recursiveApprovalCopier.persist();
+ return 0;
+ } catch (Exception e) {
+ throw die(e.getMessage(), e);
+ } finally {
+ sysManager.stop();
+ dbManager.stop();
+ }
+ }
+
+ private Injector createSysInjector() {
+ Map<String, Integer> versions = new HashMap<>();
+ boolean replica = ReplicaUtil.isReplica(globalConfig);
+ List<Module> modules = new ArrayList<>();
+ Module indexModule;
+ IndexType indexType = IndexModule.getIndexType(dbInjector);
+ if (indexType.isLucene()) {
+ indexModule =
+ LuceneIndexModule.singleVersionWithExplicitVersions(
+ versions, 1, replica, AutoFlush.DISABLED);
+ } else if (indexType.isFake()) {
+ // Use Reflection so that we can omit the fake index binary in production code. Test code does
+ // compile the component in.
+ try {
+ Class<?> clazz = Class.forName("com.google.gerrit.index.testing.FakeIndexModule");
+ Method m =
+ clazz.getMethod(
+ "singleVersionWithExplicitVersions", Map.class, int.class, boolean.class);
+ indexModule = (Module) m.invoke(null, versions, 1, replica);
+ } catch (NoSuchMethodException
+ | ClassNotFoundException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ throw new IllegalStateException("can't create index", e);
+ }
+ } else {
+ throw new IllegalStateException("unsupported index.type = " + indexType);
+ }
+ modules.add(indexModule);
+ modules.add(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ OptionalBinder.newOptionalBinder(binder(), IsFirstInsertForEntry.class)
+ .setBinding()
+ .toInstance(IsFirstInsertForEntry.YES);
+ }
+ });
+
+ modules.add(
+ new FactoryModule() {
+ @Override
+ protected void configure() {
+ factory(ChangeResource.Factory.class);
+ }
+ });
+ modules.add(new BatchProgramModule(dbInjector));
+
+ return dbInjector.createChildInjector(
+ ModuleOverloader.override(
+ modules, LibModuleLoader.loadReindexModules(cfgInjector, versions, 1, replica)));
+ }
+}
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index c2e35d2b57..08ea6bc0fb 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -20,11 +20,14 @@ import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -352,6 +355,45 @@ public class ApprovalsUtil {
return approvalInference.forPatchSet(notes, patchSet, /* rw= */ null, /* repoConfig= */ null);
}
+ /**
+ * This method should only be used when we want to dynamically compute the approvals. Generally,
+ * the copied approvals are available in {@link ChangeNotes}. However, if the patch-set is just
+ * being created, we need to dynamically compute the approvals so that we can persist them in
+ * storage. The {@link RevWalk} and {@link Config} objects that are being used to create the new
+ * patch-set are required for this method. Here we also add those votes to the provided {@link
+ * ChangeUpdate} object.
+ */
+ public void persistCopiedApprovals(
+ ChangeNotes notes,
+ PatchSet patchSet,
+ RevWalk revWalk,
+ Config repoConfig,
+ ChangeUpdate changeUpdate) {
+ Set<PatchSetApproval> current =
+ ImmutableSet.copyOf(notes.getApprovalsWithCopied().get(notes.getCurrentPatchSet().id()));
+ Set<PatchSetApproval> inferred =
+ ImmutableSet.copyOf(approvalInference.forPatchSet(notes, patchSet, revWalk, repoConfig));
+
+ // Exempt granted timestamp from comparisson, otherwise, we would persist the copied
+ // labels every time this method is called.
+ Table<LabelId, Account.Id, Short> approvalTable = HashBasedTable.create();
+ for (PatchSetApproval psa : current) {
+ Account.Id id = psa.accountId();
+ approvalTable.put(psa.labelId(), id, psa.value());
+ }
+
+ for (PatchSetApproval psa : inferred) {
+ if (approvalTable.contains(psa.labelId(), psa.accountId())) {
+ Short v = approvalTable.get(psa.labelId(), psa.accountId());
+ if (v.shortValue() != psa.value()) {
+ changeUpdate.putCopiedApproval(psa);
+ }
+ } else {
+ changeUpdate.putCopiedApproval(psa);
+ }
+ }
+ }
+
public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
return approvalCache.get(notes, psId);
}
diff --git a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
new file mode 100644
index 0000000000..53c2241a6e
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2022 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.approval;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+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.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+public class RecursiveApprovalCopier {
+
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final GitRepositoryManager repositoryManager;
+ private final InternalUser.Factory internalUserFactory;
+ private final ApprovalsUtil approvalsUtil;
+
+ @Inject
+ public RecursiveApprovalCopier(
+ BatchUpdate.Factory batchUpdateFactory,
+ GitRepositoryManager repositoryManager,
+ InternalUser.Factory internalUserFactory,
+ ApprovalsUtil approvalsUtil) {
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.repositoryManager = repositoryManager;
+ this.internalUserFactory = internalUserFactory;
+ this.approvalsUtil = approvalsUtil;
+ }
+
+ public void persist()
+ throws UpdateException, RestApiException, RepositoryNotFoundException, IOException {
+ for (Project.NameKey project : repositoryManager.list()) {
+ persist(project);
+ }
+ }
+
+ public void persist(Project.NameKey project)
+ throws IOException, UpdateException, RestApiException, RepositoryNotFoundException {
+ try (BatchUpdate bu =
+ batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs());
+ Repository repository = repositoryManager.openRepository(project)) {
+ for (Ref changeMetaRef :
+ repository.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES).stream()
+ .filter(r -> r.getName().endsWith(RefNames.META_SUFFIX))
+ .collect(toImmutableList())) {
+ Change.Id changeId = Change.Id.fromRef(changeMetaRef.getName());
+ bu.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil));
+ }
+ bu.execute();
+ }
+ }
+
+ public void persist(Change change) throws UpdateException, RestApiException {
+ Project.NameKey project = change.getProject();
+ try (BatchUpdate bu =
+ batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs())) {
+ Change.Id changeId = change.getId();
+ bu.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil));
+ bu.execute();
+ }
+ }
+
+ private static class PersistCopiedVotesOp implements BatchUpdateOp {
+ private final ApprovalsUtil approvalsUtil;
+
+ PersistCopiedVotesOp(ApprovalsUtil approvalsUtil) {
+ this.approvalsUtil = approvalsUtil;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws IOException {
+ ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ approvalsUtil.persistCopiedApprovals(
+ ctx.getNotes(),
+ ctx.getNotes().getCurrentPatchSet(),
+ ctx.getRevWalk(),
+ ctx.getRepoView().getConfig(),
+ update);
+ return update.hasCopiedApprovals();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 5acea1bf34..cc9b1934c4 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -286,6 +286,10 @@ public class ChangeUpdate extends AbstractChangeUpdate {
copiedApprovals.add(copiedPatchSetApproval);
}
+ public boolean hasCopiedApprovals() {
+ return !copiedApprovals.isEmpty();
+ }
+
public void merge(SubmissionId submissionId, Iterable<SubmitRecord> submitRecords) {
this.status = Change.Status.MERGED;
this.submissionId = submissionId.toString();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
new file mode 100644
index 0000000000..4b8862e51a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2022 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.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.server.approval.RecursiveApprovalCopier;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+public class CopyApprovalsIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
+ @Inject private RecursiveApprovalCopier recursiveApprovalCopier;
+
+ @Test
+ public void multipleProjects() throws Exception {
+ projectOperations.newProject().name("secondProject").create();
+ TestRepository<InMemoryRepository> secondRepo = cloneProject(project, admin);
+
+ PushOneCommit.Result change1 = createChange();
+ gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.recommend());
+ PushOneCommit.Result change2 = createChange(secondRepo);
+ gApi.changes().id(change2.getChangeId()).current().review(ReviewInput.dislike());
+
+ // these amends are reworks so votes will not be copied.
+ amendChange(change1.getChangeId());
+ amendChange(change1.getChangeId());
+ amendChange(change1.getChangeId());
+
+ amendChange(change2.getChangeId(), "refs/for/master", admin, secondRepo);
+ amendChange(change2.getChangeId(), "refs/for/master", admin, secondRepo);
+ amendChange(change2.getChangeId(), "refs/for/master", admin, secondRepo);
+
+ // votes don't exist on the new patch-set.
+ assertThat(gApi.changes().id(change1.getChangeId()).current().votes()).isEmpty();
+ assertThat(gApi.changes().id(change2.getChangeId()).current().votes()).isEmpty();
+
+ // change the project config to make the vote that was not copied to be copied once we do the
+ // schema upgrade.
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ u.getConfig()
+ .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyAnyScore(/* copyAnyScore= */ true));
+ u.save();
+ }
+
+ recursiveApprovalCopier.persist();
+
+ ApprovalInfo vote1 =
+ Iterables.getOnlyElement(
+ gApi.changes().id(change1.getChangeId()).current().votes().values());
+ assertThat(vote1.value).isEqualTo(1);
+ assertThat(vote1._accountId).isEqualTo(admin.id().get());
+
+ ApprovalInfo vote2 =
+ Iterables.getOnlyElement(
+ gApi.changes().id(change2.getChangeId()).current().votes().values());
+ assertThat(vote2.value).isEqualTo(-1);
+ assertThat(vote2._accountId).isEqualTo(admin.id().get());
+ }
+
+ @Test
+ public void changeWithPersistedVotesNotHarmed() throws Exception {
+ // change the project config to copy all votes
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ u.getConfig()
+ .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyAnyScore(/* copyAnyScore= */ true));
+ u.save();
+ }
+
+ PushOneCommit.Result change = createChange();
+ gApi.changes().id(change.getChangeId()).current().review(ReviewInput.recommend());
+ amendChange(change.getChangeId());
+
+ // vote exists on new patch-set.
+ ApprovalInfo vote =
+ Iterables.getOnlyElement(
+ gApi.changes().id(change.getChangeId()).current().votes().values());
+
+ ChangeNotes notes = notesFactory.createChecked(project, change.getChange().getId()).load();
+ ImmutableListMultimap<PatchSet.Id, PatchSetApproval> multimap1 = notes.getApprovalsWithCopied();
+
+ recursiveApprovalCopier.persist(change.getChange().change());
+
+ ChangeNotes notes2 = notesFactory.createChecked(project, change.getChange().getId()).load();
+ ImmutableListMultimap<PatchSet.Id, PatchSetApproval> multimap2 =
+ notes2.getApprovalsWithCopied();
+ assertThat(multimap1).containsExactlyEntriesIn(multimap2);
+
+ // the vote hasn't changed.
+ assertThat(
+ Iterables.getOnlyElement(
+ gApi.changes().id(change.getChangeId()).current().votes().values()))
+ .isEqualTo(vote);
+ }
+
+ @Test
+ public void multipleChanges() throws Exception {
+ List<Result> changes = new ArrayList<>();
+
+ // The test also passes with 1000, but we replaced this number to 5 to speed up the test.
+ for (int i = 0; i < 5; i++) {
+ PushOneCommit.Result change = createChange();
+ gApi.changes().id(change.getChangeId()).current().review(ReviewInput.recommend());
+
+ // this amend is a rework so votes will not be copied.
+ amendChange(change.getChangeId());
+
+ changes.add(change);
+
+ // votes don't exist on the new patch-set for all changes.
+ assertThat(gApi.changes().id(change.getChangeId()).current().votes()).isEmpty();
+ }
+
+ // change the project config to make the vote that was not copied to be copied once we do the
+ // schema upgrade.
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ u.getConfig()
+ .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyAnyScore(/* copyAnyScore= */ true));
+ u.save();
+ }
+
+ recursiveApprovalCopier.persist(project);
+
+ for (PushOneCommit.Result change : changes) {
+ ApprovalInfo vote1 =
+ Iterables.getOnlyElement(
+ gApi.changes().id(change.getChangeId()).current().votes().values());
+ assertThat(vote1.value).isEqualTo(1);
+ assertThat(vote1._accountId).isEqualTo(admin.id().get());
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/CopyApprovalsPgmIT.java b/javatests/com/google/gerrit/acceptance/pgm/CopyApprovalsPgmIT.java
new file mode 100644
index 0000000000..ea359bf594
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/CopyApprovalsPgmIT.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2022 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.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.launcher.GerritLauncher;
+import org.junit.Test;
+
+@UseLocalDisk
+public class CopyApprovalsPgmIT extends StandaloneSiteTest {
+ @Test
+ public void programFinishesNormally() throws Exception {
+ // This test checks that we are able to set up the injector for the program and loop over
+ // changes. The actual migration logic is tested elsewhere.
+ Project.NameKey project = Project.nameKey("reindex-project-test");
+ try (ServerContext ctx = startServer()) {
+ GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+
+ gApi.projects().create(project.get());
+
+ ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
+ in.newBranch = true;
+ gApi.changes().create(in);
+ }
+
+ int exitCode =
+ GerritLauncher.mainImpl(
+ new String[] {
+ "CopyApprovals", "-d", sitePaths.site_path.toString(), "--show-stack-trace"
+ });
+ assertThat(exitCode).isEqualTo(0);
+ }
+}