diff options
author | Luca Milanesio <luca.milanesio@gmail.com> | 2022-05-23 14:18:13 +0100 |
---|---|---|
committer | Luca Milanesio <luca.milanesio@gmail.com> | 2022-05-23 18:27:21 +0000 |
commit | 2849b7a1806ea8a66538a6ccac88d20a50feddb2 (patch) | |
tree | 2a74696fc88f5f1d279f07612c9e4c4bbd4e9194 | |
parent | e7b6b42dfd5ad727080c1e48fee5d79d490011f4 (diff) |
Expose the CopyApprovals site program as a command
Allow Gerrit admins to copy the approval labels on running
server by exposing the functionality as a Gerrit SSH command.
The copy-approvals command is limited to the Gerrit admin
because may potentially cause a significant load on the server
and generate a substantial (e.g. hundreds of thousands) ref-update
events.
NOTE: This command is going to be dropped in v3.6 because the
logic for calculating the inferred labels has been removed.
Release-Notes: Introduce copy-approvals SSH command
Change-Id: I13f469b950e2de0b366f28e089cb114781dbdb11
6 files changed, 176 insertions, 8 deletions
diff --git a/Documentation/cmd-copy-approvals.txt b/Documentation/cmd-copy-approvals.txt new file mode 100644 index 0000000000..ba5344f259 --- /dev/null +++ b/Documentation/cmd-copy-approvals.txt @@ -0,0 +1,60 @@ += gerrit copy-approvals + +== NAME +gerrit copy-approvals - Copy all inferred approvals labels to the latest patch-set. + +== SYNOPSIS +[verse] +-- +_ssh_ -p <port> <host> _gerrit copy-approvals_ + [--verbose | -v] + [PROJECT]... +-- + +== DESCRIPTION +Gerrit has historically computed votes using an inference algorithm that +was cumulating them from all the patch-sets. That was not efficient since +it had to take into account copied votes from very old patchsets. +E.g, votes sometimes need to be copied from ps1 to ps10. + +Gerrit copy the approvals from the inferred votes to the latest patch-sets +once a change receives a new label update. + +The copy-approval command scans all the changes of a project and looks for +all votes that have not been copied yet, calculate the inferred score and +apply that as copied label to the latest patch-set. + +NOTE: The label copied as part of this process receives the grant date of +the timestamp of the copy-approval command execution, not the one associated +with the inferred vote. + +== OPTIONS + +--verbose:: +-v:: + Display projects/changes impacted by the label copy operation. + +== ACCESS +Only the user with MAINTAIN_SERVER permissions can run this command. + +== SCRIPTING +This command is intended to be used in scripts. + +== EXAMPLES + +Copy all inferred labels on the project 'foo' +---- +$ ssh -p 29418 review.example.com gerrit copy-approvals foo +---- + +Copy all inferred labels on all projects +---- +$ ssh -p 29418 review.example.com gerrit copy-approvals +---- + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +--------- diff --git a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java index 53c2241a6e..87df46523e 100644 --- a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java +++ b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.approval; import static com.google.common.collect.ImmutableList.toImmutableList; +import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; @@ -30,6 +31,7 @@ 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 java.util.function.Consumer; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -56,11 +58,11 @@ public class RecursiveApprovalCopier { public void persist() throws UpdateException, RestApiException, RepositoryNotFoundException, IOException { for (Project.NameKey project : repositoryManager.list()) { - persist(project); + persist(project, null); } } - public void persist(Project.NameKey project) + public void persist(Project.NameKey project, @Nullable Consumer<Change> labelsCopiedListener) throws IOException, UpdateException, RestApiException, RepositoryNotFoundException { try (BatchUpdate bu = batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs()); @@ -70,7 +72,7 @@ public class RecursiveApprovalCopier { .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.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil, labelsCopiedListener)); } bu.execute(); } @@ -81,28 +83,39 @@ public class RecursiveApprovalCopier { try (BatchUpdate bu = batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs())) { Change.Id changeId = change.getId(); - bu.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil)); + bu.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil, null)); bu.execute(); } } private static class PersistCopiedVotesOp implements BatchUpdateOp { private final ApprovalsUtil approvalsUtil; + private final Consumer<Change> listener; - PersistCopiedVotesOp(ApprovalsUtil approvalsUtil) { + PersistCopiedVotesOp( + ApprovalsUtil approvalsUtil, @Nullable Consumer<Change> labelsCopiedListener) { this.approvalsUtil = approvalsUtil; + this.listener = labelsCopiedListener; } @Override public boolean updateChange(ChangeContext ctx) throws IOException { - ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId()); + Change change = ctx.getChange(); + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); approvalsUtil.persistCopiedApprovals( ctx.getNotes(), ctx.getNotes().getCurrentPatchSet(), ctx.getRevWalk(), ctx.getRepoView().getConfig(), update); - return update.hasCopiedApprovals(); + + boolean labelsCopied = update.hasCopiedApprovals(); + + if (labelsCopied && listener != null) { + listener.accept(change); + } + + return labelsCopied; } } } diff --git a/java/com/google/gerrit/sshd/commands/CopyApprovalsCommand.java b/java/com/google/gerrit/sshd/commands/CopyApprovalsCommand.java new file mode 100644 index 0000000000..eacec289bb --- /dev/null +++ b/java/com/google/gerrit/sshd/commands/CopyApprovalsCommand.java @@ -0,0 +1,93 @@ +// 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.sshd.commands; + +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.server.approval.RecursiveApprovalCopier; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.sshd.CommandMetaData; +import com.google.gerrit.sshd.SshCommand; +import com.google.inject.Inject; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +@CommandMetaData( + name = "copy-approvals", + description = "Copy inferred approvals labels to the latest patch-set") +@RequiresCapability(GlobalCapability.MAINTAIN_SERVER) +public class CopyApprovalsCommand extends SshCommand { + + private final Set<Project.NameKey> projects = new HashSet<>(); + private final RecursiveApprovalCopier recursiveApprovalCopier; + private final GitRepositoryManager repositoryManager; + + @Argument( + index = 0, + required = false, + multiValued = true, + metaVar = "PROJECT", + usage = "list of projects to scan for approvals (default: all projects)") + void addProject(String project) { + projects.add(Project.nameKey(project)); + } + + @Option( + name = "--verbose", + aliases = "-v", + usage = "display projects/changes impacted by the label copy operation", + metaVar = "VERBOSE") + private boolean verbose; + + @Inject + public CopyApprovalsCommand( + RecursiveApprovalCopier recursiveApprovalCopier, GitRepositoryManager repositoryManager) { + this.recursiveApprovalCopier = recursiveApprovalCopier; + this.repositoryManager = repositoryManager; + } + + @Override + protected void run() throws Exception { + AtomicInteger changesCounter = new AtomicInteger(); + stdout.println( + "Copying inferred approvals labels on " + (projects.isEmpty() ? "all projects" : projects)); + + Set<Project.NameKey> projectsList = projects.isEmpty() ? repositoryManager.list() : projects; + + for (Project.NameKey project : projectsList) { + stdout.print("> " + project + " : "); + recursiveApprovalCopier.persist( + project, + c -> { + if (verbose) { + stdout.println(" [" + c.getProject() + "," + c.getChangeId() + "] updated"); + } + changesCounter.incrementAndGet(); + }); + stdout.println("DONE"); + } + + stdout.println( + "Labels copied for " + + projectsList.size() + + " project(s) have impacted " + + changesCounter.get() + + " change(s)"); + } +} diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java index e7fe22fe1b..1a08c43b5c 100644 --- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java +++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java @@ -109,6 +109,7 @@ public class DefaultCommandModule extends CommandModule { command(gerrit, RenameGroupCommand.class); command(gerrit, ReviewCommand.class); + command(gerrit, CopyApprovalsCommand.class); command(gerrit, SetProjectCommand.class); command(gerrit, SetReviewersCommand.class); command(gerrit, SetTopicCommand.class); diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java index 4b8862e51a..90ee13ef4d 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java @@ -147,7 +147,7 @@ public class CopyApprovalsIT extends AbstractDaemonTest { u.save(); } - recursiveApprovalCopier.persist(project); + recursiveApprovalCopier.persist(project, null); for (PushOneCommit.Result change : changes) { ApprovalInfo vote1 = diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java index 7224e194cf..469630f64a 100644 --- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java +++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java @@ -63,6 +63,7 @@ public class SshCommandsIT extends AbstractDaemonTest { private static final ImmutableList<String> MASTER_ONLY_ROOT_COMMANDS = ImmutableList.of( "ban-commit", + "copy-approvals", "create-account", "create-branch", "create-group", |