Merge "Limit the number of files per change"
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user