Merge changes I5d50ebdd,Ic37f1c29
* changes:
Allow to exempt users from requiring code owner approvals
CodeOwnerApprovalCheck: Add debug logs for pure reverts
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
index 2c0128a..0cd47fa 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
@@ -71,6 +71,9 @@
/** Emails of users that should be code owners globally across all branches. */
public List<String> globalCodeOwners;
+ /** Emails of users that should be exempted from requiring code owner approvals. */
+ public List<String> exemptedUsers;
+
/** Strategy that defines for merge commits which files require code owner approvals. */
public MergeCommitStrategy mergeCommitStrategy;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 4c41510..8361d9b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -238,12 +238,25 @@
CodeOwnersPluginConfigSnapshot codeOwnersConfig =
codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
- if (codeOwnersConfig.arePureRevertsExempted() && isPureRevert(changeNotes)) {
+ Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+ ImmutableSet<Account.Id> exemptedAccounts = codeOwnersConfig.getExemptedAccounts();
+ logger.atFine().log("exemptedAccounts = %s", exemptedAccounts);
+ if (exemptedAccounts.contains(patchSetUploader)) {
+ logger.atFine().log(
+ "patch set uploader %d is exempted from requiring code owner approvals",
+ patchSetUploader.get());
+ return getAllPathsAsApproved(changeNotes, changeNotes.getCurrentPatchSet());
+ }
+
+ boolean arePureRevertsExempted = codeOwnersConfig.arePureRevertsExempted();
+ logger.atFine().log("arePureRevertsExempted = %s", arePureRevertsExempted);
+ if (arePureRevertsExempted && isPureRevert(changeNotes)) {
+ logger.atFine().log(
+ "change is a pure revert and is exempted from requiring code owner approvals");
return getAllPathsAsApproved(changeNotes, changeNotes.getCurrentPatchSet());
}
boolean enableImplicitApprovalFromUploader = codeOwnersConfig.areImplicitApprovalsEnabled();
- Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
logger.atFine().log(
"patchSetUploader = %d, implicit approval from uploader is %s",
patchSetUploader.get(), enableImplicitApprovalFromUploader ? "enabled" : "disabled");
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
index 666b2c1..72747a6 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
@@ -21,25 +21,34 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
/** Snapshot of the code-owners plugin configuration for one project. */
@@ -53,6 +62,7 @@
private final String pluginName;
private final PluginConfigFactory pluginConfigFactory;
private final ProjectCache projectCache;
+ private final AccountResolver accountResolver;
private final BackendConfig backendConfig;
private final GeneralConfig generalConfig;
private final OverrideApprovalConfig overrideApprovalConfig;
@@ -61,11 +71,14 @@
private final Project.NameKey projectName;
private final Config pluginConfig;
+ @Nullable private ImmutableSet<Account.Id> exemptedAccounts;
+
@Inject
CodeOwnersPluginConfigSnapshot(
@PluginName String pluginName,
PluginConfigFactory pluginConfigFactory,
ProjectCache projectCache,
+ AccountResolver accountResolver,
BackendConfig backendConfig,
GeneralConfig generalConfig,
OverrideApprovalConfig overrideApprovalConfig,
@@ -75,6 +88,7 @@
this.pluginName = pluginName;
this.pluginConfigFactory = pluginConfigFactory;
this.projectCache = projectCache;
+ this.accountResolver = accountResolver;
this.backendConfig = backendConfig;
this.generalConfig = generalConfig;
this.overrideApprovalConfig = overrideApprovalConfig;
@@ -148,6 +162,47 @@
return generalConfig.getGlobalCodeOwners(pluginConfig);
}
+ /** Gets the accounts that are exempted from requiring code owner approvals. */
+ public ImmutableSet<Account.Id> getExemptedAccounts() {
+ if (exemptedAccounts == null) {
+ exemptedAccounts = lookupExemptedAccounts();
+ }
+ return exemptedAccounts;
+ }
+
+ private ImmutableSet<Account.Id> lookupExemptedAccounts() {
+ ImmutableSet.Builder<Account.Id> exemptedAccounts = ImmutableSet.builder();
+ for (String exemptedUser : generalConfig.getExemptedUsers(pluginConfig)) {
+ try {
+ AccountState accountState = accountResolver.resolve(exemptedUser).asUnique();
+
+ // We only support specifying exempted users by email, if another account identifier (full
+ // name, account ID, etc.) was used in the config we want to ignore it. Hence after looking
+ // up the account we check that the identifier from the config was indeed an email of the
+ // account.
+ if (!ExternalId.getEmails(accountState.externalIds())
+ .anyMatch(email -> email.equals(exemptedUser))) {
+ logger.atWarning().log(
+ "Ignoring exempted user %s for project %s: not an email", exemptedUser, projectName);
+ continue;
+ }
+
+ exemptedAccounts.add(accountState.account().id());
+ } catch (UnresolvableAccountException e) {
+ logger.atWarning().log(
+ "Ignoring exempted user %s for project %s: %s",
+ exemptedUser, projectName, e.getMessage());
+ } catch (IOException | ConfigInvalidException e) {
+ throw new CodeOwnersInternalServerErrorException(
+ String.format(
+ "Failed to resolve exempted user %s on project %s", exemptedUser, projectName),
+ e);
+ }
+ }
+
+ return exemptedAccounts.build();
+ }
+
/** Gets the override info URL that is configured. */
public Optional<String> getOverrideInfoUrl() {
return generalConfig.getOverrideInfoUrl(pluginConfig);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
index 28c8456..ddeec46 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -66,6 +66,7 @@
public static final String KEY_MAX_PATHS_IN_CHANGE_MESSAGES = "maxPathsInChangeMessages";
public static final String KEY_MERGE_COMMIT_STRATEGY = "mergeCommitStrategy";
public static final String KEY_GLOBAL_CODE_OWNER = "globalCodeOwner";
+ public static final String KEY_EXEMPTED_USER = "exemptedUser";
public static final String KEY_ENABLE_IMPLICIT_APPROVALS = "enableImplicitApprovals";
public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl";
public static final int DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES = 100;
@@ -571,6 +572,32 @@
}
/**
+ * Gets the users which are exempted from requiring code owner approvals.
+ *
+ * <p>If a user is exempted from requiring code owner approvals changes that are uploaded by this
+ * user are automatically code-owner approved.
+ *
+ * @param pluginConfig the plugin config from which the exempted users should be read.
+ * @return the users which are exempted from requiring code owner approvals
+ */
+ ImmutableSet<String> getExemptedUsers(Config pluginConfig) {
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ if (pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPTED_USER)
+ != null) {
+ return Arrays.stream(
+ pluginConfig.getStringList(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPTED_USER))
+ .filter(value -> !value.trim().isEmpty())
+ .collect(toImmutableSet());
+ }
+
+ return Arrays.stream(pluginConfigFromGerritConfig.getStringList(KEY_EXEMPTED_USER))
+ .filter(value -> !value.trim().isEmpty())
+ .collect(toImmutableSet());
+ }
+
+ /**
* Gets an URL that leads to an information page about overrides.
*
* <p>The URL is retrieved from the given plugin config, with fallback to the {@code
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
index 308c3ac..ed130c8 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPTED_USER;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPT_PURE_REVERTS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FALLBACK_CODE_OWNERS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FILE_EXTENSION;
@@ -169,6 +170,11 @@
input.globalCodeOwners);
}
+ if (input.exemptedUsers != null) {
+ codeOwnersConfig.setStringList(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPTED_USER, input.exemptedUsers);
+ }
+
if (input.mergeCommitStrategy != null) {
codeOwnersConfig.setEnum(
SECTION_CODE_OWNERS,
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
index d8eefd3..655b45e 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -340,6 +341,25 @@
}
@Test
+ public void setExemptedUsers() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getExemptedAccounts())
+ .isEmpty();
+
+ CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+ input.exemptedUsers = ImmutableList.of(user.email(), user2.email());
+ projectCodeOwnersApiFactory.project(project).updateConfig(input);
+ assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getExemptedAccounts())
+ .containsExactly(user.id(), user2.id());
+
+ input.exemptedUsers = ImmutableList.of();
+ projectCodeOwnersApiFactory.project(project).updateConfig(input);
+ assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getExemptedAccounts())
+ .isEmpty();
+ }
+
+ @Test
public void setMergeCommitStrategy() throws Exception {
assertThat(codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy())
.isEqualTo(MergeCommitStrategy.ALL_CHANGED_FILES);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index 27167c6..f2c55e4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -2001,6 +2001,47 @@
.isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
}
+ @Test
+ @GerritConfig(name = "plugin.code-owners.exemptedUser", value = "exempted-user@example.com")
+ public void changeUploadedByExemptedUserIsApproved() throws Exception {
+ TestAccount exemptedUser =
+ accountCreator.create(
+ "exemptedUser", "exempted-user@example.com", "Exempted User", /* displayName= */ null);
+
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(exemptedUser, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Check that the file is approved since the uploader is exempted from requiring code owner
+ // approvals.
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.APPROVED);
+
+ // Amend the change by another user, so that the other non-exempted user becomes the last
+ // uploader.
+ amendChange(user, changeId);
+
+ // Check that the file is no longer approved since the uploader is not exempted from requiring
+ // code owner approvals.
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
private ChangeNotes getChangeNotes(String changeId) throws Exception {
return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number));
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
index 997eb19..529cbd4 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshotTest.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
@@ -170,6 +171,163 @@
}
@Test
+ public void getExemptedAccountsIfNoneIsConfigured() throws Exception {
+ assertThat(cfgSnapshot().getExemptedAccounts()).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUser",
+ values = {"exempted-user-1@example.com", "exempted-user-2@example.com"})
+ public void getExemptedAccountsIfNoneIsConfiguredOnProjectLevel() throws Exception {
+ TestAccount exemptedUser1 =
+ accountCreator.create(
+ "exemptedUser1",
+ "exempted-user-1@example.com",
+ "Exempted User 1",
+ /* displayName= */ null);
+ TestAccount exemptedUser2 =
+ accountCreator.create(
+ "exemptedUser2",
+ "exempted-user-2@example.com",
+ "Exempted User 2",
+ /* displayName= */ null);
+ assertThat(cfgSnapshot().getExemptedAccounts())
+ .containsExactly(exemptedUser1.id(), exemptedUser2.id());
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUser",
+ values = {"exempted-user-1@example.com", "exempted-user-2@example.com"})
+ public void exemptedAccountsOnProjectLevelOverrideGlobalExemptedAcounts() throws Exception {
+ accountCreator.create(
+ "exemptedUser1", "exempted-user-1@example.com", "Exempted User 1", /* displayName= */ null);
+ accountCreator.create(
+ "exemptedUser2", "exempted-user-2@example.com", "Exempted User 2", /* displayName= */ null);
+ TestAccount exemptedUser3 =
+ accountCreator.create(
+ "exemptedUser3",
+ "exempted-user-3@example.com",
+ "Exempted User 3",
+ /* displayName= */ null);
+ TestAccount exemptedUser4 =
+ accountCreator.create(
+ "exemptedUser4",
+ "exempted-user-4@example.com",
+ "Exempted User 4",
+ /* displayName= */ null);
+ configureExemptedUsers(allProjects, exemptedUser3.email(), exemptedUser4.email());
+ assertThat(cfgSnapshot().getExemptedAccounts())
+ .containsExactly(exemptedUser3.id(), exemptedUser4.id());
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUser",
+ values = {"exempted-user-1@example.com", "exempted-user-2@example.com"})
+ public void exemptedAccountsAreInheritedFromParentProject() throws Exception {
+ accountCreator.create(
+ "exemptedUser1", "exempted-user-1@example.com", "Exempted User 1", /* displayName= */ null);
+ accountCreator.create(
+ "exemptedUser2", "exempted-user-2@example.com", "Exempted User 2", /* displayName= */ null);
+ TestAccount exemptedUser3 =
+ accountCreator.create(
+ "exemptedUser3",
+ "exempted-user-3@example.com",
+ "Exempted User 3",
+ /* displayName= */ null);
+ TestAccount exemptedUser4 =
+ accountCreator.create(
+ "exemptedUser4",
+ "exempted-user-4@example.com",
+ "Exempted User 4",
+ /* displayName= */ null);
+ configureExemptedUsers(allProjects, exemptedUser3.email(), exemptedUser4.email());
+ assertThat(cfgSnapshot().getExemptedAccounts())
+ .containsExactly(exemptedUser3.id(), exemptedUser4.id());
+ }
+
+ @Test
+ public void inheritedExemptedAccountsCanBeOverridden() throws Exception {
+ TestAccount exemptedUser1 =
+ accountCreator.create(
+ "exemptedUser1",
+ "exempted-user-1@example.com",
+ "Exempted User 1",
+ /* displayName= */ null);
+ TestAccount exemptedUser2 =
+ accountCreator.create(
+ "exemptedUser2",
+ "exempted-user-2@example.com",
+ "Exempted User 2",
+ /* displayName= */ null);
+ TestAccount exemptedUser3 =
+ accountCreator.create(
+ "exemptedUser3",
+ "exempted-user-3@example.com",
+ "Exempted User 3",
+ /* displayName= */ null);
+ TestAccount exemptedUser4 =
+ accountCreator.create(
+ "exemptedUser4",
+ "exempted-user-4@example.com",
+ "Exempted User 4",
+ /* displayName= */ null);
+ configureExemptedUsers(allProjects, exemptedUser1.email(), exemptedUser2.email());
+ configureExemptedUsers(project, exemptedUser3.email(), exemptedUser4.email());
+ assertThat(cfgSnapshot().getExemptedAccounts())
+ .containsExactly(exemptedUser3.id(), exemptedUser4.id());
+ }
+
+ @Test
+ public void inheritedExemptedAccountsCanBeRemoved() throws Exception {
+ TestAccount exemptedUser1 =
+ accountCreator.create(
+ "exemptedUser1",
+ "exempted-user-1@example.com",
+ "Exempted User 1",
+ /* displayName= */ null);
+ TestAccount exemptedUser2 =
+ accountCreator.create(
+ "exemptedUser2",
+ "exempted-user-2@example.com",
+ "Exempted User 2",
+ /* displayName= */ null);
+ configureExemptedUsers(allProjects, exemptedUser1.email(), exemptedUser2.email());
+ configureExemptedUsers(project, "");
+ assertThat(cfgSnapshot().getExemptedAccounts()).isEmpty();
+ }
+
+ @Test
+ public void nonResolvableExemptedAccountsAreIgnored() throws Exception {
+ TestAccount exemptedUser =
+ accountCreator.create(
+ "exemptedUser", "exempted-user@example.com", "Exempted User", /* displayName= */ null);
+ configureExemptedUsers(project, exemptedUser.email(), "non-resolveable@example.com");
+ assertThat(cfgSnapshot().getExemptedAccounts()).containsExactly(exemptedUser.id());
+ }
+
+ @Test
+ public void exemptedAccountsByIdAreIgnored() throws Exception {
+ TestAccount exemptedUser1 =
+ accountCreator.create(
+ "exemptedUser1",
+ "exempted-user-1@example.com",
+ "Exempted User 1",
+ /* displayName= */ null);
+ TestAccount exemptedUser2 =
+ accountCreator.create(
+ "exemptedUser2",
+ "exempted-user-2@example.com",
+ "Exempted User 2",
+ /* displayName= */ null);
+ configureExemptedUsers(
+ project, exemptedUser1.email(), Integer.toString(exemptedUser2.id().get()));
+ assertThat(cfgSnapshot().getExemptedAccounts()).containsExactly(exemptedUser1.id());
+ }
+
+ @Test
public void getMaxPathsInChangeMessagesIfNoneIsConfigured() throws Exception {
assertThat(cfgSnapshot().getMaxPathsInChangeMessages())
.isEqualTo(GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
@@ -780,6 +938,15 @@
fallbackCodeOwners.name());
}
+ private void configureExemptedUsers(Project.NameKey project, String... exemptedUsers)
+ throws Exception {
+ setCodeOwnersConfig(
+ project,
+ /* subsection= */ null,
+ GeneralConfig.KEY_EXEMPTED_USER,
+ ImmutableList.copyOf(exemptedUsers));
+ }
+
private void configureMaxPathsInChangeMessages(
Project.NameKey project, int maxPathsInChangeMessages) throws Exception {
setCodeOwnersConfig(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
index b5716ee..ed66d23 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPTED_USER;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPT_PURE_REVERTS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FALLBACK_CODE_OWNERS;
import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FILE_EXTENSION;
@@ -713,6 +714,51 @@
}
@Test
+ public void cannotGetExemptedUsersForNullPluginConfig() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> generalConfig.getExemptedUsers(/* pluginConfig= */ null));
+ assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+ }
+
+ @Test
+ public void noExemptedUsers() throws Exception {
+ assertThat(generalConfig.getExemptedUsers(new Config())).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUser",
+ values = {"bot1@example.com", "bot2@example.com"})
+ public void exemptedUsersAreRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
+ throws Exception {
+ assertThat(generalConfig.getExemptedUsers(new Config()))
+ .containsExactly("bot1@example.com", "bot2@example.com");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUser",
+ values = {"bot1@example.com", "bot2@example.com"})
+ public void exemptedUsersInPluginConfigOverrideExemptedUsersInGerritConfig() throws Exception {
+ Config cfg = new Config();
+ cfg.setString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPTED_USER, "bot3@example.com");
+ assertThat(generalConfig.getExemptedUsers(cfg)).containsExactly("bot3@example.com");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.exemptedUsers",
+ values = {"bot1@example.com", "bot2@example.com"})
+ public void inheritedExemptedUsersCanBeRemovedOnProjectLevel() throws Exception {
+ Config cfg = new Config();
+ cfg.setString(SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPTED_USER, "");
+ assertThat(generalConfig.getExemptedUsers(cfg)).isEmpty();
+ }
+
+ @Test
public void cannotGetOverrideInfoUrlForNullPluginConfig() throws Exception {
NullPointerException npe =
assertThrows(
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 6d45d59..dc5ac94 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -153,6 +153,17 @@
`@PLUGIN@.config`.\
By default unset (no global code owners).
+<a id="pluginCodeOwnersExemptedUser">plugin.@PLUGIN@.exemptedUser</a>
+: The email of a user that should be exempted from requiring code owner
+ approvals.\
+ If a user is exempted from requiring code owner approvals changes that
+ are uploaded by this user are automatically code-owner approved.\
+ Can be specified multiple times to exempt multiple users.\
+ Can be overridden per project by setting
+ [codeOwners.exemptedUser](#codeOwnersExemptedUser) in
+ `@PLUGIN@.config`.\
+ By default unset (no exempted users).
+
<a id="pluginCodeOwnersReadOnly">plugin.@PLUGIN@.readOnly</a>
: Whether code owner config files are read-only.\
Can be overridden per project by setting
@@ -541,6 +552,20 @@
[plugin.@PLUGIN@.globalCodeOwner](#pluginCodeOwnersGlobalCodeOwner) in
`gerrit.config` is used.
+<a id="codeOwnersExemptedUser">codeOwners.exemptedUser</a>
+: The email of a user that should be exempted from requiring code owner
+ approvals.\
+ If a user is exempted from requiring code owner approvals changes that
+ are uploaded by this user are automatically code-owner approved.\
+ Can be specified multiple times to exempt multiple users.\
+ Overrides the global setting
+ [plugin.@PLUGIN@.exemptedUser](#pluginCodeOwnersExemptedUser) in
+ `gerrit.config` and the `codeOwners.exemptedUser` setting from parent
+ projects.\
+ If not set, the global setting
+ [plugin.@PLUGIN@.exemptedUser](#pluginCodeOwnersExemptedUser) in
+ `gerrit.config` is used.
+
<a id="codeOwnersReadOnly">codeOwners.readOnly</a>
: Whether code owner config files are read-only.\
Overrides the global setting
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 9d4d3a8..c0e608b 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -886,6 +886,7 @@
| `override_approvals` | optional | The approvals that count as override for the code owners submit check. Must be specified in the format "\<label-name\>+\<label-value\>".
| `fallback_code_owners` | optional | Policy that controls who should own paths that have no code owners defined. Possible values are: `NONE`: Paths for which no code owners are defined are owned by no one. `PROJECT_OWNERS`: Paths for which no code owners are defined are owned by the project owners. `ALL_USERS`: Paths for which no code owners are defined are owned by all users.
| `global_code_owners` | optional | List of emails of users that should be code owners globally across all branches.
+| `exempted_users` | optional | List of emails of users that should be exempted from requiring code owners approvals.
| `merge_commit_strategy` | optional | Strategy that defines for merge commits which files require code owner approvals. Can be `ALL_CHANGED_FILES` or `FILES_WITH_CONFLICT_RESOLUTION` (see [mergeCommitStrategy](config.html#pluginCodeOwnersMergeCommitStrategy) for an explanation of these values).
| `implicit_approvals` | optional | Whether an implicit code owner approval from the last uploader is assumed.
| `override_info_url` | optional | URL for a page that provides project/host-specific information about how to request a code owner override.