Reftable: Add convert-ref-storage ssh command
Bug: Issue 14138
Change-Id: I0ddd2370619de287ec757597ba57e8b757611808
diff --git a/Documentation/cmd-convert-ref-storage.txt b/Documentation/cmd-convert-ref-storage.txt
new file mode 100644
index 0000000..aae385f
--- /dev/null
+++ b/Documentation/cmd-convert-ref-storage.txt
@@ -0,0 +1,58 @@
+= gerrit convert-ref-storage
+
+== NAME
+gerrit convert-ref-storage - Convert ref storage to reftable (experimental).
+
+A reftable file is a portable binary file format customized for reference storage.
+References are sorted, enabling linear scans, binary search lookup, and range scans.
+
+See also link:https://www.git-scm.com/docs/reftable for more details[reftable,role=external,window=_blank]
+
+== SYNOPSIS
+[verse]
+--
+_ssh_ -p <port> <host> _gerrit convert-ref-storage_
+ [--format <format>]
+ [--backup | -b]
+ [--reflogs | -r]
+ [--project <PROJECT> | -p <PROJECT>]
+--
+
+== DESCRIPTION
+Convert ref storage to reftable.
+
+== ACCESS
+Administrators
+
+== OPTIONS
+--project::
+-p::
+ Required; Name of the project for which the ref format should be changed.
+
+--format::
+ Format to convert to: `reftable` or `refdir`.
+ Default: reftable.
+
+--backup::
+-b::
+ Create backup of old ref storage format.
+ Default: true.
+
+--reflogs::
+-r::
+ Write reflogs to reftable.
+ Default: true.
+
+== EXAMPLES
+
+Convert ref format for project "core" to reftable:
+----
+$ ssh -p 29418 review.example.com gerrit convert-ref-format -p core
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index 2816429..ddfc115 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkState;
+
import com.google.gerrit.common.UsedAt;
import java.io.File;
import java.io.IOException;
@@ -30,6 +32,7 @@
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.events.ListenerList;
import org.eclipse.jgit.events.RepositoryEvent;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BaseRepositoryBuilder;
import org.eclipse.jgit.lib.ObjectDatabase;
@@ -391,4 +394,19 @@
throws IOException {
delegate.writeRebaseTodoFile(path, steps, append);
}
+
+ /**
+ * Converts between ref storage formats.
+ *
+ * @param format the format to convert to, either "reftable" or "refdir"
+ * @param writeLogs whether to write reflogs
+ * @param backup whether to make a backup of the old data
+ * @throws IOException on I/O problems.
+ */
+ public void convertRefStorage(String format, boolean writeLogs, boolean backup)
+ throws IOException {
+ checkState(
+ delegate instanceof FileRepository, "Repository is not an instance of FileRepository!");
+ ((FileRepository) delegate).convertRefStorage(format, writeLogs, backup);
+ }
}
diff --git a/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
new file mode 100644
index 0000000..21d90ed
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2021 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 static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+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.git.DelegateRepository;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "convert-ref-storage",
+ description = "Convert ref storage to reftable (experimental)",
+ runsAt = MASTER_OR_SLAVE)
+public class ConvertRefStorage extends SshCommand {
+ @Inject private GitRepositoryManager repoManager;
+
+ private enum StorageFormatOption {
+ reftable,
+ refdir,
+ }
+
+ @Option(
+ name = "--format",
+ usage = "storage format to convert to (reftable or refdir) (default: reftable)")
+ private StorageFormatOption storageFormat = StorageFormatOption.reftable;
+
+ @Option(
+ name = "--backup",
+ aliases = {"-b"},
+ usage = "create backup of old ref storage format (default: true)")
+ private boolean backup = true;
+
+ @Option(
+ name = "--reflogs",
+ aliases = {"-r"},
+ usage = "write reflogs to reftable (default: true)")
+ private boolean writeLogs = true;
+
+ @Option(
+ name = "--project",
+ aliases = {"-p"},
+ metaVar = "PROJECT",
+ required = true,
+ usage = "project for which the storage format should be changed")
+ private ProjectState projectState;
+
+ @Override
+ public void run() throws Exception {
+ enableGracefulStop();
+ Project.NameKey projectName = projectState.getNameKey();
+ try (Repository repo = repoManager.openRepository(projectName)) {
+ if (repo instanceof DelegateRepository) {
+ ((DelegateRepository) repo).convertRefStorage(storageFormat.name(), writeLogs, backup);
+ } else {
+ checkState(
+ repo instanceof FileRepository, "Repository is not an instance of FileRepository!");
+ ((FileRepository) repo).convertRefStorage(storageFormat.name(), writeLogs, backup);
+ }
+ } catch (RepositoryNotFoundException e) {
+ throw die("'" + projectName + "': not a git archive", e);
+ } catch (IOException e) {
+ throw die("Error converting: '" + projectName + "': " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index cfd17f4..8ee6a0d 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -47,6 +47,7 @@
command(gerrit, AproposCommand.class);
command(gerrit, BanCommitCommand.class);
command(gerrit, CloseConnection.class);
+ command(gerrit, ConvertRefStorage.class);
command(gerrit, FlushCaches.class);
command(gerrit, ListProjectsCommand.class);
command(gerrit, ListMembersCommand.class);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index bbe7b81..2b37cfd 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -45,6 +45,7 @@
ImmutableList.of(
"apropos",
"close-connection",
+ "convert-ref-storage",
"flush-caches",
"gc",
"logging",