Add `--recurse-submodules` flag to supporting commands

When a repository contains git submodules, the simple `git checkout`
command will leave the working tree with uncommited changes to the
submodule references (aka dirty). This then requires a second git command
to synchronize submodules.

The checkout command already has the `--recurse-submodules` argument
that will fetch submodules of checkout, ensuring that repository is in
consistent state.

Also using this flag with repositories that doesn't have submodules, the
checkout will succeed without any complaint.

The downside is that checkout command could issue multiple parallel
fetch calls that may generate high load on the server. This is why to
enable this feature, a modification to the plugin config is required.

This change adds a new plugin configuraiton option:
`download.recurseSubmodules`, by default set to `false`. That will add
`--recurse-submodules` flag to the "checkout", "branch", "reset" and
"pull" download commands.

Bug: Issue 302090044
Change-Id: I7b003dbd0687f173e8166a5ede931f773db5439c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/BranchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/BranchCommand.java
index 5949505..7bac2c4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/BranchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/BranchCommand.java
@@ -37,7 +37,8 @@
         + QuoteUtil.quote(url)
         + " "
         + ref
-        + " && git checkout -b change-"
+        + " && "
+        + getGitCheckout("-b change-")
         + id.replaceAll("/", "-")
         + " FETCH_HEAD";
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
index 61703e9..c9be564 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommand.java
@@ -33,7 +33,13 @@
 
   @Override
   String getCommand(String url, String ref, String id) {
-    return "git fetch " + QuoteUtil.quote(url) + " " + ref + " && git checkout FETCH_HEAD";
+    return "git fetch "
+        + QuoteUtil.quote(url)
+        + " "
+        + ref
+        + " && "
+        + getGitCheckout()
+        + "FETCH_HEAD";
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
index d4babb8..908b51d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/GitDownloadCommand.java
@@ -39,6 +39,7 @@
 
   private static final String DOWNLOAD = "download";
   private static final String UPLOADPACK = "uploadpack";
+  private static final String RECURSE_SUBMODULES = "recurseSubmodules";
   private static final String KEY_ALLOW_TIP_SHA1_IN_WANT = "allowTipSHA1InWant";
   private static final String KEY_ALLOW_REACHABLE_SHA1_IN_WANT = "allowReachableSHA1InWant";
   private static final String KEY_CHECK_FOR_HIDDEN_CHANGE_REFS = "checkForHiddenChangeRefs";
@@ -47,6 +48,7 @@
   private final boolean commandAllowed;
   private final GitRepositoryManager repoManager;
   private final boolean checkForHiddenChangeRefs;
+  protected final String recurseSubmodulesFlag;
 
   GitDownloadCommand(
       @GerritServerConfig Config cfg,
@@ -57,6 +59,8 @@
     this.repoManager = repoManager;
     this.checkForHiddenChangeRefs =
         cfg.getBoolean(DOWNLOAD, KEY_CHECK_FOR_HIDDEN_CHANGE_REFS, false);
+    this.recurseSubmodulesFlag =
+        cfg.getBoolean(DOWNLOAD, RECURSE_SUBMODULES, false) ? "--recurse-submodules " : "";
   }
 
   @Nullable
@@ -89,6 +93,14 @@
     return null;
   }
 
+  protected String getGitCheckout() {
+    return getGitCheckout("");
+  }
+
+  protected String getGitCheckout(String additionalArgs) {
+    return "git checkout " + recurseSubmodulesFlag + additionalArgs;
+  }
+
   private static boolean isValidUrl(String url) {
     try {
       new URIish(url);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
index 1084800..4358b09 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/PullCommand.java
@@ -33,6 +33,6 @@
 
   @Override
   String getCommand(String url, String ref, String id) {
-    return "git pull " + QuoteUtil.quote(url) + " " + ref;
+    return "git pull " + recurseSubmodulesFlag + QuoteUtil.quote(url) + " " + ref;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/download/command/ResetCommand.java b/src/main/java/com/googlesource/gerrit/plugins/download/command/ResetCommand.java
index 8ed7fa9..2fa79c3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/download/command/ResetCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/download/command/ResetCommand.java
@@ -33,6 +33,12 @@
 
   @Override
   String getCommand(String url, String ref, String id) {
-    return "git fetch " + QuoteUtil.quote(url) + " " + ref + " && git reset --hard FETCH_HEAD";
+    return "git fetch "
+        + QuoteUtil.quote(url)
+        + " "
+        + ref
+        + " && git reset "
+        + recurseSubmodulesFlag
+        + "--hard FETCH_HEAD";
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index d86b5df..526f298 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -23,6 +23,7 @@
   scheme = anon_git
   scheme = repo
   hide = ssh
+  recurseSubmodules = true
 ```
 
 The download section configures the allowed download methods.
@@ -85,6 +86,15 @@
 
     By default, no scheme will be hidden in the UI.
 
+<a id="download.hide">download.recurseSubmodules</a>
+    Add `--recurse-submodules` to the `checkout` command to update submodules
+    while checking out change.
+
+    Note: recursive checkout can issue multiple parallel fetch requests increasing
+    the load on the server.
+
+    By default, set to `false`.
+
 <a id="download.checkForHiddenChangeRefs">download.checkForHiddenChangeRefs</a>
 :	Whether the download commands should be adapted when the change
 	refs are hidden.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/command/BranchCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/command/BranchCommandTest.java
new file mode 100644
index 0000000..9150a3c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/command/BranchCommandTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 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.googlesource.gerrit.plugins.download.command;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.download.DownloadCommandTest;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class BranchCommandTest extends DownloadCommandTest {
+  private static final String TEST_URL = "unit.test/";
+  private static final String TEST_REF = "origin/main";
+  private static final String TEST_ID = "1234";
+  private static final String CHECKOUT_COMMAND_PREFIX =
+      "git fetch " + TEST_URL + " " + TEST_REF + " && ";
+
+  @Test
+  public void buildBranchCommand() {
+    BranchCommand branchCommand = newBranchCommand(defaultGerritConfig());
+
+    String actual = branchCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual)
+        .isEqualTo(CHECKOUT_COMMAND_PREFIX + "git checkout -b change-1234 FETCH_HEAD");
+  }
+
+  @Test
+  public void buildBranchCommandWithRecurseSubmodules() {
+    Config cfg = defaultGerritConfig();
+    cfg.setBoolean("download", null, "recurseSubmodules", true);
+    BranchCommand branchCommand = newBranchCommand(cfg);
+
+    String actual = branchCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual)
+        .isEqualTo(
+            CHECKOUT_COMMAND_PREFIX
+                + "git checkout --recurse-submodules -b change-1234 FETCH_HEAD");
+  }
+
+  private BranchCommand newBranchCommand(Config cfg) {
+    GitRepositoryManager gitRepositoryManagerMock = mock(GitRepositoryManager.class);
+
+    return new BranchCommand(cfg, new DownloadConfig(cfg), gitRepositoryManagerMock);
+  }
+
+  private Config defaultGerritConfig() {
+    Config cfg = new Config();
+    cfg.setString("download", null, "command", "branch");
+
+    return cfg;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommandTest.java
new file mode 100644
index 0000000..3fc7e4d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/command/CheckoutCommandTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 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.googlesource.gerrit.plugins.download.command;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.download.DownloadCommandTest;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class CheckoutCommandTest extends DownloadCommandTest {
+  private static final String TEST_URL = "unit.test/";
+  private static final String TEST_REF = "origin/main";
+  private static final String TEST_ID = "none";
+  private static final String CHECKOUT_COMMAND_PREFIX =
+      "git fetch " + TEST_URL + " " + TEST_REF + " && ";
+
+  @Test
+  public void buildCheckoutCommand() {
+    CheckoutCommand checkoutCommand = newCheckoutCommand(defaultGerritConfig());
+
+    String actual = checkoutCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual).isEqualTo(CHECKOUT_COMMAND_PREFIX + "git checkout FETCH_HEAD");
+  }
+
+  @Test
+  public void buildCheckoutCommandWithRecurseSubmodules() {
+    Config cfg = defaultGerritConfig();
+    cfg.setBoolean("download", null, "recurseSubmodules", true);
+    CheckoutCommand checkoutCommand = newCheckoutCommand(cfg);
+
+    String actual = checkoutCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual)
+        .isEqualTo(CHECKOUT_COMMAND_PREFIX + "git checkout --recurse-submodules FETCH_HEAD");
+  }
+
+  private CheckoutCommand newCheckoutCommand(Config cfg) {
+    GitRepositoryManager gitRepositoryManagerMock = mock(GitRepositoryManager.class);
+
+    return new CheckoutCommand(cfg, new DownloadConfig(cfg), gitRepositoryManagerMock);
+  }
+
+  private Config defaultGerritConfig() {
+    Config cfg = new Config();
+    cfg.setString("download", null, "command", "checkout");
+
+    return cfg;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/command/PullCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/command/PullCommandTest.java
new file mode 100644
index 0000000..e98d6a0
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/command/PullCommandTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 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.googlesource.gerrit.plugins.download.command;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.download.DownloadCommandTest;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class PullCommandTest extends DownloadCommandTest {
+  private static final String TEST_URL = "unit.test/";
+  private static final String TEST_REF = "origin/main";
+  private static final String TEST_ID = "none";
+  private static final String PULL_COMMAND_SUFFIX = TEST_URL + " " + TEST_REF;
+
+  @Test
+  public void buildPullCommand() {
+    PullCommand pullCommand = newPullCommand(defaultGerritConfig());
+
+    String actual = pullCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual).isEqualTo("git pull " + PULL_COMMAND_SUFFIX);
+  }
+
+  @Test
+  public void buildPullCommandWithRecurseSubmodules() {
+    Config cfg = defaultGerritConfig();
+    cfg.setBoolean("download", null, "recurseSubmodules", true);
+    PullCommand pullCommand = newPullCommand(cfg);
+
+    String actual = pullCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual).isEqualTo("git pull --recurse-submodules " + PULL_COMMAND_SUFFIX);
+  }
+
+  private PullCommand newPullCommand(Config cfg) {
+    GitRepositoryManager gitRepositoryManagerMock = mock(GitRepositoryManager.class);
+
+    return new PullCommand(cfg, new DownloadConfig(cfg), gitRepositoryManagerMock);
+  }
+
+  private Config defaultGerritConfig() {
+    Config cfg = new Config();
+    cfg.setString("download", null, "command", "pull");
+
+    return cfg;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/download/command/ResetCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/download/command/ResetCommandTest.java
new file mode 100644
index 0000000..81d15df
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/download/command/ResetCommandTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 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.googlesource.gerrit.plugins.download.command;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.download.DownloadCommandTest;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class ResetCommandTest extends DownloadCommandTest {
+  private static final String TEST_URL = "unit.test/";
+  private static final String TEST_REF = "origin/main";
+  private static final String TEST_ID = "none";
+  private static final String RESET_COMMAND_PREFIX =
+      "git fetch " + TEST_URL + " " + TEST_REF + " && ";
+
+  @Test
+  public void buildResetCommand() {
+    ResetCommand resetCommand = newResetCommand(defaultGerritConfig());
+
+    String actual = resetCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual).isEqualTo(RESET_COMMAND_PREFIX + "git reset --hard FETCH_HEAD");
+  }
+
+  @Test
+  public void buildResetCommandWithRecurseSubmodules() {
+    Config cfg = defaultGerritConfig();
+    cfg.setBoolean("download", null, "recurseSubmodules", true);
+    ResetCommand resetCommand = newResetCommand(cfg);
+
+    String actual = resetCommand.getCommand(TEST_URL, TEST_REF, TEST_ID);
+
+    assertThat(actual)
+        .isEqualTo(RESET_COMMAND_PREFIX + "git reset --recurse-submodules --hard FETCH_HEAD");
+  }
+
+  private ResetCommand newResetCommand(Config cfg) {
+    GitRepositoryManager gitRepositoryManagerMock = mock(GitRepositoryManager.class);
+
+    return new ResetCommand(cfg, new DownloadConfig(cfg), gitRepositoryManagerMock);
+  }
+
+  private Config defaultGerritConfig() {
+    Config cfg = new Config();
+    cfg.setString("download", null, "command", "reset");
+
+    return cfg;
+  }
+}