Merge "Limit the number of files per change"

This commit is contained in:
Patrick Hiesel
2020-01-07 16:13:11 +00:00
committed by Gerrit Code Review
5 changed files with 126 additions and 33 deletions

View File

@@ -44,6 +44,11 @@ import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ValidationError; import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.validators.ValidationMessage.Type; import com.google.gerrit.server.git.validators.ValidationMessage.Type;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.permissions.RefPermission;
@@ -76,7 +81,8 @@ import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.SystemReader;
/** /**
* Represents a list of CommitValidationListeners to run for a push to one branch of one project. * Represents a list of {@link CommitValidationListener}s to run for a push to one branch of one
* project.
*/ */
public class CommitValidators { public class CommitValidators {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -94,15 +100,16 @@ public class CommitValidators {
private final AllProjectsName allProjects; private final AllProjectsName allProjects;
private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker; private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
private final AccountValidator accountValidator; private final AccountValidator accountValidator;
private final String installCommitMsgHookCommand;
private final ProjectCache projectCache; private final ProjectCache projectCache;
private final ProjectConfig.Factory projectConfigFactory; private final ProjectConfig.Factory projectConfigFactory;
private final PatchListCache patchListCache;
private final Config config;
@Inject @Inject
Factory( Factory(
@GerritPersonIdent PersonIdent gerritIdent, @GerritPersonIdent PersonIdent gerritIdent,
DynamicItem<UrlFormatter> urlFormatter, DynamicItem<UrlFormatter> urlFormatter,
@GerritServerConfig Config cfg, @GerritServerConfig Config config,
PluginSetContext<CommitValidationListener> pluginValidators, PluginSetContext<CommitValidationListener> pluginValidators,
GitRepositoryManager repoManager, GitRepositoryManager repoManager,
AllUsersName allUsers, AllUsersName allUsers,
@@ -110,19 +117,20 @@ public class CommitValidators {
ExternalIdsConsistencyChecker externalIdsConsistencyChecker, ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
AccountValidator accountValidator, AccountValidator accountValidator,
ProjectCache projectCache, ProjectCache projectCache,
ProjectConfig.Factory projectConfigFactory) { ProjectConfig.Factory projectConfigFactory,
PatchListCache patchListCache) {
this.gerritIdent = gerritIdent; this.gerritIdent = gerritIdent;
this.urlFormatter = urlFormatter; this.urlFormatter = urlFormatter;
this.config = config;
this.pluginValidators = pluginValidators; this.pluginValidators = pluginValidators;
this.repoManager = repoManager; this.repoManager = repoManager;
this.allUsers = allUsers; this.allUsers = allUsers;
this.allProjects = allProjects; this.allProjects = allProjects;
this.externalIdsConsistencyChecker = externalIdsConsistencyChecker; this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
this.accountValidator = accountValidator; this.accountValidator = accountValidator;
this.installCommitMsgHookCommand =
cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
this.projectCache = projectCache; this.projectCache = projectCache;
this.projectConfigFactory = projectConfigFactory; this.projectConfigFactory = projectConfigFactory;
this.patchListCache = patchListCache;
} }
public CommitValidators forReceiveCommits( public CommitValidators forReceiveCommits(
@@ -146,12 +154,7 @@ public class CommitValidators {
new CommitterUploaderValidator(user, perm, urlFormatter.get()), new CommitterUploaderValidator(user, perm, urlFormatter.get()),
new SignedOffByValidator(user, perm, projectState), new SignedOffByValidator(user, perm, projectState),
new ChangeIdValidator( new ChangeIdValidator(
projectState, projectState, user, urlFormatter.get(), config, sshInfo, change),
user,
urlFormatter.get(),
installCommitMsgHookCommand,
sshInfo,
change),
new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects), new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
new BannedCommitsValidator(rejectCommits), new BannedCommitsValidator(rejectCommits),
new PluginCommitValidationListener(pluginValidators, skipValidation), new PluginCommitValidationListener(pluginValidators, skipValidation),
@@ -176,14 +179,10 @@ public class CommitValidators {
new ProjectStateValidationListener(projectState), new ProjectStateValidationListener(projectState),
new AmendedGerritMergeCommitValidationListener(perm, gerritIdent), new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
new AuthorUploaderValidator(user, perm, urlFormatter.get()), new AuthorUploaderValidator(user, perm, urlFormatter.get()),
new FileCountValidator(patchListCache, config),
new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())), new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())),
new ChangeIdValidator( new ChangeIdValidator(
projectState, projectState, user, urlFormatter.get(), config, sshInfo, change),
user,
urlFormatter.get(),
installCommitMsgHookCommand,
sshInfo,
change),
new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects), new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
new PluginCommitValidationListener(pluginValidators), new PluginCommitValidationListener(pluginValidators),
new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker), new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -268,14 +267,14 @@ public class CommitValidators {
ProjectState projectState, ProjectState projectState,
IdentifiedUser user, IdentifiedUser user,
UrlFormatter urlFormatter, UrlFormatter urlFormatter,
String installCommitMsgHookCommand, Config config,
SshInfo sshInfo, SshInfo sshInfo,
Change change) { Change change) {
this.projectState = projectState; this.projectState = projectState;
this.urlFormatter = urlFormatter;
this.installCommitMsgHookCommand = installCommitMsgHookCommand;
this.sshInfo = sshInfo;
this.user = user; this.user = user;
this.urlFormatter = urlFormatter;
installCommitMsgHookCommand = config.getString("gerrit", null, "installCommitMsgHookCommand");
this.sshInfo = sshInfo;
this.change = change; this.change = change;
} }
@@ -387,6 +386,40 @@ public class CommitValidators {
} }
} }
/** Limits the number of files per change. */
private static class FileCountValidator implements CommitValidationListener {
private final PatchListCache patchListCache;
private final int maxFileCount;
FileCountValidator(PatchListCache patchListCache, Config config) {
this.patchListCache = patchListCache;
maxFileCount = config.getInt("change", null, "maxFiles", 50_000);
}
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
PatchListKey patchListKey =
PatchListKey.againstBase(
receiveEvent.commit.getId(), receiveEvent.commit.getParentCount());
DiffSummaryKey diffSummaryKey = DiffSummaryKey.fromPatchListKey(patchListKey);
try {
DiffSummary diffSummary =
patchListCache.getDiffSummary(diffSummaryKey, receiveEvent.project.getNameKey());
if (diffSummary.getPaths().size() > maxFileCount) {
throw new CommitValidationException(
String.format(
"Exceeding maximum number of files per change (%d > %d)",
diffSummary.getPaths().size(), maxFileCount));
}
} catch (PatchListNotAvailableException e) {
logger.atWarning().withCause(e).log("Failed to validate file count");
}
return Collections.emptyList();
}
}
/** If this is the special project configuration branch, validate the config. */ /** If this is the special project configuration branch, validate the config. */
public static class ConfigValidator implements CommitValidationListener { public static class ConfigValidator implements CommitValidationListener {
private final ProjectConfig.Factory projectConfigFactory; private final ProjectConfig.Factory projectConfigFactory;

View File

@@ -138,11 +138,10 @@ public class PatchListCacheImpl implements PatchListCache {
throws PatchListNotAvailableException { throws PatchListNotAvailableException {
Project.NameKey project = change.getProject(); Project.NameKey project = change.getProject();
ObjectId b = patchSet.commitId(); ObjectId b = patchSet.commitId();
Whitespace ws = Whitespace.IGNORE_NONE;
if (parentNum != null) { if (parentNum != null) {
return get(PatchListKey.againstParentNum(parentNum, b, ws), project); return get(PatchListKey.againstParentNum(parentNum, b, Whitespace.IGNORE_NONE), project);
} }
return get(PatchListKey.againstDefaultBase(b, ws), project); return get(PatchListKey.againstDefaultBase(b, Whitespace.IGNORE_NONE), project);
} }
@Override @Override

View File

@@ -59,6 +59,12 @@ public class PatchListKey implements Serializable {
return new PatchListKey(otherCommitId, newId, whitespace); return new PatchListKey(otherCommitId, newId, whitespace);
} }
public static PatchListKey againstBase(ObjectId id, int parentCount) {
return parentCount > 1
? PatchListKey.againstParentNum(1, id, Whitespace.IGNORE_NONE)
: PatchListKey.againstDefaultBase(id, Whitespace.IGNORE_NONE);
}
/** /**
* Old patch-set ID * Old patch-set ID
* *

View File

@@ -45,7 +45,6 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames; import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment; import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ApprovalsUtil;
@@ -396,12 +395,7 @@ public class ChangeData {
return Optional.empty(); return Optional.empty();
} }
ObjectId id = ps.commitId(); PatchListKey pk = PatchListKey.againstBase(ps.commitId(), parentCount);
Whitespace ws = Whitespace.IGNORE_NONE;
PatchListKey pk =
parentCount > 1
? PatchListKey.againstParentNum(1, id, ws)
: PatchListKey.againstDefaultBase(id, ws);
DiffSummaryKey key = DiffSummaryKey.fromPatchListKey(pk); DiffSummaryKey key = DiffSummaryKey.fromPatchListKey(pk);
try { try {
diffSummary = Optional.of(patchListCache.getDiffSummary(key, c.getProject())); diffSummary = Optional.of(patchListCache.getDiffSummary(key, c.getProject()));

View File

@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows; import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.common.data.SubmitRecord;
@@ -35,10 +36,13 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.Sequences; import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryTestEnvironment; import com.google.gerrit.testing.InMemoryTestEnvironment;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.name.Named;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
@@ -57,8 +61,9 @@ public class BatchUpdateTest {
new InMemoryTestEnvironment( new InMemoryTestEnvironment(
() -> { () -> {
Config cfg = new Config(); Config cfg = new Config();
cfg.setInt("change", null, "maxUpdates", MAX_UPDATES); cfg.setInt("change", null, "maxFiles", 2);
cfg.setInt("change", null, "maxPatchSets", MAX_PATCH_SETS); cfg.setInt("change", null, "maxPatchSets", MAX_PATCH_SETS);
cfg.setInt("change", null, "maxUpdates", MAX_UPDATES);
return cfg; return cfg;
}); });
@@ -70,6 +75,9 @@ public class BatchUpdateTest {
@Inject private Provider<CurrentUser> user; @Inject private Provider<CurrentUser> user;
@Inject private Sequences sequences; @Inject private Sequences sequences;
@Inject
private @Named("diff_summary") Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
private Project.NameKey project; private Project.NameKey project;
private TestRepository<Repository> repo; private TestRepository<Repository> repo;
@@ -243,6 +251,59 @@ public class BatchUpdateTest {
assertThat(getMetaId(changeId)).isEqualTo(oldMetaId); assertThat(getMetaId(changeId)).isEqualTo(oldMetaId);
} }
@Test
public void limitFileCount_exceed() throws Exception {
Change.Id changeId = createChangeWithUpdates(1);
ChangeNotes notes = changeNotesFactory.create(project, changeId);
try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId())
.add("bar.txt", "bar")
.add("baz.txt", "baz")
.add("boom.txt", "boom")
.message("blah")
.create();
bu.addOp(
changeId,
patchSetInserterFactory
.create(notes, PatchSet.id(changeId, 2), commitId)
.setMessage("blah"));
ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute);
assertThat(thrown)
.hasMessageThat()
.contains("Exceeding maximum number of files per change (3 > 2)");
}
}
@Test
public void limitFileCount_cacheKeyMatches() throws Exception {
Change.Id changeId = createChangeWithUpdates(1);
ChangeNotes notes = changeNotesFactory.create(project, changeId);
int cacheSizeBefore = diffSummaryCache.asMap().size();
// We don't want to depend on the test helper used above so we perform an explicit commit here.
try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId())
.add("bar.txt", "bar")
.add("baz.txt", "baz")
.message("blah")
.create();
bu.addOp(
changeId,
patchSetInserterFactory
.create(notes, PatchSet.id(changeId, 3), commitId)
.setMessage("blah"));
bu.execute();
}
// Assert that we only performed the diff computation once. This would e.g. catch
// bugs/deviations in the computation of the cache key.
assertThat(diffSummaryCache.asMap()).hasSize(cacheSizeBefore + 1);
}
private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception { private Change.Id createChangeWithUpdates(int totalUpdates) throws Exception {
checkArgument(totalUpdates > 0); checkArgument(totalUpdates > 0);
checkArgument(totalUpdates <= MAX_UPDATES); checkArgument(totalUpdates <= MAX_UPDATES);