Merge "Merge branch 'stable-3.4'"
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/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index f2a3e12..a66d3b5 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1450,7 +1450,7 @@
 
 By implementing the `com.google.gerrit.server.restapi.change.OnPostReview`
 interface plugins can extend the change message that is being posted when the
-[post review](rest-api-changes.html#set-review) REST endpoint is invoked.
+link:rest-api-changes.html#set-review[post review] REST endpoint is invoked.
 
 This is useful if certain approvals have a special meaning (e.g. custom logic
 that is implemented in Prolog submit rules, signal for triggering an action
@@ -1458,6 +1458,8 @@
 in the change message. This makes the effect of a given approval more
 transparent to the user.
 
+[[ui_extension]]
+== UI Extension
 
 [[actions]]
 === Actions
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",
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 7c63cc3..03a29da 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -83,13 +83,13 @@
         srcs = [plugin_name + ".js"],
     )
 
-def gerrit_js_bundle(name, srcs, entry_point):
+def gerrit_js_bundle(name, entry_point, srcs = []):
     """Produces a Gerrit JavaScript bundle archive.
 
     This rule bundles and minifies the javascript files of a frontend plugin and
     produces a file archive.
     Output of this rule is an archive with "${name}.jar" with specific layout for
-    Gerrit frontentd plugins. That archive should be provided to gerrit_plugin
+    Gerrit frontend plugins. That archive should be provided to gerrit_plugin
     rule as resource_jars attribute.
 
     Args:
@@ -97,8 +97,13 @@
       srcs: Plugin sources.
       entry_point: Plugin entry_point.
     """
+
+    bundle = name + "-bundle"
+    minified = name + ".min"
+    main = name + ".js"
+
     rollup_bundle(
-        name = name + "-bundle",
+        name = bundle,
         srcs = srcs,
         entry_point = entry_point,
         format = "iife",
@@ -110,22 +115,22 @@
     )
 
     terser_minified(
-        name = name + ".min",
+        name = minified,
         sourcemap = False,
-        src = name + "-bundle.js",
+        src = bundle,
     )
 
     native.genrule(
         name = name + "_rename_js",
-        srcs = [name + ".min"],
-        outs = [name + ".js"],
+        srcs = [minified],
+        outs = [main],
         cmd = "cp $< $@",
         output_to_bindir = True,
     )
 
     genrule2(
         name = name,
-        srcs = [name + ".js"],
+        srcs = [main],
         outs = [name + ".jar"],
         cmd = " && ".join([
             "mkdir $$TMP/static",