Honor repo size quota enforcer during project import

Imports from GitHub did not consult repository-size quota enforcers,
allowing projects that exceeded configured limits to proceed, leading to
quota violations.

Introduce a preflight quota check to enforce limits early:
* Add `QuotaCheckStep`, which fetches the GitHub repository size and
  performs a `QuotaBackend` dry-run against `REPOSITORY_SIZE_GROUP`,
failing on error.
* Wire the step via Guice and `GitImporter`, placing it before clone,
  project creation so we fail fast with a clear error.
* Add `QuotaEnforcedException` for consistent, user-friendly error
  reporting when quota blocks an import.

When quotas allow, the import flow is unchanged. This ensures project
imports respect site policies rather than exceeding quota size limits.

Bug: Issue 445826646
Change-Id: I2ad8d35fac57581fc5e8fbc25134e0fdb90f52ab
diff --git a/README.md b/README.md
index c497a89..f5c3d02 100644
--- a/README.md
+++ b/README.md
@@ -197,3 +197,21 @@
 ```
 
 More information on Gerrit magic refs can be found [here](https://gerrit-review.googlesource.com/Documentation/intro-user.html#upload-change)
+
+#### Enforcing Size Quota
+
+When a repository is imported from GitHub, the plugin verifies that its size does not exceed any
+configured quota constraints.
+
+A common case is a quota defined by the
+[quota plugin](https://gerrit.googlesource.com/plugins/quota/+/refs/heads/master/src/main/resources/Documentation/config.md#quota),
+which can restrict how much storage space repositories may consume or how many repositories can be
+created within a given namespace.
+
+If an imported repository exceeds the allowed quota, the operation fails with an error message.
+For example:
+
+```text
+Unable to create repository foo/bar: project cannot be created because a quota for the namespace
+'foo/*' allows at most 3 projects and 3 projects already exist.
+```
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
index ef24191..add2657 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceHttpModule.java
@@ -33,6 +33,7 @@
 import com.googlesource.gerrit.plugins.github.git.MagicRefCheckStep;
 import com.googlesource.gerrit.plugins.github.git.ProtectedBranchesCheckStep;
 import com.googlesource.gerrit.plugins.github.git.PullRequestImportJob;
+import com.googlesource.gerrit.plugins.github.git.QuotaCheckStep;
 import com.googlesource.gerrit.plugins.github.git.ReplicateProjectStep;
 import com.googlesource.gerrit.plugins.github.notification.WebhookServlet;
 import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
@@ -79,6 +80,10 @@
             .build(MagicRefCheckStep.Factory.class));
     install(
         new FactoryModuleBuilder()
+            .implement(QuotaCheckStep.class, QuotaCheckStep.class)
+            .build(QuotaCheckStep.Factory.class));
+    install(
+        new FactoryModuleBuilder()
             .implement(PullRequestImportJob.class, PullRequestImportJob.class)
             .build(PullRequestImportJob.Factory.class));
     install(
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitImporter.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitImporter.java
index ff81411..d901d8a 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitImporter.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/GitImporter.java
@@ -28,6 +28,7 @@
   private static final Logger log = LoggerFactory.getLogger(GitImporter.class);
   private final ProtectedBranchesCheckStep.Factory protectedBranchesCheckFactory;
   private final MagicRefCheckStep.Factory magicRefCheckFactory;
+  private final QuotaCheckStep.Factory quotaCheckFactory;
   private final GitCloneStep.Factory cloneFactory;
   private final CreateProjectStep.Factory projectFactory;
   private final ReplicateProjectStep.Factory replicateFactory;
@@ -39,6 +40,7 @@
       CreateProjectStep.Factory projectFactory,
       ReplicateProjectStep.Factory replicateFactory,
       MagicRefCheckStep.Factory magicRefCheckFactory,
+      QuotaCheckStep.Factory quotaCheckFactory,
       JobExecutor executor,
       IdentifiedUser user) {
     super(executor, user);
@@ -47,6 +49,7 @@
     this.projectFactory = projectFactory;
     this.replicateFactory = replicateFactory;
     this.magicRefCheckFactory = magicRefCheckFactory;
+    this.quotaCheckFactory = quotaCheckFactory;
   }
 
   public void clone(int idx, String organisation, String repository, String description) {
@@ -57,12 +60,14 @@
       MagicRefCheckStep magicRefCheckStep = magicRefCheckFactory.create(organisation, repository);
       CreateProjectStep projectStep =
           projectFactory.create(organisation, repository, description, user.getUserName().get());
+      QuotaCheckStep quotaCheckStep = quotaCheckFactory.create(organisation, repository);
       ReplicateProjectStep replicateStep = replicateFactory.create(organisation, repository);
       GitImportJob gitCloneJob =
           new GitImportJob(
               idx,
               organisation,
               repository,
+              quotaCheckStep,
               protectedBranchesCheckStep,
               magicRefCheckStep,
               cloneStep,
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaCheckStep.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaCheckStep.java
new file mode 100644
index 0000000..057e565
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaCheckStep.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2025 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.github.git;
+
+import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaResponse;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.github.GitHubConfig;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class QuotaCheckStep extends ImportStep {
+  private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckStep.class);
+  private final QuotaBackend quotaBackend;
+
+  private static final long BYTES_PER_KB = 1024L;
+
+  public interface Factory {
+    QuotaCheckStep create(
+        @Assisted("organisation") String organisation, @Assisted("repository") String repository);
+  }
+
+  @Inject
+  public QuotaCheckStep(
+      QuotaBackend quotaBackend,
+      GitHubConfig config,
+      GitHubRepository.Factory gitHubRepoFactory,
+      @Assisted("organisation") String organisation,
+      @Assisted("repository") String repository) {
+    super(config.gitHubUrl, organisation, repository, gitHubRepoFactory);
+    this.quotaBackend = quotaBackend;
+  }
+
+  @Override
+  public void doImport(ProgressMonitor progress) throws GitException {
+    Project.NameKey fullProjectName =
+        Project.nameKey(getOrganisation() + "/" + getRepositoryName());
+    try {
+      progress.beginTask("Getting repository size", 1);
+      LOG.debug("{}|Getting repository size", fullProjectName);
+      long size = getRepository().getSize() * BYTES_PER_KB;
+      LOG.debug("{}|Repository size: {} Kb", fullProjectName, size);
+
+      LOG.debug("{}|Checking repository size is allowed by quota", fullProjectName);
+      if (size > 0) {
+        QuotaResponse.Aggregated aggregated =
+            quotaBackend.currentUser().project(fullProjectName).dryRun(REPOSITORY_SIZE_GROUP, size);
+        aggregated.throwOnError();
+      }
+      progress.update(1);
+    } catch (Exception e) {
+      LOG.error("{}|Quota does not allow importing repo", fullProjectName, e);
+      throw new QuotaEnforcedException(e.getMessage(), e);
+    } finally {
+      progress.endTask();
+    }
+    LOG.debug("{}|SUCCESS repository size is allowed", fullProjectName);
+  }
+
+  @Override
+  public boolean rollback() {
+    return true;
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaEnforcedException.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaEnforcedException.java
new file mode 100644
index 0000000..8ee4aea
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/QuotaEnforcedException.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2025 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.github.git;
+
+public class QuotaEnforcedException extends GitException {
+
+  private static final long serialVersionUID = 1L;
+
+  public QuotaEnforcedException(String message, Exception e) {
+    super(message, e);
+  }
+
+  @Override
+  public String getErrorDescription() {
+    return String.format("Quota enforcing error. %s", getMessage());
+  }
+}