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());
+ }
+}