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