Merge branch 'stable-3.3'
* stable-3.3:
Remove the bug-report link from code-owners
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Id8613eda43ba5ef08952b6a15f365537b9d4bf37
diff --git a/BUILD b/BUILD
index 578af9d..5333141 100644
--- a/BUILD
+++ b/BUILD
@@ -9,52 +9,24 @@
"//tools/bzl:plugin.bzl",
"gerrit_plugin",
)
-load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:js.bzl", "polygerrit_plugin")
-load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
gerrit_plugin(
name = "code-owners",
- srcs = glob(["java/com/google/gerrit/plugins/codeowners/**/*.java"]),
+ srcs = glob(["java/com/google/gerrit/plugins/codeowners/module/*.java"]),
manifest_entries = [
"Gerrit-PluginName: code-owners",
- "Gerrit-Module: com.google.gerrit.plugins.codeowners.Module",
- "Gerrit-HttpModule: com.google.gerrit.plugins.codeowners.HttpModule",
+ "Gerrit-Module: com.google.gerrit.plugins.codeowners.module.Module",
+ "Gerrit-HttpModule: com.google.gerrit.plugins.codeowners.module.HttpModule",
+ "Gerrit-BatchModule: com.google.gerrit.plugins.codeowners.module.BatchModule",
],
- resource_jars = [":code-owners-fe-static"],
+ resource_jars = ["//plugins/code-owners/ui:code-owners"],
resource_strip_prefix = "plugins/code-owners/resources",
resources = glob(["resources/**/*"]),
- deps = ["//plugins/code-owners/proto:owners_metadata_java_proto"],
-)
-
-polygerrit_plugin(
- name = "code-owners-fe",
- app = "plugin-bundle.js",
- plugin_name = "code-owners",
-)
-
-rollup_bundle(
- name = "plugin-bundle",
- srcs = glob([
- "ui/**/*.js",
- ]),
- entry_point = "ui/plugin.js",
- format = "iife",
- rollup_bin = "//tools/node_tools:rollup-bin",
- sourcemap = "hidden",
deps = [
- "@tools_npm//rollup-plugin-node-resolve",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api/impl",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/restapi",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/validation",
],
)
-
-genrule2(
- name = "code-owners-fe-static",
- srcs = [":code-owners-fe"],
- outs = ["code-owners-fe-static.jar"],
- cmd = " && ".join([
- "mkdir $$TMP/static",
- "cp -r $(locations :code-owners-fe) $$TMP/static",
- "cd $$TMP",
- "zip -Drq $$ROOT/$@ -g .",
- ]),
-)
\ No newline at end of file
diff --git a/README.md b/README.md
index 26e2c93..02330f1 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,7 @@
If the code-owners plugin is enabled, changes can only be submitted if all
touched files are covered by approvals from code owners.
-Also see resources/Documentation/about.md
+Also see [resources/Documentation/about.md](./resources/Documentation/about.md).
IMPORTANT: Before installing/enabling the plugin follow the instructions from
-the setup guide, see resources/Documentation/setup-guide.md
-
+the setup guide, see [resources/Documentation/setup-guide.md](./resources/Documentation/setup-guide.md).
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
index da0b172..e42501d 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
@@ -15,15 +15,17 @@
package com.google.gerrit.plugins.codeowners.acceptance;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.TruthJUnit.assume;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
-import com.google.gerrit.plugins.codeowners.api.ChangeCodeOwnersFactory;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigsFactory;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnersFactory;
-import com.google.gerrit.plugins.codeowners.api.ProjectCodeOwnersFactory;
+import com.google.gerrit.plugins.codeowners.api.impl.ChangeCodeOwnersFactory;
+import com.google.gerrit.plugins.codeowners.api.impl.CodeOwnerConfigsFactory;
+import com.google.gerrit.plugins.codeowners.api.impl.CodeOwnersFactory;
+import com.google.gerrit.plugins.codeowners.api.impl.ProjectCodeOwnersFactory;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
-import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
import com.google.gerrit.testing.ConfigSuite;
import java.util.Arrays;
import org.eclipse.jgit.lib.Config;
@@ -76,6 +78,8 @@
protected ChangeCodeOwnersFactory changeCodeOwnersApiFactory;
protected ProjectCodeOwnersFactory projectCodeOwnersApiFactory;
+ private BackendConfig backendConfig;
+
@Before
public void baseSetup() throws Exception {
codeOwnerConfigOperations =
@@ -85,5 +89,20 @@
changeCodeOwnersApiFactory = plugin.getSysInjector().getInstance(ChangeCodeOwnersFactory.class);
projectCodeOwnersApiFactory =
plugin.getSysInjector().getInstance(ProjectCodeOwnersFactory.class);
+ backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+ }
+
+ protected void skipTestIfImportsNotSupportedByCodeOwnersBackend() {
+ // the proto backend doesn't support imports
+ assumeThatCodeOwnersBackendIsNotProtoBackend();
+ }
+
+ protected void skipTestIfIgnoreParentCodeOwnersNotSupportedByCodeOwnersBackend() {
+ // the proto backend doesn't support ignoring parent code owners
+ assumeThatCodeOwnersBackendIsNotProtoBackend();
+ }
+
+ protected void assumeThatCodeOwnersBackendIsNotProtoBackend() {
+ assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
index 0ed3041..1309568 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.plugins.codeowners.acceptance;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.common.collect.ImmutableList;
@@ -34,11 +35,15 @@
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.LabelDefinitionInput;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation.Builder;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.plugins.codeowners.config.StatusConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.StatusConfig;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.inject.Inject;
import java.nio.file.Path;
import java.util.Map;
@@ -67,11 +72,13 @@
@Inject private ProjectOperations projectOperations;
private CodeOwnerConfigOperations codeOwnerConfigOperations;
+ private BackendConfig backendConfig;
@Before
public void testSetup() throws Exception {
codeOwnerConfigOperations =
plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class);
+ backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
}
protected String createChangeWithFileDeletion(Path filePath) throws Exception {
@@ -168,7 +175,7 @@
CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, subsection, key, values));
}
- private void updateCodeOwnersConfig(Project.NameKey project, Consumer<Config> configUpdater)
+ protected void updateCodeOwnersConfig(Project.NameKey project, Consumer<Config> configUpdater)
throws Exception {
Config codeOwnersConfig = new Config();
configUpdater.accept(codeOwnersConfig);
@@ -193,6 +200,7 @@
protected void createOwnersOverrideLabel(String labelName) throws RestApiException {
LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = "NoOp";
input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
gApi.projects().name(project.get()).label(labelName).create(input).get();
@@ -210,27 +218,6 @@
}
/**
- * Creates an arbitrary code owner config file.
- *
- * <p>Can be used to create an arbitrary code owner config in order to avoid entering the
- * bootstrapping code path in {@link
- * com.google.gerrit.plugins.codeowners.backend.CodeOwnerApprovalCheck}.
- */
- protected void createArbitraryCodeOwnerConfigFile() throws Exception {
- TestAccount arbitraryUser =
- accountCreator.create(
- "arbitrary-user", "arbitrary-user@example.com", "Arbitrary User", null);
-
- codeOwnerConfigOperations
- .newCodeOwnerConfig()
- .project(project)
- .branch("master")
- .folderPath("/arbitrary/path/")
- .addCodeOwnerEmail(arbitraryUser.email())
- .create();
- }
-
- /**
* Creates a non-parseable code owner config file at the given path.
*
* @param path path of the code owner config file
@@ -246,6 +233,26 @@
}
/**
+ * Returns the parsing error message for the non-parseable code owner config that was created by
+ * {@link #createNonParseableCodeOwnerConfig(String)}.
+ */
+ protected String getParsingErrorMessageForNonParseableCodeOwnerConfig() {
+ return getParsingErrorMessage(
+ ImmutableMap.of(
+ FindOwnersBackend.class,
+ "invalid line: INVALID",
+ ProtoBackend.class,
+ "1:8: Expected \"{\"."));
+ }
+
+ protected String getParsingErrorMessage(
+ ImmutableMap<Class<? extends CodeOwnerBackend>, String> messagesByBackend) {
+ CodeOwnerBackend codeOwnerBackend = backendConfig.getDefaultBackend();
+ assertThat(messagesByBackend).containsKey(codeOwnerBackend.getClass());
+ return messagesByBackend.get(codeOwnerBackend.getClass());
+ }
+
+ /**
* Creates a default code owner config with the given test accounts as code owners.
*
* @param testAccounts the accounts of the users that should be code owners
@@ -324,4 +331,14 @@
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, files);
return push.to("refs/for/master");
}
+
+ protected String getCodeOwnerConfigFileName() {
+ CodeOwnerBackend backend = backendConfig.getDefaultBackend();
+ if (backend instanceof FindOwnersBackend) {
+ return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ } else if (backend instanceof ProtoBackend) {
+ return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ }
+ throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/BUILD b/java/com/google/gerrit/plugins/codeowners/acceptance/BUILD
index bd5bc0b..65883e0 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/BUILD
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/BUILD
@@ -12,6 +12,9 @@
"//java/com/google/gerrit/acceptance:lib",
"//plugins/code-owners:code-owners__plugin",
"//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api/impl",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
"//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/testing/backend:testutil",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
],
)
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/TestModule.java b/java/com/google/gerrit/plugins/codeowners/acceptance/TestModule.java
index b8643fc..0394508 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/TestModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/TestModule.java
@@ -15,9 +15,9 @@
package com.google.gerrit.plugins.codeowners.acceptance;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.plugins.codeowners.Module;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperationsImpl;
+import com.google.gerrit.plugins.codeowners.module.Module;
import com.google.gerrit.plugins.codeowners.testing.backend.TestCodeOwnerConfigStorage;
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/BUILD b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/BUILD
index b08b998..2660baa 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/BUILD
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/BUILD
@@ -10,6 +10,7 @@
srcs = glob(["*.java"]),
deps = [
"//java/com/google/gerrit/acceptance:lib",
- "//plugins/code-owners:code-owners__plugin",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
],
)
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
index 8462d52..5fe97c9 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
@@ -16,14 +16,14 @@
import static com.google.common.base.Preconditions.checkState;
-import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportModification;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigUpdate;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSetModification;
import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnersUpdate;
-import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
index ac9ee4d..d2acaed 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestPathExpressions.java
@@ -15,10 +15,11 @@
package com.google.gerrit.plugins.codeowners.acceptance.testsuite;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.FindOwnersGlobMatcher;
import com.google.gerrit.plugins.codeowners.backend.GlobMatcher;
import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
import com.google.gerrit.plugins.codeowners.backend.SimplePathExpressionMatcher;
-import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
import com.google.inject.Inject;
/**
@@ -40,15 +41,20 @@
}
/**
- * Creates a path expression that matches all files of the given file type in the current folder.
+ * Creates a path expression that matches all files of the given file type in the current folder
+ * and all subfolders.
*
* @param fileType the file type
*/
- public String matchFileTypeInCurrentFolder(String fileType) {
+ public String matchFileType(String fileType) {
PathExpressionMatcher pathExpressionMatcher = getPathExpressionMatcher();
- if (pathExpressionMatcher instanceof GlobMatcher
- || pathExpressionMatcher instanceof SimplePathExpressionMatcher) {
+ if (pathExpressionMatcher instanceof GlobMatcher) {
+ return "**." + fileType;
+ } else if (pathExpressionMatcher instanceof FindOwnersGlobMatcher) {
return "*." + fileType;
+ } else if (pathExpressionMatcher instanceof SimplePathExpressionMatcher) {
+ // '...' (matches any string, including slashes), followed by '.<file-type>'
+ return "...." + fileType;
}
throw new IllegalStateException(
String.format(
@@ -63,7 +69,8 @@
*/
public String matchAllFilesInSubfolder(String subfolder) {
PathExpressionMatcher pathExpressionMatcher = getPathExpressionMatcher();
- if (pathExpressionMatcher instanceof GlobMatcher) {
+ if (pathExpressionMatcher instanceof GlobMatcher
+ || pathExpressionMatcher instanceof FindOwnersGlobMatcher) {
return subfolder + "/**";
} else if (pathExpressionMatcher instanceof SimplePathExpressionMatcher) {
return subfolder + "/...";
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BUILD b/java/com/google/gerrit/plugins/codeowners/api/BUILD
new file mode 100644
index 0000000..9ab1591
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/BUILD
@@ -0,0 +1,13 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "api",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK + [
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
+ "//plugins/code-owners/proto:owners_metadata_java_proto",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 22bc543..1155e35 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -31,6 +31,7 @@
/** Create a request to retrieve code owner config files from the branch. */
CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException;
+ /** Request to retrieve code owner config files from the branch. */
abstract class CodeOwnerConfigFilesRequest {
private boolean includeNonParsableFiles;
private String email;
@@ -57,7 +58,7 @@
return this;
}
- /** Returns the email that should appear in the returned code owner config files/ */
+ /** Returns the email that should appear in the returned code owner config files. */
@Nullable
public String getEmail() {
return email;
@@ -74,13 +75,13 @@
return this;
}
- /** Returns the path glob that should be matched by the returned code owner config files/ */
+ /** Returns the path glob that should be matched by the returned code owner config files. */
@Nullable
public String getPath() {
return path;
}
- /** Executes the request and retrieves the paths of the requested code owner config file */
+ /** Executes the request and retrieves the paths of the requested code owner config file. */
public abstract List<String> paths() throws RestApiException;
}
@@ -88,6 +89,88 @@
RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
throws RestApiException;
+ /** Checks the code ownership of a user for a path in a branch. */
+ CodeOwnerCheckRequest checkCodeOwner() throws RestApiException;
+
+ /** Request for checking the code ownership of a user for a path in a branch. */
+ abstract class CodeOwnerCheckRequest {
+ private String email;
+ private String path;
+ private String change;
+ private String user;
+
+ /**
+ * Sets the email for which the code ownership should be checked.
+ *
+ * @param email the email for which the code ownership should be checked
+ */
+ public CodeOwnerCheckRequest email(String email) {
+ this.email = email;
+ return this;
+ }
+
+ /** Returns the email for which the code ownership should be checked. */
+ @Nullable
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Sets the path for which the code ownership should be checked.
+ *
+ * @param path the path for which the code ownership should be checked
+ */
+ public CodeOwnerCheckRequest path(String path) {
+ this.path = path;
+ return this;
+ }
+
+ /** Returns the path for which the code ownership should be checked. */
+ @Nullable
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the change for which permissions should be checked.
+ *
+ * <p>If not specified change permissions are not checked.
+ *
+ * @param change the change for which permissions should be checked
+ */
+ public CodeOwnerCheckRequest change(@Nullable String change) {
+ this.change = change;
+ return this;
+ }
+
+ /** Returns the change for which permissions should be checked. */
+ @Nullable
+ public String getChange() {
+ return change;
+ }
+
+ /**
+ * Sets the user for which the code owner visibility should be checked.
+ *
+ * <p>If not specified the code owner visibility is not checked.
+ *
+ * @param user the user for which the code owner visibility should be checked
+ */
+ public CodeOwnerCheckRequest user(@Nullable String user) {
+ this.user = user;
+ return this;
+ }
+
+ /** Returns the user for which the code owner visibility should be checked. */
+ @Nullable
+ public String getUser() {
+ return user;
+ }
+
+ /** Executes the request and retrieves the result. */
+ public abstract CodeOwnerCheckInfo check() throws RestApiException;
+ }
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -108,5 +191,10 @@
throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwners.java
index 8c1a7dd..12aebb4 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwners.java
@@ -16,15 +16,16 @@
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.Optional;
/**
* Java API for change code owners.
*
- * <p>To create an instance for a change use {@link ChangeCodeOwnersFactory}.
+ * <p>To create an instance for a change use {@code ChangeCodeOwnersFactory}.
*/
public interface ChangeCodeOwners {
- /** Returns the code owner status for the files in the change. */
- CodeOwnerStatusInfo getCodeOwnerStatus() throws RestApiException;
+ /** Creates a request to retrieve the code owner status for the files in the change. */
+ CodeOwnerStatusRequest getCodeOwnerStatus() throws RestApiException;
/** Returns the revision-level code owners API for the current revision. */
default RevisionCodeOwners current() throws RestApiException {
@@ -32,20 +33,62 @@
}
/** Returns the revision-level code owners API for the given revision. */
- default RevisionCodeOwners revision(int id) throws RestApiException {
- return revision(Integer.toString(id));
- }
-
- /** Returns the revision-level code owners API for the given revision. */
RevisionCodeOwners revision(String id) throws RestApiException;
/**
+ * Request to compute code owner status.
+ *
+ * <p>Allows to set parameters on the request before executing it by calling {@link #get()}.
+ */
+ abstract class CodeOwnerStatusRequest {
+ private Integer start;
+ private Integer limit;
+
+ /**
+ * Sets a limit on the number of code owner statuses that should be returned.
+ *
+ * @param start number of code owner statuses to skip
+ */
+ public CodeOwnerStatusRequest withStart(int start) {
+ this.start = start;
+ return this;
+ }
+
+ /** Returns the number of code owner statuses to skip. */
+ public Optional<Integer> getStart() {
+ return Optional.ofNullable(start);
+ }
+
+ /**
+ * Sets a limit on the number of code owner statuses that should be returned.
+ *
+ * @param limit the limit
+ */
+ public CodeOwnerStatusRequest withLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ /** Returns the limit. */
+ public Optional<Integer> getLimit() {
+ return Optional.ofNullable(limit);
+ }
+
+ /**
+ * Executes this request and retrieves the code owner status.
+ *
+ * @return the code owner status
+ */
+ public abstract CodeOwnerStatusInfo get() throws RestApiException;
+ }
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
class NotImplemented implements ChangeCodeOwners {
@Override
- public CodeOwnerStatusInfo getCodeOwnerStatus() {
+ public CodeOwnerStatusRequest getCodeOwnerStatus() {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerBranchConfigInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerBranchConfigInfo.java
index 6fb3fc7..8cf28cc 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerBranchConfigInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerBranchConfigInfo.java
@@ -63,16 +63,4 @@
* <p>Not set if {@link #disabled} is {@code true}.
*/
public List<RequiredApprovalInfo> overrideApproval;
-
- /**
- * Whether the branch doesn't contain any code owner config file yet.
- *
- * <p>If a branch doesn't contain any code owner config file yet, the projects owners are
- * considered as code owners. Once a first code owner config file is added to the branch, the
- * project owners are no longer code owners (unless code ownership is granted to them via the code
- * owner config file).
- *
- * <p>Not set if {@code false} or if {@link #disabled} is {@code true}.
- */
- public Boolean noCodeOwnersDefined;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
new file mode 100644
index 0000000..c96645c
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * REST API representation of the result of checking a code owner via {code
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner}.
+ *
+ * <p>This class determines the JSON format of check result in the REST API.
+ */
+public class CodeOwnerCheckInfo {
+ /**
+ * Whether the given email owns the specified path in the branch.
+ *
+ * <p>True if:
+ *
+ * <ul>
+ * <li>the given email is resolvable (see {@link #isResolvable}) and
+ * <li>any code owner config file assigns codeownership to the email for the path (see {@link
+ * #codeOwnerConfigFilePaths}) or the email is configured as default code owner (see {@link
+ * CodeOwnerCheckInfo#isDefaultCodeOwner} field) or the email is configured as global code
+ * owner (see {@link #isGlobalCodeOwner} field) or the user is a fallback code owner (see
+ * {@link #isFallbackCodeOwner} field)
+ * </ul>
+ */
+ public boolean isCodeOwner;
+
+ /**
+ * Whether the given email is resolvable for the specified user or the calling user if no user was
+ * specified.
+ */
+ public boolean isResolvable;
+
+ /**
+ * Whether the user to which the given email was resolved has read permissions on the branch.
+ *
+ * <p>Not set if:
+ *
+ * <ul>
+ * <li>the given email is not resolvable
+ * <li>the given email is the all users wildcard (aka {@code *})
+ * </ul>
+ */
+ public Boolean canReadRef;
+
+ /**
+ * Whether the user to which the given email was resolved can see the specified change.
+ *
+ * <p>Not set if:
+ *
+ * <ul>
+ * <li>the given email is not resolvable
+ * <li>the given email is the all users wildcard (aka {@code *})
+ * <li>no change was specified
+ * </ul>
+ */
+ public Boolean canSeeChange;
+
+ /**
+ * Whether the user to which the given email was resolved can code-owner approve the specified
+ * change.
+ *
+ * <p>Being able to code-owner approve the change means that the user has permissions to vote on
+ * the label that is required as code owner approval. Other permissions are not considered for
+ * computing this flag. In particular missing read permissions on the change don't have any effect
+ * on this flag. Whether the user misses read permissions on the change (and hence cannot apply
+ * the code owner approval) can be seen from the {@link #canSeeChange} flag.
+ *
+ * <p>Not set if:
+ *
+ * <ul>
+ * <li>the given email is not resolvable
+ * <li>the given email is the all users wildcard (aka {@code *})
+ * <li>no change was specified
+ * </ul>
+ */
+ public Boolean canApproveChange;
+
+ /**
+ * Paths of the code owner config files that assign code ownership to the given email for the
+ * specified path.
+ *
+ * <p>If code ownership is assigned to the email via a code owner config files, but the email is
+ * not resolvable (see {@link #isResolvable} field), the user is not a code owner.
+ */
+ public List<String> codeOwnerConfigFilePaths;
+
+ /**
+ * Whether the given email is a fallback code owner of the specified path in the branch.
+ *
+ * <p>True if:
+ *
+ * <ul>
+ * <li>the given email is resolvable (see {@link #isResolvable}) and
+ * <li>no code owners are defined for the specified path in the branch and
+ * <li>parent code owners are not ignored and
+ * <li>the user is a fallback code owner according to the configured fallback code owner policy
+ */
+ public boolean isFallbackCodeOwner;
+
+ /**
+ * Whether the given email is configured as a default code owner.
+ *
+ * <p>If the email is configured as default code owner, but the email is not resolvable (see
+ * {@link #isResolvable} field), the user is not a code owner.
+ */
+ public boolean isDefaultCodeOwner;
+
+ /**
+ * Whether the given email is configured as a global code owner.
+ *
+ * <p>If the email is configured as global code owner, but the email is not resolvable (see {@link
+ * #isResolvable} field), the user is not a code owner.
+ */
+ public boolean isGlobalCodeOwner;
+
+ /** Whether the the specified path in the branch is owned by all users (aka {@code *}). */
+ public boolean isOwnedByAllUsers;
+
+ /** Debug logs that may help to understand why the user is or isn't a code owner. */
+ public List<String> debugLogs;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigInfo.java
index b74d8ea..f877f64 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigInfo.java
@@ -14,9 +14,7 @@
package com.google.gerrit.plugins.codeowners.api;
-import com.google.common.base.MoreObjects;
import java.util.List;
-import java.util.Objects;
/**
* Representation of a {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig} in the
@@ -39,23 +37,4 @@
* <p>Not set if there are no code owner sets defined in this code owner config.
*/
public List<CodeOwnerSetInfo> codeOwnerSets;
-
- @Override
- public int hashCode() {
- return Objects.hashCode(codeOwnerSets);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CodeOwnerConfigInfo)) {
- return false;
- }
- CodeOwnerConfigInfo other = (CodeOwnerConfigInfo) o;
- return Objects.equals(codeOwnerSets, other.codeOwnerSets);
- }
-
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this).add("codeOwnerSets", codeOwnerSets).toString();
- }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigValidationPolicy.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigValidationPolicy.java
deleted file mode 100644
index db1e6ca..0000000
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigValidationPolicy.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.api;
-
-/** Policy that should be used to validate code owner config files. */
-public enum CodeOwnerConfigValidationPolicy {
- /**
- * The code owner config file validation is enabled and invalid code owner config files are
- * rejected.
- */
- TRUE,
-
- /**
- * The code owner config file validation is disabled. Invalid code owner config files are not
- * rejected.
- */
- FALSE,
-
- /**
- * Code owner config files are validated, but invalid code owner config files are not rejected.
- */
- DRY_RUN;
-
- public boolean isDryRun() {
- return this == CodeOwnerConfigValidationPolicy.DRY_RUN;
- }
-
- public boolean runValidation() {
- return this != CodeOwnerConfigValidationPolicy.FALSE;
- }
-}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigs.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigs.java
index 24e054c..8754809 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigs.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigs.java
@@ -23,7 +23,7 @@
/**
* Java API for code owners configs in a branch.
*
- * <p>To create an instance for a branch use {@link CodeOwnerConfigsFactory}.
+ * <p>To create an instance for a branch use {@code CodeOwnerConfigsFactory}.
*/
public interface CodeOwnerConfigs {
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerInfo.java
index 934674a..c141bea 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerInfo.java
@@ -14,9 +14,7 @@
package com.google.gerrit.plugins.codeowners.api;
-import com.google.common.base.MoreObjects;
import com.google.gerrit.extensions.common.AccountInfo;
-import java.util.Objects;
/**
* Representation of a code owner in the REST API.
@@ -26,23 +24,4 @@
public class CodeOwnerInfo {
/** The account of the code owner. */
public AccountInfo account;
-
- @Override
- public int hashCode() {
- return Objects.hashCode(account);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CodeOwnerInfo)) {
- return false;
- }
- CodeOwnerInfo other = (CodeOwnerInfo) o;
- return Objects.equals(account, other.account);
- }
-
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this).add("account", account).toString();
- }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
new file mode 100644
index 0000000..8bc182e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.api;
+
+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 java.util.List;
+
+/**
+ * Input for the {@code com.google.gerrit.plugins.codeowners.restapi.PutCodeOwnerProjectConfig} REST
+ * endpoint
+ *
+ * <p>This class determines the JSON format of the input in the REST API.
+ *
+ * <p>If a field in this input entity is not set, the corresponding parameter in the {@code
+ * code-owners.config} file is not updated.
+ */
+public class CodeOwnerProjectConfigInput {
+ /** Whether the code owners functionality should be disabled/enabled for the project. */
+ public Boolean disabled;
+
+ /**
+ * Branches for which the code owners functionality is disabled.
+ *
+ * <p>Can be exact refs, ref patterns or regular expressions.
+ *
+ * <p>Overrides any existing disabled branch configuration.
+ */
+ public List<String> disabledBranches;
+
+ /** The file extension that should be used for code owner config files in this project. */
+ public String fileExtension;
+
+ /**
+ * The approval that is required from code owners.
+ *
+ * <p>The required approval must be specified in the format {@code <label-name>+<label-value>}.
+ *
+ * <p>If an empty string is provided the required approval configuration is unset. Unsetting the
+ * required approval means that the inherited required approval configuration or the default
+ * required approval ({@code Code-Review+1}) will apply.
+ *
+ * <p>In contrast to providing an empty string, providing {@code null} (or not setting the value)
+ * means that the required approval configuration is not updated.
+ */
+ public String requiredApproval;
+
+ /**
+ * The approvals that count as override for the code owners submit check.
+ *
+ * <p>The override approvals must be specified in the format {@code <label-name>+<label-value>}.
+ */
+ public List<String> overrideApprovals;
+
+ /** Policy that controls who should own paths that have no code owners defined. */
+ public FallbackCodeOwners fallbackCodeOwners;
+
+ /** 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;
+
+ /** Whether an implicit code owner approval from the last uploader is assumed. */
+ public Boolean implicitApprovals;
+
+ /**
+ * URL for a page that provides project/host-specific information about how to request a code
+ * owner override.
+ */
+ public String overrideInfoUrl;
+
+ /**
+ * URL for a page that provides project/host-specific information about how to deal with invalid
+ * code owner config files.
+ */
+ public String invalidCodeOwnerConfigInfoUrl;
+
+ /** Whether code owner config files are read-only. */
+ public Boolean readOnly;
+
+ /** Whether pure revert changes are exempted from needing code owner approvals for submit. */
+ public Boolean exemptPureReverts;
+
+ /** Policy for validating code owner config files when a commit is received. */
+ public CodeOwnerConfigValidationPolicy enableValidationOnCommitReceived;
+
+ /** Policy for validating code owner config files when a change is submitted. */
+ public CodeOwnerConfigValidationPolicy enableValidationOnSubmit;
+
+ /**
+ * Whether modifications of code owner config files that newly add non-resolvable code owners
+ * should be rejected on commit received and submit.
+ */
+ public Boolean rejectNonResolvableCodeOwners;
+
+ /**
+ * Whether modifications of code owner config files that newly add non-resolvable imports should
+ * be rejected on commit received an submit.
+ */
+ public Boolean rejectNonResolvableImports;
+
+ /** The maximum number of paths that are included in change messages. */
+ public Integer maxPathsInChangeMessages;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerReferenceInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerReferenceInfo.java
index 1ca0c63..6ba7f69 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerReferenceInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerReferenceInfo.java
@@ -14,9 +14,6 @@
package com.google.gerrit.plugins.codeowners.api;
-import com.google.common.base.MoreObjects;
-import java.util.Objects;
-
/**
* Representation of a {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference} in
* the REST API.
@@ -26,23 +23,4 @@
public class CodeOwnerReferenceInfo {
/** The email of the code owner. */
public String email;
-
- @Override
- public int hashCode() {
- return Objects.hashCode(email);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CodeOwnerReferenceInfo)) {
- return false;
- }
- CodeOwnerReferenceInfo other = (CodeOwnerReferenceInfo) o;
- return Objects.equals(email, other.email);
- }
-
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this).add("email", email).toString();
- }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerSetInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerSetInfo.java
index 54ccc3f..8dccbd6 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerSetInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerSetInfo.java
@@ -14,9 +14,7 @@
package com.google.gerrit.plugins.codeowners.api;
-import com.google.common.base.MoreObjects;
import java.util.List;
-import java.util.Objects;
/**
* Representation of a {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet} in the REST
@@ -31,23 +29,4 @@
* <p>Not set if there are no code owners defined in this code owner set.
*/
public List<CodeOwnerReferenceInfo> codeOwners;
-
- @Override
- public int hashCode() {
- return Objects.hashCode(codeOwners);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CodeOwnerSetInfo)) {
- return false;
- }
- CodeOwnerSetInfo other = (CodeOwnerSetInfo) o;
- return Objects.equals(codeOwners, other.codeOwners);
- }
-
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this).add("codeOwners", codeOwners).toString();
- }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatusInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatusInfo.java
index b10e2be..e9ae34d 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatusInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatusInfo.java
@@ -34,4 +34,11 @@
/** List of the code owner statuses for the files in the change. */
public List<FileCodeOwnerStatusInfo> fileCodeOwnerStatuses;
+
+ /**
+ * Whether the request would deliver more results if not limited.
+ *
+ * <p>Not set if {@code false}.
+ */
+ public Boolean more;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
index 5d1182c..a2079b8 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
@@ -24,14 +24,13 @@
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.EnumSet;
-import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Java API for code owners in a branch or change revision.
*
- * <p>To create an instance for a branch or change revision use {@link CodeOwnersFactory}.
+ * <p>To create an instance for a branch or change revision use {@code CodeOwnersFactory}.
*/
public interface CodeOwners {
/** Query code owners for a path. */
@@ -47,6 +46,10 @@
private Set<ListAccountsOption> options = EnumSet.noneOf(ListAccountsOption.class);
private Integer limit;
private String revision;
+ private Long seed;
+ private Boolean resolveAllUsers;
+ private Boolean highestScoreOnly;
+ private Boolean debug;
/**
* Lists the code owners for the given path.
@@ -55,7 +58,7 @@
* exist
* @return the code owners for the given path
*/
- public abstract List<CodeOwnerInfo> get(Path path) throws RestApiException;
+ public abstract CodeOwnersInfo get(Path path) throws RestApiException;
/**
* Lists the code owners for the given path.
@@ -64,7 +67,7 @@
* exist
* @return the code owners for the given path
*/
- public List<CodeOwnerInfo> get(String path) throws RestApiException {
+ public CodeOwnersInfo get(String path) throws RestApiException {
return get(Paths.get(path));
}
@@ -93,6 +96,51 @@
}
/**
+ * Sets the seed that should be used to shuffle code owners that have the same score.
+ *
+ * @param seed seed that should be used to shuffle code owners that have the same score
+ */
+ public QueryRequest withSeed(long seed) {
+ this.seed = seed;
+ return this;
+ }
+
+ /**
+ * Sets whether code ownerships that are assigned to all users should be resolved to random
+ * users.
+ *
+ * @param resolveAllUsers whether code ownerships that are assigned to all users should be
+ * resolved to random users
+ */
+ public QueryRequest setResolveAllUsers(boolean resolveAllUsers) {
+ this.resolveAllUsers = resolveAllUsers;
+ return this;
+ }
+
+ /**
+ * Sets whether only the code owners with the highest score should be returned.
+ *
+ * @param highestScoreOnly whether only the code owners with the highest score should be
+ * returned
+ */
+ public QueryRequest withHighestScoreOnly(boolean highestScoreOnly) {
+ this.highestScoreOnly = highestScoreOnly;
+ return this;
+ }
+
+ /**
+ * Sets whether debug logs should be included into the response.
+ *
+ * <p>Requires the 'Check Code Owner' global capability.
+ *
+ * @param debug whether debug logs should be included into the response
+ */
+ public QueryRequest withDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ /**
* Sets the branch revision from which the code owner configs should be read.
*
* <p>Not supported for querying code owners for a path in a change.
@@ -114,20 +162,32 @@
return Optional.ofNullable(limit);
}
+ /** Returns the seed that should be used to shuffle code owners that have the same score. */
+ public Optional<Long> getSeed() {
+ return Optional.ofNullable(seed);
+ }
+
+ /**
+ * Whether code ownerships that are assigned to all users should be resolved to random users.
+ */
+ public Optional<Boolean> getResolveAllUsers() {
+ return Optional.ofNullable(resolveAllUsers);
+ }
+
+ /** Whether only the code owners with the highest score should be returned. */
+ public Optional<Boolean> getHighestScoreOnly() {
+ return Optional.ofNullable(highestScoreOnly);
+ }
+
+ /** Whether debug logs should be included into the response. */
+ public Optional<Boolean> getDebug() {
+ return Optional.ofNullable(debug);
+ }
+
/** Returns the branch revision from which the code owner configs should be read. */
public Optional<String> getRevision() {
return Optional.ofNullable(revision);
}
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('{');
- if (!options.isEmpty()) {
- sb.append("options=").append(options);
- }
- sb.append('}');
- return sb.toString();
- }
}
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
new file mode 100644
index 0000000..d699029
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * Representation of a code owners list in the REST API.
+ *
+ * <p>This class determines the JSON format for the response of the {@code
+ * com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch} and {@code
+ * com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInChange} REST endpoints.
+ */
+public class CodeOwnersInfo {
+ /** List of code owners. */
+ public List<CodeOwnerInfo> codeOwners;
+
+ /**
+ * Whether the path is owned by all users.
+ *
+ * <p>Not set if {@code false}.
+ */
+ public Boolean ownedByAllUsers;
+
+ /**
+ * Debug logs that may help to understand why a user is or isn't suggested as a code owner.
+ *
+ * <p>Only set if requested via {@code --debug}.
+ */
+ public List<String> debugLogs;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java b/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
index 8caa0fe..d0c123f 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
@@ -15,6 +15,7 @@
package com.google.gerrit.plugins.codeowners.api;
import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
/**
* Representation of the general code owners configuration in the REST API.
@@ -46,6 +47,12 @@
*/
public String overrideInfoUrl;
+ /**
+ * Optional URL for a page that provides project/host-specific information about how to deal with
+ * invalid code owner config files.
+ */
+ public String invalidCodeOwnerConfigInfoUrl;
+
/** Policy that controls who should own paths that have no code owners defined. */
public FallbackCodeOwners fallbackCodeOwners;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
new file mode 100644
index 0000000..ba41418
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * Representation of a list of owned paths in the REST API.
+ *
+ * <p>This class determines the JSON format for the response of the {@code
+ * com.google.gerrit.plugins.codeowners.restapi.GetOwnedPaths} REST endpoint.
+ */
+public class OwnedPathsInfo {
+ /**
+ * List of the owned paths.
+ *
+ * <p>The paths are returned as absolute paths.
+ *
+ * <p>The paths are sorted alphabetically.
+ */
+ public List<String> ownedPaths;
+
+ /**
+ * Whether the request would deliver more results if not limited.
+ *
+ * <p>Not set if {@code false}.
+ */
+ public Boolean more;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/PathCodeOwnerStatusInfo.java b/java/com/google/gerrit/plugins/codeowners/api/PathCodeOwnerStatusInfo.java
index 80e1f70..7643e53 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/PathCodeOwnerStatusInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/PathCodeOwnerStatusInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.plugins.codeowners.api;
+import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
+
/** JSON entity that describes the code owner status for a path that was touched in a change. */
public class PathCodeOwnerStatusInfo {
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
index ccce1ed..5f99cd1 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
@@ -25,12 +25,21 @@
/**
* Project-level Java API of the code-owners plugin.
*
- * <p>To create an instance for a project use {@link ProjectCodeOwnersFactory}.
+ * <p>To create an instance for a project use {@code ProjectCodeOwnersFactory}.
*/
public interface ProjectCodeOwners {
/** Returns the code owner project configuration. */
CodeOwnerProjectConfigInfo getConfig() throws RestApiException;
+ /**
+ * Update the code owner project configuration.
+ *
+ * @param input the input specifying which parameters should be updated
+ * @return the update code owner project configuration
+ */
+ CodeOwnerProjectConfigInfo updateConfig(CodeOwnerProjectConfigInput input)
+ throws RestApiException;
+
/** Create a request to check the code owner config files in the project. */
CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException;
@@ -147,6 +156,12 @@
}
@Override
+ public CodeOwnerProjectConfigInfo updateConfig(CodeOwnerProjectConfigInput input)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java
index df9ed04..7b2655b 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* Revision-level Java API of the code-owners plugin.
@@ -34,6 +35,11 @@
*/
CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException;
+ /**
+ * Retrieves the paths of the revision that owned by the user that is specified in the request.
+ */
+ OwnedPathsRequest getOwnedPaths() throws RestApiException;
+
/** Request to check code owner config files. */
abstract class CheckCodeOwnerConfigFilesRequest {
private String path;
@@ -88,6 +94,57 @@
public abstract Map<String, List<ConsistencyProblemInfo>> check() throws RestApiException;
}
+ /** Request to check code owner config files. */
+ abstract class OwnedPathsRequest {
+ private Integer start;
+ private Integer limit;
+ private String user;
+
+ /**
+ * Sets a limit on the number of owned paths that should be returned.
+ *
+ * @param start number of owned paths to skip
+ */
+ public OwnedPathsRequest withStart(int start) {
+ this.start = start;
+ return this;
+ }
+
+ /** Returns the number of owned paths to skip. */
+ public Optional<Integer> getStart() {
+ return Optional.ofNullable(start);
+ }
+
+ /**
+ * Sets a limit on the number of owned paths that should be returned.
+ *
+ * @param limit the limit
+ */
+ public OwnedPathsRequest withLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ /** Returns the limit. */
+ public Optional<Integer> getLimit() {
+ return Optional.ofNullable(limit);
+ }
+
+ /** Sets the user for which the owned paths should be retrieved. */
+ public OwnedPathsRequest forUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+ /** Returns the user for which the owned paths should be retrieved. */
+ public String getUser() {
+ return user;
+ }
+
+ /** Executes the request and retrieves the paths that are owned by the user. */
+ public abstract OwnedPathsInfo get() throws RestApiException;
+ }
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -97,5 +154,10 @@
public CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public OwnedPathsRequest getOwnedPaths() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ApiModule.java b/java/com/google/gerrit/plugins/codeowners/api/impl/ApiModule.java
similarity index 95%
rename from java/com/google/gerrit/plugins/codeowners/api/ApiModule.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/ApiModule.java
index 92fce1e..838f2f8 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/ApiModule.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import com.google.gerrit.extensions.config.FactoryModule;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/impl/BUILD b/java/com/google/gerrit/plugins/codeowners/api/impl/BUILD
new file mode 100644
index 0000000..0fe77d6
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/BUILD
@@ -0,0 +1,12 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "impl",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK + [
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/restapi",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
similarity index 71%
rename from java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
index 7052a1b..0bf1917 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
@@ -12,11 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.BranchCodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerBranchConfigInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailInput;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailResultInfo;
+import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerBranchConfig;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
import com.google.gerrit.plugins.codeowners.restapi.RenameEmail;
@@ -35,6 +41,7 @@
private final GetCodeOwnerBranchConfig getCodeOwnerBranchConfig;
private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
private final RenameEmail renameEmail;
+ private final Provider<CheckCodeOwner> checkCodeOwnerProvider;
private final BranchResource branchResource;
@Inject
@@ -42,10 +49,12 @@
GetCodeOwnerBranchConfig getCodeOwnerBranchConfig,
Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
RenameEmail renameEmail,
+ Provider<CheckCodeOwner> checkCodeOwnerProvider,
@Assisted BranchResource branchResource) {
this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
this.getCodeOwnerBranchConfig = getCodeOwnerBranchConfig;
this.renameEmail = renameEmail;
+ this.checkCodeOwnerProvider = checkCodeOwnerProvider;
this.branchResource = branchResource;
}
@@ -81,4 +90,23 @@
throw asRestApiException("Cannot rename email", e);
}
}
+
+ @Override
+ public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+ return new CodeOwnerCheckRequest() {
+ @Override
+ public CodeOwnerCheckInfo check() throws RestApiException {
+ CheckCodeOwner checkCodeOwner = checkCodeOwnerProvider.get();
+ checkCodeOwner.setEmail(getEmail());
+ checkCodeOwner.setPath(getPath());
+ checkCodeOwner.setChange(getChange());
+ checkCodeOwner.setUser(getUser());
+ try {
+ return checkCodeOwner.apply(branchResource).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check code owner", e);
+ }
+ }
+ };
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersFactory.java b/java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersFactory.java
similarity index 91%
rename from java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersFactory.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersFactory.java
index 732c9e7..22b73a5 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersFactory.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersFactory.java
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.api.ChangeCodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwners;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.restapi.change.ChangesCollection;
import com.google.inject.Inject;
@@ -62,7 +64,7 @@
try {
return changesCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId));
} catch (Exception e) {
- throw asRestApiException("Cannot retrieve revision", e);
+ throw asRestApiException("Cannot retrieve change", e);
}
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersImpl.java
similarity index 66%
rename from java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersImpl.java
index ad6ed18..17c78cf 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ChangeCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/ChangeCodeOwnersImpl.java
@@ -12,17 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.ChangeCodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatusInfo;
+import com.google.gerrit.plugins.codeowners.api.RevisionCodeOwners;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerStatus;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.restapi.change.Revisions;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
/** Implementation of the {@link ChangeCodeOwners} API. */
@@ -34,27 +38,35 @@
private final Revisions revisions;
private final RevisionCodeOwnersImpl.Factory revisionCodeOwnersApi;
private final ChangeResource changeResource;
- private final GetCodeOwnerStatus getCodeOwnerStatus;
+ private final Provider<GetCodeOwnerStatus> getCodeOwnerStatusProvider;
@Inject
public ChangeCodeOwnersImpl(
Revisions revisions,
RevisionCodeOwnersImpl.Factory revisionCodeOwnersApi,
- GetCodeOwnerStatus getCodeOwnerStatus,
+ Provider<GetCodeOwnerStatus> getCodeOwnerStatusProvider,
@Assisted ChangeResource changeResource) {
this.revisions = revisions;
this.revisionCodeOwnersApi = revisionCodeOwnersApi;
- this.getCodeOwnerStatus = getCodeOwnerStatus;
+ this.getCodeOwnerStatusProvider = getCodeOwnerStatusProvider;
this.changeResource = changeResource;
}
@Override
- public CodeOwnerStatusInfo getCodeOwnerStatus() throws RestApiException {
- try {
- return getCodeOwnerStatus.apply(changeResource).value();
- } catch (Exception e) {
- throw asRestApiException("Cannot get code owner status", e);
- }
+ public CodeOwnerStatusRequest getCodeOwnerStatus() throws RestApiException {
+ return new CodeOwnerStatusRequest() {
+ @Override
+ public CodeOwnerStatusInfo get() throws RestApiException {
+ try {
+ GetCodeOwnerStatus getCodeOwnerStatus = getCodeOwnerStatusProvider.get();
+ getStart().ifPresent(getCodeOwnerStatus::setStart);
+ getLimit().ifPresent(getCodeOwnerStatus::setLimit);
+ return getCodeOwnerStatus.apply(changeResource).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get code owner status", e);
+ }
+ }
+ };
}
@Override
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsFactory.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsFactory.java
similarity index 96%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsFactory.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsFactory.java
index 0be3d07..f11c9ed 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsFactory.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsFactory.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigs;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.BranchesCollection;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsInBranchImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsInBranchImpl.java
similarity index 93%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsInBranchImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsInBranchImpl.java
index 54573b2..27b90b7 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigsInBranchImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnerConfigsInBranchImpl.java
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigs;
import com.google.gerrit.plugins.codeowners.restapi.CodeOwnerConfigsInBranchCollection;
import com.google.gerrit.plugins.codeowners.restapi.CodeOwnerConfigsInBranchCollection.PathResource;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigForPathInBranch;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersFactory.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersFactory.java
similarity index 97%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnersFactory.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersFactory.java
index 28acd8a..b5bc249 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersFactory.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersFactory.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.api.CodeOwners;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.project.BranchResource;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInBranchImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
similarity index 84%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInBranchImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
index d5cb276..fff0b9c 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInBranchImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
@@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInBranchCollection;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch;
import com.google.gerrit.server.project.BranchResource;
@@ -25,7 +27,6 @@
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.nio.file.Path;
-import java.util.List;
/** Implementation of the {@link CodeOwners} API for a branch. */
public class CodeOwnersInBranchImpl implements CodeOwners {
@@ -51,11 +52,15 @@
public QueryRequest query() {
return new QueryRequest() {
@Override
- public List<CodeOwnerInfo> get(Path path) throws RestApiException {
+ public CodeOwnersInfo get(Path path) throws RestApiException {
try {
GetCodeOwnersForPathInBranch getCodeOwners = getCodeOwnersProvider.get();
getOptions().forEach(getCodeOwners::addOption);
getLimit().ifPresent(getCodeOwners::setLimit);
+ getSeed().ifPresent(getCodeOwners::setSeed);
+ getResolveAllUsers().ifPresent(getCodeOwners::setResolveAllUsers);
+ getHighestScoreOnly().ifPresent(getCodeOwners::setHighestScoreOnly);
+ getDebug().ifPresent(getCodeOwners::setDebug);
getRevision().ifPresent(getCodeOwners::setRevision);
CodeOwnersInBranchCollection.PathResource pathInBranchResource =
codeOwnersInBranchCollection.parse(
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInChangeImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
similarity index 84%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInChangeImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
index 4c8cfdc..97a5715 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInChangeImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInChangeCollection;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInChange;
import com.google.gerrit.server.change.RevisionResource;
@@ -26,7 +28,6 @@
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.nio.file.Path;
-import java.util.List;
/** Implementation of the {@link CodeOwners} API for a revision in a change. */
public class CodeOwnersInChangeImpl implements CodeOwners {
@@ -52,7 +53,7 @@
public QueryRequest query() {
return new QueryRequest() {
@Override
- public List<CodeOwnerInfo> get(Path path) throws RestApiException {
+ public CodeOwnersInfo get(Path path) throws RestApiException {
try {
if (getRevision().isPresent()) {
throw new BadRequestException("specifying revision is not supported");
@@ -61,6 +62,10 @@
GetCodeOwnersForPathInChange getCodeOwners = getCodeOwnersProvider.get();
getOptions().forEach(getCodeOwners::addOption);
getLimit().ifPresent(getCodeOwners::setLimit);
+ getSeed().ifPresent(getCodeOwners::setSeed);
+ getResolveAllUsers().ifPresent(getCodeOwners::setResolveAllUsers);
+ getHighestScoreOnly().ifPresent(getCodeOwners::setHighestScoreOnly);
+ getDebug().ifPresent(getCodeOwners::setDebug);
CodeOwnersInChangeCollection.PathResource pathInChangeResource =
codeOwnersInChangeCollection.parse(
revisionResource, IdString.fromDecoded(path.toString()));
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersFactory.java b/java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersFactory.java
similarity index 95%
rename from java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersFactory.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersFactory.java
index 633eebb..cb85a1f 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersFactory.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersFactory.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.api.ProjectCodeOwners;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersImpl.java
similarity index 79%
rename from java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersImpl.java
index 54b37ec..b8b0b17 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/ProjectCodeOwnersImpl.java
@@ -12,15 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.BranchCodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CheckCodeOwnerConfigFilesInput;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerProjectConfigInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerProjectConfigInput;
+import com.google.gerrit.plugins.codeowners.api.ProjectCodeOwners;
import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerProjectConfig;
+import com.google.gerrit.plugins.codeowners.restapi.PutCodeOwnerProjectConfig;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.BranchesCollection;
@@ -38,6 +44,7 @@
private final BranchesCollection branchesCollection;
private final BranchCodeOwnersImpl.Factory branchCodeOwnersApi;
private final GetCodeOwnerProjectConfig getCodeOwnerProjectConfig;
+ private final PutCodeOwnerProjectConfig putCodeOwnerProjectConfig;
private final CheckCodeOwnerConfigFiles checkCodeOwnerConfigFiles;
private final ProjectResource projectResource;
@@ -46,11 +53,13 @@
BranchesCollection branchesCollection,
BranchCodeOwnersImpl.Factory branchCodeOwnersApi,
GetCodeOwnerProjectConfig getCodeOwnerProjectConfig,
+ PutCodeOwnerProjectConfig putCodeOwnerProjectConfig,
CheckCodeOwnerConfigFiles checkCodeOwnerConfigFiles,
@Assisted ProjectResource projectResource) {
this.branchesCollection = branchesCollection;
this.branchCodeOwnersApi = branchCodeOwnersApi;
this.getCodeOwnerProjectConfig = getCodeOwnerProjectConfig;
+ this.putCodeOwnerProjectConfig = putCodeOwnerProjectConfig;
this.checkCodeOwnerConfigFiles = checkCodeOwnerConfigFiles;
this.projectResource = projectResource;
}
@@ -76,6 +85,16 @@
}
@Override
+ public CodeOwnerProjectConfigInfo updateConfig(CodeOwnerProjectConfigInput input)
+ throws RestApiException {
+ try {
+ return putCodeOwnerProjectConfig.apply(projectResource, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update code owner project config", e);
+ }
+ }
+
+ @Override
public CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException {
return new CheckCodeOwnerConfigFilesRequest() {
@Override
diff --git a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java
similarity index 68%
rename from java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwnersImpl.java
rename to java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java
index 817639f..db8cb27 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java
@@ -12,15 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.api.impl;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.api.CheckCodeOwnerConfigFilesInRevisionInput;
+import com.google.gerrit.plugins.codeowners.api.OwnedPathsInfo;
+import com.google.gerrit.plugins.codeowners.api.RevisionCodeOwners;
import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFilesInRevision;
+import com.google.gerrit.plugins.codeowners.restapi.GetOwnedPaths;
import com.google.gerrit.server.change.RevisionResource;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.util.List;
import java.util.Map;
@@ -32,13 +37,16 @@
}
private final CheckCodeOwnerConfigFilesInRevision checkCodeOwnerConfigFilesInRevision;
+ private final Provider<GetOwnedPaths> getOwnedPathsProvider;
private final RevisionResource revisionResource;
@Inject
public RevisionCodeOwnersImpl(
CheckCodeOwnerConfigFilesInRevision checkCodeOwnerConfigFilesInRevision,
+ Provider<GetOwnedPaths> getOwnedPathsProvider,
@Assisted RevisionResource revisionResource) {
this.checkCodeOwnerConfigFilesInRevision = checkCodeOwnerConfigFilesInRevision;
+ this.getOwnedPathsProvider = getOwnedPathsProvider;
this.revisionResource = revisionResource;
}
@@ -59,4 +67,22 @@
}
};
}
+
+ @Override
+ public OwnedPathsRequest getOwnedPaths() throws RestApiException {
+ return new OwnedPathsRequest() {
+ @Override
+ public OwnedPathsInfo get() throws RestApiException {
+ try {
+ GetOwnedPaths getOwnedPaths = getOwnedPathsProvider.get();
+ getStart().ifPresent(getOwnedPaths::setStart);
+ getLimit().ifPresent(getOwnedPaths::setLimit);
+ getOwnedPaths.setUser(getUser());
+ return getOwnedPaths.apply(revisionResource).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get owned paths", e);
+ }
+ }
+ };
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index 1de1325..20a44c2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -20,8 +20,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -53,6 +52,7 @@
private final MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory;
private final RetryHelper retryHelper;
private final String defaultFileName;
+ private final CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory;
private final CodeOwnerConfigParser codeOwnerConfigParser;
protected AbstractFileBasedCodeOwnerBackend(
@@ -62,6 +62,7 @@
MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
RetryHelper retryHelper,
String defaultFileName,
+ CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
CodeOwnerConfigParser codeOwnerConfigParser) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.repoManager = repoManager;
@@ -69,6 +70,7 @@
this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory;
this.retryHelper = retryHelper;
this.defaultFileName = defaultFileName;
+ this.codeOwnerConfigFileFactory = codeOwnerConfigFileFactory;
this.codeOwnerConfigParser = codeOwnerConfigParser;
}
@@ -104,7 +106,7 @@
@Nullable ObjectId revision) {
try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
if (revision == null) {
- return CodeOwnerConfigFile.loadCurrent(
+ return codeOwnerConfigFileFactory.loadCurrent(
fileName, codeOwnerConfigParser, repository, codeOwnerConfigKey);
}
@@ -114,7 +116,7 @@
revWalk = new RevWalk(repository);
}
try {
- return CodeOwnerConfigFile.load(
+ return codeOwnerConfigFileFactory.load(
fileName, codeOwnerConfigParser, revWalk, revision, codeOwnerConfigKey);
} finally {
if (closeRevWalk) {
@@ -122,10 +124,10 @@
}
}
} catch (IOException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format("failed to load code owner config %s", codeOwnerConfigKey), e);
} catch (ConfigInvalidException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format(
"invalid code owner config file %s (project = %s, branch = %s)",
codeOwnerConfigKey.filePath(defaultFileName),
@@ -172,7 +174,8 @@
String quotedFileExtension =
Pattern.quote(
codeOwnersPluginConfiguration
- .getFileExtension(project)
+ .getProjectConfig(project)
+ .getFileExtension()
.map(ext -> "." + ext)
.orElse(""));
String nameExtension = "(\\w)+";
@@ -189,7 +192,11 @@
private String getFileName(Project.NameKey project) {
return defaultFileName
- + codeOwnersPluginConfiguration.getFileExtension(project).map(ext -> "." + ext).orElse("");
+ + codeOwnersPluginConfiguration
+ .getProjectConfig(project)
+ .getFileExtension()
+ .map(ext -> "." + ext)
+ .orElse("");
}
@Override
@@ -207,7 +214,8 @@
.call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
- throw new StorageException(e);
+ throw new CodeOwnersInternalServerErrorException(
+ String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
}
}
@@ -217,7 +225,8 @@
CodeOwnerConfigUpdate codeOwnerConfigUpdate) {
try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) {
CodeOwnerConfigFile codeOwnerConfigFile =
- CodeOwnerConfigFile.loadCurrent(
+ codeOwnerConfigFileFactory
+ .loadCurrent(
getFileName(codeOwnerConfigKey.project()),
codeOwnerConfigParser,
repository,
@@ -231,7 +240,7 @@
return codeOwnerConfigFile.getLoadedCodeOwnerConfig();
} catch (IOException | ConfigInvalidException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
}
}
@@ -255,7 +264,7 @@
} catch (Throwable t) {
metaDataUpdate.close();
Throwables.throwIfUnchecked(t);
- throw new StorageException("Failed to create MetaDataUpdate", t);
+ throw new CodeOwnersInternalServerErrorException("Failed to create MetaDataUpdate", t);
}
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BUILD b/java/com/google/gerrit/plugins/codeowners/backend/BUILD
new file mode 100644
index 0000000..83671a0
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BUILD
@@ -0,0 +1,14 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "backend",
+ srcs = glob(["**/*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK + [
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/metrics",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
+ "//plugins/code-owners/proto:owners_metadata_java_proto",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
index adccb0f..e137ba7 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -16,12 +16,17 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.events.ReviewerAddedListener;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginGlobalConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.restapi.change.OnPostReview;
import com.google.inject.Provides;
/** Guice module to bind code owner backends. */
@@ -30,6 +35,9 @@
protected void configure() {
factory(CodeOwnersUpdate.Factory.class);
factory(CodeOwnerConfigScanner.Factory.class);
+ factory(CodeOwnersPluginGlobalConfigSnapshot.Factory.class);
+ factory(CodeOwnersPluginProjectConfigSnapshot.Factory.class);
+ factory(CodeOwnersPluginConfig.Factory.class);
DynamicMap.mapOf(binder(), CodeOwnerBackend.class);
@@ -45,6 +53,9 @@
install(new CodeOwnerSubmitRule.Module());
DynamicSet.bind(binder(), ExceptionHook.class).to(CodeOwnersExceptionHook.class);
+ DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerApproval.class);
+ DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerOverride.class);
+ DynamicSet.bind(binder(), ReviewerAddedListener.class).to(CodeOwnersOnAddReviewer.class);
}
@Provides
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
index 57e72ef..2835935 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -15,59 +15,125 @@
package com.google.gerrit.plugins.codeowners.backend;
import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.common.ChangedFile;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.patch.AutoMerger;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
- * Class to compute the files that have been changed in a revision.
+ * Class to get/compute the files that have been changed in a revision.
*
- * <p>The file diff is newly computed on each access and not retrieved from any cache. This is
- * better than using {@link com.google.gerrit.server.patch.PatchListCache} which does a lot of
- * unneeded computations and hence is slower. The Gerrit diff caches are currently being redesigned.
- * Once the envisioned {@code ModifiedFilesCache} is available we should consider using it.
+ * <p>The {@link #getFromDiffCache(Project.NameKey, ObjectId)} method is retrieving the file diff
+ * from the diff cache and has rename detection enabled.
+ *
+ * <p>In contrast to this, for the {@code compute} methods the file diff is newly computed on each
+ * access and rename detection is disabled (as it's too expensive to do it on each access).
+ *
+ * <p>If possible, using {@link #getFromDiffCache(Project.NameKey, ObjectId)} is preferred, however
+ * {@link #getFromDiffCache(Project.NameKey, ObjectId)} cannot be used for newly created commits
+ * that are only available from a specific {@link RevWalk} instance since the {@link RevWalk}
+ * instance cannot be passed in.
+ *
+ * <p>The {@link com.google.gerrit.server.patch.PatchListCache} is deprecated, and hence it not
+ * being used here.
*/
@Singleton
public class ChangedFiles {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static int MAX_CHANGED_FILES_TO_LOG = 25;
+
private final GitRepositoryManager repoManager;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
- private final PatchListCache patchListCache;
+ private final DiffOperations diffOperations;
+ private final Provider<AutoMerger> autoMergerProvider;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+ private final ThreeWayMergeStrategy mergeStrategy;
+ private final ExperimentFeatures experimentFeatures;
@Inject
public ChangedFiles(
+ @GerritServerConfig Config cfg,
GitRepositoryManager repoManager,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
- PatchListCache patchListCache) {
+ DiffOperations diffOperations,
+ Provider<AutoMerger> autoMergerProvider,
+ CodeOwnerMetrics codeOwnerMetrics,
+ ExperimentFeatures experimentFeatures) {
this.repoManager = repoManager;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
- this.patchListCache = patchListCache;
+ this.diffOperations = diffOperations;
+ this.autoMergerProvider = autoMergerProvider;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ this.experimentFeatures = experimentFeatures;
+ this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
+ }
+
+ /**
+ * Returns the changed files for the given revision.
+ *
+ * <p>By default the changed files are computed on access (see {@link #compute(Project.NameKey,
+ * ObjectId)}).
+ *
+ * <p>Only if enabled via the {@link CodeOwnersExperimentFeaturesConstants#USE_DIFF_CACHE}
+ * experiment feature flag the changed files are retrieved from the diff cache (see {@link
+ * #getFromDiffCache(Project.NameKey, ObjectId)}).
+ *
+ * @param project the project
+ * @param revision the revision for which the changed files should be computed
+ * @return the files that have been changed in the given revision, sorted alphabetically by path
+ */
+ public ImmutableList<ChangedFile> getOrCompute(Project.NameKey project, ObjectId revision)
+ throws IOException, PatchListNotAvailableException, DiffNotAvailableException {
+ if (experimentFeatures.isFeatureEnabled(CodeOwnersExperimentFeaturesConstants.USE_DIFF_CACHE)) {
+ if (isInitialCommit(project, revision)) {
+ // DiffOperations doesn't support getting the list of modified files for the initial commit.
+ return compute(project, revision);
+ }
+
+ return getFromDiffCache(project, revision);
+ }
+ return compute(project, revision);
}
/**
@@ -75,13 +141,15 @@
*
* <p>The diff is computed against the parent commit.
*
+ * <p>Rename detection is disabled.
+ *
* @param revisionResource the revision resource for which the changed files should be computed
- * @return the files that have been changed in the given revision
+ * @return the files that have been changed in the given revision, sorted alphabetically by path
* @throws IOException thrown if the computation fails due to an I/O error
* @throws PatchListNotAvailableException thrown if getting the patch list for a merge commit
* against the auto merge failed
*/
- public ImmutableSet<ChangedFile> compute(RevisionResource revisionResource)
+ public ImmutableList<ChangedFile> compute(RevisionResource revisionResource)
throws IOException, PatchListNotAvailableException {
requireNonNull(revisionResource, "revisionResource");
return compute(revisionResource.getProject(), revisionResource.getPatchSet().commitId());
@@ -92,14 +160,16 @@
*
* <p>The diff is computed against the parent commit.
*
+ * <p>Rename detection is disabled.
+ *
* @param project the project
* @param revision the revision for which the changed files should be computed
- * @return the files that have been changed in the given revision
+ * @return the files that have been changed in the given revision, sorted alphabetically by path
* @throws IOException thrown if the computation fails due to an I/O error
* @throws PatchListNotAvailableException thrown if getting the patch list for a merge commit
* against the auto merge failed
*/
- public ImmutableSet<ChangedFile> compute(Project.NameKey project, ObjectId revision)
+ public ImmutableList<ChangedFile> compute(Project.NameKey project, ObjectId revision)
throws IOException, PatchListNotAvailableException {
requireNonNull(project, "project");
requireNonNull(revision, "revision");
@@ -111,24 +181,24 @@
}
}
- public ImmutableSet<ChangedFile> compute(
+ public ImmutableList<ChangedFile> compute(
Project.NameKey project, Config repoConfig, RevWalk revWalk, RevCommit revCommit)
- throws IOException, PatchListNotAvailableException {
+ throws IOException {
return compute(
project,
repoConfig,
revWalk,
revCommit,
- codeOwnersPluginConfiguration.getMergeCommitStrategy(project));
+ codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy());
}
- public ImmutableSet<ChangedFile> compute(
+ public ImmutableList<ChangedFile> compute(
Project.NameKey project,
Config repoConfig,
RevWalk revWalk,
RevCommit revCommit,
MergeCommitStrategy mergeCommitStrategy)
- throws IOException, PatchListNotAvailableException {
+ throws IOException {
requireNonNull(project, "project");
requireNonNull(repoConfig, "repoConfig");
requireNonNull(revWalk, "revWalk");
@@ -140,63 +210,131 @@
if (revCommit.getParentCount() > 1
&& MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION.equals(mergeCommitStrategy)) {
- return computeByComparingAgainstAutoMerge(project, revCommit);
+ RevCommit autoMergeCommit = getAutoMergeCommit(project, revCommit);
+ return compute(repoConfig, revWalk, revCommit, autoMergeCommit);
}
- return computeByComparingAgainstFirstParent(repoConfig, revWalk, revCommit);
+ RevCommit baseCommit = revCommit.getParentCount() > 0 ? revCommit.getParent(0) : null;
+ return compute(repoConfig, revWalk, revCommit, baseCommit);
+ }
+
+ private RevCommit getAutoMergeCommit(Project.NameKey project, RevCommit mergeCommit)
+ throws IOException {
+ try (Timer0.Context ctx = codeOwnerMetrics.getAutoMerge.start();
+ Repository repository = repoManager.openRepository(project);
+ InMemoryInserter inserter = new InMemoryInserter(repository);
+ ObjectReader reader = inserter.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
+ return autoMergerProvider
+ .get()
+ .lookupFromGitOrMergeInMemory(repository, revWalk, inserter, mergeCommit, mergeStrategy);
+ }
}
/**
- * Computes the changed files by comparing the given merge commit against the auto merge.
- *
- * <p>Since computing the auto merge is expensive, we do not compute it and diff against it on our
- * own, but rather ask the patch list cache for it.
- *
- * @param project the project that contains the merge commit
- * @param mergeCommit the merge commit for which the changed files should be computed
- * @return the changed files for the given merge commit
- */
- private ImmutableSet<ChangedFile> computeByComparingAgainstAutoMerge(
- Project.NameKey project, RevCommit mergeCommit) throws PatchListNotAvailableException {
- checkState(
- mergeCommit.getParentCount() > 1, "expected %s to be a merge commit", mergeCommit.name());
-
- // for merge commits the default base is the auto merge commit
- PatchListKey patchListKey =
- PatchListKey.againstDefaultBase(mergeCommit, Whitespace.IGNORE_NONE);
-
- return patchListCache.get(patchListKey, project).getPatches().stream()
- .filter(
- patchListEntry ->
- patchListEntry.getNewName() == null || !Patch.isMagic(patchListEntry.getNewName()))
- .map(ChangedFile::create)
- .collect(toImmutableSet());
- }
-
- /**
- * Computes the changed files by comparing the given commit against its first parent.
+ * Computes the changed files by comparing the given commit against the given base commit.
*
* <p>The computation also works if the commit doesn't have any parent.
*
+ * <p>Rename detection is disabled.
+ *
* @param repoConfig the repository configuration
* @param revWalk the rev walk
- * @param revCommit the commit for which the changed files should be computed
- * @return the changed files for the given commit
+ * @param commit the commit for which the changed files should be computed
+ * @param baseCommit the base commit against which the given commit should be compared, {@code
+ * null} if the commit doesn't have any parent commit
+ * @return the changed files for the given commit, sorted alphabetically by path
*/
- private ImmutableSet<ChangedFile> computeByComparingAgainstFirstParent(
- Config repoConfig, RevWalk revWalk, RevCommit revCommit) throws IOException {
- RevCommit baseCommit = revCommit.getParentCount() > 0 ? revCommit.getParent(0) : null;
+ private ImmutableList<ChangedFile> compute(
+ Config repoConfig, RevWalk revWalk, RevCommit commit, @Nullable RevCommit baseCommit)
+ throws IOException {
logger.atFine().log("baseCommit = %s", baseCommit != null ? baseCommit.name() : "n/a");
-
- try (DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- diffFormatter.setReader(revWalk.getObjectReader(), repoConfig);
- diffFormatter.setDiffComparator(RawTextComparator.DEFAULT);
- diffFormatter.setDetectRenames(true);
- List<DiffEntry> diffEntries = diffFormatter.scan(baseCommit, revCommit);
- ImmutableSet<ChangedFile> changedFiles =
- diffEntries.stream().map(ChangedFile::create).collect(toImmutableSet());
- logger.atFine().log("changed files = %s", changedFiles);
- return changedFiles;
+ try (Timer0.Context ctx = codeOwnerMetrics.computeChangedFiles.start()) {
+ // Detecting renames is expensive (since it requires Git to load and compare file contents of
+ // added and deleted files) and can significantly increase the latency for changes that touch
+ // large files. To avoid this latency we do not enable the rename detection on the
+ // DiffFormater. As a result of this renamed files will be returned as 2 ChangedFile's, one
+ // for the deletion of the old path and one for the addition of the new path.
+ try (DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+ diffFormatter.setReader(revWalk.getObjectReader(), repoConfig);
+ diffFormatter.setDiffComparator(RawTextComparator.DEFAULT);
+ List<DiffEntry> diffEntries = diffFormatter.scan(baseCommit, commit);
+ ImmutableList<ChangedFile> changedFiles =
+ diffEntries.stream().map(ChangedFile::create).collect(toImmutableList());
+ if (changedFiles.size() <= MAX_CHANGED_FILES_TO_LOG) {
+ logger.atFine().log("changed files = %s", changedFiles);
+ } else {
+ logger.atFine().log(
+ "changed files = %s (and %d more)",
+ changedFiles.asList().subList(0, MAX_CHANGED_FILES_TO_LOG),
+ changedFiles.size() - MAX_CHANGED_FILES_TO_LOG);
+ }
+ return changedFiles;
+ }
}
}
+
+ /**
+ * Gets the changed files from the diff cache.
+ *
+ * <p>Doesn't support getting changed files for an initial revision. This is because the diff
+ * cache doesn't support getting changed files for commits that don't have any parent.
+ *
+ * <p>Rename detection is enabled.
+ *
+ * @throws IllegalStateException thrown if invoked for an initial revision
+ */
+ @VisibleForTesting
+ ImmutableList<ChangedFile> getFromDiffCache(Project.NameKey project, ObjectId revision)
+ throws IOException, DiffNotAvailableException {
+ requireNonNull(project, "project");
+ requireNonNull(revision, "revision");
+
+ checkState(!isInitialCommit(project, revision), "diff cache doesn't support initial commits");
+
+ MergeCommitStrategy mergeCommitStrategy =
+ codeOwnersPluginConfiguration.getProjectConfig(project).getMergeCommitStrategy();
+
+ try (Timer0.Context ctx = codeOwnerMetrics.getChangedFiles.start()) {
+ Map<String, FileDiffOutput> fileDiffOutputs;
+ if (mergeCommitStrategy.equals(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION)) {
+ // Use parentNum=null to do the comparison against the default base.
+ // For non-merge commits the default base is the only parent (aka parent 1, initial commits
+ // are not supported).
+ // For merge commits the default base is the auto-merge commit which should be used as base
+ // if the merge commit strategy is FILES_WITH_CONFLICT_RESOLUTION.
+ fileDiffOutputs =
+ diffOperations.listModifiedFilesAgainstParent(project, revision, /* parentNum=*/ null);
+ } else {
+ checkState(mergeCommitStrategy.equals(MergeCommitStrategy.ALL_CHANGED_FILES));
+ // Always use parent 1 to do the comparison.
+ // Non-merge commits should always be compared against against the first parent (initial
+ // commits are not supported).
+ // For merge commits also the first parent should be used if the merge commit strategy is
+ // ALL_CHANGED_FILES.
+ fileDiffOutputs = diffOperations.listModifiedFilesAgainstParent(project, revision, 1);
+ }
+
+ return toChangedFiles(filterOutMagicFilesAndSort(fileDiffOutputs)).collect(toImmutableList());
+ }
+ }
+
+ private boolean isInitialCommit(Project.NameKey project, ObjectId objectId) throws IOException {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk revWalk = new RevWalk(repo)) {
+ return revWalk.parseCommit(objectId).getParentCount() == 0;
+ }
+ }
+
+ private Stream<Map.Entry<String, FileDiffOutput>> filterOutMagicFilesAndSort(
+ Map<String, FileDiffOutput> fileDiffOutputs) {
+ return fileDiffOutputs.entrySet().stream()
+ .filter(e -> !Patch.isMagic(e.getKey()))
+ .sorted(comparing(Map.Entry::getKey));
+ }
+
+ private Stream<ChangedFile> toChangedFiles(
+ Stream<Map.Entry<String, FileDiffOutput>> fileDiffOutputs) {
+ return fileDiffOutputs.map(Map.Entry::getValue).map(ChangedFile::create);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 2751067..65f4d78 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -15,33 +15,41 @@
package com.google.gerrit.plugins.codeowners.backend;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.Objects.requireNonNull;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.Change;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.plugins.codeowners.config.RequiredApproval;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.plugins.codeowners.common.ChangedFile;
+import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.git.PureRevertCache;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.util.LabelVote;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -52,7 +60,6 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -81,10 +88,11 @@
private final GitRepositoryManager repoManager;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final ChangedFiles changedFiles;
- private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
- private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
+ private final PureRevertCache pureRevertCache;
+ private final Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider;
private final Provider<CodeOwnerResolver> codeOwnerResolver;
private final ApprovalsUtil approvalsUtil;
+ private final CodeOwnerMetrics codeOwnerMetrics;
@Inject
CodeOwnerApprovalCheck(
@@ -92,18 +100,72 @@
GitRepositoryManager repoManager,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
ChangedFiles changedFiles,
- CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
- CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
+ PureRevertCache pureRevertCache,
+ Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider,
Provider<CodeOwnerResolver> codeOwnerResolver,
- ApprovalsUtil approvalsUtil) {
+ ApprovalsUtil approvalsUtil,
+ CodeOwnerMetrics codeOwnerMetrics) {
this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.changedFiles = changedFiles;
- this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
- this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
+ this.pureRevertCache = pureRevertCache;
+ this.codeOwnerConfigHierarchyProvider = codeOwnerConfigHierarchyProvider;
this.codeOwnerResolver = codeOwnerResolver;
this.approvalsUtil = approvalsUtil;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ /**
+ * Returns the paths of the files in the given patch set that are owned by the specified account.
+ *
+ * @param changeNotes the change notes for which the owned files should be returned
+ * @param patchSet the patch set for which the owned files should be returned
+ * @param accountId account ID of the code owner for which the owned files should be returned
+ * @param start number of owned paths to skip
+ * @param limit the max number of owned paths that should be returned (0 = unlimited)
+ * @return the paths of the files in the given patch set that are owned by the specified account
+ * @throws ResourceConflictException if the destination branch of the change no longer exists
+ */
+ public ImmutableList<Path> getOwnedPaths(
+ ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId, int start, int limit)
+ throws ResourceConflictException {
+ try (Timer0.Context ctx = codeOwnerMetrics.computeOwnedPaths.start()) {
+ logger.atFine().log(
+ "compute owned paths for account %d (project = %s, change = %d, patch set = %d,"
+ + " start = %d, limit = %d)",
+ accountId.get(),
+ changeNotes.getProjectName(),
+ changeNotes.getChangeId().get(),
+ patchSet.id().get(),
+ start,
+ limit);
+ Stream<Path> ownedPaths =
+ getFileStatusesForAccount(changeNotes, patchSet, accountId)
+ .flatMap(
+ fileCodeOwnerStatus ->
+ Stream.of(
+ fileCodeOwnerStatus.newPathStatus(),
+ fileCodeOwnerStatus.oldPathStatus())
+ .filter(Optional::isPresent)
+ .map(Optional::get))
+ .filter(
+ pathCodeOwnerStatus -> pathCodeOwnerStatus.status() == CodeOwnerStatus.APPROVED)
+ .map(PathCodeOwnerStatus::path);
+ if (start > 0) {
+ ownedPaths = ownedPaths.skip(start);
+ }
+ if (limit > 0) {
+ ownedPaths = ownedPaths.limit(limit);
+ }
+ return ownedPaths.collect(toImmutableList());
+ } catch (IOException | PatchListNotAvailableException | DiffNotAvailableException e) {
+ throw new CodeOwnersInternalServerErrorException(
+ String.format(
+ "failed to compute owned paths of patch set %s for account %d",
+ patchSet.id(), accountId.get()),
+ e);
+ }
}
/**
@@ -113,27 +175,65 @@
* @return whether the given change has sufficient code owner approvals to be submittable
*/
public boolean isSubmittable(ChangeNotes changeNotes)
- throws ResourceConflictException, IOException, PatchListNotAvailableException {
+ throws ResourceConflictException, IOException, PatchListNotAvailableException,
+ DiffNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
logger.atFine().log(
"checking if change %d in project %s is submittable",
changeNotes.getChangeId().get(), changeNotes.getProjectName());
- boolean isSubmittable =
- !getFileStatuses(changeNotes)
- .anyMatch(
- fileStatus ->
- (fileStatus.newPathStatus().isPresent()
- && fileStatus.newPathStatus().get().status()
- != CodeOwnerStatus.APPROVED)
- || (fileStatus.oldPathStatus().isPresent()
- && fileStatus.oldPathStatus().get().status()
- != CodeOwnerStatus.APPROVED));
- logger.atFine().log(
- "change %d in project %s %s submittable",
- changeNotes.getChangeId().get(),
- changeNotes.getProjectName(),
- isSubmittable ? "is" : "is not");
- return isSubmittable;
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
+ try {
+ boolean isSubmittable =
+ !getFileStatuses(codeOwnerConfigHierarchy, changeNotes)
+ .anyMatch(
+ fileStatus ->
+ (fileStatus.newPathStatus().isPresent()
+ && fileStatus.newPathStatus().get().status()
+ != CodeOwnerStatus.APPROVED)
+ || (fileStatus.oldPathStatus().isPresent()
+ && fileStatus.oldPathStatus().get().status()
+ != CodeOwnerStatus.APPROVED));
+ logger.atFine().log(
+ "change %d in project %s %s submittable",
+ changeNotes.getChangeId().get(),
+ changeNotes.getProjectName(),
+ isSubmittable ? "is" : "is not");
+ return isSubmittable;
+ } finally {
+ codeOwnerMetrics.codeOwnerConfigBackendReadsPerChange.record(
+ codeOwnerConfigHierarchy.getCodeOwnerConfigCounters().getBackendReadCount());
+ codeOwnerMetrics.codeOwnerConfigCacheReadsPerChange.record(
+ codeOwnerConfigHierarchy.getCodeOwnerConfigCounters().getCacheReadCount());
+ }
+ }
+
+ /**
+ * Gets the code owner statuses for all files/paths that were changed in the current revision of
+ * the given change as a set.
+ *
+ * @param start number of file statuses to skip
+ * @param limit the max number of file statuses that should be returned (0 = unlimited)
+ * @see #getFileStatuses(CodeOwnerConfigHierarchy, ChangeNotes)
+ */
+ public ImmutableSet<FileCodeOwnerStatus> getFileStatusesAsSet(
+ ChangeNotes changeNotes, int start, int limit)
+ throws ResourceConflictException, IOException, PatchListNotAvailableException,
+ DiffNotAvailableException {
+ requireNonNull(changeNotes, "changeNotes");
+ try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatuses.start()) {
+ logger.atFine().log(
+ "compute file statuses (project = %s, change = %d, start = %d, limit = %d)",
+ changeNotes.getProjectName(), changeNotes.getChangeId().get(), start, limit);
+ Stream<FileCodeOwnerStatus> fileStatuses =
+ getFileStatuses(codeOwnerConfigHierarchyProvider.get(), changeNotes);
+ if (start > 0) {
+ fileStatuses = fileStatuses.skip(start);
+ }
+ if (limit > 0) {
+ fileStatuses = fileStatuses.limit(limit);
+ }
+ return fileStatuses.collect(toImmutableSet());
+ }
}
/**
@@ -159,35 +259,72 @@
* approvals that were present on an old revision) would only confuse users
* </ul>
*
+ * @param codeOwnerConfigHierarchy {@link CodeOwnerConfigHierarchy} instance that should be used
+ * to iterate over code owner config hierarchies
* @param changeNotes the notes of the change for which the current code owner statuses should be
* returned
*/
- public Stream<FileCodeOwnerStatus> getFileStatuses(ChangeNotes changeNotes)
- throws ResourceConflictException, IOException, PatchListNotAvailableException {
+ private Stream<FileCodeOwnerStatus> getFileStatuses(
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, ChangeNotes changeNotes)
+ throws ResourceConflictException, IOException, PatchListNotAvailableException,
+ DiffNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Compute file statuses",
- Metadata.builder()
- .projectName(changeNotes.getProjectName().get())
- .changeId(changeNotes.getChangeId().get())
- .build())) {
- boolean enableImplicitApprovalFromUploader =
- codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName());
- Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
+ try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputation.start()) {
logger.atFine().log(
- "patchSetUploader = %d, implicit approval from uploader is %s",
- patchSetUploader.get(), enableImplicitApprovalFromUploader ? "enabled" : "disabled");
+ "prepare stream to compute file statuses (project = %s, change = %d)",
+ changeNotes.getProjectName(), changeNotes.getChangeId().get());
- RequiredApproval requiredApproval =
- codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+ CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
+ codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
+
+ Account.Id changeOwner = changeNotes.getChange().getOwner();
+ 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 implicitApprovalConfig = codeOwnersConfig.areImplicitApprovalsEnabled();
+ boolean enableImplicitApproval =
+ implicitApprovalConfig && changeOwner.equals(patchSetUploader);
+ logger.atFine().log(
+ "changeOwner = %d, patchSetUploader = %d, implict approval config = %s\n=> implicit approval is %s",
+ changeOwner.get(),
+ patchSetUploader.get(),
+ implicitApprovalConfig,
+ enableImplicitApproval ? "enabled" : "disabled");
+
+ ImmutableList<PatchSetApproval> currentPatchSetApprovals =
+ getCurrentPatchSetApprovals(changeNotes);
+
+ RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
logger.atFine().log("requiredApproval = %s", requiredApproval);
- ImmutableSet<RequiredApproval> overrideApprovals =
- codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName());
- boolean hasOverride = hasOverride(overrideApprovals, changeNotes, patchSetUploader);
+ ImmutableSet<RequiredApproval> overrideApprovals = codeOwnersConfig.getOverrideApprovals();
+ boolean hasOverride =
+ hasOverride(currentPatchSetApprovals, overrideApprovals, patchSetUploader);
logger.atFine().log(
- "hasOverride = %s (overrideApprovals = %s)", hasOverride, overrideApprovals);
+ "hasOverride = %s (overrideApprovals = %s)",
+ hasOverride,
+ overrideApprovals.stream()
+ .map(
+ overrideApproval ->
+ String.format(
+ "%s (ignoreSelfApproval = %s)",
+ overrideApproval, overrideApproval.labelType().isIgnoreSelfApproval()))
+ .collect(toImmutableList()));
BranchNameKey branch = changeNotes.getChange().getDest();
ObjectId revision = getDestBranchRevision(changeNotes.getChange());
@@ -200,34 +337,29 @@
.resolveGlobalCodeOwners(changeNotes.getProjectName());
logger.atFine().log("global code owners = %s", globalCodeOwners);
- // If the branch doesn't contain any code owner config file yet, we apply special logic
- // (project owners count as code owners) to allow bootstrapping the code owner configuration
- // in the branch.
- boolean isBootstrapping =
- !codeOwnerConfigScannerFactory.create().containsAnyCodeOwnerConfigFile(branch);
- logger.atFine().log("isBootstrapping = %s", isBootstrapping);
-
ImmutableSet<Account.Id> reviewerAccountIds =
getReviewerAccountIds(requiredApproval, changeNotes, patchSetUploader);
ImmutableSet<Account.Id> approverAccountIds =
- getApproverAccountIds(requiredApproval, changeNotes, patchSetUploader);
+ getApproverAccountIds(currentPatchSetApprovals, requiredApproval, patchSetUploader);
logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds);
+ FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
+
return changedFiles
- .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
+ .getOrCompute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
.stream()
.map(
changedFile ->
getFileStatus(
+ codeOwnerConfigHierarchy,
branch,
revision,
globalCodeOwners,
- enableImplicitApprovalFromUploader,
- patchSetUploader,
+ enableImplicitApproval ? changeOwner : null,
reviewerAccountIds,
approverAccountIds,
+ fallbackCodeOwners,
hasOverride,
- isBootstrapping,
changedFile));
}
}
@@ -241,66 +373,51 @@
* <p>The purpose of this method is to find the files/paths in a change that are owned by the
* given account.
*
- * @param changeNotes the notes of the change for which the current code owner statuses should be
- * returned
+ * @param changeNotes the notes of the change for which the code owner statuses should be returned
+ * @param patchSet the patch set for which the code owner statuses should be returned
* @param accountId the ID of the account for which an approval should be assumed
*/
+ @VisibleForTesting
public Stream<FileCodeOwnerStatus> getFileStatusesForAccount(
- ChangeNotes changeNotes, Account.Id accountId)
- throws ResourceConflictException, IOException, PatchListNotAvailableException {
+ ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId)
+ throws ResourceConflictException, IOException, PatchListNotAvailableException,
+ DiffNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
+ requireNonNull(patchSet, "patchSet");
requireNonNull(accountId, "accountId");
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Compute file statuses for account",
- Metadata.builder()
- .projectName(changeNotes.getProjectName().get())
- .changeId(changeNotes.getChangeId().get())
- .build())) {
- RequiredApproval requiredApproval =
- codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName());
+ try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputationForAccount.start()) {
+ logger.atFine().log(
+ "prepare stream to compute file statuses for account %d (project = %s, change = %d,"
+ + " patch set = %d)",
+ accountId.get(),
+ changeNotes.getProjectName(),
+ changeNotes.getChangeId().get(),
+ patchSet.id().get());
+
+ CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
+ codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
+
+ RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
logger.atFine().log("requiredApproval = %s", requiredApproval);
BranchNameKey branch = changeNotes.getChange().getDest();
ObjectId revision = getDestBranchRevision(changeNotes.getChange());
logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name());
- // If the branch doesn't contain any code owner config file yet, we apply special logic
- // (project owners count as code owners) to allow bootstrapping the code owner configuration
- // in the branch.
- boolean isBootstrapping =
- !codeOwnerConfigScannerFactory.create().containsAnyCodeOwnerConfigFile(branch);
boolean isProjectOwner = isProjectOwner(changeNotes.getProjectName(), accountId);
+ FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
logger.atFine().log(
- "isBootstrapping = %s (isProjectOwner = %s)", isBootstrapping, isProjectOwner);
- if (isBootstrapping && isProjectOwner) {
- // Return all paths as approved.
- return changedFiles
- .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
- .stream()
- .map(
- changedFile ->
- FileCodeOwnerStatus.create(
- changedFile,
- changedFile
- .newPath()
- .map(
- newPath ->
- PathCodeOwnerStatus.create(newPath, CodeOwnerStatus.APPROVED)),
- changedFile
- .oldPath()
- .map(
- oldPath ->
- PathCodeOwnerStatus.create(
- oldPath, CodeOwnerStatus.APPROVED))));
+ "fallbackCodeOwner = %s, isProjectOwner = %s", fallbackCodeOwners, isProjectOwner);
+ if (fallbackCodeOwners.equals(FallbackCodeOwners.PROJECT_OWNERS) && isProjectOwner) {
+ return getAllPathsAsApproved(changeNotes, patchSet);
}
- return changedFiles
- .compute(changeNotes.getProjectName(), changeNotes.getCurrentPatchSet().commitId())
- .stream()
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get();
+ return changedFiles.getOrCompute(changeNotes.getProjectName(), patchSet.commitId()).stream()
.map(
changedFile ->
getFileStatus(
+ codeOwnerConfigHierarchy,
branch,
revision,
/* globalCodeOwners= */ CodeOwnerResolverResult.createEmpty(),
@@ -308,86 +425,121 @@
// should be ignored. For the given account we do not need to check for
// implicit approvals since all owned files are already covered by the
// explicit approval.
- /* enableImplicitApprovalFromUploader= */ false,
- /* patchSetUploader= */ null,
+ /* implicitApprover= */ null,
/* reviewerAccountIds= */ ImmutableSet.of(),
// Assume an explicit approval of the given account.
/* approverAccountIds= */ ImmutableSet.of(accountId),
+ fallbackCodeOwners,
/* hasOverride= */ false,
- /* isBootstrapping= */ false,
changedFile));
}
}
+ private boolean isPureRevert(ChangeNotes changeNotes) throws IOException {
+ try {
+ return changeNotes.getChange().getRevertOf() != null
+ && pureRevertCache.isPureRevert(changeNotes);
+ } catch (BadRequestException e) {
+ throw new CodeOwnersInternalServerErrorException(
+ String.format(
+ "failed to check if change %s in project %s is a pure revert",
+ changeNotes.getChangeId(), changeNotes.getProjectName()),
+ e);
+ }
+ }
+
+ private Stream<FileCodeOwnerStatus> getAllPathsAsApproved(
+ ChangeNotes changeNotes, PatchSet patchSet)
+ throws IOException, PatchListNotAvailableException, DiffNotAvailableException {
+ return changedFiles.getOrCompute(changeNotes.getProjectName(), patchSet.commitId()).stream()
+ .map(
+ changedFile ->
+ FileCodeOwnerStatus.create(
+ changedFile,
+ changedFile
+ .newPath()
+ .map(
+ newPath ->
+ PathCodeOwnerStatus.create(newPath, CodeOwnerStatus.APPROVED)),
+ changedFile
+ .oldPath()
+ .map(
+ oldPath ->
+ PathCodeOwnerStatus.create(oldPath, CodeOwnerStatus.APPROVED))));
+ }
+
private FileCodeOwnerStatus getFileStatus(
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
BranchNameKey branch,
ObjectId revision,
CodeOwnerResolverResult globalCodeOwners,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader,
+ @Nullable Account.Id implicitApprover,
ImmutableSet<Account.Id> reviewerAccountIds,
ImmutableSet<Account.Id> approverAccountIds,
+ FallbackCodeOwners fallbackCodeOwners,
boolean hasOverride,
- boolean isBootstrapping,
ChangedFile changedFile) {
- logger.atFine().log("computing file status for %s", changedFile);
+ try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatus.start()) {
+ logger.atFine().log("computing file status for %s", changedFile);
- // Compute the code owner status for the new path, if there is a new path.
- Optional<PathCodeOwnerStatus> newPathStatus =
- changedFile
- .newPath()
- .map(
- newPath ->
- getPathCodeOwnerStatus(
- branch,
- revision,
- globalCodeOwners,
- enableImplicitApprovalFromUploader,
- patchSetUploader,
- reviewerAccountIds,
- approverAccountIds,
- hasOverride,
- isBootstrapping,
- newPath));
+ // Compute the code owner status for the new path, if there is a new path.
+ Optional<PathCodeOwnerStatus> newPathStatus =
+ changedFile
+ .newPath()
+ .map(
+ newPath ->
+ getPathCodeOwnerStatus(
+ codeOwnerConfigHierarchy,
+ branch,
+ revision,
+ globalCodeOwners,
+ implicitApprover,
+ reviewerAccountIds,
+ approverAccountIds,
+ fallbackCodeOwners,
+ hasOverride,
+ newPath));
- // Compute the code owner status for the old path, if the file was deleted or renamed.
- Optional<PathCodeOwnerStatus> oldPathStatus = Optional.empty();
- if (changedFile.isDeletion() || changedFile.isRename()) {
- checkState(changedFile.oldPath().isPresent(), "old path must be present for deletion/rename");
- logger.atFine().log(
- "file was %s (old path = %s)",
- changedFile.isDeletion() ? "deleted" : "renamed", changedFile.oldPath().get());
- oldPathStatus =
- Optional.of(
- getPathCodeOwnerStatus(
- branch,
- revision,
- globalCodeOwners,
- enableImplicitApprovalFromUploader,
- patchSetUploader,
- reviewerAccountIds,
- approverAccountIds,
- hasOverride,
- isBootstrapping,
- changedFile.oldPath().get()));
+ // Compute the code owner status for the old path, if the file was deleted or renamed.
+ Optional<PathCodeOwnerStatus> oldPathStatus = Optional.empty();
+ if (changedFile.isDeletion() || changedFile.isRename()) {
+ checkState(
+ changedFile.oldPath().isPresent(), "old path must be present for deletion/rename");
+ logger.atFine().log(
+ "file was %s (old path = %s)",
+ changedFile.isDeletion() ? "deleted" : "renamed", changedFile.oldPath().get());
+ oldPathStatus =
+ Optional.of(
+ getPathCodeOwnerStatus(
+ codeOwnerConfigHierarchy,
+ branch,
+ revision,
+ globalCodeOwners,
+ implicitApprover,
+ reviewerAccountIds,
+ approverAccountIds,
+ fallbackCodeOwners,
+ hasOverride,
+ changedFile.oldPath().get()));
+ }
+
+ FileCodeOwnerStatus fileCodeOwnerStatus =
+ FileCodeOwnerStatus.create(changedFile, newPathStatus, oldPathStatus);
+ logger.atFine().log("fileCodeOwnerStatus = %s", fileCodeOwnerStatus);
+ return fileCodeOwnerStatus;
}
-
- FileCodeOwnerStatus fileCodeOwnerStatus =
- FileCodeOwnerStatus.create(changedFile, newPathStatus, oldPathStatus);
- logger.atFine().log("fileCodeOwnerStatus = %s", fileCodeOwnerStatus);
- return fileCodeOwnerStatus;
}
private PathCodeOwnerStatus getPathCodeOwnerStatus(
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
BranchNameKey branch,
ObjectId revision,
CodeOwnerResolverResult globalCodeOwners,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader,
+ @Nullable Account.Id implicitApprover,
ImmutableSet<Account.Id> reviewerAccountIds,
ImmutableSet<Account.Id> approverAccountIds,
+ FallbackCodeOwners fallbackCodeOwners,
boolean hasOverride,
- boolean isBootstrapping,
Path absolutePath) {
logger.atFine().log("computing path status for %s", absolutePath);
@@ -398,99 +550,134 @@
return PathCodeOwnerStatus.create(absolutePath, CodeOwnerStatus.APPROVED);
}
- return isBootstrapping
- ? getPathCodeOwnerStatusBootstrappingMode(
- branch,
- globalCodeOwners,
- enableImplicitApprovalFromUploader,
- patchSetUploader,
- reviewerAccountIds,
- approverAccountIds,
- absolutePath)
- : getPathCodeOwnerStatusRegularMode(
- branch,
- globalCodeOwners,
- enableImplicitApprovalFromUploader,
- patchSetUploader,
- revision,
- reviewerAccountIds,
- approverAccountIds,
- absolutePath);
- }
+ AtomicReference<CodeOwnerStatus> codeOwnerStatus =
+ new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
- /**
- * Gets the code owner status for the given path when the branch doesn't contain any code owner
- * config file yet (bootstrapping mode).
- *
- * <p>If we are in bootstrapping mode we consider project owners as code owners. This allows
- * bootstrapping the code owner configuration in the branch.
- */
- private PathCodeOwnerStatus getPathCodeOwnerStatusBootstrappingMode(
- BranchNameKey branch,
- CodeOwnerResolverResult globalCodeOwners,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader,
- ImmutableSet<Account.Id> reviewerAccountIds,
- ImmutableSet<Account.Id> approverAccountIds,
- Path absolutePath) {
- logger.atFine().log("computing path status for %s (bootstrapping mode)", absolutePath);
+ if (isApproved(absolutePath, globalCodeOwners, approverAccountIds, implicitApprover)) {
+ logger.atFine().log("%s was approved by a global code owner", absolutePath);
+ codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ } else {
+ logger.atFine().log("%s was not approved by a global code owner", absolutePath);
- CodeOwnerStatus codeOwnerStatus = CodeOwnerStatus.INSUFFICIENT_REVIEWERS;
- if (isApprovedBootstrappingMode(
- branch.project(),
- absolutePath,
- globalCodeOwners,
- approverAccountIds,
- enableImplicitApprovalFromUploader,
- patchSetUploader)) {
- codeOwnerStatus = CodeOwnerStatus.APPROVED;
- } else if (isPendingBootstrappingMode(
- branch.project(), absolutePath, globalCodeOwners, reviewerAccountIds)) {
- codeOwnerStatus = CodeOwnerStatus.PENDING;
- }
+ if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) {
+ logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath);
+ codeOwnerStatus.set(CodeOwnerStatus.PENDING);
+ }
- // Since there are no code owner config files in bootstrapping mode, fallback code owners also
- // apply if they are configured. We can skip checking them if we already found that the file was
- // approved.
- if (codeOwnerStatus != CodeOwnerStatus.APPROVED) {
- codeOwnerStatus =
- getCodeOwnerStatusForFallbackCodeOwners(
- codeOwnerStatus,
- branch.project(),
- enableImplicitApprovalFromUploader,
- reviewerAccountIds,
- approverAccountIds,
- absolutePath);
+ AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
+ AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false);
+ codeOwnerConfigHierarchy.visitForFile(
+ branch,
+ revision,
+ absolutePath,
+ (PathCodeOwnersVisitor)
+ pathCodeOwners -> {
+ CodeOwnerResolverResult codeOwners = resolveCodeOwners(pathCodeOwners);
+ logger.atFine().log(
+ "code owners = %s (code owner config folder path = %s, file name = %s)",
+ codeOwners,
+ pathCodeOwners.getCodeOwnerConfig().key().folderPath(),
+ pathCodeOwners.getCodeOwnerConfig().key().fileName().orElse("<default>"));
+
+ if (codeOwners.hasRevelantCodeOwnerDefinitions()) {
+ hasRevelantCodeOwnerDefinitions.set(true);
+ }
+
+ if (isApproved(absolutePath, codeOwners, approverAccountIds, implicitApprover)) {
+ codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ return false;
+ } else if (isPending(absolutePath, codeOwners, reviewerAccountIds)) {
+ codeOwnerStatus.set(CodeOwnerStatus.PENDING);
+
+ // We need to continue to check if any of the higher-level code owners approved
+ // the change.
+ return true;
+ }
+
+ // We need to continue to check if any of the higher-level code owners approved the
+ // change or is a reviewer.
+ return true;
+ },
+ codeOwnerConfigKey -> {
+ logger.atFine().log(
+ "code owner config %s ignores parent code owners for %s",
+ codeOwnerConfigKey, absolutePath);
+ parentCodeOwnersAreIgnored.set(true);
+ });
+
+ // If no code owners have been defined for the file and if parent code owners are not ignored,
+ // the fallback code owners apply if they are configured. We can skip checking them if we
+ // already found that the file was approved.
+ if (codeOwnerStatus.get() != CodeOwnerStatus.APPROVED
+ && !hasRevelantCodeOwnerDefinitions.get()
+ && !parentCodeOwnersAreIgnored.get()) {
+ codeOwnerStatus.set(
+ getCodeOwnerStatusForFallbackCodeOwners(
+ codeOwnerStatus.get(),
+ branch,
+ globalCodeOwners,
+ implicitApprover,
+ reviewerAccountIds,
+ approverAccountIds,
+ fallbackCodeOwners,
+ absolutePath));
+ }
}
PathCodeOwnerStatus pathCodeOwnerStatus =
- PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus);
+ PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get());
logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus);
return pathCodeOwnerStatus;
}
- private boolean isApprovedBootstrappingMode(
+ /**
+ * Gets the code owner status for the given path when project owners are configured as fallback
+ * code owners.
+ */
+ private CodeOwnerStatus getCodeOwnerStatusForProjectOwnersAsFallbackCodeOwners(
+ BranchNameKey branch,
+ CodeOwnerResolverResult globalCodeOwners,
+ @Nullable Account.Id implicitApprover,
+ ImmutableSet<Account.Id> reviewerAccountIds,
+ ImmutableSet<Account.Id> approverAccountIds,
+ Path absolutePath) {
+ logger.atFine().log(
+ "computing code owner status for %s with project owners as fallback code owners",
+ absolutePath);
+
+ CodeOwnerStatus codeOwnerStatus = CodeOwnerStatus.INSUFFICIENT_REVIEWERS;
+ if (isApprovedByProjectOwnerOrGlobalOwner(
+ branch.project(), absolutePath, globalCodeOwners, approverAccountIds, implicitApprover)) {
+ codeOwnerStatus = CodeOwnerStatus.APPROVED;
+ } else if (isPendingByProjectOwnerOrGlobalOwner(
+ branch.project(), absolutePath, globalCodeOwners, reviewerAccountIds)) {
+ codeOwnerStatus = CodeOwnerStatus.PENDING;
+ }
+
+ logger.atFine().log("codeOwnerStatus = %s", codeOwnerStatus);
+ return codeOwnerStatus;
+ }
+
+ private boolean isApprovedByProjectOwnerOrGlobalOwner(
Project.NameKey projectName,
Path absolutePath,
CodeOwnerResolverResult globalCodeOwners,
ImmutableSet<Account.Id> approverAccountIds,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader) {
- return (enableImplicitApprovalFromUploader
- && isImplicitlyApprovedBootstrappingMode(
- projectName, absolutePath, globalCodeOwners, patchSetUploader))
- || isExplicitlyApprovedBootstrappingMode(
+ @Nullable Account.Id implicitApprover) {
+ return (implicitApprover != null
+ && isImplicitlyApprovedByProjectOwnerOrGlobalOwner(
+ projectName, absolutePath, globalCodeOwners, implicitApprover))
+ || isExplicitlyApprovedByProjectOwnerOrGlobalOwner(
projectName, absolutePath, globalCodeOwners, approverAccountIds);
}
- private boolean isImplicitlyApprovedBootstrappingMode(
+ private boolean isImplicitlyApprovedByProjectOwnerOrGlobalOwner(
Project.NameKey projectName,
Path absolutePath,
CodeOwnerResolverResult globalCodeOwners,
- Account.Id patchSetUploader) {
- requireNonNull(
- patchSetUploader, "patchSetUploader must be set if implicit approvals are enabled");
- if (isProjectOwner(projectName, patchSetUploader)) {
+ Account.Id implicitApprover) {
+ requireNonNull(implicitApprover, "implicitApprover");
+ if (isProjectOwner(projectName, implicitApprover)) {
// The uploader of the patch set is a project owner and thus a code owner. This means there
// is an implicit code owner approval from the patch set uploader so that the path is
// automatically approved.
@@ -501,7 +688,7 @@
}
if (globalCodeOwners.ownedByAllUsers()
- || globalCodeOwners.codeOwnersAccountIds().contains(patchSetUploader)) {
+ || globalCodeOwners.codeOwnersAccountIds().contains(implicitApprover)) {
// If the uploader of the patch set is a global code owner, there is an implicit code owner
// approval from the patch set uploader so that the path is automatically approved.
logger.atFine().log(
@@ -513,7 +700,7 @@
return false;
}
- private boolean isExplicitlyApprovedBootstrappingMode(
+ private boolean isExplicitlyApprovedByProjectOwnerOrGlobalOwner(
Project.NameKey projectName,
Path absolutePath,
CodeOwnerResolverResult globalCodeOwners,
@@ -535,7 +722,7 @@
return false;
}
- private boolean isPendingBootstrappingMode(
+ private boolean isPendingByProjectOwnerOrGlobalOwner(
Project.NameKey projectName,
Path absolutePath,
CodeOwnerResolverResult globalCodeOwners,
@@ -557,119 +744,17 @@
}
/**
- * Gets the code owner status for the given path when the branch contains at least one code owner
- * config file (regular mode).
- */
- private PathCodeOwnerStatus getPathCodeOwnerStatusRegularMode(
- BranchNameKey branch,
- CodeOwnerResolverResult globalCodeOwners,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader,
- ObjectId revision,
- ImmutableSet<Account.Id> reviewerAccountIds,
- ImmutableSet<Account.Id> approverAccountIds,
- Path absolutePath) {
- logger.atFine().log("computing path status for %s (regular mode)", absolutePath);
-
- AtomicReference<CodeOwnerStatus> codeOwnerStatus =
- new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
-
- if (isApproved(
- absolutePath,
- globalCodeOwners,
- approverAccountIds,
- enableImplicitApprovalFromUploader,
- patchSetUploader)) {
- logger.atFine().log("%s was approved by a global code owner", absolutePath);
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
- } else {
- logger.atFine().log("%s was not approved by a global code owner", absolutePath);
-
- if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) {
- logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath);
- codeOwnerStatus.set(CodeOwnerStatus.PENDING);
- }
-
- AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
- AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false);
- codeOwnerConfigHierarchy.visit(
- branch,
- revision,
- absolutePath,
- codeOwnerConfig -> {
- CodeOwnerResolverResult codeOwners = getCodeOwners(codeOwnerConfig, absolutePath);
- logger.atFine().log(
- "code owners = %s (code owner config folder path = %s, file name = %s)",
- codeOwners,
- codeOwnerConfig.key().folderPath(),
- codeOwnerConfig.key().fileName().orElse("<default>"));
-
- if (codeOwners.hasRevelantCodeOwnerDefinitions()) {
- hasRevelantCodeOwnerDefinitions.set(true);
- }
-
- if (isApproved(
- absolutePath,
- codeOwners,
- approverAccountIds,
- enableImplicitApprovalFromUploader,
- patchSetUploader)) {
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
- return false;
- } else if (isPending(absolutePath, codeOwners, reviewerAccountIds)) {
- codeOwnerStatus.set(CodeOwnerStatus.PENDING);
-
- // We need to continue to check if any of the higher-level code owners approved the
- // change.
- return true;
- }
-
- // We need to continue to check if any of the higher-level code owners approved the
- // change or is a reviewer.
- return true;
- },
- codeOwnerConfigKey -> {
- logger.atFine().log(
- "code owner config %s ignores parent code owners for %s",
- codeOwnerConfigKey, absolutePath);
- parentCodeOwnersAreIgnored.set(true);
- });
-
- // If no code owners have been defined for the file and if parent code owners are not ignored,
- // the fallback code owners apply if they are configured. We can skip checking them if we
- // already found that the file was approved.
- if (codeOwnerStatus.get() != CodeOwnerStatus.APPROVED
- && !hasRevelantCodeOwnerDefinitions.get()
- && !parentCodeOwnersAreIgnored.get()) {
- codeOwnerStatus.set(
- getCodeOwnerStatusForFallbackCodeOwners(
- codeOwnerStatus.get(),
- branch.project(),
- enableImplicitApprovalFromUploader,
- reviewerAccountIds,
- approverAccountIds,
- absolutePath));
- }
- }
-
- PathCodeOwnerStatus pathCodeOwnerStatus =
- PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get());
- logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus);
- return pathCodeOwnerStatus;
- }
-
- /**
* Computes the code owner status for the given path based on the configured fallback code owners.
*/
private CodeOwnerStatus getCodeOwnerStatusForFallbackCodeOwners(
CodeOwnerStatus codeOwnerStatus,
- Project.NameKey project,
- boolean enableImplicitApprovalFromUploader,
+ BranchNameKey branch,
+ CodeOwnerResolverResult globalCodeOwners,
+ @Nullable Account.Id implicitApprover,
ImmutableSet<Account.Id> reviewerAccountIds,
ImmutableSet<Account.Id> approverAccountIds,
+ FallbackCodeOwners fallbackCodeOwners,
Path absolutePath) {
- FallbackCodeOwners fallbackCodeOwners =
- codeOwnersPluginConfiguration.getFallbackCodeOwners(project);
logger.atFine().log(
"getting code owner status for fallback code owners (fallback code owners = %s)",
fallbackCodeOwners);
@@ -677,15 +762,20 @@
case NONE:
logger.atFine().log("no fallback code owners");
return codeOwnerStatus;
- case ALL_USERS:
- return getCodeOwnerStatusIfAllUsersAreCodeOwners(
- enableImplicitApprovalFromUploader,
+ case PROJECT_OWNERS:
+ return getCodeOwnerStatusForProjectOwnersAsFallbackCodeOwners(
+ branch,
+ globalCodeOwners,
+ implicitApprover,
reviewerAccountIds,
approverAccountIds,
absolutePath);
+ case ALL_USERS:
+ return getCodeOwnerStatusIfAllUsersAreCodeOwners(
+ implicitApprover != null, reviewerAccountIds, approverAccountIds, absolutePath);
}
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format("unknown fallback code owners configured: %s", fallbackCodeOwners));
}
@@ -722,12 +812,9 @@
Path absolutePath,
CodeOwnerResolverResult codeOwners,
ImmutableSet<Account.Id> approverAccountIds,
- boolean enableImplicitApprovalFromUploader,
- @Nullable Account.Id patchSetUploader) {
- if (enableImplicitApprovalFromUploader) {
- requireNonNull(
- patchSetUploader, "patchSetUploader must be set if implicit approvals are enabled");
- if (codeOwners.codeOwnersAccountIds().contains(patchSetUploader)
+ @Nullable Account.Id implicitApprover) {
+ if (implicitApprover != null) {
+ if (codeOwners.codeOwnersAccountIds().contains(implicitApprover)
|| codeOwners.ownedByAllUsers()) {
// If the uploader of the patch set owns the path, there is an implicit code owner
// approval from the patch set uploader so that the path is automatically approved.
@@ -772,7 +859,7 @@
}
return isProjectOwner;
} catch (PermissionBackendException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format(
"failed to check owner permission of project %s for account %d",
project.get(), accountId.get()),
@@ -781,17 +868,12 @@
}
/**
- * Gets the code owners that own the given path according to the given code owner config.
+ * Resolves the given path code owners.
*
- * @param codeOwnerConfig the code owner config from which the code owners should be retrieved
- * @param absolutePath the path for which the code owners should be retrieved
+ * @param pathCodeOwners the path code owners that should be resolved
*/
- private CodeOwnerResolverResult getCodeOwners(
- CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
- return codeOwnerResolver
- .get()
- .enforceVisibility(false)
- .resolvePathCodeOwners(codeOwnerConfig, absolutePath);
+ private CodeOwnerResolverResult resolveCodeOwners(PathCodeOwners pathCodeOwners) {
+ return codeOwnerResolver.get().enforceVisibility(false).resolvePathCodeOwners(pathCodeOwners);
}
/**
@@ -820,23 +902,13 @@
*
* @param requiredApproval approval that is required from code owners to approve the files in a
* change
- * @param changeNotes the change notes
*/
private ImmutableSet<Account.Id> getApproverAccountIds(
- RequiredApproval requiredApproval, ChangeNotes changeNotes, Account.Id patchSetUploader) {
+ ImmutableList<PatchSetApproval> currentPatchSetApprovals,
+ RequiredApproval requiredApproval,
+ Account.Id patchSetUploader) {
ImmutableSet<Account.Id> approverAccountIds =
- StreamSupport.stream(
- approvalsUtil
- .byPatchSet(
- changeNotes,
- changeNotes.getCurrentPatchSet().id(),
- /** revWalk */
- null,
- /** repoConfig */
- null)
- .spliterator(),
- /** parallel */
- false)
+ currentPatchSetApprovals.stream()
.filter(requiredApproval::isApprovedBy)
.map(PatchSetApproval::accountId)
.collect(toImmutableSet());
@@ -853,6 +925,19 @@
return approverAccountIds;
}
+ private ImmutableList<PatchSetApproval> getCurrentPatchSetApprovals(ChangeNotes changeNotes) {
+ try (Timer0.Context ctx = codeOwnerMetrics.computePatchSetApprovals.start()) {
+ return ImmutableList.copyOf(
+ approvalsUtil.byPatchSet(
+ changeNotes,
+ changeNotes.getCurrentPatchSet().id(),
+ /** revWalk */
+ null,
+ /** repoConfig */
+ null));
+ }
+ }
+
private ImmutableSet<Account.Id> filterOutAccount(
ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) {
return accountIds.stream()
@@ -864,19 +949,18 @@
* Checks whether the given change has an override approval.
*
* @param overrideApprovals approvals that count as override for the code owners submit check.
- * @param changeNotes the change notes
* @param patchSetUploader account ID of the patch set uploader
* @return whether the given change has an override approval
*/
private boolean hasOverride(
+ ImmutableList<PatchSetApproval> currentPatchSetApprovals,
ImmutableSet<RequiredApproval> overrideApprovals,
- ChangeNotes changeNotes,
Account.Id patchSetUploader) {
ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals =
overrideApprovals.stream()
.filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval())
.collect(toImmutableSet());
- return changeNotes.getApprovals().get(changeNotes.getCurrentPatchSet().id()).stream()
+ return currentPatchSetApprovals.stream()
.filter(
approval -> {
// If the approval is from the patch set uploader and if it matches any of the labels
@@ -889,6 +973,9 @@
.labelType()
.getLabelId()
.equals(approval.key().labelId()))) {
+ logger.atFine().log(
+ "Filtered out self-override %s of patch set uploader",
+ LabelVote.create(approval.label(), approval.value()));
return false;
}
return true;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
index be5318d..3775f89 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
@@ -112,9 +112,7 @@
* backend. It this case all {@link CodeOwnerSet}s that have path expressions are ignored and will
* not have any effect.
*/
- default Optional<PathExpressionMatcher> getPathExpressionMatcher() {
- return Optional.empty();
- }
+ Optional<PathExpressionMatcher> getPathExpressionMatcher();
/**
* Replaces the old email in the given code owner config file content with the new email.
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
index d01c4a3..fc78ab8 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
@@ -306,6 +306,11 @@
return folderPath().resolve(fileName().orElse(defaultCodeOwnerConfigFileName));
}
+ /** User-readable string representing of this code owner config key. */
+ public String format(CodeOwners codeOwners) {
+ return String.format("%s:%s:%s", project(), shortBranchName(), codeOwners.getFilePath(this));
+ }
+
/**
* Creates a builder from this code owner config key.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
index 5ecddb3..90fd878 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -19,9 +19,13 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
-import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -36,9 +40,9 @@
* A representation of a code owner config that is stored as an {@code OWNERS} file in a source
* branch.
*
- * <p>For reading code owner configs or creating/updating them, refer to {@link #load(String,
- * CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)} and {@link #loadCurrent(String,
- * CodeOwnerConfigParser, Repository, CodeOwnerConfig.Key)}.
+ * <p>For reading code owner configs or creating/updating them, refer to {@link Factory#load(String,
+ * CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)} and {@link
+ * Factory#loadCurrent(String, CodeOwnerConfigParser, Repository, CodeOwnerConfig.Key)}.
*
* <p><strong>Note:</strong> Any modification (code owner config creation or update) only becomes
* permanent (and hence written to repository) if {@link
@@ -46,83 +50,95 @@
*/
@VisibleForTesting
public class CodeOwnerConfigFile extends VersionedMetaData {
- /**
- * Creates a {@link CodeOwnerConfigFile} for a code owner config.
- *
- * <p>The code owner config is automatically loaded within this method and can be accessed via
- * {@link #getLoadedCodeOwnerConfig()}.
- *
- * <p>It's safe to call this method for non-existing code owner configs. In that case, {@link
- * #getLoadedCodeOwnerConfig()} won't return any code owner config. Thus, the existence of a code
- * owner config can be easily tested.
- *
- * <p>The code owner config represented by the returned {@link CodeOwnerConfigFile} can be
- * created/updated by setting an {@link CodeOwnerConfigUpdate} via {@link
- * #setCodeOwnerConfigUpdate(CodeOwnerConfigUpdate)} and committing the {@link
- * CodeOwnerConfigUpdate} via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate)}.
- *
- * @param defaultFileName the name of the code owner configuration files that should be used if
- * none is specified in the code owner config key
- * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
- * @param revWalk the revWalk that should be used to load the revision
- * @param revision the branch revision from which the code owner config file should be loaded
- * @param codeOwnerConfigKey the key of the code owner config
- * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
- * @throws IOException if the repository can't be accessed for some reason
- * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
- * invalid format
- */
- public static CodeOwnerConfigFile load(
- String defaultFileName,
- CodeOwnerConfigParser codeOwnerConfigParser,
- RevWalk revWalk,
- ObjectId revision,
- CodeOwnerConfig.Key codeOwnerConfigKey)
- throws IOException, ConfigInvalidException {
- requireNonNull(defaultFileName, "defaultFileName");
- requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
- requireNonNull(revWalk, "revWalk");
- requireNonNull(revision, "revision");
- requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+ public static class Factory {
+ private final CodeOwnerMetrics codeOwnerMetrics;
- CodeOwnerConfigFile codeOwnerConfigFile =
- new CodeOwnerConfigFile(defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
- codeOwnerConfigFile.load(codeOwnerConfigKey.project(), revWalk, revision);
- return codeOwnerConfigFile;
+ @Inject
+ Factory(CodeOwnerMetrics codeOwnerMetrics) {
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ /**
+ * Creates a {@link CodeOwnerConfigFile} for a code owner config.
+ *
+ * <p>The code owner config is automatically loaded within this method and can be accessed via
+ * {@link #getLoadedCodeOwnerConfig()}.
+ *
+ * <p>It's safe to call this method for non-existing code owner configs. In that case, {@link
+ * #getLoadedCodeOwnerConfig()} won't return any code owner config. Thus, the existence of a
+ * code owner config can be easily tested.
+ *
+ * <p>The code owner config represented by the returned {@link CodeOwnerConfigFile} can be
+ * created/updated by setting an {@link CodeOwnerConfigUpdate} via {@link
+ * #setCodeOwnerConfigUpdate(CodeOwnerConfigUpdate)} and committing the {@link
+ * CodeOwnerConfigUpdate} via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate)}.
+ *
+ * @param defaultFileName the name of the code owner configuration files that should be used if
+ * none is specified in the code owner config key
+ * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
+ * @param revWalk the revWalk that should be used to load the revision
+ * @param revision the branch revision from which the code owner config file should be loaded
+ * @param codeOwnerConfigKey the key of the code owner config
+ * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
+ * @throws IOException if the repository can't be accessed for some reason
+ * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
+ * invalid format
+ */
+ public CodeOwnerConfigFile load(
+ String defaultFileName,
+ CodeOwnerConfigParser codeOwnerConfigParser,
+ RevWalk revWalk,
+ ObjectId revision,
+ CodeOwnerConfig.Key codeOwnerConfigKey)
+ throws IOException, ConfigInvalidException {
+ requireNonNull(defaultFileName, "defaultFileName");
+ requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
+ requireNonNull(revWalk, "revWalk");
+ requireNonNull(revision, "revision");
+ requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+
+ CodeOwnerConfigFile codeOwnerConfigFile =
+ new CodeOwnerConfigFile(
+ codeOwnerMetrics, defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
+ codeOwnerConfigFile.load(codeOwnerConfigKey.project(), revWalk, revision);
+ return codeOwnerConfigFile;
+ }
+
+ /**
+ * Creates a {@link CodeOwnerConfigFile} for a code owner config from the current revision in
+ * the branch.
+ *
+ * @param defaultFileName the name of the code owner configuration files that should be used if
+ * none is specified in the code owner config key
+ * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
+ * @param repository the repository in which the code owner config is stored
+ * @param codeOwnerConfigKey the key of the code owner config
+ * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
+ * @throws IOException if the repository can't be accessed for some reason
+ * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
+ * invalid format
+ * @see #load(String, CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)
+ */
+ public CodeOwnerConfigFile loadCurrent(
+ String defaultFileName,
+ CodeOwnerConfigParser codeOwnerConfigParser,
+ Repository repository,
+ CodeOwnerConfig.Key codeOwnerConfigKey)
+ throws IOException, ConfigInvalidException {
+ requireNonNull(defaultFileName, "defaultFileName");
+ requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
+ requireNonNull(repository, "repository");
+ requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+
+ CodeOwnerConfigFile codeOwnerConfigFile =
+ new CodeOwnerConfigFile(
+ codeOwnerMetrics, defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
+ codeOwnerConfigFile.load(codeOwnerConfigKey.project(), repository);
+ return codeOwnerConfigFile;
+ }
}
- /**
- * Creates a {@link CodeOwnerConfigFile} for a code owner config from the current revision in the
- * branch.
- *
- * @param defaultFileName the name of the code owner configuration files that should be used if
- * none is specified in the code owner config key
- * @param codeOwnerConfigParser the parser that should be used to parse code owner config files
- * @param repository the repository in which the code owner config is stored
- * @param codeOwnerConfigKey the key of the code owner config
- * @return a {@link CodeOwnerConfigFile} for the code owner config with the specified key
- * @throws IOException if the repository can't be accessed for some reason
- * @throws ConfigInvalidException if the code owner config exists but can't be read due to an
- * invalid format
- * @see #load(String, CodeOwnerConfigParser, RevWalk, ObjectId, CodeOwnerConfig.Key)
- */
- public static CodeOwnerConfigFile loadCurrent(
- String defaultFileName,
- CodeOwnerConfigParser codeOwnerConfigParser,
- Repository repository,
- CodeOwnerConfig.Key codeOwnerConfigKey)
- throws IOException, ConfigInvalidException {
- requireNonNull(defaultFileName, "defaultFileName");
- requireNonNull(codeOwnerConfigParser, "codeOwnerConfigParser");
- requireNonNull(repository, "repository");
- requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
-
- CodeOwnerConfigFile codeOwnerConfigFile =
- new CodeOwnerConfigFile(defaultFileName, codeOwnerConfigParser, codeOwnerConfigKey);
- codeOwnerConfigFile.load(codeOwnerConfigKey.project(), repository);
- return codeOwnerConfigFile;
- }
-
+ private final CodeOwnerMetrics codeOwnerMetrics;
private final String defaultFileName;
private final CodeOwnerConfigParser codeOwnerConfigParser;
private final CodeOwnerConfig.Key codeOwnerConfigKey;
@@ -132,9 +148,11 @@
private Optional<CodeOwnerConfigUpdate> codeOwnerConfigUpdate = Optional.empty();
private CodeOwnerConfigFile(
+ CodeOwnerMetrics codeOwnerMetrics,
String defaultFileName,
CodeOwnerConfigParser codeOwnerConfigParser,
CodeOwnerConfig.Key codeOwnerConfigKey) {
+ this.codeOwnerMetrics = codeOwnerMetrics;
this.defaultFileName = defaultFileName;
this.codeOwnerConfigParser = codeOwnerConfigParser;
this.codeOwnerConfigKey = codeOwnerConfigKey;
@@ -186,18 +204,33 @@
}
@Override
+ protected byte[] readFile(String fileName) throws IOException {
+ try (Timer0.Context ctx = codeOwnerMetrics.readCodeOwnerConfig.start()) {
+ return super.readFile(fileName);
+ }
+ }
+
+ @Override
protected void onLoad() throws IOException, ConfigInvalidException {
if (revision != null) {
- Optional<String> codeOwnerConfigFileContent =
- getFileIfItExists(JgitPath.of(codeOwnerConfigKey.filePath(defaultFileName)).get());
+ String codeOwnerConfigFilePath =
+ JgitPath.of(codeOwnerConfigKey.filePath(defaultFileName)).get();
+ Optional<String> codeOwnerConfigFileContent = getFileIfItExists(codeOwnerConfigFilePath);
if (codeOwnerConfigFileContent.isPresent()) {
- try {
+ try (Timer1.Context<String> ctx =
+ codeOwnerMetrics.parseCodeOwnerConfig.start(
+ codeOwnerConfigParser.getClass().getSimpleName())) {
loadedCodeOwnersConfig =
Optional.of(
codeOwnerConfigParser.parse(
revision, codeOwnerConfigKey, codeOwnerConfigFileContent.get()));
} catch (CodeOwnerConfigParseException e) {
- throw new ConfigInvalidException(e.getFullMessage(defaultFileName), e);
+ throw new InvalidCodeOwnerConfigException(
+ e.getFullMessage(defaultFileName),
+ projectName,
+ getRefName(),
+ codeOwnerConfigFilePath,
+ e);
}
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
index 6222b7d..9e15746 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -19,9 +19,8 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.RefUpdateUtil;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -93,7 +92,10 @@
requireNonNull(commitMessage, "commitMessage");
requireNonNull(codeOwnerConfigFileUpdater, "codeOwnerConfigFileUpdater");
- CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+ CodeOwnerBackend codeOwnerBackend =
+ codeOwnersPluginConfiguration
+ .getProjectConfig(branchNameKey.project())
+ .getBackend(branchNameKey.branch());
logger.atFine().log(
"updating code owner files in branch %s of project %s",
branchNameKey.branch(), branchNameKey.project());
@@ -138,7 +140,7 @@
updateBranch(branchNameKey.branch(), repository, revision, commitId);
return Optional.of(rw.parseCommit(commitId));
} catch (IOException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format(
"Failed to scan for code owner configs in branch %s of project %s",
branchNameKey.branch(), branchNameKey.project()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index e9db8c8..7ba3352 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -21,10 +21,8 @@
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
@@ -45,18 +43,21 @@
* using {@code set noparent} in the root code owner config if the {@code find-owners} backend is
* used).
*/
-@Singleton
public class CodeOwnerConfigHierarchy {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final GitRepositoryManager repoManager;
private final PathCodeOwners.Factory pathCodeOwnersFactory;
+ private final TransientCodeOwnerConfigCache transientCodeOwnerConfigCache;
@Inject
CodeOwnerConfigHierarchy(
- GitRepositoryManager repoManager, PathCodeOwners.Factory pathCodeOwnersFactory) {
+ GitRepositoryManager repoManager,
+ PathCodeOwners.Factory pathCodeOwnersFactory,
+ TransientCodeOwnerConfigCache transientCodeOwnerConfigCache) {
this.repoManager = repoManager;
this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+ this.transientCodeOwnerConfigCache = transientCodeOwnerConfigCache;
}
/**
@@ -102,10 +103,89 @@
Path absolutePath,
CodeOwnerConfigVisitor codeOwnerConfigVisitor,
Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
+ requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
+ PathCodeOwnersVisitor pathCodeOwnersVisitor =
+ pathCodeOwners -> codeOwnerConfigVisitor.visit(pathCodeOwners.getCodeOwnerConfig());
+ visit(
+ branchNameKey,
+ revision,
+ absolutePath,
+ pathCodeOwnersVisitor,
+ parentCodeOwnersIgnoredCallback);
+ }
+
+ /**
+ * Visits the path code owners in the given branch that apply for the given path by following the
+ * path hierarchy from the given path up to the root folder.
+ *
+ * @param branchNameKey project and branch from which the code owner configs should be visited
+ * @param revision the branch revision from which the code owner configs should be loaded
+ * @param absolutePath the path for which the code owner configs should be visited; the path must
+ * be absolute; can be the path of a file or folder; the path may or may not exist
+ * @param pathCodeOwnersVisitor visitor that should be invoked for the applying path code owners
+ * @param parentCodeOwnersIgnoredCallback callback that is invoked for the first visited code
+ * owner config that ignores parent code owners
+ */
+ public void visit(
+ BranchNameKey branchNameKey,
+ ObjectId revision,
+ Path absolutePath,
+ PathCodeOwnersVisitor pathCodeOwnersVisitor,
+ Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
+ visit(
+ branchNameKey,
+ revision,
+ absolutePath,
+ absolutePath,
+ pathCodeOwnersVisitor,
+ parentCodeOwnersIgnoredCallback);
+ }
+
+ /**
+ * Visits the path code owners in the given branch that apply for the given file path by following
+ * the path hierarchy from the given path up to the root folder.
+ *
+ * <p>Same as {@link #visit(BranchNameKey, ObjectId, Path, PathCodeOwnersVisitor, Consumer)} with
+ * the only difference that the provided path must be a file path (no folder path). Knowing that
+ * that the path is a file path allows us to skip checking if there is a code owner config file in
+ * this path (if it's a file it cannot contain a code owner config file). This is a performance
+ * optimization that matters if code owner config files need to be looked up for 1000s of files
+ * (e.g. for large changes).
+ *
+ * @param branchNameKey project and branch from which the code owner configs should be visited
+ * @param revision the branch revision from which the code owner configs should be loaded
+ * @param absoluteFilePath the path for which the code owner configs should be visited; the path
+ * must be absolute; must be the path of a file; the path may or may not exist
+ * @param pathCodeOwnersVisitor visitor that should be invoked for the applying path code owners
+ * @param parentCodeOwnersIgnoredCallback callback that is invoked for the first visited code
+ * owner config that ignores parent code owners
+ */
+ public void visitForFile(
+ BranchNameKey branchNameKey,
+ ObjectId revision,
+ Path absoluteFilePath,
+ PathCodeOwnersVisitor pathCodeOwnersVisitor,
+ Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
+ visit(
+ branchNameKey,
+ revision,
+ absoluteFilePath,
+ absoluteFilePath.getParent(),
+ pathCodeOwnersVisitor,
+ parentCodeOwnersIgnoredCallback);
+ }
+
+ private void visit(
+ BranchNameKey branchNameKey,
+ ObjectId revision,
+ Path absolutePath,
+ Path startFolder,
+ PathCodeOwnersVisitor pathCodeOwnersVisitor,
+ Consumer<CodeOwnerConfig.Key> parentCodeOwnersIgnoredCallback) {
requireNonNull(branchNameKey, "branch");
requireNonNull(revision, "revision");
requireNonNull(absolutePath, "absolutePath");
- requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
+ requireNonNull(pathCodeOwnersVisitor, "pathCodeOwnersVisitor");
requireNonNull(parentCodeOwnersIgnoredCallback, "parentCodeOwnersIgnoredCallback");
checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
@@ -113,9 +193,9 @@
"visiting code owner configs for '%s' in branch '%s' in project '%s' (revision = '%s')",
absolutePath, branchNameKey.shortName(), branchNameKey.project(), revision.name());
- // Next path in which we look for a code owner configuration. We start at the given path and
+ // Next path in which we look for a code owner configuration. We start at the given folder and
// then go up the parent hierarchy.
- Path ownerConfigFolder = absolutePath;
+ Path ownerConfigFolder = startFolder;
// Iterate over the parent code owner configurations.
while (ownerConfigFolder != null) {
@@ -125,13 +205,13 @@
CodeOwnerConfig.Key codeOwnerConfigKey =
CodeOwnerConfig.Key.create(branchNameKey, ownerConfigFolder);
Optional<PathCodeOwners> pathCodeOwners =
- pathCodeOwnersFactory.create(codeOwnerConfigKey, revision, absolutePath);
+ pathCodeOwnersFactory.create(
+ transientCodeOwnerConfigCache, codeOwnerConfigKey, revision, absolutePath);
if (pathCodeOwners.isPresent()) {
logger.atFine().log("visit code owner config for %s", ownerConfigFolder);
- boolean visitFurtherCodeOwnerConfigs =
- codeOwnerConfigVisitor.visit(pathCodeOwners.get().getCodeOwnerConfig());
+ boolean visitFurtherCodeOwnerConfigs = pathCodeOwnersVisitor.visit(pathCodeOwners.get());
boolean ignoreParentCodeOwners =
- pathCodeOwners.get().resolveCodeOwnerConfig().ignoreParentCodeOwners();
+ pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners();
if (ignoreParentCodeOwners) {
parentCodeOwnersIgnoredCallback.accept(codeOwnerConfigKey);
}
@@ -156,7 +236,7 @@
if (!RefNames.REFS_CONFIG.equals(branchNameKey.branch())) {
visitCodeOwnerConfigInRefsMetaConfig(
- branchNameKey.project(), absolutePath, codeOwnerConfigVisitor);
+ branchNameKey.project(), absolutePath, pathCodeOwnersVisitor);
}
}
@@ -175,11 +255,10 @@
* the {@code refs/meta/config} branch
* @param absolutePath the path for which the code owner configs should be visited; the path must
* be absolute; can be the path of a file or folder; the path may or may not exist
- * @param codeOwnerConfigVisitor visitor that should be invoked for the applying code owner
- * configs
+ * @param pathCodeOwnersVisitor visitor that should be invoked
*/
private void visitCodeOwnerConfigInRefsMetaConfig(
- Project.NameKey project, Path absolutePath, CodeOwnerConfigVisitor codeOwnerConfigVisitor) {
+ Project.NameKey project, Path absolutePath, PathCodeOwnersVisitor pathCodeOwnersVisitor) {
CodeOwnerConfig.Key metaCodeOwnerConfigKey =
CodeOwnerConfig.Key.create(project, RefNames.REFS_CONFIG, "/");
logger.atFine().log("visiting code owner config %s", metaCodeOwnerConfigKey);
@@ -192,15 +271,22 @@
}
RevCommit metaRevision = rw.parseCommit(ref.getObjectId());
Optional<PathCodeOwners> pathCodeOwners =
- pathCodeOwnersFactory.create(metaCodeOwnerConfigKey, metaRevision, absolutePath);
+ pathCodeOwnersFactory.create(
+ transientCodeOwnerConfigCache, metaCodeOwnerConfigKey, metaRevision, absolutePath);
if (pathCodeOwners.isPresent()) {
logger.atFine().log("visit code owner config %s", metaCodeOwnerConfigKey);
- codeOwnerConfigVisitor.visit(pathCodeOwners.get().getCodeOwnerConfig());
+ pathCodeOwnersVisitor.visit(pathCodeOwners.get());
} else {
logger.atFine().log("code owner config %s not found", metaCodeOwnerConfigKey);
}
} catch (IOException e) {
- throw new StorageException(String.format("failed to read %s", metaCodeOwnerConfigKey), e);
+ throw new CodeOwnersInternalServerErrorException(
+ String.format("failed to read %s", metaCodeOwnerConfigKey), e);
}
}
+
+ /** Returns the counters for cache and backend reads of code owner config files. */
+ public TransientCodeOwnerConfigCache.Counters getCodeOwnerConfigCounters() {
+ return transientCodeOwnerConfigCache.getCounters();
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java
new file mode 100644
index 0000000..b263c20
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigLoader.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** API to load {@link CodeOwnerConfig}s. */
+public interface CodeOwnerConfigLoader {
+ /**
+ * Retrieves the code owner config for the given key from the given branch revision.
+ *
+ * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
+ * @param revision the branch revision from which the code owner config should be loaded
+ * @return the code owner config for the given key if it exists, otherwise {@link
+ * Optional#empty()}
+ */
+ public Optional<CodeOwnerConfig> get(CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision);
+
+ /**
+ * Retrieves the code owner config for the given key from the current revision of the branch.
+ *
+ * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
+ * @return the code owner config for the given key if it exists, otherwise {@link
+ * Optional#empty()}
+ */
+ public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey);
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigParseException.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigParseException.java
index 3de255d..b8fc54b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigParseException.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigParseException.java
@@ -40,7 +40,12 @@
/** Returns all validation as a single, formatted string. */
public String getFullMessage(String defaultCodeOwnerConfigFileName) {
StringBuilder sb = new StringBuilder(getMessage());
- sb.append(" '").append(codeOwnerConfigKey.filePath(defaultCodeOwnerConfigFileName)).append("'");
+ sb.append(
+ String.format(
+ " '%s' (project = %s, branch = %s)",
+ codeOwnerConfigKey.filePath(defaultCodeOwnerConfigFileName),
+ codeOwnerConfigKey.project(),
+ codeOwnerConfigKey.shortBranchName()));
if (!messages.isEmpty()) {
sb.append(':');
for (ValidationError msg : messages) {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigReference.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigReference.java
index 2bc571c..7f9d4c4 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigReference.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigReference.java
@@ -75,6 +75,19 @@
return filePath().getFileName().toString();
}
+ /** User-readable string representing this code owner config reference. */
+ public String format() {
+ StringBuilder formatted = new StringBuilder();
+ if (project().isPresent()) {
+ formatted.append(project().get()).append(":");
+ }
+ if (branch().isPresent()) {
+ formatted.append(branch().get()).append(":");
+ }
+ formatted.append(filePath());
+ return formatted.toString();
+ }
+
/**
* Creates a builder from this code owner config reference.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index c8c96ab..20d7a4c 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -14,21 +14,18 @@
package com.google.gerrit.plugins.codeowners.backend;
-import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidConfigCause;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidCodeOwnerConfigCause;
import static java.util.Objects.requireNonNull;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -70,26 +67,6 @@
}
/**
- * Whether there is at least one code owner config file in the given project and branch.
- *
- * @param branchNameKey the project and branch for which if should be checked if it contains any
- * code owner config file
- * @return {@code true} if there is at least one code owner config file in the given project and
- * branch, otherwise {@code false}
- */
- public boolean containsAnyCodeOwnerConfigFile(BranchNameKey branchNameKey) {
- AtomicBoolean found = new AtomicBoolean(false);
- visit(
- branchNameKey,
- codeOwnerConfig -> {
- found.set(true);
- return false;
- },
- (codeOwnerConfigFilePath, configInvalidException) -> found.set(true));
- return found.get();
- }
-
- /**
* Visits all code owner config files in the given project and branch.
*
* @param branchNameKey the project and branch for which the code owner config files should be
@@ -128,7 +105,10 @@
requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
requireNonNull(invalidCodeOwnerConfigCallback, "invalidCodeOwnerConfigCallback");
- CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+ CodeOwnerBackend codeOwnerBackend =
+ codeOwnersPluginConfiguration
+ .getProjectConfig(branchNameKey.project())
+ .getBackend(branchNameKey.branch());
logger.atFine().log(
"scanning code owner files in branch %s of project %s (path glob = %s)",
branchNameKey.branch(), branchNameKey.project(), pathGlob);
@@ -162,17 +142,17 @@
CodeOwnerConfig codeOwnerConfig;
try {
codeOwnerConfig = treeWalk.getCodeOwnerConfig();
- } catch (StorageException storageException) {
- Optional<ConfigInvalidException> configInvalidException =
- getInvalidConfigCause(storageException);
- if (!configInvalidException.isPresent()) {
+ } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
+ Optional<InvalidCodeOwnerConfigException> invalidCodeOwnerConfigException =
+ getInvalidCodeOwnerConfigCause(codeOwnersInternalServerErrorException);
+ if (!invalidCodeOwnerConfigException.isPresent()) {
// Propagate any failure that is not related to the contents of the code owner config.
- throw storageException;
+ throw codeOwnersInternalServerErrorException;
}
// The code owner config is invalid and cannot be parsed.
invalidCodeOwnerConfigCallback.onInvalidCodeOwnerConfig(
- treeWalk.getFilePath(), configInvalidException.get());
+ treeWalk.getFilePath(), invalidCodeOwnerConfigException.get());
continue;
}
@@ -182,7 +162,7 @@
}
}
} catch (IOException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format(
"Failed to scan for code owner configs in branch %s of project %s",
branchNameKey.branch(), branchNameKey.project()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java
index 1ba8f74..0836a53 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java
@@ -21,7 +21,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 74e2db6..f78804f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -19,12 +19,15 @@
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
@@ -32,9 +35,6 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -42,6 +42,8 @@
import com.google.inject.Provider;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -59,6 +61,8 @@
private final AccountCache accountCache;
private final AccountControl.Factory accountControlFactory;
private final PathCodeOwners.Factory pathCodeOwnersFactory;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+ private final UnresolvedImportFormatter unresolvedImportFormatter;
// Enforce visibility by default.
private boolean enforceVisibility = true;
@@ -76,7 +80,9 @@
ExternalIds externalIds,
AccountCache accountCache,
AccountControl.Factory accountControlFactory,
- PathCodeOwners.Factory pathCodeOwnersFactory) {
+ PathCodeOwners.Factory pathCodeOwnersFactory,
+ CodeOwnerMetrics codeOwnerMetrics,
+ UnresolvedImportFormatter unresolvedImportFormatter) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.permissionBackend = permissionBackend;
this.currentUser = currentUser;
@@ -84,6 +90,8 @@
this.accountCache = accountCache;
this.accountControlFactory = accountControlFactory;
this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ this.unresolvedImportFormatter = unresolvedImportFormatter;
}
/**
@@ -130,6 +138,10 @@
* Resolves the code owners from the given code owner config for the given path from {@link
* CodeOwnerReference}s to a {@link CodeOwner}s.
*
+ * <p>If the code owner config has already been resolved to {@link PathCodeOwners}, prefer calling
+ * {@link #resolvePathCodeOwners(PathCodeOwners)} instead, so that {@link PathCodeOwners} do not
+ * need to be created again.
+ *
* <p>Non-resolvable code owners are filtered out.
*
* @param codeOwnerConfig the code owner config for which the local owners for the given path
@@ -143,20 +155,32 @@
requireNonNull(codeOwnerConfig, "codeOwnerConfig");
requireNonNull(absolutePath, "absolutePath");
checkState(absolutePath.isAbsolute(), "path %s must be absolute", absolutePath);
+ return resolvePathCodeOwners(
+ pathCodeOwnersFactory.createWithoutCache(codeOwnerConfig, absolutePath));
+ }
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Resolve path code owners",
- Metadata.builder()
- .projectName(codeOwnerConfig.key().project().get())
- .branchName(codeOwnerConfig.key().ref())
- .filePath(codeOwnerConfig.key().fileName().orElse("<default>"))
- .build())) {
- logger.atFine().log("resolving path code owners for path %s", absolutePath);
- PathCodeOwnersResult pathCodeOwnersResult =
- pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
+ /**
+ * Resolves the the given path code owners from {@link CodeOwnerReference}s to a {@link
+ * CodeOwner}s.
+ *
+ * <p>Non-resolvable code owners are filtered out.
+ *
+ * @param pathCodeOwners the path code owners that should be resolved
+ * @return the resolved code owners
+ */
+ public CodeOwnerResolverResult resolvePathCodeOwners(PathCodeOwners pathCodeOwners) {
+ requireNonNull(pathCodeOwners, "pathCodeOwners");
+
+ try (Timer0.Context ctx = codeOwnerMetrics.resolvePathCodeOwners.start()) {
+ logger.atFine().log(
+ "resolve path code owners (code owner config = %s, path = %s)",
+ pathCodeOwners.getCodeOwnerConfig().key(), pathCodeOwners.getPath());
+ OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult =
+ pathCodeOwners.resolveCodeOwnerConfig();
return resolve(
- pathCodeOwnersResult.getPathCodeOwners(), pathCodeOwnersResult.hasUnresolvedImports());
+ pathCodeOwnersResult.get().getPathCodeOwners(),
+ pathCodeOwnersResult.get().unresolvedImports(),
+ pathCodeOwnersResult.messages());
}
}
@@ -167,7 +191,8 @@
* @return the resolved global code owners of the given project
*/
public CodeOwnerResolverResult resolveGlobalCodeOwners(Project.NameKey projectName) {
- return resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName));
+ return resolve(
+ codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners());
}
/**
@@ -178,45 +203,67 @@
* @see #resolve(CodeOwnerReference)
*/
public CodeOwnerResolverResult resolve(Set<CodeOwnerReference> codeOwnerReferences) {
- return resolve(codeOwnerReferences, /* hasUnresolvedImports= */ false);
+ return resolve(
+ codeOwnerReferences,
+ /* unresolvedImports= */ ImmutableList.of(),
+ /* pathCodeOwnersMessages= */ ImmutableList.of());
}
/**
* Resolves the given {@link CodeOwnerReference}s to {@link CodeOwner}s.
*
* @param codeOwnerReferences the code owner references that should be resolved
- * @param hasUnresolvedImports whether there are unresolved imports
+ * @param unresolvedImports list of unresolved imports
+ * @param pathCodeOwnersMessages messages that were collected when resolving path code owners
* @return the {@link CodeOwner} for the given code owner references
* @see #resolve(CodeOwnerReference)
*/
private CodeOwnerResolverResult resolve(
- Set<CodeOwnerReference> codeOwnerReferences, boolean hasUnresolvedImports) {
+ Set<CodeOwnerReference> codeOwnerReferences,
+ List<UnresolvedImport> unresolvedImports,
+ ImmutableList<String> pathCodeOwnersMessages) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
- AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
- AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
- ImmutableSet<CodeOwner> codeOwners =
- codeOwnerReferences.stream()
- .filter(
- codeOwnerReference -> {
- if (ALL_USERS_WILDCARD.equals(codeOwnerReference.email())) {
- ownedByAllUsers.set(true);
- return false;
- }
- return true;
- })
- .map(this::resolve)
- .filter(
- codeOwner -> {
- if (!codeOwner.isPresent()) {
- hasUnresolvedCodeOwners.set(true);
- return false;
- }
- return true;
- })
- .map(Optional::get)
- .collect(toImmutableSet());
- return CodeOwnerResolverResult.create(
- codeOwners, ownedByAllUsers.get(), hasUnresolvedCodeOwners.get(), hasUnresolvedImports);
+ requireNonNull(unresolvedImports, "unresolvedImports");
+ requireNonNull(pathCodeOwnersMessages, "pathCodeOwnersMessages");
+
+ try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerReferences.start()) {
+ AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
+ AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
+ List<String> messages = new ArrayList<>(pathCodeOwnersMessages);
+ unresolvedImports.forEach(
+ unresolvedImport -> messages.add(unresolvedImportFormatter.format(unresolvedImport)));
+ ImmutableSet<CodeOwner> codeOwners =
+ codeOwnerReferences.stream()
+ .filter(
+ codeOwnerReference -> {
+ if (ALL_USERS_WILDCARD.equals(codeOwnerReference.email())) {
+ ownedByAllUsers.set(true);
+ return false;
+ }
+ return true;
+ })
+ .map(this::resolveWithMessages)
+ .filter(
+ resolveResult -> {
+ messages.addAll(resolveResult.messages());
+ if (!resolveResult.isPresent()) {
+ hasUnresolvedCodeOwners.set(true);
+ return false;
+ }
+ return true;
+ })
+ .map(OptionalResultWithMessages::get)
+ .collect(toImmutableSet());
+ CodeOwnerResolverResult codeOwnerResolverResult =
+ CodeOwnerResolverResult.create(
+ codeOwners,
+ ownedByAllUsers.get(),
+ hasUnresolvedCodeOwners.get(),
+ !unresolvedImports.isEmpty(),
+ messages);
+ logger.atFine().log("resolve result = %s", codeOwnerResolverResult);
+ return codeOwnerResolverResult;
+ }
}
/**
@@ -240,11 +287,12 @@
* Gerrit core that also treats ambiguous identifiers as non-resolveable.
* </ul>
*
- * <p>This methods checks whether the calling user can see the accounts of the code owners and
- * returns code owners whose accounts are visible.
+ * <p>This methods checks whether the {@link #user} or the calling user (if {@link #user} is
+ * unset) can see the accounts of the code owners and returns code owners whose accounts are
+ * visible.
*
* <p>In addition code owners that are referenced by a secondary email are only returned if the
- * calling user can see the secondary email:
+ * {@link #user} or the calling user (if {@link #user} is unset) can see the secondary email:
*
* <ul>
* <li>every user can see the own secondary emails
@@ -260,33 +308,45 @@
* Optional#empty()}
*/
public Optional<CodeOwner> resolve(CodeOwnerReference codeOwnerReference) {
+ OptionalResultWithMessages<CodeOwner> resolveResult = resolveWithMessages(codeOwnerReference);
+ logger.atFine().log("resolve result = %s", resolveResult);
+ return resolveResult.result();
+ }
+
+ public OptionalResultWithMessages<CodeOwner> resolveWithMessages(
+ CodeOwnerReference codeOwnerReference) {
String email = requireNonNull(codeOwnerReference, "codeOwnerReference").email();
- logger.atFine().log("resolving code owner reference %s", codeOwnerReference);
- if (!isEmailDomainAllowed(email)) {
- logger.atFine().log("domain of email %s is not allowed", email);
- return Optional.empty();
+ List<String> messages = new ArrayList<>();
+ messages.add(String.format("resolving code owner reference %s", codeOwnerReference));
+
+ OptionalResultWithMessages<Boolean> emailDomainAllowedResult = isEmailDomainAllowed(email);
+ messages.addAll(emailDomainAllowedResult.messages());
+ if (!emailDomainAllowedResult.get()) {
+ return OptionalResultWithMessages.createEmpty(messages);
}
- Optional<AccountState> accountState =
- lookupEmail(email).flatMap(accountId -> lookupAccount(accountId, email));
- if (!accountState.isPresent()) {
- logger.atFine().log("no account for email %s", email);
- return Optional.empty();
- }
- if (!accountState.get().account().isActive()) {
- logger.atFine().log("account for email %s is inactive", email);
- return Optional.empty();
- }
- if (enforceVisibility && !isVisible(accountState.get(), email)) {
- logger.atFine().log(
- "account %d or email %s not visible", accountState.get().account().id().get(), email);
- return Optional.empty();
+ OptionalResultWithMessages<AccountState> activeAccountResult =
+ lookupActiveAccountForEmail(email);
+ messages.addAll(activeAccountResult.messages());
+ if (activeAccountResult.isEmpty()) {
+ return OptionalResultWithMessages.createEmpty(messages);
}
- CodeOwner codeOwner = CodeOwner.create(accountState.get().account().id());
- logger.atFine().log("resolved to code owner %s", codeOwner);
- return Optional.of(codeOwner);
+ AccountState accountState = activeAccountResult.get();
+ if (enforceVisibility) {
+ OptionalResultWithMessages<Boolean> isVisibleResult = isVisible(accountState, email);
+ messages.addAll(isVisibleResult.messages());
+ if (!isVisibleResult.get()) {
+ return OptionalResultWithMessages.createEmpty(messages);
+ }
+ } else {
+ messages.add("code owner visibility is not checked");
+ }
+
+ CodeOwner codeOwner = CodeOwner.create(accountState.account().id());
+ messages.add(String.format("resolved to account %s", codeOwner.accountId()));
+ return OptionalResultWithMessages.create(codeOwner, messages);
}
/** Whether the given account can be seen. */
@@ -297,34 +357,74 @@
}
/**
- * Looks up an email and returns the ID of the account to which it belongs.
+ * Looks up an email and returns the ID of the active account to which it belongs.
*
- * <p>If the email is ambiguous (it belongs to multiple accounts) it is considered as
- * non-resolvable and {@link Optional#empty()} is returned.
+ * <p>If the email is ambiguous (it belongs to multiple active accounts) it is considered as
+ * non-resolvable and empty result is returned.
*
* @param email the email that should be looked up
* @return the ID of the account to which the email belongs if was found
*/
- private Optional<Account.Id> lookupEmail(String email) {
+ private OptionalResultWithMessages<AccountState> lookupActiveAccountForEmail(String email) {
ImmutableSet<ExternalId> extIds;
try {
extIds = externalIds.byEmail(email);
} catch (IOException e) {
- throw new StorageException(String.format("cannot resolve code owner email %s", email), e);
+ throw new CodeOwnersInternalServerErrorException(
+ String.format("cannot resolve code owner email %s", email), e);
}
if (extIds.isEmpty()) {
- logger.atFine().log(
- "cannot resolve code owner email %s: no account with this email exists", email);
- return Optional.empty();
+ return OptionalResultWithMessages.createEmpty(
+ String.format(
+ "cannot resolve code owner email %s: no account with this email exists", email));
}
- if (extIds.stream().map(ExternalId::accountId).distinct().count() > 1) {
- logger.atFine().log("cannot resolve code owner email %s: email is ambiguous", email);
- return Optional.empty();
+ List<String> messages = new ArrayList<>();
+ OptionalResultWithMessages<ImmutableSet<AccountState>> activeAccountsResult =
+ lookupActiveAccounts(extIds, email);
+ ImmutableSet<AccountState> activeAccounts = activeAccountsResult.get();
+ messages.addAll(activeAccountsResult.messages());
+
+ if (activeAccounts.isEmpty()) {
+ messages.add(
+ String.format(
+ "cannot resolve code owner email %s: no active account with this email found",
+ email));
+ return OptionalResultWithMessages.createEmpty(messages);
}
- return Optional.of(extIds.stream().findFirst().get().accountId());
+ if (activeAccounts.size() > 1) {
+ messages.add(String.format("cannot resolve code owner email %s: email is ambiguous", email));
+ return OptionalResultWithMessages.createEmpty(messages);
+ }
+
+ return OptionalResultWithMessages.create(Iterables.getOnlyElement(activeAccounts));
+ }
+
+ private OptionalResultWithMessages<ImmutableSet<AccountState>> lookupActiveAccounts(
+ ImmutableSet<ExternalId> extIds, String email) {
+ ImmutableSet<OptionalResultWithMessages<AccountState>> accountStateResults =
+ extIds.stream()
+ .map(externalId -> lookupAccount(externalId.accountId(), externalId.email()))
+ .collect(toImmutableSet());
+
+ ImmutableSet.Builder<AccountState> activeAccounts = ImmutableSet.builder();
+ List<String> messages = new ArrayList<>();
+ for (OptionalResultWithMessages<AccountState> accountStateResult : accountStateResults) {
+ messages.addAll(accountStateResult.messages());
+ if (accountStateResult.isPresent()) {
+ AccountState accountState = accountStateResult.get();
+ if (accountState.account().isActive()) {
+ activeAccounts.add(accountState);
+ } else {
+ messages.add(
+ String.format(
+ "account %s for email %s is inactive", accountState.account().id(), email));
+ }
+ }
+ }
+ return OptionalResultWithMessages.create(activeAccounts.build(), messages);
}
/**
@@ -335,67 +435,115 @@
* @param email the email that was resolved to the account ID
* @return the {@link AccountState} of the account with the given account ID, if it exists
*/
- private Optional<AccountState> lookupAccount(Account.Id accountId, String email) {
+ private OptionalResultWithMessages<AccountState> lookupAccount(
+ Account.Id accountId, String email) {
Optional<AccountState> accountState = accountCache.get(accountId);
if (!accountState.isPresent()) {
- logger.atFine().log(
- "cannot resolve code owner email %s: email belongs to account %s,"
- + " but no account with this ID exists",
- email, accountId);
- return Optional.empty();
+ return OptionalResultWithMessages.createEmpty(
+ String.format(
+ "cannot resolve account %s for email %s: account does not exists", accountId, email));
}
- return accountState;
+ return OptionalResultWithMessages.create(accountState.get());
}
/**
- * Checks whether the given account and email are visible to the calling user.
+ * Checks whether the given account and email are visible to the {@link #user} or the calling user
+ * (if {@link #user} is unset).
*
- * <p>If the email is a secondary email it is only visible if it is owned by the calling user or
- * if the calling user has the {@code Modify Account} global capability.
+ * <p>If the email is a secondary email it is only visible if
*
- * @param accountState the account for which it should be checked whether it's visible to the
- * calling user
+ * <ul>
+ * <li>it is owned by the {@link #user} or the calling user (if {@link #user} is unset)
+ * <li>if the {@link #user} or the calling user (if {@link #user} is unset) has the {@code
+ * Modify Account} global capability
+ * </ul>
+ *
+ * @param accountState the account for which it should be checked whether it's visible to the user
* @param email email that was used to reference the account
- * @return {@code true} if the given account and email are visible to the calling user, otherwise
- * {@code false}
+ * @return {@code true} if the given account and email are visible to the user, otherwise {@code
+ * false}
*/
- private boolean isVisible(AccountState accountState, String email) {
+ private OptionalResultWithMessages<Boolean> isVisible(AccountState accountState, String email) {
if (!canSee(accountState)) {
- logger.atFine().log(
- "cannot resolve code owner email %s: account %s is not visible to calling user",
- email, accountState.account().id());
- return false;
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is not visible to user %s",
+ email,
+ accountState.account().id(),
+ user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
}
if (!email.equals(accountState.account().preferredEmail())) {
// the email is a secondary email of the account
- if (currentUser.get().isIdentifiedUser()
+ if (user != null) {
+ if (user.hasEmailAddress(email)) {
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "email %s is visible to user %s: email is a secondary email that is owned by this"
+ + " user",
+ email, user.getLoggableName()));
+ }
+ } else if (currentUser.get().isIdentifiedUser()
&& currentUser.get().asIdentifiedUser().hasEmailAddress(email)) {
// it's a secondary email of the calling user, users can always see their own secondary
// emails
- return true;
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "email %s is visible to the calling user %s: email is a secondary email that is"
+ + " owned by this user",
+ email, currentUser.get().getLoggableName()));
}
- // the email is a secondary email of another account, check if the calling user can see
- // secondary emails
+ // the email is a secondary email of another account, check if the user can see secondary
+ // emails
try {
- if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
- logger.atFine().log(
- "cannot resolve code owner email %s: account %s is referenced by secondary email,"
- + " but the calling user cannot see secondary emails",
- email, accountState.account().id());
- return false;
+ if (user != null) {
+ if (!permissionBackend.user(user).test(GlobalPermission.MODIFY_ACCOUNT)) {
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email"
+ + " but user %s cannot see secondary emails",
+ email, accountState.account().id(), user.getLoggableName()));
+ }
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email"
+ + " and user %s can see secondary emails",
+ email, accountState.account().id(), user.getLoggableName()));
+ } else if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email"
+ + " but the calling user %s cannot see secondary emails",
+ email, accountState.account().id(), currentUser.get().getLoggableName()));
+ } else {
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email"
+ + " and the calling user %s can see secondary emails",
+ email, accountState.account().id(), currentUser.get().getLoggableName()));
}
} catch (PermissionBackendException e) {
- throw new StorageException(
+ throw new CodeOwnersInternalServerErrorException(
String.format(
"failed to test the %s global capability", GlobalPermission.MODIFY_ACCOUNT),
e);
}
}
-
- return true;
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "account %s is visible to user %s",
+ accountState.account().id(),
+ user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
}
/**
@@ -405,29 +553,30 @@
* @return {@code true} if the domain of the given email is allowed for code owners, otherwise
* {@code false}
*/
- public boolean isEmailDomainAllowed(String email) {
+ public OptionalResultWithMessages<Boolean> isEmailDomainAllowed(String email) {
requireNonNull(email, "email");
ImmutableSet<String> allowedEmailDomains =
- codeOwnersPluginConfiguration.getAllowedEmailDomains();
+ codeOwnersPluginConfiguration.getGlobalConfig().getAllowedEmailDomains();
if (allowedEmailDomains.isEmpty()) {
- // all domains are allowed
- return true;
+ return OptionalResultWithMessages.create(true, "all domains are allowed");
}
if (email.equals(ALL_USERS_WILDCARD)) {
- return true;
+ return OptionalResultWithMessages.create(true, "all users wildcard is allowed");
}
int emailAtIndex = email.lastIndexOf('@');
if (emailAtIndex >= 0 && emailAtIndex < email.length() - 1) {
String emailDomain = email.substring(emailAtIndex + 1);
- logger.atFine().log("email domain = %s", emailDomain);
- return allowedEmailDomains.contains(emailDomain);
+ boolean isEmailDomainAllowed = allowedEmailDomains.contains(emailDomain);
+ return OptionalResultWithMessages.create(
+ isEmailDomainAllowed,
+ String.format(
+ "domain %s of email %s is %s",
+ emailDomain, email, isEmailDomainAllowed ? "allowed" : "not allowed"));
}
- // email has no domain
- logger.atFine().log("email %s has no domain", email);
- return false;
+ return OptionalResultWithMessages.create(false, String.format("email %s has no domain", email));
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index b5b71c0..0744e5b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -18,8 +18,10 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
+import java.util.List;
/** The result of resolving code owner references via {@link CodeOwnerResolver}. */
@AutoValue
@@ -49,6 +51,9 @@
/** Whether there are imports which couldn't be resolved. */
public abstract boolean hasUnresolvedImports();
+ /** Gets messages that were collected while resolving the code owners. */
+ public abstract ImmutableList<String> messages();
+
/**
* Whether there are any code owners defined for the path, regardless of whether they can be
* resolved or not.
@@ -61,12 +66,13 @@
}
@Override
- public String toString() {
+ public final String toString() {
return MoreObjects.toStringHelper(this)
.add("codeOwners", codeOwners())
.add("ownedByAllUsers", ownedByAllUsers())
.add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners())
.add("hasUnresolvedImports", hasUnresolvedImports())
+ .add("messages", messages())
.toString();
}
@@ -75,9 +81,14 @@
ImmutableSet<CodeOwner> codeOwners,
boolean ownedByAllUsers,
boolean hasUnresolvedCodeOwners,
- boolean hasUnresolvedImports) {
+ boolean hasUnresolvedImports,
+ List<String> messages) {
return new AutoValue_CodeOwnerResolverResult(
- codeOwners, ownedByAllUsers, hasUnresolvedCodeOwners, hasUnresolvedImports);
+ codeOwners,
+ ownedByAllUsers,
+ hasUnresolvedCodeOwners,
+ hasUnresolvedImports,
+ ImmutableList.copyOf(messages));
}
/** Creates a empty {@link CodeOwnerResolverResult} instance. */
@@ -86,6 +97,7 @@
/* codeOwners= */ ImmutableSet.of(),
/* ownedByAllUsers= */ false,
/* hasUnresolvedCodeOwners= */ false,
- /* hasUnresolvedImports= */ false);
+ /* hasUnresolvedImports= */ false,
+ /* messages= */ ImmutableList.of());
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
index 065425d..523c2ee 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
@@ -14,6 +14,11 @@
package com.google.gerrit.plugins.codeowners.backend;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
/**
* Scores by which we rate how good we consider a code owner as reviewer/approver for a certain
* path.
@@ -36,7 +41,30 @@
* user Y is a better reviewer/approver for '/foo/bar/baz/' than user X as they have a lower
* distance.
*/
- DISTANCE(Kind.LOWER_VALUE_IS_BETTER);
+ DISTANCE(Kind.LOWER_VALUE_IS_BETTER, /* weight= */ 1, /* maxValue= */ null),
+
+ /**
+ * Score to take into account whether a code owner is a reviewer.
+ *
+ * <p>Code owners that are reviewers get scored with 1 (see {@link #IS_REVIEWER_SCORING_VALUE}),
+ * while code owners that are not a reviewer get scored with 0 (see {@link
+ * #NO_REVIEWER_SCORING_VALUE}).
+ *
+ * <p>The IS_REVIEWER score has a higher weight than the {@link #DISTANCE} score so that it takes
+ * precedence and code owners that are reviewers are always returned first.
+ */
+ IS_REVIEWER(Kind.GREATER_VALUE_IS_BETTER, /* weight= */ 2, /* maxValue= */ 1);
+
+ /**
+ * Scoring value for the {@link #IS_REVIEWER} score for users that are not a reviewer of the
+ * change.
+ */
+ public static int NO_REVIEWER_SCORING_VALUE = 0;
+
+ /**
+ * Scoring value for the {@link #IS_REVIEWER} score for users that are a reviewer of the change.
+ */
+ public static int IS_REVIEWER_SCORING_VALUE = 1;
/**
* Score kind.
@@ -57,17 +85,58 @@
*/
private final Kind kind;
- private CodeOwnerScore(Kind kind) {
+ /**
+ * The weight that this score should have when sorting code owners.
+ *
+ * <p>The higher the weight the larger the impact that this score has on the sorting.
+ */
+ private final double weight;
+
+ /**
+ * The max value that this score can have.
+ *
+ * <p>Not set if max value is not hard-coded, but is different case by case.
+ *
+ * <p>For scores that have a max value set scorings must be created by the {@link
+ * #createScoring()} method, for scores with flexible max values (maxValue = null) scorings must
+ * be created by the {@link #createScoring(int)} method.
+ */
+ @Nullable private final Integer maxValue;
+
+ private CodeOwnerScore(Kind kind, double weight, @Nullable Integer maxValue) {
this.kind = kind;
+ this.weight = weight;
+ this.maxValue = maxValue;
}
/**
* Creates a {@link CodeOwnerScoring.Builder} instance for this score.
*
+ * <p>Use {@link #createScoring()} instead if the score has a max value set.
+ *
* @param maxValue the max possible scoring value
* @return the created {@link CodeOwnerScoring.Builder} instance
*/
public CodeOwnerScoring.Builder createScoring(int maxValue) {
+ checkState(
+ this.maxValue == null,
+ "score %s has defined a maxValue, setting maxValue not allowed",
+ name());
+ return CodeOwnerScoring.builder(this, maxValue);
+ }
+
+ /**
+ * Creates a {@link CodeOwnerScoring.Builder} instance for this score.
+ *
+ * <p>Use {@link #createScoring(int)} instead if the score doesn't have a max value set.
+ *
+ * @return the created {@link CodeOwnerScoring.Builder} instance
+ */
+ public CodeOwnerScoring.Builder createScoring() {
+ checkState(
+ maxValue != null,
+ "score %s doesn't have a maxValue defined, setting maxValue is required",
+ name());
return CodeOwnerScoring.builder(this, maxValue);
}
@@ -79,4 +148,12 @@
boolean isLowerValueBetter() {
return Kind.LOWER_VALUE_IS_BETTER.equals(kind);
}
+
+ double weight() {
+ return weight;
+ }
+
+ Optional<Integer> maxValue() {
+ return Optional.ofNullable(maxValue);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoring.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoring.java
index 4694fd8..ce89e07 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoring.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoring.java
@@ -110,14 +110,15 @@
}
/**
- * Returns a comparator to sort code owners by the scorings collected in this {@link
- * CodeOwnerScoring} instance.
+ * Computes the weighted scoring for a code owner.
*
- * <p>Code owners with higher scoring come first. The order of code owners with the same scoring
- * is undefined.
+ * <p>The result of {@link #scoring(CodeOwner)} multiplied with the {@code score().weight()}.
+ *
+ * @param codeOwner for which the weighted scoring should be computed
+ * @return the weighted scoring for the code owner
*/
- public Comparator<CodeOwner> comparingByScoring() {
- return Comparator.comparingDouble(this::scoring).reversed();
+ public double weightedScoring(CodeOwner codeOwner) {
+ return score().weight() * scoring(codeOwner);
}
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScorings.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScorings.java
new file mode 100644
index 0000000..84bddff
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScorings.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Class to sort code owners based on their scorings on different {@link CodeOwnerScore}s.
+ *
+ * <p>To determine the sort order the scorings are weighted based on the {@link
+ * CodeOwnerScore#weight()} of the {@link CodeOwnerScore} on which the scoring was done.
+ */
+@AutoValue
+public abstract class CodeOwnerScorings {
+ /** The scorings that should be taken into account for sorting the code owners. */
+ public abstract ImmutableSet<CodeOwnerScoring> scorings();
+
+ public static CodeOwnerScorings create(CodeOwnerScoring... codeOwnerScorings) {
+ return new AutoValue_CodeOwnerScorings(ImmutableSet.copyOf(codeOwnerScorings));
+ }
+
+ public static CodeOwnerScorings create(Set<CodeOwnerScoring> codeOwnerScorings) {
+ return new AutoValue_CodeOwnerScorings(ImmutableSet.copyOf(codeOwnerScorings));
+ }
+
+ /**
+ * Returns the total scorings for the given code owners.
+ *
+ * @param codeOwners the code owners for which the scorings should be returned
+ */
+ public ImmutableMap<CodeOwner, Double> getScorings(ImmutableSet<CodeOwner> codeOwners) {
+ return codeOwners.stream()
+ .collect(toImmutableMap(Function.identity(), this::sumWeightedScorings));
+ }
+
+ /** Returns the sum of all weighted scorings that available for the given code owner. */
+ private double sumWeightedScorings(CodeOwner codeOwner) {
+ double sum =
+ scorings().stream()
+ .map(scoring -> scoring.weightedScoring(codeOwner))
+ .collect(Collectors.summingDouble(Double::doubleValue));
+ return sum;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
index 373ec91..df96cc4 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
@@ -18,15 +18,16 @@
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.SubmitRecord;
-import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.SubmitRule;
@@ -34,6 +35,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.nio.file.InvalidPathException;
import java.util.Optional;
/** Submit rule that checks that all files in a change have been approved by their code owners. */
@@ -50,18 +52,24 @@
}
}
- private static final SubmitRequirement SUBMIT_REQUIREMENT =
- SubmitRequirement.builder().setFallbackText("Code Owners").setType("code-owners").build();
+ private static final LegacySubmitRequirement SUBMIT_REQUIREMENT =
+ LegacySubmitRequirement.builder()
+ .setFallbackText("Code Owners")
+ .setType("code-owners")
+ .build();
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+ private final CodeOwnerMetrics codeOwnerMetrics;
@Inject
CodeOwnerSubmitRule(
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
- CodeOwnerApprovalCheck codeOwnerApprovalCheck) {
+ CodeOwnerApprovalCheck codeOwnerApprovalCheck,
+ CodeOwnerMetrics codeOwnerMetrics) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
+ this.codeOwnerMetrics = codeOwnerMetrics;
}
@Override
@@ -69,14 +77,19 @@
try {
requireNonNull(changeData, "changeData");
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Run code owner submit rule",
- Metadata.builder()
- .projectName(changeData.project().get())
- .changeId(changeData.getId().get())
- .build())) {
- if (codeOwnersPluginConfiguration.isDisabled(changeData.change().getDest())) {
+ if (changeData.change().isClosed()) {
+ return Optional.empty();
+ }
+
+ try (Timer0.Context ctx = codeOwnerMetrics.runCodeOwnerSubmitRule.start()) {
+ codeOwnerMetrics.countCodeOwnerSubmitRuleRuns.increment();
+ logger.atFine().log(
+ "run code owner submit rule (project = %s, change = %d)",
+ changeData.project().get(), changeData.getId().get());
+
+ if (codeOwnersPluginConfiguration
+ .getProjectConfig(changeData.project())
+ .isDisabled(changeData.change().getDest().branch())) {
logger.atFine().log(
"code owners functionality is disabled for branch %s", changeData.change().getDest());
return Optional.empty();
@@ -84,7 +97,19 @@
return Optional.of(getSubmitRecord(changeData.notes()));
}
+ } catch (RestApiException e) {
+ logger.atFine().withCause(e).log(
+ String.format(
+ "Couldn't evaluate code owner statuses for patch set %d of change %d.",
+ changeData.currentPatchSet().id().get(), changeData.change().getId().get()));
+ return Optional.of(notReady());
} catch (Throwable t) {
+ // Whether the exception should be treated as RULE_ERROR.
+ // RULE_ERROR must only be returned if the exception is caused by user misconfiguration (e.g.
+ // an invalid OWNERS file), but not for internal server errors.
+ boolean isRuleError = false;
+
+ String cause = t.getClass().getSimpleName();
String errorMessage = "Failed to evaluate code owner statuses";
if (changeData != null) {
errorMessage +=
@@ -92,14 +117,49 @@
" for patch set %d of change %d",
changeData.currentPatchSet().id().get(), changeData.change().getId().get());
}
+ Optional<InvalidPathException> invalidPathException =
+ CodeOwnersExceptionHook.getInvalidPathException(t);
+ Optional<InvalidCodeOwnerConfigException> invalidCodeOwnerConfigException =
+ CodeOwners.getInvalidCodeOwnerConfigCause(t);
+ if (invalidPathException.isPresent()) {
+ isRuleError = true;
+ cause = "invalid_path";
+ errorMessage += String.format(" (cause: %s)", invalidPathException.get().getMessage());
+ } else if (invalidCodeOwnerConfigException.isPresent()) {
+ isRuleError = true;
+ codeOwnerMetrics.countInvalidCodeOwnerConfigFiles.increment(
+ invalidCodeOwnerConfigException.get().getProjectName().get(),
+ invalidCodeOwnerConfigException.get().getRef(),
+ invalidCodeOwnerConfigException.get().getCodeOwnerConfigFilePath());
+
+ cause = "invalid_code_owner_config_file";
+ errorMessage +=
+ String.format(" (cause: %s)", invalidCodeOwnerConfigException.get().getMessage());
+
+ Optional<String> invalidCodeOwnerConfigInfoUrl =
+ codeOwnersPluginConfiguration
+ .getProjectConfig(invalidCodeOwnerConfigException.get().getProjectName())
+ .getInvalidCodeOwnerConfigInfoUrl();
+ if (invalidCodeOwnerConfigInfoUrl.isPresent()) {
+ errorMessage +=
+ String.format(".\nFor help check %s", invalidCodeOwnerConfigInfoUrl.get());
+ }
+ }
errorMessage += ".";
- logger.atSevere().withCause(t).log(errorMessage);
- return Optional.of(ruleError(errorMessage));
+
+ if (isRuleError) {
+ codeOwnerMetrics.countCodeOwnerSubmitRuleErrors.increment(cause);
+
+ logger.atWarning().log(errorMessage);
+ return Optional.of(ruleError(errorMessage));
+ }
+ throw new CodeOwnersInternalServerErrorException(errorMessage, t);
}
}
private SubmitRecord getSubmitRecord(ChangeNotes changeNotes)
- throws ResourceConflictException, IOException, PatchListNotAvailableException {
+ throws ResourceConflictException, IOException, PatchListNotAvailableException,
+ DiffNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
return codeOwnerApprovalCheck.isSubmittable(changeNotes) ? ok() : notReady();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 2165fda..b9058d4 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -17,12 +17,13 @@
import static java.util.Objects.requireNonNull;
import com.google.common.base.Throwables;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.nio.file.Path;
import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -35,42 +36,45 @@
* that we avoid code repetition in the code owner backends.
*/
@Singleton
-public class CodeOwners {
+public class CodeOwners implements CodeOwnerConfigLoader {
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerMetrics codeOwnerMetrics;
@Inject
- CodeOwners(CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) {
+ CodeOwners(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerMetrics codeOwnerMetrics) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerMetrics = codeOwnerMetrics;
}
- /**
- * Retrieves the code owner config for the given key from the given branch revision.
- *
- * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
- * @param revision the branch revision from which the code owner config should be loaded
- * @return the code owner config for the given key if it exists, otherwise {@link
- * Optional#empty()}
- */
+ @Override
public Optional<CodeOwnerConfig> get(CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision) {
requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
requireNonNull(revision, "revision");
+ codeOwnerMetrics.countCodeOwnerConfigReads.increment();
CodeOwnerBackend codeOwnerBackend =
- codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
- return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+ codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
+ try (Timer1.Context<String> ctx =
+ codeOwnerMetrics.loadCodeOwnerConfig.start(codeOwnerBackend.getClass().getSimpleName())) {
+ return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+ }
}
- /**
- * Retrieves the code owner config for the given key from the current revision of the branch.
- *
- * @param codeOwnerConfigKey the key of the code owner config that should be retrieved
- * @return the code owner config for the given key if it exists, otherwise {@link
- * Optional#empty()}
- */
+ @Override
public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey) {
requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+ codeOwnerMetrics.countCodeOwnerConfigReads.increment();
CodeOwnerBackend codeOwnerBackend =
- codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
- return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
+ codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
+ try (Timer1.Context<String> ctx =
+ codeOwnerMetrics.loadCodeOwnerConfig.start(codeOwnerBackend.getClass().getSimpleName())) {
+ return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
+ }
}
/**
@@ -88,19 +92,22 @@
public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
CodeOwnerBackend codeOwnerBackend =
- codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+ codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
return codeOwnerBackend.getFilePath(codeOwnerConfigKey);
}
/**
* Checks whether the given exception was caused by a non-parseable code owner config ({@link
- * ConfigInvalidException}). If yes, the {@link ConfigInvalidException} is returned. If no, {@link
- * Optional#empty()} is returned.
+ * InvalidCodeOwnerConfigException}). If yes, the {@link InvalidCodeOwnerConfigException} is
+ * returned. If no, {@link Optional#empty()} is returned.
*/
- public static Optional<ConfigInvalidException> getInvalidConfigCause(Throwable e) {
+ public static Optional<InvalidCodeOwnerConfigException> getInvalidCodeOwnerConfigCause(
+ Throwable e) {
return Throwables.getCausalChain(e).stream()
- .filter(t -> t instanceof ConfigInvalidException)
- .map(t -> (ConfigInvalidException) t)
+ .filter(t -> t instanceof InvalidCodeOwnerConfigException)
+ .map(t -> (InvalidCodeOwnerConfigException) t)
.findFirst();
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
index b4e2931..468f5bb 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
@@ -17,8 +17,12 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.plugins.codeowners.config.InvalidPluginConfigurationException;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.InvalidPluginConfigurationException;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.ExceptionHook;
+import com.google.inject.Inject;
+import java.nio.file.InvalidPathException;
import java.util.Optional;
/**
@@ -35,36 +39,108 @@
* </ul>
*/
public class CodeOwnersExceptionHook implements ExceptionHook {
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+
+ @Inject
+ CodeOwnersExceptionHook(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerMetrics codeOwnerMetric) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerMetrics = codeOwnerMetric;
+ }
+
@Override
public boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) {
return isInvalidPluginConfigurationException(throwable)
- || isInvalidCodeOwnerConfigException(throwable);
+ || isInvalidCodeOwnerConfigException(throwable)
+ || isInvalidPathException(throwable);
}
@Override
public ImmutableList<String> getUserMessages(Throwable throwable, @Nullable String traceId) {
- if (isInvalidPluginConfigurationException(throwable)
- || isInvalidCodeOwnerConfigException(throwable)) {
- return ImmutableList.of(throwable.getMessage());
+ Optional<InvalidPluginConfigurationException> invalidPluginConfigurationException =
+ getInvalidPluginConfigurationCause(throwable);
+ if (invalidPluginConfigurationException.isPresent()) {
+ return ImmutableList.of(invalidPluginConfigurationException.get().getMessage());
}
+
+ Optional<InvalidCodeOwnerConfigException> invalidCodeOwnerConfigException =
+ CodeOwners.getInvalidCodeOwnerConfigCause(throwable);
+ if (invalidCodeOwnerConfigException.isPresent()) {
+ codeOwnerMetrics.countInvalidCodeOwnerConfigFiles.increment(
+ invalidCodeOwnerConfigException.get().getProjectName().get(),
+ invalidCodeOwnerConfigException.get().getRef(),
+ invalidCodeOwnerConfigException.get().getCodeOwnerConfigFilePath());
+
+ ImmutableList.Builder<String> messages = ImmutableList.builder();
+ messages.add(invalidCodeOwnerConfigException.get().getMessage());
+ codeOwnersPluginConfiguration
+ .getProjectConfig(invalidCodeOwnerConfigException.get().getProjectName())
+ .getInvalidCodeOwnerConfigInfoUrl()
+ .ifPresent(
+ invalidCodeOwnerConfigInfoUrl ->
+ messages.add(String.format("For help check %s", invalidCodeOwnerConfigInfoUrl)));
+ return messages.build();
+ }
+
+ Optional<InvalidPathException> invalidPathException = getInvalidPathException(throwable);
+ if (invalidPathException.isPresent()) {
+ return ImmutableList.of(invalidPathException.get().getMessage());
+ }
+
+ // This must be done last since some of the exceptions we handle above may be wrapped in a
+ // CodeOwnersInternalServerErrorException.
+ Optional<CodeOwnersInternalServerErrorException> codeOwnersInternalServerErrorException =
+ getCodeOwnersInternalServerErrorException(throwable);
+ if (codeOwnersInternalServerErrorException.isPresent()) {
+ return ImmutableList.of(codeOwnersInternalServerErrorException.get().getUserVisibleMessage());
+ }
+
return ImmutableList.of();
}
@Override
public Optional<Status> getStatus(Throwable throwable) {
if (isInvalidPluginConfigurationException(throwable)
- || isInvalidCodeOwnerConfigException(throwable)) {
+ || isInvalidCodeOwnerConfigException(throwable)
+ || isInvalidPathException(throwable)) {
return Optional.of(Status.create(409, "Conflict"));
}
return Optional.empty();
}
+ private static Optional<CodeOwnersInternalServerErrorException>
+ getCodeOwnersInternalServerErrorException(Throwable throwable) {
+ return getCause(CodeOwnersInternalServerErrorException.class, throwable);
+ }
+
private static boolean isInvalidPluginConfigurationException(Throwable throwable) {
+ return getInvalidPluginConfigurationCause(throwable).isPresent();
+ }
+
+ private static Optional<InvalidPluginConfigurationException> getInvalidPluginConfigurationCause(
+ Throwable throwable) {
+ return getCause(InvalidPluginConfigurationException.class, throwable);
+ }
+
+ private static boolean isInvalidPathException(Throwable throwable) {
+ return getInvalidPathException(throwable).isPresent();
+ }
+
+ public static Optional<InvalidPathException> getInvalidPathException(Throwable throwable) {
+ return getCause(InvalidPathException.class, throwable);
+ }
+
+ private static <T extends Throwable> Optional<T> getCause(
+ Class<T> exceptionClass, Throwable throwable) {
return Throwables.getCausalChain(throwable).stream()
- .anyMatch(t -> t instanceof InvalidPluginConfigurationException);
+ .filter(exceptionClass::isInstance)
+ .map(exceptionClass::cast)
+ .findFirst();
}
private static boolean isInvalidCodeOwnerConfigException(Throwable throwable) {
- return CodeOwners.getInvalidConfigCause(throwable).isPresent();
+ return CodeOwners.getInvalidCodeOwnerConfigCause(throwable).isPresent();
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java
new file mode 100644
index 0000000..a0e82ad
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExperimentFeaturesConstants.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+/**
+ * Constants for {@link com.google.gerrit.server.experiments.ExperimentFeatures} in the code-owners
+ * plugin.
+ */
+public final class CodeOwnersExperimentFeaturesConstants {
+ /**
+ * Whether {@link com.google.gerrit.server.patch.DiffOperations}, and thus the diff cache, should
+ * be used to get changed files, instead of computing the changed files on our own.
+ *
+ * @see ChangedFiles#getOrCompute(com.google.gerrit.entities.Project.NameKey,
+ * org.eclipse.jgit.lib.ObjectId)
+ */
+ public static final String USE_DIFF_CACHE =
+ "GerritBackendRequestFeature__code_owners_use_diff_cache";
+
+ /**
+ * Private constructor to prevent instantiation of this class.
+ *
+ * <p>The class only contains static fields, hence the class never needs to be instantiated.
+ */
+ private CodeOwnersExperimentFeaturesConstants() {}
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
new file mode 100644
index 0000000..13b1a9d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+/** Exception signaling an internal server error in the code-owners plugin. */
+public class CodeOwnersInternalServerErrorException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private static final String USER_MESSAGE = "Internal server in code-owners plugin";
+
+ public CodeOwnersInternalServerErrorException(String message) {
+ super(message);
+ }
+
+ public CodeOwnersInternalServerErrorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public String getUserVisibleMessage() {
+ return USER_MESSAGE;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
new file mode 100644
index 0000000..1aa9660
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -0,0 +1,207 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.events.ReviewerAddedListener;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Callback that is invoked when a user is added as a reviewer.
+ *
+ * <p>If a code owner was added as reviewer add a change message that lists the files that are owned
+ * by the reviewer.
+ */
+@Singleton
+public class CodeOwnersOnAddReviewer implements ReviewerAddedListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String TAG_ADD_REVIEWER =
+ ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addReviewer";
+
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+ private final Provider<CurrentUser> userProvider;
+ private final RetryHelper retryHelper;
+ private final ChangeNotes.Factory changeNotesFactory;
+ private final AccountCache accountCache;
+ private final ChangeMessagesUtil changeMessageUtil;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+
+ @Inject
+ CodeOwnersOnAddReviewer(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerApprovalCheck codeOwnerApprovalCheck,
+ Provider<CurrentUser> userProvider,
+ RetryHelper retryHelper,
+ ChangeNotes.Factory changeNotesFactory,
+ AccountCache accountCache,
+ ChangeMessagesUtil changeMessageUtil,
+ CodeOwnerMetrics codeOwnerMetrics) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
+ this.userProvider = userProvider;
+ this.retryHelper = retryHelper;
+ this.changeNotesFactory = changeNotesFactory;
+ this.accountCache = accountCache;
+ this.changeMessageUtil = changeMessageUtil;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ @Override
+ public void onReviewersAdded(Event event) {
+ Change.Id changeId = Change.id(event.getChange()._number);
+ Project.NameKey projectName = Project.nameKey(event.getChange().project);
+
+ CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
+ codeOwnersPluginConfiguration.getProjectConfig(projectName);
+ int maxPathsInChangeMessages = codeOwnersConfig.getMaxPathsInChangeMessages();
+ if (codeOwnersConfig.isDisabled(event.getChange().branch) || maxPathsInChangeMessages <= 0) {
+ return;
+ }
+
+ try (Timer0.Context ctx = codeOwnerMetrics.addChangeMessageOnAddReviewer.start()) {
+ retryHelper
+ .changeUpdate(
+ "addCodeOwnersMessageOnAddReviewer",
+ updateFactory -> {
+ try (BatchUpdate batchUpdate =
+ updateFactory.create(projectName, userProvider.get(), TimeUtil.nowTs())) {
+ batchUpdate.addOp(
+ changeId, new Op(event.getReviewers(), maxPathsInChangeMessages));
+ batchUpdate.execute();
+ }
+ return null;
+ })
+ .call();
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log(
+ String.format(
+ "Failed to post code-owners change message for reviewer on change %s in project %s.",
+ changeId, projectName));
+ }
+ }
+
+ private class Op implements BatchUpdateOp {
+ private final List<AccountInfo> reviewers;
+ private final int limit;
+
+ Op(List<AccountInfo> reviewers, int limit) {
+ this.reviewers = reviewers;
+ this.limit = limit;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws Exception {
+ String message =
+ reviewers.stream()
+ .map(accountInfo -> Account.id(accountInfo._accountId))
+ .map(
+ reviewerAccountId ->
+ buildMessageForReviewer(
+ ctx.getProject(), ctx.getChange().getId(), reviewerAccountId))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(joining("\n"));
+
+ if (message.isEmpty()) {
+ return false;
+ }
+
+ ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(ctx, message, TAG_ADD_REVIEWER);
+ changeMessageUtil.addChangeMessage(
+ ctx.getUpdate(ctx.getChange().currentPatchSetId()), changeMessage);
+ return true;
+ }
+
+ private Optional<String> buildMessageForReviewer(
+ Project.NameKey projectName, Change.Id changeId, Account.Id reviewerAccountId) {
+ ChangeNotes changeNotes = changeNotesFactory.create(projectName, changeId);
+
+ ImmutableList<Path> ownedPaths;
+ try {
+ // limit + 1, so that we can show an indicator if there are more than <limit> files.
+ ownedPaths =
+ codeOwnerApprovalCheck.getOwnedPaths(
+ changeNotes,
+ changeNotes.getCurrentPatchSet(),
+ reviewerAccountId,
+ /* start= */ 0,
+ limit + 1);
+ } catch (RestApiException e) {
+ logger.atFine().withCause(e).log(
+ "Couldn't compute owned paths of change %s for account %s",
+ changeNotes.getChangeId(), reviewerAccountId.get());
+ return Optional.empty();
+ }
+
+ if (ownedPaths.isEmpty()) {
+ // this reviewer doesn't own any of the modified paths
+ return Optional.empty();
+ }
+
+ Account reviewerAccount = accountCache.getEvenIfMissing(reviewerAccountId).account();
+
+ StringBuilder message = new StringBuilder();
+ message.append(
+ String.format(
+ "%s who was added as reviewer owns the following files:\n",
+ reviewerAccount.getName()));
+
+ if (ownedPaths.size() <= limit) {
+ appendPaths(message, ownedPaths.stream());
+ } else {
+ appendPaths(message, ownedPaths.stream().limit(limit));
+ message.append("(more files)\n");
+ }
+
+ return Optional.of(message.toString());
+ }
+
+ private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) {
+ pathsToAppend.forEach(
+ path -> message.append(String.format("* %s\n", JgitPath.of(path).get())));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
index e123b84..5e41d70 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.plugins.codeowners.backend;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
@@ -96,7 +96,9 @@
public Optional<CodeOwnerConfig> upsertCodeOwnerConfig(
CodeOwnerConfig.Key codeOwnerConfigKey, CodeOwnerConfigUpdate codeOwnerConfigUpdate) {
CodeOwnerBackend codeOwnerBackend =
- codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+ codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
return codeOwnerBackend.upsertCodeOwnerConfig(
codeOwnerConfigKey, codeOwnerConfigUpdate, currentUser.orElse(null));
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java b/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java
new file mode 100644
index 0000000..bcad5e8
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/EnableImplicitApprovals.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+/** Enum to control whether implicit code-owner approvals by the patch set uploader are enabled. */
+public enum EnableImplicitApprovals {
+ /** Implicit code-owner approvals of the the patch set uploader are disabled. */
+ FALSE,
+
+ /**
+ * Implicit code-owner approvals of the patch set uploader are enabled, but only if the configured
+ * required label allows self approvals.
+ */
+ TRUE,
+
+ /**
+ * Implicit code-owner approvals of the patch set uploader are enabled, even if the configured
+ * required label disallows self approvals.
+ */
+ FORCED;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java
index 5ad4079..872473b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/FallbackCodeOwners.java
@@ -32,5 +32,11 @@
* with configuring code owners can easily happen. This is why this option is intended to be only
* used if requiring code owner approvals should not be enforced.
*/
- ALL_USERS;
+ ALL_USERS,
+
+ /**
+ * Paths for which no code owners are defined are owned by the project owners. This means changes
+ * to these paths can be approved by the project owners.
+ */
+ PROJECT_OWNERS;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java b/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java
index 3d084db..330917d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java
@@ -14,7 +14,13 @@
package com.google.gerrit.plugins.codeowners.backend;
+import static java.util.Objects.requireNonNull;
+
import com.google.auto.value.AutoValue;
+import com.google.gerrit.plugins.codeowners.common.ChangedFile;
+import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
+import java.nio.file.Path;
import java.util.Optional;
/** Code owner status for a particular file that was changed in a change. */
@@ -55,4 +61,77 @@
return new AutoValue_FileCodeOwnerStatus(
changedFile, newPathCodeOwnerStatus, oldPathCodeOwnerStatus);
}
+
+ public static FileCodeOwnerStatus addition(String path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+
+ return addition(JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus);
+ }
+
+ public static FileCodeOwnerStatus addition(Path path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+ requireNonNull(codeOwnerStatus, "codeOwnerStatus");
+
+ return create(
+ ChangedFile.addition(path),
+ Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus)),
+ Optional.empty());
+ }
+
+ public static FileCodeOwnerStatus modification(Path path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+ requireNonNull(codeOwnerStatus, "codeOwnerStatus");
+
+ return create(
+ ChangedFile.modification(path),
+ Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus)),
+ Optional.empty());
+ }
+
+ public static FileCodeOwnerStatus deletion(String path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+
+ return deletion(JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus);
+ }
+
+ public static FileCodeOwnerStatus deletion(Path path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+ requireNonNull(codeOwnerStatus, "codeOwnerStatus");
+
+ return create(
+ ChangedFile.deletion(path),
+ Optional.empty(),
+ Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus)));
+ }
+
+ public static FileCodeOwnerStatus rename(
+ String oldPath,
+ CodeOwnerStatus oldPathCodeOwnerStatus,
+ String newPath,
+ CodeOwnerStatus newPathCodeOwnerStatus) {
+ requireNonNull(oldPath, "oldPath");
+ requireNonNull(newPath, "newPath");
+
+ return rename(
+ JgitPath.of(oldPath).getAsAbsolutePath(),
+ oldPathCodeOwnerStatus,
+ JgitPath.of(newPath).getAsAbsolutePath(),
+ newPathCodeOwnerStatus);
+ }
+
+ public static FileCodeOwnerStatus rename(
+ Path oldPath,
+ CodeOwnerStatus oldPathCodeOwnerStatus,
+ Path newPath,
+ CodeOwnerStatus newPathCodeOwnerStatus) {
+ requireNonNull(oldPath, "oldPath");
+ requireNonNull(oldPathCodeOwnerStatus, "oldPathCodeOwnerStatus");
+ requireNonNull(newPath, "newPath");
+ requireNonNull(newPathCodeOwnerStatus, "newPathCodeOwnerStatus");
+
+ return create(
+ ChangedFile.rename(newPath, oldPath),
+ Optional.of(PathCodeOwnerStatus.create(newPath, newPathCodeOwnerStatus)),
+ Optional.of(PathCodeOwnerStatus.create(oldPath, oldPathCodeOwnerStatus)));
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java b/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java
new file mode 100644
index 0000000..650ae50
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend;
+
+import java.nio.file.Path;
+
+/**
+ * Glob matcher that is compatible with how globs are interpreted by the {@code find-owners} plugin.
+ *
+ * <p>This matcher has the same behaviour as the {@link GlobMatcher} except that:
+ *
+ * <ul>
+ * <li>'*': matches any string, including slashes (same as '**')
+ * </ul>
+ */
+public class FindOwnersGlobMatcher implements PathExpressionMatcher {
+ /** Singleton instance. */
+ public static FindOwnersGlobMatcher INSTANCE = new FindOwnersGlobMatcher();
+
+ /** Private constructor to prevent creation of further instances. */
+ private FindOwnersGlobMatcher() {}
+
+ @Override
+ public boolean matches(String glob, Path relativePath) {
+ // always match files in all subdirectories
+ return GlobMatcher.INSTANCE.matches("{**/,}" + glob, relativePath);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java
index fed838a..400432e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java
@@ -15,7 +15,6 @@
package com.google.gerrit.plugins.codeowners.backend;
import java.nio.file.Path;
-import org.eclipse.jgit.errors.ConfigInvalidException;
/** Callback interface to let callers handle invalid code owner config files. */
public interface InvalidCodeOwnerConfigCallback {
@@ -23,8 +22,9 @@
* Invoked when an invalid code owner config file is found.
*
* @param codeOwnerConfigFilePath the path of the invalid code owner config file
- * @param configInvalidException the parsing exception
+ * @param invalidCodeOwnerConfigException the parsing exception
*/
void onInvalidCodeOwnerConfig(
- Path codeOwnerConfigFilePath, ConfigInvalidException configInvalidException);
+ Path codeOwnerConfigFilePath,
+ InvalidCodeOwnerConfigException invalidCodeOwnerConfigException);
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java
new file mode 100644
index 0000000..7806c59
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Exception that is thrown if there is an invalid code owner config file. */
+public class InvalidCodeOwnerConfigException extends ConfigInvalidException {
+ private static final long serialVersionUID = 1L;
+
+ private final Project.NameKey projectName;
+ private final String ref;
+ private final String codeOwnerConfigFilePath;
+
+ public InvalidCodeOwnerConfigException(
+ String message, Project.NameKey projectName, String ref, String codeOwnerConfigFilePath) {
+ this(message, projectName, ref, codeOwnerConfigFilePath, /* cause= */ null);
+ }
+
+ public InvalidCodeOwnerConfigException(
+ String message,
+ Project.NameKey projectName,
+ String ref,
+ String codeOwnerConfigFilePath,
+ @Nullable Throwable cause) {
+ super(message, cause);
+
+ this.projectName = requireNonNull(projectName, "projectName");
+ this.ref = requireNonNull(ref, "ref");
+ this.codeOwnerConfigFilePath =
+ requireNonNull(codeOwnerConfigFilePath, "codeOwnerConfigFilePath");
+ }
+
+ public Project.NameKey getProjectName() {
+ return projectName;
+ }
+
+ public String getRef() {
+ return ref;
+ }
+
+ public String getCodeOwnerConfigFilePath() {
+ return codeOwnerConfigFilePath;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
new file mode 100644
index 0000000..d6beb5f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -0,0 +1,281 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.restapi.change.OnPostReview;
+import com.google.gerrit.server.util.LabelVote;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Callback that is invoked on post review and that extends the change message if a code owner
+ * approval was changed.
+ *
+ * <p>If a code owner approval was added, removed or changed, include in the change message that is
+ * being posted on vote, which of the files:
+ *
+ * <ul>
+ * <li>are approved now
+ * <li>are no longer approved
+ * <li>are still approved
+ * </ul>
+ */
+@Singleton
+class OnCodeOwnerApproval implements OnPostReview {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+
+ @Inject
+ OnCodeOwnerApproval(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerApprovalCheck codeOwnerApprovalCheck,
+ CodeOwnerMetrics codeOwnerMetrics) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ @Override
+ public Optional<String> getChangeMessageAddOn(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals) {
+ CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
+ codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
+ int maxPathsInChangeMessage = codeOwnersConfig.getMaxPathsInChangeMessages();
+ if (codeOwnersConfig.isDisabled(changeNotes.getChange().getDest().branch())
+ || maxPathsInChangeMessage <= 0) {
+ return Optional.empty();
+ }
+
+ // code owner approvals are only computed for the current patch set
+ if (!changeNotes.getChange().currentPatchSetId().equals(patchSet.id())) {
+ return Optional.empty();
+ }
+
+ RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval();
+
+ if (oldApprovals.get(requiredApproval.labelType().getName()) == null) {
+ // If oldApprovals doesn't contain the label or if the labels value in it is null, the label
+ // was not changed.
+ // This means that the user only voted on unrelated labels.
+ return Optional.empty();
+ }
+
+ try (Timer0.Context ctx = codeOwnerMetrics.extendChangeMessageOnPostReview.start()) {
+ return buildMessageForCodeOwnerApproval(
+ user,
+ changeNotes,
+ patchSet,
+ oldApprovals,
+ approvals,
+ requiredApproval,
+ maxPathsInChangeMessage);
+ }
+ }
+
+ private Optional<String> buildMessageForCodeOwnerApproval(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals,
+ RequiredApproval requiredApproval,
+ int limit) {
+ LabelVote newVote = getNewVote(requiredApproval, approvals);
+
+ ImmutableList<Path> ownedPaths;
+ try {
+ // limit + 1, so that we can show an indicator if there are more than <limit> files.
+ ownedPaths =
+ codeOwnerApprovalCheck.getOwnedPaths(
+ changeNotes,
+ changeNotes.getCurrentPatchSet(),
+ user.getAccountId(),
+ /* start= */ 0,
+ limit + 1);
+ } catch (RestApiException e) {
+ logger.atFine().withCause(e).log(
+ "Couldn't compute owned paths of change %s for account %s",
+ changeNotes.getChangeId(), user.getAccountId().get());
+ return Optional.empty();
+ }
+
+ if (ownedPaths.isEmpty()) {
+ // the user doesn't own any of the modified paths
+ return Optional.empty();
+ }
+
+ if (isIgnoredDueToSelfApproval(user, patchSet, requiredApproval)) {
+ if (isCodeOwnerApprovalNewlyApplied(requiredApproval, oldApprovals, newVote)
+ || isCodeOwnerApprovalUpOrDowngraded(requiredApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "The vote %s is ignored as code-owner approval since the label doesn't allow"
+ + " self approval of the patch set uploader.",
+ newVote));
+ }
+ return Optional.empty();
+ }
+
+ boolean hasImplicitApprovalByUser =
+ codeOwnersPluginConfiguration
+ .getProjectConfig(changeNotes.getProjectName())
+ .areImplicitApprovalsEnabled()
+ && patchSet.uploader().equals(user.getAccountId());
+
+ boolean noLongerExplicitlyApproved = false;
+ StringBuilder message = new StringBuilder();
+ if (isCodeOwnerApprovalNewlyApplied(requiredApproval, oldApprovals, newVote)) {
+ if (hasImplicitApprovalByUser) {
+ message.append(
+ String.format(
+ "By voting %s the following files are now explicitly code-owner approved by %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are now code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ } else if (isCodeOwnerApprovalRemoved(requiredApproval, oldApprovals, newVote)) {
+ if (newVote.value() == 0) {
+ if (hasImplicitApprovalByUser) {
+ noLongerExplicitlyApproved = true;
+ message.append(
+ String.format(
+ "By removing the %s vote the following files are no longer explicitly code-owner"
+ + " approved by %s:\n",
+ newVote.label(), user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By removing the %s vote the following files are no longer code-owner approved"
+ + " by %s:\n",
+ newVote.label(), user.getName()));
+ }
+ } else {
+ if (hasImplicitApprovalByUser) {
+ noLongerExplicitlyApproved = true;
+ message.append(
+ String.format(
+ "By voting %s the following files are no longer explicitly code-owner approved by"
+ + " %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are no longer code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ }
+ } else if (isCodeOwnerApprovalUpOrDowngraded(requiredApproval, oldApprovals, newVote)) {
+ if (hasImplicitApprovalByUser) {
+ message.append(
+ String.format(
+ "By voting %s the following files are still explicitly code-owner approved by"
+ + " %s:\n",
+ newVote, user.getName()));
+ } else {
+ message.append(
+ String.format(
+ "By voting %s the following files are still code-owner approved by %s:\n",
+ newVote, user.getName()));
+ }
+ } else {
+ // non-approval was downgraded (e.g. -1 to -2)
+ return Optional.empty();
+ }
+
+ if (ownedPaths.size() <= limit) {
+ appendPaths(message, ownedPaths.stream());
+ } else {
+ appendPaths(message, ownedPaths.stream().limit(limit));
+ message.append("(more files)\n");
+ }
+
+ if (hasImplicitApprovalByUser && noLongerExplicitlyApproved) {
+ message.append(
+ String.format(
+ "\nThe listed files are still implicitly approved by %s.\n", user.getName()));
+ }
+
+ return Optional.of(message.toString());
+ }
+
+ private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) {
+ pathsToAppend.forEach(path -> message.append(String.format("* %s\n", JgitPath.of(path).get())));
+ }
+
+ private boolean isIgnoredDueToSelfApproval(
+ IdentifiedUser user, PatchSet patchSet, RequiredApproval requiredApproval) {
+ return patchSet.uploader().equals(user.getAccountId())
+ && requiredApproval.labelType().isIgnoreSelfApproval();
+ }
+
+ private boolean isCodeOwnerApprovalNewlyApplied(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) < requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerApprovalRemoved(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() < requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerApprovalUpOrDowngraded(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private LabelVote getNewVote(RequiredApproval requiredApproval, Map<String, Short> approvals) {
+ String labelName = requiredApproval.labelType().getName();
+ checkState(
+ approvals.containsKey(labelName),
+ "expected that approval on label %s exists (approvals = %s)",
+ labelName,
+ approvals);
+ return LabelVote.create(labelName, approvals.get(labelName));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
new file mode 100644
index 0000000..09e3624
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.restapi.change.OnPostReview;
+import com.google.gerrit.server.util.LabelVote;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Callback that is invoked on post review and that extends the change message if a code owner
+ * override was changed.
+ *
+ * <p>If a code owner override was added, removed or changed, include in the change message that is
+ * being posted on vote, that the vote is a code owner override to let users know about its effect.
+ */
+@Singleton
+class OnCodeOwnerOverride implements OnPostReview {
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerMetrics codeOwnerMetrics;
+
+ @Inject
+ OnCodeOwnerOverride(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerMetrics codeOwnerMetrics) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ @Override
+ public Optional<String> getChangeMessageAddOn(
+ IdentifiedUser user,
+ ChangeNotes changeNotes,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals) {
+ CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig =
+ codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName());
+ if (codeOwnersConfig.isDisabled(changeNotes.getChange().getDest().branch())) {
+ return Optional.empty();
+ }
+
+ // code owner overrides are only relevant for the current patch set
+ if (!changeNotes.getChange().currentPatchSetId().equals(patchSet.id())) {
+ return Optional.empty();
+ }
+
+ ImmutableList<RequiredApproval> appliedOverrideApprovals =
+ codeOwnersConfig.getOverrideApprovals().stream()
+ .sorted(comparing(RequiredApproval::toString))
+ // If oldApprovals doesn't contain the label or if the labels value in it is null, the
+ // label was not changed.
+ .filter(
+ overrideApproval ->
+ oldApprovals.get(overrideApproval.labelType().getName()) != null)
+ .collect(toImmutableList());
+
+ if (appliedOverrideApprovals.isEmpty()) {
+ return Optional.empty();
+ }
+
+ try (Timer0.Context ctx = codeOwnerMetrics.extendChangeMessageOnPostReview.start()) {
+ List<String> messages = new ArrayList<>();
+ appliedOverrideApprovals.forEach(
+ overrideApproval ->
+ buildMessageForCodeOwnerOverride(
+ user, patchSet, oldApprovals, approvals, overrideApproval)
+ .ifPresent(messages::add));
+ if (messages.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(Joiner.on("\n\n").join(messages));
+ }
+ }
+
+ private Optional<String> buildMessageForCodeOwnerOverride(
+ IdentifiedUser user,
+ PatchSet patchSet,
+ Map<String, Short> oldApprovals,
+ Map<String, Short> approvals,
+ RequiredApproval overrideApproval) {
+ LabelVote newVote = getNewVote(overrideApproval, approvals);
+
+ if (isIgnoredDueToSelfApproval(user, patchSet, overrideApproval)) {
+ if (isCodeOwnerOverrideNewlyApplied(overrideApproval, oldApprovals, newVote)
+ || isCodeOwnerOverrideUpOrDowngraded(overrideApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "The vote %s is ignored as code-owner override since the label doesn't allow"
+ + " self approval of the patch set uploader.",
+ newVote));
+ }
+ return Optional.empty();
+ }
+
+ if (isCodeOwnerOverrideNewlyApplied(overrideApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is overridden by %s",
+ newVote, user.getName()));
+ } else if (isCodeOwnerOverrideRemoved(overrideApproval, oldApprovals, newVote)) {
+ if (newVote.value() == 0) {
+ return Optional.of(
+ String.format(
+ "By removing the %s vote the code-owners submit requirement is no longer overridden"
+ + " by %s",
+ newVote.label(), user.getName()));
+ }
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is no longer overridden by %s",
+ newVote, user.getName()));
+ } else if (isCodeOwnerOverrideUpOrDowngraded(overrideApproval, oldApprovals, newVote)) {
+ return Optional.of(
+ String.format(
+ "By voting %s the code-owners submit requirement is still overridden by %s",
+ newVote, user.getName()));
+ }
+ // non-approval was downgraded (e.g. -1 to -2)
+ return Optional.empty();
+ }
+
+ private boolean isIgnoredDueToSelfApproval(
+ IdentifiedUser user, PatchSet patchSet, RequiredApproval requiredApproval) {
+ return patchSet.uploader().equals(user.getAccountId())
+ && requiredApproval.labelType().isIgnoreSelfApproval();
+ }
+
+ private boolean isCodeOwnerOverrideNewlyApplied(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) < requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerOverrideRemoved(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() < requiredApproval.value();
+ }
+
+ private boolean isCodeOwnerOverrideUpOrDowngraded(
+ RequiredApproval requiredApproval, Map<String, Short> oldApprovals, LabelVote newVote) {
+ String labelName = requiredApproval.labelType().getName();
+ return oldApprovals.get(labelName) >= requiredApproval.value()
+ && newVote.value() >= requiredApproval.value();
+ }
+
+ private LabelVote getNewVote(RequiredApproval requiredApproval, Map<String, Short> approvals) {
+ String labelName = requiredApproval.labelType().getName();
+ checkState(
+ approvals.containsKey(labelName),
+ "expected that approval on label %s exists (approvals = %s)",
+ labelName,
+ approvals);
+ return LabelVote.create(labelName, approvals.get(labelName));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
new file mode 100644
index 0000000..66fce67
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * An optional result of an operation with optional messages.
+ *
+ * @param <T> type of the optional result
+ */
+@AutoValue
+public abstract class OptionalResultWithMessages<T> {
+ /** Gets the result. */
+ public abstract Optional<T> result();
+
+ /** Whether the result is present. */
+ public boolean isPresent() {
+ return result().isPresent();
+ }
+
+ /** Whether the result is empty. */
+ public boolean isEmpty() {
+ return !result().isPresent();
+ }
+
+ /** Returns the result value, if present. Fails if the result is not present. */
+ public T get() {
+ return result().get();
+ }
+
+ /** Gets the messages. */
+ public abstract ImmutableList<String> messages();
+
+ /** Creates a {@link OptionalResultWithMessages} instance without messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result) {
+ return create(result, ImmutableList.of());
+ }
+
+ /** Creates an empty {@link OptionalResultWithMessages} instance with a single message. */
+ public static <T> OptionalResultWithMessages<T> createEmpty(String message) {
+ requireNonNull(message, "message");
+ return createEmpty(ImmutableList.of(message));
+ }
+
+ /** Creates an empty {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> createEmpty(List<String> messages) {
+ requireNonNull(messages, "messages");
+ return new AutoValue_OptionalResultWithMessages<>(
+ Optional.empty(), ImmutableList.copyOf(messages));
+ }
+
+ /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result, String message) {
+ requireNonNull(message, "message");
+ return create(result, ImmutableList.of(message));
+ }
+
+ /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result, List<String> messages) {
+ requireNonNull(result, "result");
+ requireNonNull(messages, "messages");
+ return new AutoValue_OptionalResultWithMessages<>(
+ Optional.of(result), ImmutableList.copyOf(messages));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java
index 9285a52..b5f0976 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java
@@ -14,8 +14,11 @@
package com.google.gerrit.plugins.codeowners.backend;
+import static java.util.Objects.requireNonNull;
+
import com.google.auto.value.AutoValue;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import java.nio.file.Path;
/** Code owner status for a particular path that has been modified in a change. */
@@ -41,4 +44,18 @@
public static PathCodeOwnerStatus create(Path path, CodeOwnerStatus codeOwnerStatus) {
return new AutoValue_PathCodeOwnerStatus(path, codeOwnerStatus);
}
+
+ /**
+ * Creates a {@link PathCodeOwnerStatus} instance.
+ *
+ * @param path the path to which the code owner status belongs
+ * @param codeOwnerStatus the code owner status
+ * @return the created {@link PathCodeOwnerStatus} instance
+ */
+ public static PathCodeOwnerStatus create(String path, CodeOwnerStatus codeOwnerStatus) {
+ requireNonNull(path, "path");
+ requireNonNull(codeOwnerStatus, "codeOwnerStatus");
+
+ return create(JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index 078ba17..8272bb6 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -19,22 +19,27 @@
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
+import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
@@ -52,24 +57,29 @@
@Singleton
public static class Factory {
+ private final CodeOwnerMetrics codeOwnerMetrics;
private final ProjectCache projectCache;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwners codeOwners;
@Inject
Factory(
+ CodeOwnerMetrics codeOwnerMetrics,
ProjectCache projectCache,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwners codeOwners) {
+ this.codeOwnerMetrics = codeOwnerMetrics;
this.projectCache = projectCache;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwners = codeOwners;
}
- public PathCodeOwners create(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
+ public PathCodeOwners createWithoutCache(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
requireNonNull(codeOwnerConfig, "codeOwnerConfig");
return new PathCodeOwners(
+ codeOwnerMetrics,
projectCache,
+ /* transientCodeOwnerConfigCache= */ null,
codeOwners,
codeOwnerConfig,
absolutePath,
@@ -77,13 +87,21 @@
}
public Optional<PathCodeOwners> create(
- CodeOwnerConfig.Key codeOwnerConfigKey, ObjectId revision, Path absolutePath) {
- return codeOwners
+ TransientCodeOwnerConfigCache transientCodeOwnerConfigCache,
+ CodeOwnerConfig.Key codeOwnerConfigKey,
+ ObjectId revision,
+ Path absolutePath) {
+ requireNonNull(transientCodeOwnerConfigCache, "transientCodeOwnerConfigCache");
+ requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
+ requireNonNull(revision, "revision");
+ return transientCodeOwnerConfigCache
.get(codeOwnerConfigKey, revision)
.map(
codeOwnerConfig ->
new PathCodeOwners(
+ codeOwnerMetrics,
projectCache,
+ transientCodeOwnerConfigCache,
codeOwners,
codeOwnerConfig,
absolutePath,
@@ -109,28 +127,37 @@
*/
private PathExpressionMatcher getMatcher(CodeOwnerConfig.Key codeOwnerConfigKey) {
CodeOwnerBackend codeOwnerBackend =
- codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
+ codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
return codeOwnerBackend
.getPathExpressionMatcher()
.orElse((pathExpression, relativePath) -> false);
}
}
+ private final CodeOwnerMetrics codeOwnerMetrics;
private final ProjectCache projectCache;
+ private final CodeOwnerConfigLoader codeOwnerConfigLoader;
private final CodeOwners codeOwners;
private final CodeOwnerConfig codeOwnerConfig;
private final Path path;
private final PathExpressionMatcher pathExpressionMatcher;
- private PathCodeOwnersResult pathCodeOwnersResult;
+ private OptionalResultWithMessages<PathCodeOwnersResult> pathCodeOwnersResult;
private PathCodeOwners(
+ CodeOwnerMetrics codeOwnerMetrics,
ProjectCache projectCache,
+ @Nullable TransientCodeOwnerConfigCache transientCodeOwnerConfigCache,
CodeOwners codeOwners,
CodeOwnerConfig codeOwnerConfig,
Path path,
PathExpressionMatcher pathExpressionMatcher) {
+ this.codeOwnerMetrics = requireNonNull(codeOwnerMetrics, "codeOwnerMetrics");
this.projectCache = requireNonNull(projectCache, "projectCache");
+ this.codeOwnerConfigLoader =
+ transientCodeOwnerConfigCache != null ? transientCodeOwnerConfigCache : codeOwners;
this.codeOwners = requireNonNull(codeOwners, "codeOwners");
this.codeOwnerConfig = requireNonNull(codeOwnerConfig, "codeOwnerConfig");
this.path = requireNonNull(path, "path");
@@ -144,6 +171,11 @@
return codeOwnerConfig;
}
+ /** Returns the absolute path for which code owners were computed. */
+ public Path getPath() {
+ return path;
+ }
+
/**
* Resolves the {@link #codeOwnerConfig}.
*
@@ -180,57 +212,124 @@
*
* @return the resolved code owner config
*/
- public PathCodeOwnersResult resolveCodeOwnerConfig() {
+ public OptionalResultWithMessages<PathCodeOwnersResult> resolveCodeOwnerConfig() {
if (this.pathCodeOwnersResult != null) {
return this.pathCodeOwnersResult;
}
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Resolve code owner config",
- Metadata.builder()
- .projectName(codeOwnerConfig.key().project().get())
- .filePath(path.toString())
- .build())) {
+ try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfig.start()) {
logger.atFine().log(
"resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key());
+ List<String> messages = new ArrayList<>();
+ messages.add(
+ String.format(
+ "resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key()));
+
+ // Create a code owner config builder to create the resolved code owner config (= code owner
+ // config that is scoped to the path and which has imports resolved)
CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder =
CodeOwnerConfig.builder(codeOwnerConfig.key(), codeOwnerConfig.revision());
- // Add all data from the importing code owner config.
+ // Add all data from the original code owner config that is relevant for the path
+ // (ignoreParentCodeOwners flag, global code owner sets and matching per-file code owner
+ // sets). Effectively this means we are dropping all non-matching per-file rules.
resolvedCodeOwnerConfigBuilder.setIgnoreParentCodeOwners(
codeOwnerConfig.ignoreParentCodeOwners());
getGlobalCodeOwnerSets(codeOwnerConfig)
.forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
- getMatchingPerFileCodeOwnerSets(codeOwnerConfig)
- .forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
+ boolean globalCodeOwnersIgnored = false;
+ for (CodeOwnerSet codeOwnerSet :
+ getMatchingPerFileCodeOwnerSets(codeOwnerConfig).collect(toImmutableSet())) {
+ messages.add(
+ String.format(
+ "per-file code owner set with path expressions %s matches",
+ codeOwnerSet.pathExpressions()));
+ resolvedCodeOwnerConfigBuilder.addCodeOwnerSet(codeOwnerSet);
+ if (codeOwnerSet.ignoreGlobalAndParentCodeOwners()) {
+ globalCodeOwnersIgnored = true;
+ }
+ }
- boolean hasUnresolvedImports =
- !resolveImports(codeOwnerConfig, resolvedCodeOwnerConfigBuilder);
+ // Resolve global imports.
+ ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder();
+ ImmutableSet<CodeOwnerConfigImport> globalImports = getGlobalImports(0, codeOwnerConfig);
+ OptionalResultWithMessages<List<UnresolvedImport>> unresolvedGlobalImports;
+ if (!globalCodeOwnersIgnored) {
+ unresolvedGlobalImports =
+ resolveImports(codeOwnerConfig.key(), globalImports, resolvedCodeOwnerConfigBuilder);
+ } else {
+ // skip global import with mode GLOBAL_CODE_OWNER_SETS_ONLY,
+ // since we already know that global code owners will be ignored, we do not need to resolve
+ // these imports
+ unresolvedGlobalImports =
+ resolveImports(
+ codeOwnerConfig.key(),
+ globalImports.stream()
+ .filter(
+ codeOwnerConfigImport ->
+ codeOwnerConfigImport.referenceToImportedCodeOwnerConfig().importMode()
+ != CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY)
+ .collect(toImmutableSet()),
+ resolvedCodeOwnerConfigBuilder);
+ }
+ messages.addAll(unresolvedGlobalImports.messages());
+ unresolvedImports.addAll(unresolvedGlobalImports.get());
- CodeOwnerConfig resolvedCodeOwnerConfig = resolvedCodeOwnerConfigBuilder.build();
-
- // Remove global code owner sets if any per-file code owner set has the
- // ignoreGlobalAndParentCodeOwners flag set to true.
+ // Remove all global code owner sets if any per-file code owner set has the
+ // ignoreGlobalAndParentCodeOwners flag set to true (as in this case they are ignored and
+ // hence not relevant).
// In this case also set ignoreParentCodeOwners to true, so that we do not need to inspect the
- // ignoreGlobalAndParentCodeOwners flags again.
- if (getMatchingPerFileCodeOwnerSets(resolvedCodeOwnerConfig)
- .anyMatch(CodeOwnerSet::ignoreGlobalAndParentCodeOwners)) {
- logger.atFine().log("remove global code owner sets and set ignoreParentCodeOwners to true");
- resolvedCodeOwnerConfig =
- resolvedCodeOwnerConfig
+ // ignoreGlobalAndParentCodeOwners flags on per-file code owner sets again, but can just rely
+ // on the global ignoreParentCodeOwners flag.
+ Optional<CodeOwnerSet> matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners =
+ getMatchingPerFileCodeOwnerSets(resolvedCodeOwnerConfigBuilder.build())
+ .filter(CodeOwnerSet::ignoreGlobalAndParentCodeOwners)
+ .findAny();
+ if (matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners.isPresent()) {
+ logger.atFine().log("remove folder code owner sets and set ignoreParentCodeOwners to true");
+ messages.add(
+ String.format(
+ "found matching per-file code owner set (with path expressions = %s) that ignores"
+ + " parent code owners, hence ignoring the folder code owners",
+ matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners
+ .get()
+ .pathExpressions()));
+ // We use resolvedCodeOwnerConfigBuilder to build up a code owner config that is scoped to
+ // the path and which has imports resolved. When resolving imports the relevant code owner
+ // sets from the imported code owner configs are added to the builder.
+ // If a per-file rule ignores global and parent code owners we have to drop all global code
+ // owner sets. The problem is that AutoValue doesn't allow us to remove/override code owner
+ // sets that have previously been added to the builder (we cannot call setCodeOwnerSets(...)
+ // after addCodeOwnerSet(...) or codeOwnerSetsBuilder() has been invoked). To override the
+ // code owner sets we build the code owner config and then create a fresh builder from it.
+ // Since the builder is fresh addCodeOwnerSet(...) and codeOwnerSetsBuilder() haven't been
+ // invoked on it yet we can now call setCodeOwnerSets(...).
+ resolvedCodeOwnerConfigBuilder =
+ resolvedCodeOwnerConfigBuilder
+ .build()
.toBuilder()
.setIgnoreParentCodeOwners()
.setCodeOwnerSets(
- resolvedCodeOwnerConfig.codeOwnerSets().stream()
+ resolvedCodeOwnerConfigBuilder.codeOwnerSets().stream()
.filter(codeOwnerSet -> !codeOwnerSet.pathExpressions().isEmpty())
- .collect(toImmutableSet()))
- .build();
+ .collect(toImmutableSet()));
}
+ // Resolve per-file imports.
+ ImmutableSet<CodeOwnerConfigImport> perFileImports =
+ getPerFileImports(
+ 0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets());
+ OptionalResultWithMessages<List<UnresolvedImport>> unresolvedPerFileImports =
+ resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder);
+ messages.addAll(unresolvedPerFileImports.messages());
+ unresolvedImports.addAll(unresolvedPerFileImports.get());
+
this.pathCodeOwnersResult =
- PathCodeOwnersResult.create(path, resolvedCodeOwnerConfig, hasUnresolvedImports);
+ OptionalResultWithMessages.create(
+ PathCodeOwnersResult.create(
+ path, resolvedCodeOwnerConfigBuilder.build(), unresolvedImports.build()),
+ messages);
logger.atFine().log("path code owners result = %s", pathCodeOwnersResult);
return this.pathCodeOwnersResult;
}
@@ -239,22 +338,20 @@
/**
* Resolve the imports of the given code owner config.
*
- * @param importingCodeOwnerConfig the code owner config for which imports should be resolved
+ * @param keyOfImportingCodeOwnerConfig the key of the importing code owner config
+ * @param codeOwnerConfigImports the code owner configs that should be imported
* @param resolvedCodeOwnerConfigBuilder the builder for the resolved code owner config
- * @return whether all imports have been resolved successfully
+ * @return list of unresolved imports, empty list if all imports were successfully resolved
*/
- private boolean resolveImports(
- CodeOwnerConfig importingCodeOwnerConfig,
+ private OptionalResultWithMessages<List<UnresolvedImport>> resolveImports(
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+ Set<CodeOwnerConfigImport> codeOwnerConfigImports,
CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) {
- boolean hasUnresolvedImports = false;
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Resolve code owner config imports",
- Metadata.builder()
- .projectName(codeOwnerConfig.key().project().get())
- .branchName(codeOwnerConfig.key().ref())
- .filePath(codeOwnerConfig.key().filePath("<default>").toString())
- .build())) {
+ ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder();
+ StringBuilder messageBuilder = new StringBuilder();
+ try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) {
+ logger.atFine().log("resolve imports of codeOwnerConfig %s", keyOfImportingCodeOwnerConfig);
+
// To detect cyclic dependencies we keep track of all seen code owner configs.
Set<CodeOwnerConfig.Key> seenCodeOwnerConfigs = new HashSet<>();
seenCodeOwnerConfigs.add(codeOwnerConfig.key());
@@ -264,49 +361,54 @@
Map<BranchNameKey, ObjectId> revisionMap = new HashMap<>();
revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision());
- Queue<CodeOwnerConfigReference> codeOwnerConfigsToImport = new ArrayDeque<>();
- codeOwnerConfigsToImport.addAll(importingCodeOwnerConfig.imports());
- codeOwnerConfigsToImport.addAll(
- resolvedCodeOwnerConfigBuilder.codeOwnerSets().stream()
- .flatMap(codeOwnerSet -> codeOwnerSet.imports().stream())
- .collect(toImmutableSet()));
-
+ Queue<CodeOwnerConfigImport> codeOwnerConfigsToImport = new ArrayDeque<>();
+ codeOwnerConfigsToImport.addAll(codeOwnerConfigImports);
+ if (!codeOwnerConfigsToImport.isEmpty()) {
+ messageBuilder.append(
+ String.format(
+ "Code owner config %s imports:\n",
+ keyOfImportingCodeOwnerConfig.format(codeOwners)));
+ }
while (!codeOwnerConfigsToImport.isEmpty()) {
- CodeOwnerConfigReference codeOwnerConfigReference = codeOwnerConfigsToImport.poll();
+ CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll();
+ messageBuilder.append(codeOwnerConfigImport.format());
+
+ CodeOwnerConfigReference codeOwnerConfigReference =
+ codeOwnerConfigImport.referenceToImportedCodeOwnerConfig();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
createKeyForImportedCodeOwnerConfig(
- importingCodeOwnerConfig.key(), codeOwnerConfigReference);
- try (TraceTimer traceTimer2 =
- TraceContext.newTimer(
- "Resolve code owner config import",
- Metadata.builder()
- .projectName(keyOfImportedCodeOwnerConfig.project().get())
- .branchName(keyOfImportedCodeOwnerConfig.ref())
- .filePath(
- keyOfImportedCodeOwnerConfig
- .filePath(codeOwnerConfigReference.fileName())
- .toString())
- .build())) {
+ keyOfImportingCodeOwnerConfig, codeOwnerConfigReference);
+
+ try (Timer0.Context ctx2 = codeOwnerMetrics.resolveCodeOwnerConfigImport.start()) {
+ logger.atFine().log(
+ "resolve import of code owner config %s", keyOfImportedCodeOwnerConfig);
+
Optional<ProjectState> projectState =
projectCache.get(keyOfImportedCodeOwnerConfig.project());
if (!projectState.isPresent()) {
- hasUnresolvedImports = true;
- logger.atWarning().log(
- "cannot resolve code owner config %s that is imported by code owner config %s:"
- + " project %s not found",
- keyOfImportedCodeOwnerConfig,
- importingCodeOwnerConfig.key(),
- keyOfImportedCodeOwnerConfig.project().get());
+ unresolvedImports.add(
+ UnresolvedImport.create(
+ codeOwnerConfigImport.importingCodeOwnerConfig(),
+ keyOfImportedCodeOwnerConfig,
+ codeOwnerConfigReference,
+ String.format(
+ "project %s not found", keyOfImportedCodeOwnerConfig.project().get())));
+ messageBuilder.append(
+ codeOwnerConfigImport.formatSubItem("failed to resolve (project not found)\n"));
continue;
}
if (!projectState.get().statePermitsRead()) {
- hasUnresolvedImports = true;
- logger.atWarning().log(
- "cannot resolve code owner config %s that is imported by code owner config %s:"
- + " state of project %s doesn't permit read",
- keyOfImportedCodeOwnerConfig,
- importingCodeOwnerConfig.key(),
- keyOfImportedCodeOwnerConfig.project().get());
+ unresolvedImports.add(
+ UnresolvedImport.create(
+ codeOwnerConfigImport.importingCodeOwnerConfig(),
+ keyOfImportedCodeOwnerConfig,
+ codeOwnerConfigReference,
+ String.format(
+ "state of project %s doesn't permit read",
+ keyOfImportedCodeOwnerConfig.project().get())));
+ messageBuilder.append(
+ codeOwnerConfigImport.formatSubItem(
+ "failed to resolve (project state doesn't allow read)\n"));
continue;
}
@@ -318,17 +420,21 @@
Optional<CodeOwnerConfig> mayBeImportedCodeOwnerConfig =
revision.isPresent()
- ? codeOwners.get(keyOfImportedCodeOwnerConfig, revision.get())
- : codeOwners.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
+ ? codeOwnerConfigLoader.get(keyOfImportedCodeOwnerConfig, revision.get())
+ : codeOwnerConfigLoader.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
if (!mayBeImportedCodeOwnerConfig.isPresent()) {
- hasUnresolvedImports = true;
- logger.atWarning().log(
- "cannot resolve code owner config %s that is imported by code owner config %s"
- + " (revision = %s)",
- keyOfImportedCodeOwnerConfig,
- importingCodeOwnerConfig.key(),
- revision.map(ObjectId::name).orElse("current"));
+ unresolvedImports.add(
+ UnresolvedImport.create(
+ codeOwnerConfigImport.importingCodeOwnerConfig(),
+ keyOfImportedCodeOwnerConfig,
+ codeOwnerConfigReference,
+ String.format(
+ "code owner config does not exist (revision = %s)",
+ revision.map(ObjectId::name).orElse("current"))));
+ messageBuilder.append(
+ codeOwnerConfigImport.formatSubItem(
+ "failed to resolve (code owner config not found)\n"));
continue;
}
@@ -351,21 +457,32 @@
.forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
}
+ ImmutableSet<CodeOwnerSet> matchingPerFileCodeOwnerSets =
+ getMatchingPerFileCodeOwnerSets(importedCodeOwnerConfig).collect(toImmutableSet());
if (importMode.importPerFileCodeOwnerSets()) {
logger.atFine().log("import per-file code owners");
- getMatchingPerFileCodeOwnerSets(importedCodeOwnerConfig)
- .forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
+ matchingPerFileCodeOwnerSets.forEach(
+ codeOwnerSet -> {
+ messageBuilder.append(
+ codeOwnerConfigImport.formatSubItem(
+ String.format(
+ "per-file code owner set with path expressions %s matches\n",
+ codeOwnerSet.pathExpressions())));
+ resolvedCodeOwnerConfigBuilder.addCodeOwnerSet(codeOwnerSet);
+ });
}
if (importMode.resolveImportsOfImport()
&& seenCodeOwnerConfigs.add(keyOfImportedCodeOwnerConfig)) {
logger.atFine().log("resolve imports of imported code owner config");
- Set<CodeOwnerConfigReference> transitiveImports = new HashSet<>();
- transitiveImports.addAll(importedCodeOwnerConfig.imports());
+ Set<CodeOwnerConfigImport> transitiveImports = new HashSet<>();
transitiveImports.addAll(
- importedCodeOwnerConfig.codeOwnerSets().stream()
- .flatMap(codeOwnerSet -> codeOwnerSet.imports().stream())
- .collect(toImmutableSet()));
+ getGlobalImports(codeOwnerConfigImport.importLevel() + 1, importedCodeOwnerConfig));
+ transitiveImports.addAll(
+ getPerFileImports(
+ codeOwnerConfigImport.importLevel() + 1,
+ importedCodeOwnerConfig.key(),
+ matchingPerFileCodeOwnerSets));
if (importMode == CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY) {
// If only global code owners should be imported, transitive imports should also only
@@ -377,10 +494,14 @@
transitiveImports =
transitiveImports.stream()
.map(
- codeOwnerCfgRef ->
- CodeOwnerConfigReference.copyWithNewImportMode(
- codeOwnerCfgRef,
- CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY))
+ codeOwnerCfgImport ->
+ CodeOwnerConfigImport.create(
+ codeOwnerCfgImport.importLevel(),
+ codeOwnerCfgImport.importingCodeOwnerConfig(),
+ CodeOwnerConfigReference.copyWithNewImportMode(
+ codeOwnerCfgImport.referenceToImportedCodeOwnerConfig(),
+ CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY),
+ codeOwnerCfgImport.codeOwnerSet()))
.collect(toSet());
}
@@ -390,7 +511,42 @@
}
}
}
- return !hasUnresolvedImports;
+ String message = messageBuilder.toString();
+ if (message.endsWith("\n")) {
+ message = message.substring(0, message.length() - 1);
+ }
+ return OptionalResultWithMessages.create(
+ unresolvedImports.build(),
+ !message.isEmpty() ? ImmutableList.of(message) : ImmutableList.of());
+ }
+
+ private ImmutableSet<CodeOwnerConfigImport> getGlobalImports(
+ int importLevel, CodeOwnerConfig codeOwnerConfig) {
+ return codeOwnerConfig.imports().stream()
+ .map(
+ codeOwnerConfigReference ->
+ CodeOwnerConfigImport.create(
+ importLevel, codeOwnerConfig.key(), codeOwnerConfigReference))
+ .collect(toImmutableSet());
+ }
+
+ private ImmutableSet<CodeOwnerConfigImport> getPerFileImports(
+ int importLevel,
+ CodeOwnerConfig.Key importingCodeOwnerConfig,
+ Set<CodeOwnerSet> codeOwnerSets) {
+ ImmutableSet.Builder<CodeOwnerConfigImport> codeOwnerConfigImports = ImmutableSet.builder();
+ for (CodeOwnerSet codeOwnerSet : codeOwnerSets) {
+ codeOwnerSet.imports().stream()
+ .forEach(
+ codeOwnerConfigReference ->
+ codeOwnerConfigImports.add(
+ CodeOwnerConfigImport.create(
+ importLevel,
+ importingCodeOwnerConfig,
+ codeOwnerConfigReference,
+ codeOwnerSet)));
+ }
+ return codeOwnerConfigImports.build();
}
public static CodeOwnerConfig.Key createKeyForImportedCodeOwnerConfig(
@@ -465,4 +621,95 @@
return codeOwnerSet.pathExpressions().stream()
.anyMatch(pathExpression -> matcher.matches(pathExpression, relativePath));
}
+
+ @AutoValue
+ abstract static class CodeOwnerConfigImport {
+ /**
+ * The import level.
+ *
+ * <p>{@code 0} for direct import, {@code 1} if imported by a directly imported file, {@code 2},
+ * if imported by a file that was imported by an directly imported file, etc.
+ */
+ public abstract int importLevel();
+
+ /** The key of the code owner config that contains the import. */
+ public abstract CodeOwnerConfig.Key importingCodeOwnerConfig();
+
+ /** The reference to the imported code owner config */
+ public abstract CodeOwnerConfigReference referenceToImportedCodeOwnerConfig();
+
+ /** The code owner set that specified the import, empty if it is a global import. */
+ public abstract Optional<CodeOwnerSet> codeOwnerSet();
+
+ public String format() {
+ if (codeOwnerSet().isPresent()) {
+ return getPrefix()
+ + String.format(
+ "* %s (per-file import, import mode = %s, path expressions = %s)\n",
+ referenceToImportedCodeOwnerConfig().format(),
+ referenceToImportedCodeOwnerConfig().importMode(),
+ codeOwnerSet().get().pathExpressions());
+ }
+ return getPrefix()
+ + String.format(
+ "* %s (global import, import mode = %s)\n",
+ referenceToImportedCodeOwnerConfig().format(),
+ referenceToImportedCodeOwnerConfig().importMode());
+ }
+
+ public String formatSubItem(String message) {
+ return getPrefixForSubItem() + message;
+ }
+
+ private String getPrefix() {
+ return getPrefix(importLevel());
+ }
+
+ private String getPrefixForSubItem() {
+ return getPrefix(importLevel() + 1) + "* ";
+ }
+
+ private String getPrefix(int levels) {
+ // 2 spaces per level
+ //
+ // String.format("%<num>s", "") creates a string with <num> spaces:
+ // * '%' introduces a format sequence
+ // * <num> means that the resulting string should be <num> characters long
+ // * 's' is the character string format code, and ends the format sequence
+ // * the second parameter for String.format, is the string that should be
+ // prefixed with as many spaces as are needed to make the string <num>
+ // characters long
+ // * <num> must be > 0, hence we special case the handling of levels == 0
+ return levels > 0 ? String.format("%" + (levels * 2) + "s", "") : "";
+ }
+
+ public static CodeOwnerConfigImport create(
+ int importLevel,
+ CodeOwnerConfig.Key importingCodeOwnerConfig,
+ CodeOwnerConfigReference codeOwnerConfigReference) {
+ return create(
+ importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, Optional.empty());
+ }
+
+ public static CodeOwnerConfigImport create(
+ int importLevel,
+ CodeOwnerConfig.Key importingCodeOwnerConfig,
+ CodeOwnerConfigReference codeOwnerConfigReference,
+ CodeOwnerSet codeOwnerSet) {
+ return create(
+ importLevel,
+ importingCodeOwnerConfig,
+ codeOwnerConfigReference,
+ Optional.of(codeOwnerSet));
+ }
+
+ public static CodeOwnerConfigImport create(
+ int importLevel,
+ CodeOwnerConfig.Key importingCodeOwnerConfig,
+ CodeOwnerConfigReference codeOwnerConfigReference,
+ Optional<CodeOwnerSet> codeOwnerSet) {
+ return new AutoValue_PathCodeOwners_CodeOwnerConfigImport(
+ importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, codeOwnerSet);
+ }
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
index a463aef..bc45ded 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -18,9 +18,11 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import java.nio.file.Path;
+import java.util.List;
/** The result of resolving path code owners via {@link PathCodeOwners}. */
@AutoValue
@@ -33,8 +35,13 @@
/** Gets the resolved code owner config. */
abstract CodeOwnerConfig codeOwnerConfig();
+ /** Gets a list of unresolved imports. */
+ public abstract ImmutableList<UnresolvedImport> unresolvedImports();
+
/** Whether there are unresolved imports. */
- public abstract boolean hasUnresolvedImports();
+ public boolean hasUnresolvedImports() {
+ return !unresolvedImports().isEmpty();
+ }
/**
* Gets the code owners from the code owner config that apply to the path.
@@ -64,17 +71,18 @@
}
@Override
- public String toString() {
+ public final String toString() {
return MoreObjects.toStringHelper(this)
.add("path", path())
.add("codeOwnerConfig", codeOwnerConfig())
- .add("hasUnresolvedImports", hasUnresolvedImports())
+ .add("unresolvedImports", unresolvedImports())
.toString();
}
- /** Creates a {@link CodeOwnerResolverResult} instance. */
+ /** Creates a {@link PathCodeOwnersResult} instance. */
public static PathCodeOwnersResult create(
- Path path, CodeOwnerConfig codeOwnerConfig, boolean hasUnresolvedImports) {
- return new AutoValue_PathCodeOwnersResult(path, codeOwnerConfig, hasUnresolvedImports);
+ Path path, CodeOwnerConfig codeOwnerConfig, List<UnresolvedImport> unresolvedImports) {
+ return new AutoValue_PathCodeOwnersResult(
+ path, codeOwnerConfig, ImmutableList.copyOf(unresolvedImports));
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java
new file mode 100644
index 0000000..2d69ca2
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersVisitor.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend;
+
+/** Callback interface to visit {@link PathCodeOwners}. */
+@FunctionalInterface
+public interface PathCodeOwnersVisitor {
+ /**
+ * Callback for {@link PathCodeOwners}.
+ *
+ * @param pathCodeOwners the path code owners
+ * @return whether further path code owner configs should be visited
+ */
+ boolean visit(PathCodeOwners pathCodeOwners);
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
new file mode 100644
index 0000000..e8a1ab7
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
@@ -0,0 +1,183 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Class to load and cache {@link CodeOwnerConfig}s within a request.
+ *
+ * <p>This cache is transient, which means the code owner configs stay cached only for the lifetime
+ * of the {@code TransientCodeOwnerConfigCache} instance.
+ *
+ * <p><strong>Note</strong>: This class is not thread-safe.
+ */
+public class TransientCodeOwnerConfigCache implements CodeOwnerConfigLoader {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final GitRepositoryManager repoManager;
+ private final CodeOwners codeOwners;
+ private final Optional<Integer> maxCacheSize;
+ private final Counters counters;
+ private final HashMap<CacheKey, Optional<CodeOwnerConfig>> cache = new HashMap<>();
+
+ @Inject
+ TransientCodeOwnerConfigCache(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ GitRepositoryManager repoManager,
+ CodeOwners codeOwners,
+ CodeOwnerMetrics codeOwnerMetrics) {
+ this.repoManager = repoManager;
+ this.codeOwners = codeOwners;
+ this.maxCacheSize =
+ codeOwnersPluginConfiguration.getGlobalConfig().getMaxCodeOwnerConfigCacheSize();
+ this.counters = new Counters(codeOwnerMetrics);
+ }
+
+ /**
+ * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise
+ * loads and returns the code owner config.
+ */
+ @Override
+ public Optional<CodeOwnerConfig> get(
+ CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
+ CacheKey cacheKey = CacheKey.create(codeOwnerConfigKey, revision);
+ Optional<CodeOwnerConfig> cachedCodeOwnerConfig = cache.get(cacheKey);
+ if (cachedCodeOwnerConfig != null) {
+ counters.incrementCacheReads();
+ return cachedCodeOwnerConfig;
+ }
+ return loadAndCache(cacheKey);
+ }
+
+ /**
+ * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise
+ * loads and returns the code owner config.
+ */
+ @Override
+ public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey) {
+ return get(codeOwnerConfigKey, /* revision= */ null);
+ }
+
+ /** Load a code owner config and puts it into the cache. */
+ private Optional<CodeOwnerConfig> loadAndCache(CacheKey cacheKey) {
+ counters.incrementBackendReads();
+ Optional<CodeOwnerConfig> codeOwnerConfig;
+ if (cacheKey.revision().isPresent()) {
+ codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), cacheKey.revision().get());
+ } else {
+ Optional<ObjectId> revision = getRevision(cacheKey.codeOwnerConfigKey().branchNameKey());
+ if (revision.isPresent()) {
+ codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), revision.get());
+ } else {
+ // branch does not exists, hence the code owner config also doesn't exist
+ codeOwnerConfig = Optional.empty();
+ }
+ }
+ if (!maxCacheSize.isPresent() || cache.size() < maxCacheSize.get()) {
+ cache.put(cacheKey, codeOwnerConfig);
+ } else if (maxCacheSize.isPresent()) {
+ logger.atWarning().atMostEvery(1, TimeUnit.DAYS).log(
+ "exceeded limit of %s (project = %s)",
+ getClass().getSimpleName(), cacheKey.codeOwnerConfigKey().project());
+ }
+ return codeOwnerConfig;
+ }
+
+ /**
+ * Gets the revision for the given branch.
+ *
+ * <p>Returns {@link Optional#empty()} if the branch doesn't exist.
+ */
+ private Optional<ObjectId> getRevision(BranchNameKey branchNameKey) {
+ try (Repository repo = repoManager.openRepository(branchNameKey.project())) {
+ Ref ref = repo.exactRef(branchNameKey.branch());
+ if (ref == null) {
+ // branch does not exist
+ return Optional.empty();
+ }
+ return Optional.of(ref.getObjectId());
+ } catch (IOException e) {
+ throw new CodeOwnersInternalServerErrorException(
+ String.format(
+ "failed to get revision of branch %s in project %s",
+ branchNameKey.shortName(), branchNameKey.project()),
+ e);
+ }
+ }
+
+ @AutoValue
+ abstract static class CacheKey {
+ /** The key of the code owner config. */
+ public abstract CodeOwnerConfig.Key codeOwnerConfigKey();
+
+ /** The revision from which the code owner config was loaded. */
+ public abstract Optional<ObjectId> revision();
+
+ public static CacheKey create(
+ CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
+ return new AutoValue_TransientCodeOwnerConfigCache_CacheKey(
+ codeOwnerConfigKey, Optional.ofNullable(revision));
+ }
+ }
+
+ public Counters getCounters() {
+ return counters;
+ }
+
+ public static class Counters {
+ private final CodeOwnerMetrics codeOwnerMetrics;
+
+ private int cacheReadCount;
+ private int backendReadCount;
+
+ private Counters(CodeOwnerMetrics codeOwnerMetrics) {
+ this.codeOwnerMetrics = codeOwnerMetrics;
+ }
+
+ private void incrementCacheReads() {
+ codeOwnerMetrics.countCodeOwnerConfigCacheReads.increment();
+ cacheReadCount++;
+ }
+
+ private void incrementBackendReads() {
+ // we do not increase the countCodeOwnerConfigReads metric here, since this is already done in
+ // CodeOwners
+ backendReadCount++;
+ }
+
+ public int getBackendReadCount() {
+ return backendReadCount;
+ }
+
+ public int getCacheReadCount() {
+ return cacheReadCount;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java
new file mode 100644
index 0000000..bea1e08
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
+
+/** Information about an unresolved import. */
+@AutoValue
+public abstract class UnresolvedImport {
+ /** Key of the importing code owner config. */
+ public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig();
+
+ /** Key of the imported code owner config. */
+ public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig();
+
+ /** The code owner config reference that was attempted to be resolved. */
+ public abstract CodeOwnerConfigReference codeOwnerConfigReference();
+
+ /** Message explaining why the code owner config reference couldn't be resolved. */
+ public abstract String message();
+
+ @Override
+ public final String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig())
+ .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig())
+ .add("codeOwnerConfigReference", codeOwnerConfigReference())
+ .add("message", message())
+ .toString();
+ }
+
+ /** Creates a {@link UnresolvedImport} instance. */
+ static UnresolvedImport create(
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+ CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig,
+ CodeOwnerConfigReference codeOwnerConfigReference,
+ String message) {
+ return new AutoValue_UnresolvedImport(
+ keyOfImportingCodeOwnerConfig,
+ keyOfImportedCodeOwnerConfig,
+ codeOwnerConfigReference,
+ message);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
new file mode 100644
index 0000000..52958d9
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend;
+
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import java.nio.file.Path;
+
+/** Class to format an {@link UnresolvedImport} as a user-readable string. */
+public class UnresolvedImportFormatter {
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final ProjectCache projectCache;
+ private final BackendConfig backendConfig;
+
+ @Inject
+ UnresolvedImportFormatter(
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ ProjectCache projectCache,
+ BackendConfig backendConfig) {
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.projectCache = projectCache;
+ this.backendConfig = backendConfig;
+ }
+
+ /** Returns a user-readable string representation of the given unresolved import. */
+ public String format(UnresolvedImport unresolvedImport) {
+ return String.format(
+ "The import of %s:%s:%s in %s:%s:%s cannot be resolved: %s",
+ unresolvedImport.keyOfImportedCodeOwnerConfig().project(),
+ unresolvedImport.keyOfImportedCodeOwnerConfig().shortBranchName(),
+ getFilePath(unresolvedImport.keyOfImportedCodeOwnerConfig()),
+ unresolvedImport.keyOfImportingCodeOwnerConfig().project(),
+ unresolvedImport.keyOfImportingCodeOwnerConfig().shortBranchName(),
+ getFilePath(unresolvedImport.keyOfImportingCodeOwnerConfig()),
+ unresolvedImport.message());
+ }
+
+ private Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
+ return getBackend(codeOwnerConfigKey).getFilePath(codeOwnerConfigKey);
+ }
+
+ /**
+ * Returns the code owner backend for the given code owner config key.
+ *
+ * <p>If the project of the code owner config key doesn't exist, the default code owner backend is
+ * returned.
+ */
+ private CodeOwnerBackend getBackend(CodeOwnerConfig.Key codeOwnerConfigKey) {
+ if (projectCache.get(codeOwnerConfigKey.project()).isPresent()) {
+ return codeOwnersPluginConfiguration
+ .getProjectConfig(codeOwnerConfigKey.project())
+ .getBackend(codeOwnerConfigKey.branchNameKey().branch());
+ }
+ return backendConfig.getDefaultBackend();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/AbstractRequiredApprovalConfig.java
similarity index 67%
rename from java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/AbstractRequiredApprovalConfig.java
index 260d9d9..59cce10 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/AbstractRequiredApprovalConfig.java
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
-import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
@@ -22,7 +22,6 @@
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.project.ProjectLevelConfig;
import com.google.gerrit.server.project.ProjectState;
import org.eclipse.jgit.lib.Config;
@@ -54,6 +53,12 @@
* Reads the required approvals for the specified project from the given plugin config with
* fallback to {@code gerrit.config}.
*
+ * <p>Inherited required approvals are included into the returned list at the first position (see
+ * {@link Config#getStringList(String, String, String)}).
+ *
+ * <p>The returned list contains duplicates if the exact same require approval is set for
+ * different projects in the line of parent projects.
+ *
* @param projectState state of the project for which the required approvals should be read
* @param pluginConfig the plugin config from which the required approvals should be read
* @return the required approvals, an empty list if none was configured
@@ -63,47 +68,33 @@
requireNonNull(pluginConfig, "pluginConfig");
ImmutableList.Builder<RequiredApproval> requiredApprovalList = ImmutableList.builder();
- String[] requiredApprovals =
- pluginConfig.getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey());
- if (requiredApprovals.length > 0) {
- for (String requiredApproval : requiredApprovals) {
- try {
- requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
- } catch (IllegalStateException | IllegalArgumentException e) {
- throw new InvalidPluginConfigurationException(
- pluginName,
- String.format(
- "Required approval '%s' that is configured in %s.config"
- + " (parameter %s.%s) is invalid: %s",
- requiredApproval,
- pluginName,
- SECTION_CODE_OWNERS,
- getConfigKey(),
- e.getMessage()));
- }
+ for (String requiredApproval :
+ pluginConfigFactory.getFromGerritConfig(pluginName).getStringList(getConfigKey())) {
+ try {
+ requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ throw new InvalidPluginConfigurationException(
+ pluginName,
+ String.format(
+ "Required approval '%s' that is configured in gerrit.config"
+ + " (parameter plugin.%s.%s) is invalid: %s",
+ requiredApproval, pluginName, getConfigKey(), e.getMessage()));
}
- return requiredApprovalList.build();
}
-
- requiredApprovals =
- pluginConfigFactory.getFromGerritConfig(pluginName).getStringList(getConfigKey());
- if (requiredApprovals.length > 0) {
- for (String requiredApproval : requiredApprovals) {
- try {
- requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
- } catch (IllegalStateException | IllegalArgumentException e) {
- throw new InvalidPluginConfigurationException(
- pluginName,
- String.format(
- "Required approval '%s' that is configured in gerrit.config"
- + " (parameter plugin.%s.%s) is invalid: %s",
- requiredApproval, pluginName, getConfigKey(), e.getMessage()));
- }
+ for (String requiredApproval :
+ pluginConfig.getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey())) {
+ try {
+ requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ throw new InvalidPluginConfigurationException(
+ pluginName,
+ String.format(
+ "Required approval '%s' that is configured in %s.config"
+ + " (parameter %s.%s) is invalid: %s",
+ requiredApproval, pluginName, SECTION_CODE_OWNERS, getConfigKey(), e.getMessage()));
}
- return requiredApprovalList.build();
}
-
- return ImmutableList.of();
+ return requiredApprovalList.build();
}
/**
@@ -115,15 +106,14 @@
* validation errors
*/
ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
- ProjectState projectState, String fileName, ProjectLevelConfig.Bare projectLevelConfig) {
+ ProjectState projectState, String fileName, Config projectLevelConfig) {
requireNonNull(projectState, "projectState");
requireNonNull(fileName, "fileName");
requireNonNull(projectLevelConfig, "projectLevelConfig");
String[] requiredApprovals =
- projectLevelConfig
- .getConfig()
- .getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey());
+ projectLevelConfig.getStringList(
+ SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey());
ImmutableList.Builder<CommitValidationMessage> validationMessages = ImmutableList.builder();
for (String requiredApproval : requiredApprovals) {
try {
diff --git a/java/com/google/gerrit/plugins/codeowners/config/BackendConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
similarity index 93%
rename from java/com/google/gerrit/plugins/codeowners/config/BackendConfig.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
index a820739..cbeb83d 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/BackendConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/BackendConfig.java
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
-import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
@@ -29,7 +29,6 @@
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.project.ProjectLevelConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
@@ -88,14 +87,13 @@
* validation errors
*/
ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
- String fileName, ProjectLevelConfig.Bare projectLevelConfig) {
+ String fileName, Config projectLevelConfig) {
requireNonNull(fileName, "fileName");
requireNonNull(projectLevelConfig, "projectLevelConfig");
List<CommitValidationMessage> validationMessages = new ArrayList<>();
- String backendName =
- projectLevelConfig.getConfig().getString(SECTION_CODE_OWNERS, null, KEY_BACKEND);
+ String backendName = projectLevelConfig.getString(SECTION_CODE_OWNERS, null, KEY_BACKEND);
if (backendName != null) {
if (!lookupBackend(backendName).isPresent()) {
validationMessages.add(
@@ -107,9 +105,8 @@
}
}
- for (String subsection : projectLevelConfig.getConfig().getSubsections(SECTION_CODE_OWNERS)) {
- backendName =
- projectLevelConfig.getConfig().getString(SECTION_CODE_OWNERS, subsection, KEY_BACKEND);
+ for (String subsection : projectLevelConfig.getSubsections(SECTION_CODE_OWNERS)) {
+ backendName = projectLevelConfig.getString(SECTION_CODE_OWNERS, subsection, KEY_BACKEND);
if (backendName != null) {
if (!lookupBackend(backendName).isPresent()) {
validationMessages.add(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfig.java
new file mode 100644
index 0000000..7bd0971
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfig.java
@@ -0,0 +1,179 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.util.Arrays;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Class to read the {@code code-owners.config} file in the {@code refs/meta/config} branch of a
+ * project with taking inherited config parameters from parent projects into account.
+ *
+ * <p>For inheriting config parameters from parent projects we rely on base config support in JGit's
+ * {@link Config} class.
+ *
+ * <p>For single-value parameters (string, boolean, enum, int, long) this means:
+ *
+ * <ul>
+ * <li>If a parameter is not set, it is read from the parent project.
+ * <li>If a parameter is set, it overrides any value that is set in the parent project.
+ * </ul>
+ *
+ * <p>For multi-value parameters (string list) this means:
+ *
+ * <ul>
+ * <li>If a parameter is not set, the values are read from the parent projects.
+ * <li>If any value for the parameter is set, it is added to the inherited value list (the
+ * inherited value list is extended).
+ * <li>If the exact same value is set for different projects in the line of parent projects this
+ * value appears multiple times in the value list (list may contain duplicates).
+ * <li>The inherited value list cannot be overridden (this means the inherited values cannot be
+ * unset/overridden).
+ * </ul>
+ *
+ * <p>Please note that this inheritance behavior is different from what {@link
+ * com.google.gerrit.server.config.PluginConfigFactory} does. {@code PluginConfigFactory} has 2
+ * modes:
+ *
+ * <ul>
+ * <li>merge = false: Inherited list values are overridden.
+ * <li>merge = true: Inherited list values are extended the same way as in this class, but for
+ * single-value parameters the inherited value from the parent project takes precedence.
+ * </ul>
+ *
+ * <p>For the {@code code-owners.config} we want that:
+ *
+ * <ul>
+ * <li>Single-value parameters override inherited settings so that they can be controlled per
+ * project (e.g. whether validation of OWNERS files should be done).
+ * <li>Multi-value parameters cannot be overridden, but only extended (e.g. this allows to enforce
+ * global code owners or exempted users globally).
+ * </ul>
+ */
+public class CodeOwnersPluginConfig {
+ public interface Factory {
+ CodeOwnersPluginConfig create(Project.NameKey projectName);
+ }
+
+ private static final String CONFIG_EXTENSION = ".config";
+
+ private final String pluginName;
+ private final ProjectCache projectCache;
+ private final Project.NameKey projectName;
+ private Config config;
+
+ @Inject
+ CodeOwnersPluginConfig(
+ @PluginName String pluginName,
+ ProjectCache projectCache,
+ @Assisted Project.NameKey projectName) {
+ this.pluginName = pluginName;
+ this.projectCache = projectCache;
+ this.projectName = projectName;
+ }
+
+ public Config get() {
+ if (config == null) {
+ config = load();
+ }
+ return config;
+ }
+
+ /**
+ * Load the {@code code-owners.config} file of the project and sets all parent {@code
+ * code-owners.config}s as base configs.
+ *
+ * @throws IllegalStateException if the project doesn't exist
+ */
+ private Config load() {
+ try {
+ ProjectState projectState =
+ projectCache.get(projectName).orElseThrow(noSuchProject(projectName));
+ String fileName = pluginName + CONFIG_EXTENSION;
+
+ Config mergedConfig = null;
+
+ // Iterate in-order from All-Projects through the project hierarchy to this project. For each
+ // project read the code-owners.config and set the parent code-owners.config as base config.
+ for (ProjectState p : projectState.treeInOrder()) {
+ Config currentConfig = p.getConfig(fileName).get();
+ if (mergedConfig == null) {
+ mergedConfig = currentConfig;
+ } else {
+ mergedConfig = createConfigWithBase(currentConfig, mergedConfig);
+ }
+ }
+ return mergedConfig;
+ } catch (NoSuchProjectException e) {
+ throw new IllegalStateException(
+ String.format(
+ "cannot get %s plugin config for non-existing project %s", pluginName, projectName),
+ e);
+ }
+ }
+
+ /**
+ * Creates a copy of the given {@code config} with the given {@code baseConfig} as base config.
+ *
+ * <p>JGit doesn't allow to set a base config on an existing {@link Config}. Hence create a new
+ * (empty) config with the base config and then copy over all sections and subsection.
+ *
+ * @param config config that should be copied
+ * @param baseConfig config that should be set as base config
+ */
+ private Config createConfigWithBase(Config config, Config baseConfig) {
+ // Create a new Config with the parent Config as base config.
+ Config configWithBase = new Config(baseConfig);
+
+ // Copy all sections and subsections from the given config.
+ for (String section : config.getSections()) {
+ for (String name : config.getNames(section)) {
+ configWithBase.setStringList(
+ section,
+ /* subsection = */ null,
+ name,
+ Arrays.asList(config.getStringList(section, /* subsection = */ null, name)));
+ }
+
+ for (String subsection : config.getSubsections(section)) {
+ Set<String> allNames = config.getNames(section, subsection);
+ if (allNames.isEmpty()) {
+ // Set empty subsection.
+ configWithBase.setString(section, subsection, /* name= */ null, /* value= */ null);
+ } else {
+ for (String name : allNames) {
+ configWithBase.setStringList(
+ section,
+ subsection,
+ name,
+ Arrays.asList(config.getStringList(section, subsection, name)));
+ }
+ }
+ }
+ }
+
+ return configWithBase;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
similarity index 70%
rename from java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
index 8f20fc1..c6181b5 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigValidator.java
@@ -12,22 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.backend.ChangedFiles;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectLevelConfig;
import com.google.gerrit.server.project.ProjectState;
@@ -37,17 +35,15 @@
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
/** Validates modifications to the {@code code-owners.config} file in {@code refs/meta/config}. */
@Singleton
-class CodeOwnersPluginConfigValidator implements CommitValidationListener {
+public class CodeOwnersPluginConfigValidator implements CommitValidationListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final String pluginName;
- private final GitRepositoryManager repoManager;
private final ProjectConfig.Factory projectConfigFactory;
private final ProjectState.Factory projectStateFactory;
private final ChangedFiles changedFiles;
@@ -60,7 +56,6 @@
@Inject
CodeOwnersPluginConfigValidator(
@PluginName String pluginName,
- GitRepositoryManager repoManager,
ProjectConfig.Factory projectConfigFactory,
ProjectState.Factory projectStateFactory,
ChangedFiles changedFiles,
@@ -70,7 +65,6 @@
RequiredApprovalConfig requiredApprovalConfig,
OverrideApprovalConfig overrideApprovalConfig) {
this.pluginName = pluginName;
- this.repoManager = repoManager;
this.projectConfigFactory = projectConfigFactory;
this.projectStateFactory = projectStateFactory;
this.changedFiles = changedFiles;
@@ -85,70 +79,79 @@
public ImmutableList<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
String fileName = pluginName + ".config";
- Project.NameKey project = receiveEvent.project.getNameKey();
try {
if (!receiveEvent.refName.equals(RefNames.REFS_CONFIG)
- || !isFileChanged(project, receiveEvent.commit, fileName)) {
+ || !isFileChanged(receiveEvent, fileName)) {
// the code-owners.config file in refs/meta/config was not modified, hence we do not need to
// validate it
return ImmutableList.of();
}
- ProjectState projectState = getProjectState(project, receiveEvent.commit);
- ProjectLevelConfig.Bare cfg = loadConfig(project, fileName, receiveEvent.commit);
- validateConfig(projectState, fileName, cfg);
+ ProjectState projectState = getProjectState(receiveEvent);
+ ProjectLevelConfig.Bare cfg = loadConfig(receiveEvent, fileName);
+ ImmutableList<CommitValidationMessage> validationMessages =
+ validateConfig(projectState, fileName, cfg.getConfig());
+ if (!validationMessages.isEmpty()) {
+ throw new CommitValidationException(
+ exceptionMessage(fileName, cfg.getRevision()), validationMessages);
+ }
return ImmutableList.of();
- } catch (IOException | ConfigInvalidException | PatchListNotAvailableException e) {
+ } catch (IOException | ConfigInvalidException e) {
String errorMessage =
String.format(
"failed to validate file %s for revision %s in ref %s of project %s",
- fileName, receiveEvent.commit.getName(), RefNames.REFS_CONFIG, project);
- logger.atSevere().log(errorMessage);
+ fileName,
+ receiveEvent.commit.getName(),
+ RefNames.REFS_CONFIG,
+ receiveEvent.project.getNameKey());
+ logger.atSevere().withCause(e).log(errorMessage);
throw new CommitValidationException(errorMessage, e);
}
}
- private ProjectState getProjectState(Project.NameKey projectName, RevCommit commit)
+ private ProjectState getProjectState(CommitReceivedEvent receiveEvent)
throws IOException, ConfigInvalidException {
- try (Repository repo = repoManager.openRepository(projectName)) {
- ProjectConfig projectConfig = projectConfigFactory.create(projectName);
- projectConfig.load(repo, commit);
- return projectStateFactory.create(projectConfig.getCacheable());
- }
+ ProjectConfig projectConfig = projectConfigFactory.create(receiveEvent.project.getNameKey());
+ projectConfig.load(receiveEvent.revWalk, receiveEvent.commit);
+ return projectStateFactory.create(projectConfig.getCacheable());
}
/**
* Whether the given file was changed in the given revision.
*
- * @param project the name of the project
- * @param revision the revision
+ * @param receiveEvent the receive event
* @param fileName the name of the file
*/
- private boolean isFileChanged(Project.NameKey project, ObjectId revision, String fileName)
- throws IOException, PatchListNotAvailableException {
- return changedFiles.compute(project, revision).stream()
+ private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
+ throws IOException {
+ return changedFiles
+ .compute(
+ receiveEvent.project.getNameKey(),
+ receiveEvent.repoConfig,
+ receiveEvent.revWalk,
+ receiveEvent.commit,
+ MergeCommitStrategy.ALL_CHANGED_FILES)
+ .stream()
.anyMatch(changedFile -> changedFile.hasNewPath(JgitPath.of(fileName).getAsAbsolutePath()));
}
/**
* Loads the configuration from the file and revision.
*
- * @param project the project name
+ * @param receiveEvent the receive event
* @param fileName the name of the config file
- * @param revision the revision from which the configuration should be loaded
* @return the loaded configuration
* @throws CommitValidationException thrown if the configuration is invalid and cannot be parsed
*/
- private ProjectLevelConfig.Bare loadConfig(
- Project.NameKey project, String fileName, ObjectId revision)
+ private ProjectLevelConfig.Bare loadConfig(CommitReceivedEvent receiveEvent, String fileName)
throws CommitValidationException, IOException {
ProjectLevelConfig.Bare cfg = new ProjectLevelConfig.Bare(fileName);
- try (Repository git = repoManager.openRepository(project)) {
- cfg.load(project, git, revision);
+ try {
+ cfg.load(receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit);
} catch (ConfigInvalidException e) {
throw new CommitValidationException(
- exceptionMessage(fileName, revision),
+ exceptionMessage(fileName, receiveEvent.commit),
new CommitValidationMessage(e.getMessage(), ValidationMessage.Type.ERROR));
}
return cfg;
@@ -160,11 +163,10 @@
* @param projectState the project state
* @param fileName the name of the config file
* @param cfg the project-level code-owners configuration that should be validated
- * @throws CommitValidationException throw if there are any validation errors
+ * @return list of messages with validation issues, empty list if there are no issues
*/
- private void validateConfig(
- ProjectState projectState, String fileName, ProjectLevelConfig.Bare cfg)
- throws CommitValidationException {
+ public ImmutableList<CommitValidationMessage> validateConfig(
+ ProjectState projectState, String fileName, Config cfg) {
List<CommitValidationMessage> validationMessages = new ArrayList<>();
validationMessages.addAll(backendConfig.validateProjectLevelConfig(fileName, cfg));
validationMessages.addAll(generalConfig.validateProjectLevelConfig(fileName, cfg));
@@ -173,10 +175,7 @@
requiredApprovalConfig.validateProjectLevelConfig(projectState, fileName, cfg));
validationMessages.addAll(
overrideApprovalConfig.validateProjectLevelConfig(projectState, fileName, cfg));
- if (!validationMessages.isEmpty()) {
- throw new CommitValidationException(
- exceptionMessage(fileName, cfg.getRevision()), validationMessages);
- }
+ return ImmutableList.copyOf(validationMessages);
}
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
new file mode 100644
index 0000000..a216eb1
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend.config;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The configuration of the code-owners plugin.
+ *
+ * <p>The global configuration of the code-owners plugin is stored in the {@code gerrit.config} file
+ * in the {@code plugin.code-owners} subsection.
+ *
+ * <p>In addition there is configuration on project level that is stored in {@code
+ * code-owners.config} files that are stored in the {@code refs/meta/config} branches of the
+ * projects.
+ *
+ * <p>Parameters that are not set for a project are inherited from the parent project.
+ */
+@Singleton
+public class CodeOwnersPluginConfiguration {
+ public static final String SECTION_CODE_OWNERS = "codeOwners";
+
+ private static final String GLOBAL_CONFIG_IDENTIFIER = "GLOBAL_CONFIG";
+
+ private final CodeOwnersPluginGlobalConfigSnapshot.Factory
+ codeOwnersPluginGlobalConfigSnapshotFactory;
+ private final CodeOwnersPluginProjectConfigSnapshot.Factory
+ codeOwnersPluginProjectConfigSnapshotFactory;
+
+ @Inject
+ CodeOwnersPluginConfiguration(
+ CodeOwnersPluginGlobalConfigSnapshot.Factory codeOwnersPluginGlobalConfigSnapshotFactory,
+ CodeOwnersPluginProjectConfigSnapshot.Factory codeOwnersPluginProjectConfigSnapshotFactory) {
+ this.codeOwnersPluginGlobalConfigSnapshotFactory = codeOwnersPluginGlobalConfigSnapshotFactory;
+ this.codeOwnersPluginProjectConfigSnapshotFactory =
+ codeOwnersPluginProjectConfigSnapshotFactory;
+ }
+
+ /** Returns the global code-owner plugin configuration. */
+ public CodeOwnersPluginGlobalConfigSnapshot getGlobalConfig() {
+ return PerThreadCache.getOrCompute(
+ PerThreadCache.Key.create(
+ CodeOwnersPluginGlobalConfigSnapshot.class, GLOBAL_CONFIG_IDENTIFIER),
+ () -> codeOwnersPluginGlobalConfigSnapshotFactory.create());
+ }
+
+ /**
+ * Returns the code-owner plugin configuration for the given project.
+ *
+ * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
+ * exist the call fails with {@link IllegalStateException}.
+ */
+ public CodeOwnersPluginProjectConfigSnapshot getProjectConfig(Project.NameKey projectName) {
+ requireNonNull(projectName, "projectName");
+ return PerThreadCache.getOrCompute(
+ PerThreadCache.Key.create(CodeOwnersPluginProjectConfigSnapshot.class, projectName),
+ () -> codeOwnersPluginProjectConfigSnapshotFactory.create(projectName));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginGlobalConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginGlobalConfigSnapshot.java
new file mode 100644
index 0000000..c466b8a
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginGlobalConfigSnapshot.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import java.util.Optional;
+
+/** Snapshot of the global code-owners plugin configuration. */
+public class CodeOwnersPluginGlobalConfigSnapshot {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @VisibleForTesting
+ static final String KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS = "enableExperimentalRestEndpoints";
+
+ @VisibleForTesting static final int DEFAULT_MAX_CODE_OWNER_CONFIG_CACHE_SIZE = 10000;
+
+ private static final String KEY_MAX_CODE_OWNER_CONFIG_CACHE_SIZE = "maxCodeOwnerConfigCacheSize";
+
+ public interface Factory {
+ CodeOwnersPluginGlobalConfigSnapshot create();
+ }
+
+ private final String pluginName;
+ private final PluginConfigFactory pluginConfigFactory;
+ private final GeneralConfig generalConfig;
+
+ @Nullable private ImmutableSet<String> allowedEmailDomains;
+ @Nullable private Boolean enabledExperimentalRestEndpoints;
+ @Nullable private Optional<Integer> maxCodeOwnerConfigCacheSize;
+
+ @Inject
+ CodeOwnersPluginGlobalConfigSnapshot(
+ @PluginName String pluginName,
+ PluginConfigFactory pluginConfigFactory,
+ GeneralConfig generalConfig) {
+ this.pluginName = pluginName;
+ this.pluginConfigFactory = pluginConfigFactory;
+ this.generalConfig = generalConfig;
+ }
+
+ /**
+ * Returns the email domains that are allowed to be used for code owners.
+ *
+ * @return the email domains that are allowed to be used for code owners, an empty set if all
+ * email domains are allowed (if {@code plugin.code-owners.allowedEmailDomain} is not set or
+ * set to an empty value)
+ */
+ public ImmutableSet<String> getAllowedEmailDomains() {
+ if (allowedEmailDomains == null) {
+ allowedEmailDomains = generalConfig.getAllowedEmailDomains();
+ }
+ return allowedEmailDomains;
+ }
+
+ /**
+ * Checks whether experimental REST endpoints are enabled.
+ *
+ * @throws MethodNotAllowedException thrown if experimental REST endpoints are disabled
+ */
+ public void checkExperimentalRestEndpointsEnabled() throws MethodNotAllowedException {
+ if (!areExperimentalRestEndpointsEnabled()) {
+ throw new MethodNotAllowedException("experimental code owners REST endpoints are disabled");
+ }
+ }
+
+ /** Whether experimental REST endpoints are enabled. */
+ public boolean areExperimentalRestEndpointsEnabled() {
+ if (enabledExperimentalRestEndpoints == null) {
+ enabledExperimentalRestEndpoints = readEnabledExperimentalRestEndpoints();
+ }
+ return enabledExperimentalRestEndpoints;
+ }
+
+ private boolean readEnabledExperimentalRestEndpoints() {
+ try {
+ return pluginConfigFactory
+ .getFromGerritConfig(pluginName)
+ .getBoolean(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS, /* defaultValue= */ false);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Value '%s' in gerrit.config (parameter plugin.%s.%s) is invalid.",
+ pluginConfigFactory
+ .getFromGerritConfig(pluginName)
+ .getString(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS),
+ pluginName,
+ KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS);
+ return false;
+ }
+ }
+
+ /**
+ * Gets the maximum size for the {@link
+ * com.google.gerrit.plugins.codeowners.backend.TransientCodeOwnerConfigCache}.
+ *
+ * @return the maximum cache size, {@link Optional#empty()} if the cache size is not limited
+ */
+ public Optional<Integer> getMaxCodeOwnerConfigCacheSize() {
+ if (maxCodeOwnerConfigCacheSize == null) {
+ maxCodeOwnerConfigCacheSize = readMaxCodeOwnerConfigCacheSize();
+ }
+ return maxCodeOwnerConfigCacheSize;
+ }
+
+ private Optional<Integer> readMaxCodeOwnerConfigCacheSize() {
+ try {
+ int maxCodeOwnerConfigCacheSize =
+ pluginConfigFactory
+ .getFromGerritConfig(pluginName)
+ .getInt(
+ KEY_MAX_CODE_OWNER_CONFIG_CACHE_SIZE, DEFAULT_MAX_CODE_OWNER_CONFIG_CACHE_SIZE);
+ return maxCodeOwnerConfigCacheSize > 0
+ ? Optional.of(maxCodeOwnerConfigCacheSize)
+ : Optional.empty();
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Value '%s' in gerrit.config (parameter plugin.%s.%s) is invalid.",
+ pluginConfigFactory
+ .getFromGerritConfig(pluginName)
+ .getString(KEY_MAX_CODE_OWNER_CONFIG_CACHE_SIZE),
+ pluginName,
+ KEY_MAX_CODE_OWNER_CONFIG_CACHE_SIZE);
+ return Optional.empty();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
new file mode 100644
index 0000000..ea7c88b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -0,0 +1,620 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ImmutableSortedSet;
+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.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.Emails;
+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.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+
+/** Snapshot of the project-specific code-owners plugin configuration. */
+public class CodeOwnersPluginProjectConfigSnapshot {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ CodeOwnersPluginProjectConfigSnapshot create(Project.NameKey projectName);
+ }
+
+ private final ProjectCache projectCache;
+ private final Emails emails;
+ private final BackendConfig backendConfig;
+ private final GeneralConfig generalConfig;
+ private final OverrideApprovalConfig overrideApprovalConfig;
+ private final RequiredApprovalConfig requiredApprovalConfig;
+ private final StatusConfig statusConfig;
+ private final Project.NameKey projectName;
+ private final Config pluginConfig;
+
+ @Nullable private Optional<String> fileExtension;
+ @Nullable private Boolean codeOwnerConfigsReadOnly;
+ @Nullable private Boolean exemptPureReverts;
+ @Nullable private Boolean rejectNonResolvableCodeOwners;
+ @Nullable private Boolean rejectNonResolvableImports;
+
+ @Nullable
+ private CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicyForCommitReceived;
+
+ @Nullable private CodeOwnerConfigValidationPolicy codeOwnerConfigValidationPolicyForSubmit;
+ @Nullable private MergeCommitStrategy mergeCommitStrategy;
+ @Nullable private FallbackCodeOwners fallbackCodeOwners;
+ @Nullable private Integer maxPathsInChangeMessages;
+ @Nullable private ImmutableSet<CodeOwnerReference> globalCodeOwners;
+ @Nullable private ImmutableSet<Account.Id> exemptedAccounts;
+ @Nullable private Optional<String> overrideInfoUrl;
+ @Nullable private Optional<String> invalidCodeOwnerConfigInfoUrl;
+ private Map<String, Boolean> disabledByBranch = new HashMap<>();
+ @Nullable private Boolean isDisabled;
+ private Map<String, CodeOwnerBackend> backendByBranch = new HashMap<>();
+ @Nullable private CodeOwnerBackend backend;
+ @Nullable private Boolean implicitApprovalsEnabled;
+ @Nullable private RequiredApproval requiredApproval;
+ @Nullable private ImmutableSortedSet<RequiredApproval> overrideApprovals;
+
+ @Inject
+ CodeOwnersPluginProjectConfigSnapshot(
+ CodeOwnersPluginConfig.Factory codeOwnersPluginConfigFactory,
+ ProjectCache projectCache,
+ Emails emails,
+ BackendConfig backendConfig,
+ GeneralConfig generalConfig,
+ OverrideApprovalConfig overrideApprovalConfig,
+ RequiredApprovalConfig requiredApprovalConfig,
+ StatusConfig statusConfig,
+ @Assisted Project.NameKey projectName) {
+ this.projectCache = projectCache;
+ this.emails = emails;
+ this.backendConfig = backendConfig;
+ this.generalConfig = generalConfig;
+ this.overrideApprovalConfig = overrideApprovalConfig;
+ this.requiredApprovalConfig = requiredApprovalConfig;
+ this.statusConfig = statusConfig;
+ this.projectName = projectName;
+ this.pluginConfig = codeOwnersPluginConfigFactory.create(projectName).get();
+ }
+
+ /** Gets the file extension of code owner config files, if any configured. */
+ public Optional<String> getFileExtension() {
+ if (fileExtension == null) {
+ fileExtension = generalConfig.getFileExtension(pluginConfig);
+ }
+ return fileExtension;
+ }
+
+ /** Whether code owner configs are read-only. */
+ public boolean areCodeOwnerConfigsReadOnly() {
+ if (codeOwnerConfigsReadOnly == null) {
+ codeOwnerConfigsReadOnly = generalConfig.getReadOnly(projectName, pluginConfig);
+ }
+ return codeOwnerConfigsReadOnly;
+ }
+
+ /** Whether pure revert changes are exempted from needing code owner approvals for submit. */
+ public boolean arePureRevertsExempted() {
+ if (exemptPureReverts == null) {
+ exemptPureReverts = generalConfig.getExemptPureReverts(projectName, pluginConfig);
+ }
+ return exemptPureReverts;
+ }
+
+ /**
+ * Whether newly added non-resolvable code owners should be rejected on commit received and
+ * submit.
+ *
+ * @param branchName the branch for which it should be checked whether non-resolvable code owners
+ * should be rejected
+ */
+ public boolean rejectNonResolvableCodeOwners(String branchName) {
+ if (rejectNonResolvableCodeOwners == null) {
+ rejectNonResolvableCodeOwners = readRejectNonResolvableCodeOwners(branchName);
+ }
+ return rejectNonResolvableCodeOwners;
+ }
+
+ private boolean readRejectNonResolvableCodeOwners(String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ Optional<Boolean> branchSpecificFlag =
+ generalConfig.getRejectNonResolvableCodeOwnersForBranch(
+ BranchNameKey.create(projectName, branchName), pluginConfig);
+ if (branchSpecificFlag.isPresent()) {
+ return branchSpecificFlag.get();
+ }
+
+ return generalConfig.getRejectNonResolvableCodeOwners(projectName, pluginConfig);
+ }
+
+ /**
+ * Whether newly added non-resolvable imports should be rejected on commit received and submit.
+ *
+ * @param branchName the branch for which it should be checked whether non-resolvable imports
+ * should be rejected
+ */
+ public boolean rejectNonResolvableImports(String branchName) {
+ if (rejectNonResolvableImports == null) {
+ rejectNonResolvableImports = readRejectNonResolvableImports(branchName);
+ }
+ return rejectNonResolvableImports;
+ }
+
+ private boolean readRejectNonResolvableImports(String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ Optional<Boolean> branchSpecificFlag =
+ generalConfig.getRejectNonResolvableImportsForBranch(
+ BranchNameKey.create(projectName, branchName), pluginConfig);
+ if (branchSpecificFlag.isPresent()) {
+ return branchSpecificFlag.get();
+ }
+
+ return generalConfig.getRejectNonResolvableImports(projectName, pluginConfig);
+ }
+
+ /**
+ * Whether code owner configs should be validated when a commit is received.
+ *
+ * @param branchName the branch for which it should be checked whether code owner configs should
+ * be validated on commit received
+ */
+ public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
+ String branchName) {
+ if (codeOwnerConfigValidationPolicyForCommitReceived == null) {
+ codeOwnerConfigValidationPolicyForCommitReceived =
+ readCodeOwnerConfigValidationPolicyForCommitReceived(branchName);
+ }
+ return codeOwnerConfigValidationPolicyForCommitReceived;
+ }
+
+ private CodeOwnerConfigValidationPolicy readCodeOwnerConfigValidationPolicyForCommitReceived(
+ String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ Optional<CodeOwnerConfigValidationPolicy> branchSpecificPolicy =
+ generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceivedForBranch(
+ BranchNameKey.create(projectName, branchName), pluginConfig);
+ if (branchSpecificPolicy.isPresent()) {
+ return branchSpecificPolicy.get();
+ }
+
+ return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived(
+ projectName, pluginConfig);
+ }
+
+ /**
+ * Whether code owner configs should be validated when a change is submitted.
+ *
+ * @param branchName the branch for which it should be checked whether code owner configs should
+ * be validated on submit
+ */
+ public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
+ String branchName) {
+ if (codeOwnerConfigValidationPolicyForSubmit == null) {
+ codeOwnerConfigValidationPolicyForSubmit =
+ readCodeOwnerConfigValidationPolicyForSubmit(branchName);
+ }
+ return codeOwnerConfigValidationPolicyForSubmit;
+ }
+
+ private CodeOwnerConfigValidationPolicy readCodeOwnerConfigValidationPolicyForSubmit(
+ String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ Optional<CodeOwnerConfigValidationPolicy> branchSpecificPolicy =
+ generalConfig.getCodeOwnerConfigValidationPolicyForSubmitForBranch(
+ BranchNameKey.create(projectName, branchName), pluginConfig);
+ if (branchSpecificPolicy.isPresent()) {
+ return branchSpecificPolicy.get();
+ }
+
+ return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(projectName, pluginConfig);
+ }
+
+ /** Gets the merge commit strategy. */
+ public MergeCommitStrategy getMergeCommitStrategy() {
+ if (mergeCommitStrategy == null) {
+ mergeCommitStrategy = generalConfig.getMergeCommitStrategy(projectName, pluginConfig);
+ }
+ return mergeCommitStrategy;
+ }
+
+ /** Gets the fallback code owners. */
+ public FallbackCodeOwners getFallbackCodeOwners() {
+ if (fallbackCodeOwners == null) {
+ fallbackCodeOwners = generalConfig.getFallbackCodeOwners(projectName, pluginConfig);
+ }
+ return fallbackCodeOwners;
+ }
+
+ /** Gets the max paths in change messages. */
+ public int getMaxPathsInChangeMessages() {
+ if (maxPathsInChangeMessages == null) {
+ maxPathsInChangeMessages =
+ generalConfig.getMaxPathsInChangeMessages(projectName, pluginConfig);
+ }
+ return maxPathsInChangeMessages;
+ }
+
+ /** Gets the global code owners. */
+ public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners() {
+ if (globalCodeOwners == null) {
+ globalCodeOwners = generalConfig.getGlobalCodeOwners(pluginConfig);
+ }
+ return globalCodeOwners;
+ }
+
+ /** 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<String> exemptedUsers = generalConfig.getExemptedUsers(pluginConfig);
+
+ try {
+ ImmutableSetMultimap<String, Account.Id> exemptedAccounts =
+ emails.getAccountsFor(exemptedUsers.toArray(new String[0]));
+
+ exemptedUsers.stream()
+ .filter(exemptedUser -> !exemptedAccounts.containsKey(exemptedUser))
+ .forEach(
+ exemptedUser ->
+ logger.atWarning().log(
+ "Ignoring exempted user %s for project %s: not found",
+ exemptedUser, projectName));
+
+ return ImmutableSet.copyOf(exemptedAccounts.values());
+ } catch (IOException e) {
+ throw new CodeOwnersInternalServerErrorException(
+ String.format(
+ "Failed to resolve exempted users %s on project %s", exemptedUsers, projectName),
+ e);
+ }
+ }
+
+ /** Gets the override info URL that is configured. */
+ public Optional<String> getOverrideInfoUrl() {
+ if (overrideInfoUrl == null) {
+ overrideInfoUrl = generalConfig.getOverrideInfoUrl(pluginConfig);
+ }
+ return overrideInfoUrl;
+ }
+
+ /** Gets the invalid code owner config info URL that is configured. */
+ public Optional<String> getInvalidCodeOwnerConfigInfoUrl() {
+ if (invalidCodeOwnerConfigInfoUrl == null) {
+ invalidCodeOwnerConfigInfoUrl = generalConfig.getInvalidCodeOwnerConfigInfoUrl(pluginConfig);
+ }
+ return invalidCodeOwnerConfigInfoUrl;
+ }
+
+ /**
+ * Whether the code owners functionality is disabled for the given branch.
+ *
+ * <p>The configuration is evaluated in the following order:
+ *
+ * <ul>
+ * <li>disabled configuration for the branch (with inheritance)
+ * <li>disabled configuration for the project (with inheritance)
+ * <li>hard-coded default (not disabled)
+ * </ul>
+ *
+ * <p>The first disabled configuration that exists counts and the evaluation is stopped.
+ *
+ * @param branchName the branch for which it should be checked whether the code owners
+ * functionality is disabled
+ * @return {@code true} if the code owners functionality is disabled for the given branch,
+ * otherwise {@code false}
+ */
+ public boolean isDisabled(String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName);
+ return disabledByBranch.computeIfAbsent(
+ branchNameKey.branch(),
+ b -> {
+ boolean isDisabled = statusConfig.isDisabledForBranch(pluginConfig, branchNameKey);
+ if (isDisabled) {
+ return true;
+ }
+ return isDisabled();
+ });
+ }
+
+ /**
+ * Whether the code owners functionality is disabled for the given project.
+ *
+ * <p>The configuration is evaluated in the following order:
+ *
+ * <ul>
+ * <li>disabled configuration for the project (with inheritance)
+ * <li>hard-coded default (not disabled)
+ * </ul>
+ *
+ * <p>The first disabled configuration that exists counts and the evaluation is stopped.
+ *
+ * @return {@code true} if the code owners functionality is disabled, otherwise {@code false}
+ */
+ public boolean isDisabled() {
+ if (isDisabled == null) {
+ isDisabled = statusConfig.isDisabledForProject(pluginConfig, projectName);
+ }
+ return isDisabled;
+ }
+
+ /**
+ * Returns the configured {@link CodeOwnerBackend} for the given branch.
+ *
+ * <p>The code owner backend configuration is evaluated in the following order:
+ *
+ * <ul>
+ * <li>backend configuration for branch (with inheritance, first by full branch name, then by
+ * short branch name)
+ * <li>backend configuration for project (with inheritance)
+ * <li>default backend (first globally configured backend, then hard-coded default backend)
+ * </ul>
+ *
+ * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
+ *
+ * @param branchName the branch for which the configured code owner backend should be returned
+ * @return the {@link CodeOwnerBackend} that should be used for the branch
+ */
+ public CodeOwnerBackend getBackend(String branchName) {
+ requireNonNull(branchName, "branchName");
+
+ BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName);
+ return backendByBranch.computeIfAbsent(
+ branchNameKey.branch(),
+ b -> {
+ Optional<CodeOwnerBackend> codeOwnerBackend =
+ backendConfig.getBackendForBranch(
+ pluginConfig, BranchNameKey.create(projectName, branchName));
+ if (codeOwnerBackend.isPresent()) {
+ return codeOwnerBackend.get();
+ }
+ return getBackend();
+ });
+ }
+
+ /**
+ * Returns the configured {@link CodeOwnerBackend}.
+ *
+ * <p>The code owner backend configuration is evaluated in the following order:
+ *
+ * <ul>
+ * <li>backend configuration for project (with inheritance)
+ * <li>default backend (first globally configured backend, then hard-coded default backend)
+ * </ul>
+ *
+ * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
+ *
+ * @return the {@link CodeOwnerBackend} that should be used
+ */
+ public CodeOwnerBackend getBackend() {
+ if (backend == null) {
+ backend = readBackend();
+ }
+ return backend;
+ }
+
+ private CodeOwnerBackend readBackend() {
+ // check if a project specific backend is configured
+ Optional<CodeOwnerBackend> codeOwnerBackend =
+ backendConfig.getBackendForProject(pluginConfig, projectName);
+ if (codeOwnerBackend.isPresent()) {
+ return codeOwnerBackend.get();
+ }
+
+ // fall back to the default backend
+ return backendConfig.getDefaultBackend();
+ }
+
+ /**
+ * Checks whether implicit code owner approvals are enabled.
+ *
+ * <p>If enabled, an implict code owner approval from the change owner is assumed if the last
+ * patch set was uploaded by the change owner.
+ */
+ public boolean areImplicitApprovalsEnabled() {
+ if (implicitApprovalsEnabled == null) {
+ implicitApprovalsEnabled = readImplicitApprovalsEnabled();
+ }
+ return implicitApprovalsEnabled;
+ }
+
+ private boolean readImplicitApprovalsEnabled() {
+ EnableImplicitApprovals enableImplicitApprovals =
+ generalConfig.getEnableImplicitApprovals(projectName, pluginConfig);
+ switch (enableImplicitApprovals) {
+ case FALSE:
+ logger.atFine().log("implicit approvals on project %s are disabled", projectName);
+ return false;
+ case TRUE:
+ LabelType requiredLabel = getRequiredApproval().labelType();
+ if (requiredLabel.isIgnoreSelfApproval()) {
+ logger.atFine().log(
+ "ignoring implicit approval configuration on project %s since the label of the required"
+ + " approval (%s) is configured to ignore self approvals",
+ projectName, requiredLabel);
+ return false;
+ }
+ return true;
+ case FORCED:
+ logger.atFine().log("implicit approvals on project %s are enforced", projectName);
+ return true;
+ }
+ throw new IllegalStateException(
+ String.format(
+ "unknown value %s for enableImplicitApprovals configuration in project %s",
+ enableImplicitApprovals, projectName));
+ }
+
+ /**
+ * Returns the approval that is required from code owners to approve the files in a change.
+ *
+ * <p>Defines which approval counts as code owner approval.
+ *
+ * <p>The code owner required approval configuration is evaluated in the following order:
+ *
+ * <ul>
+ * <li>required approval configuration for project (with inheritance)
+ * <li>globally configured required approval
+ * <li>hard-coded default required approval
+ * </ul>
+ *
+ * <p>The first required code owner approval configuration that exists counts and the evaluation
+ * is stopped.
+ *
+ * <p>If the code owner configuration contains multiple required approvals values, the last value
+ * is used.
+ *
+ * @return the required code owner approval that should be used
+ */
+ public RequiredApproval getRequiredApproval() {
+ if (requiredApproval == null) {
+ requiredApproval = readRequiredApproval();
+ }
+ return requiredApproval;
+ }
+
+ private RequiredApproval readRequiredApproval() {
+ ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
+ getConfiguredRequiredApproval(requiredApprovalConfig);
+ if (!configuredRequiredApprovalConfig.isEmpty()) {
+ // There can be only one required approval. If multiple ones are configured just use the last
+ // one, this is also what Config#getString(String, String, String) does.
+ return Iterables.getLast(configuredRequiredApprovalConfig);
+ }
+
+ // fall back to hard-coded default required approval
+ ProjectState projectState =
+ projectCache.get(projectName).orElseThrow(illegalState(projectName));
+ return requiredApprovalConfig.createDefault(projectState);
+ }
+
+ /**
+ * Returns the approvals that are required to override the code owners submit check for a change.
+ *
+ * <p>If multiple approvals are returned, any of them is sufficient to override the code owners
+ * submit check.
+ *
+ * <p>The override approval configuration is read from:
+ *
+ * <ul>
+ * <li>the override approval configuration for project (with inheritance)
+ * <li>the globally configured override approval
+ * </ul>
+ *
+ * <p>Override approvals that are configured on project-level extend the inherited override
+ * approval configuration.
+ *
+ * <p>The returned override approvals are sorted alphabetically by their string representation
+ * (e.g. {@code Owners-Override+1}).
+ *
+ * @return the override approvals that should be used, an empty set if no override approval is
+ * configured, in this case the override functionality is disabled
+ */
+ public ImmutableSortedSet<RequiredApproval> getOverrideApprovals() {
+ if (overrideApprovals == null) {
+ overrideApprovals = readOverrideApprovals();
+ }
+ return overrideApprovals;
+ }
+
+ private ImmutableSortedSet<RequiredApproval> readOverrideApprovals() {
+ try {
+ return ImmutableSortedSet.copyOf(
+ filterOutDuplicateRequiredApprovals(
+ getConfiguredRequiredApproval(overrideApprovalConfig)));
+ } catch (InvalidPluginConfigurationException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid override approval configuration for project %s."
+ + " Overrides are disabled.",
+ projectName.get());
+ }
+
+ return ImmutableSortedSet.of();
+ }
+
+ /**
+ * Filters out duplicate required approvals from the input list.
+ *
+ * <p>The following entries are considered as duplicate:
+ *
+ * <ul>
+ * <li>exact identical required approvals (e.g. "Code-Review+2" and "Code-Review+2")
+ * <li>required approvals with the same label name and a higher value (e.g. "Code-Review+2" is
+ * not needed if "Code-Review+1" is already contained, since "Code-Review+1" covers all
+ * "Code-Review" approvals >= 1)
+ * </ul>
+ */
+ private Collection<RequiredApproval> filterOutDuplicateRequiredApprovals(
+ ImmutableList<RequiredApproval> requiredApprovals) {
+ Map<String, RequiredApproval> requiredApprovalsByLabel = new HashMap<>();
+ for (RequiredApproval requiredApproval : requiredApprovals) {
+ String labelName = requiredApproval.labelType().getName();
+ RequiredApproval otherRequiredApproval = requiredApprovalsByLabel.get(labelName);
+ if (otherRequiredApproval != null
+ && otherRequiredApproval.value() <= requiredApproval.value()) {
+ continue;
+ }
+ requiredApprovalsByLabel.put(labelName, requiredApproval);
+ }
+ return requiredApprovalsByLabel.values();
+ }
+
+ /**
+ * Gets the required approvals that are configured.
+ *
+ * @param requiredApprovalConfig the config from which the required approvals should be read
+ * @return the required approvals that are configured, an empty list if no required approvals are
+ * configured
+ */
+ private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
+ AbstractRequiredApprovalConfig requiredApprovalConfig) {
+ ProjectState projectState =
+ projectCache.get(projectName).orElseThrow(illegalState(projectName));
+ return requiredApprovalConfig.get(projectState, pluginConfig);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersProjectConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersProjectConfigFile.java
new file mode 100644
index 0000000..1f51e93
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersProjectConfigFile.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.git.meta.VersionedMetaData;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Reads/writes the code-owners project configuration from/to the {@code code-owners.config} file in
+ * the {@code refs/meta/config} branch.
+ */
+public class CodeOwnersProjectConfigFile extends VersionedMetaData {
+ public static final String FILE_NAME = "code-owners.config";
+
+ private boolean isLoaded = false;
+ private Config config;
+
+ @Override
+ protected String getRefName() {
+ return RefNames.REFS_CONFIG;
+ }
+
+ /**
+ * Returns the loaded code owners config.
+ *
+ * <p>Fails if loading was not done yet.
+ */
+ public Config getConfig() {
+ checkLoaded();
+ return config;
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ if (revision != null) {
+ config = readConfig(FILE_NAME);
+ } else {
+ config = new Config();
+ }
+ isLoaded = true;
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+ checkLoaded();
+ saveConfig(FILE_NAME, config);
+ return true;
+ }
+
+ private void checkLoaded() {
+ checkState(isLoaded, "%s not loaded yet", FILE_NAME);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/ConfigModule.java b/java/com/google/gerrit/plugins/codeowners/backend/config/ConfigModule.java
similarity index 94%
rename from java/com/google/gerrit/plugins/codeowners/config/ConfigModule.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/ConfigModule.java
index ec439ad..5d8522b 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/ConfigModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/ConfigModule.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.git.validators.CommitValidationListener;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
new file mode 100644
index 0000000..7ab6706
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -0,0 +1,878 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend.config;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+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.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.project.RefPatternMatcher;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Class to read the general code owners configuration from {@code gerrit.config} and from {@code
+ * code-owners.config} in {@code refs/meta/config}.
+ *
+ * <p>Default values are configured in {@code gerrit.config}.
+ *
+ * <p>The default values can be overridden on project-level in {@code code-owners.config} in {@code
+ * refs/meta/config}.
+ *
+ * <p>Projects that have no configuration inherit the configuration from their parent projects.
+ */
+@Singleton
+public class GeneralConfig {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static final String SECTION_VALIDATION = "validation";
+
+ public static final String KEY_FILE_EXTENSION = "fileExtension";
+ public static final String KEY_READ_ONLY = "readOnly";
+ public static final String KEY_EXEMPT_PURE_REVERTS = "exemptPureReverts";
+ public static final String KEY_FALLBACK_CODE_OWNERS = "fallbackCodeOwners";
+ public static final String KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED =
+ "enableValidationOnCommitReceived";
+ public static final String KEY_ENABLE_VALIDATION_ON_SUBMIT = "enableValidationOnSubmit";
+ 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 String KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL =
+ "invalidCodeOwnerConfigInfoUrl";
+ public static final String KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS =
+ "rejectNonResolvableCodeOwners";
+ public static final String KEY_REJECT_NON_RESOLVABLE_IMPORTS = "rejectNonResolvableImports";
+
+ public static final int DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES = 50;
+
+ private static final String KEY_ALLOWED_EMAIL_DOMAIN = "allowedEmailDomain";
+
+ private final String pluginName;
+ private final PluginConfig pluginConfigFromGerritConfig;
+
+ @Inject
+ GeneralConfig(@PluginName String pluginName, PluginConfigFactory pluginConfigFactory) {
+ this.pluginName = pluginName;
+ this.pluginConfigFromGerritConfig = pluginConfigFactory.getFromGerritConfig(pluginName);
+ }
+
+ /**
+ * Validates the backend configuration in the given project level configuration.
+ *
+ * @param fileName the name of the config file
+ * @param projectLevelConfig the project level plugin configuration
+ * @return list of validation messages for validation errors, empty list if there are no
+ * validation errors
+ */
+ ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
+ String fileName, Config projectLevelConfig) {
+ requireNonNull(fileName, "fileName");
+ requireNonNull(projectLevelConfig, "projectLevelConfig");
+
+ List<CommitValidationMessage> validationMessages = new ArrayList<>();
+
+ try {
+ projectLevelConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_MERGE_COMMIT_STRATEGY,
+ MergeCommitStrategy.ALL_CHANGED_FILES);
+ } catch (IllegalArgumentException e) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ "Merge commit strategy '%s' that is configured in %s (parameter %s.%s) is invalid.",
+ projectLevelConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MERGE_COMMIT_STRATEGY),
+ fileName,
+ SECTION_CODE_OWNERS,
+ KEY_MERGE_COMMIT_STRATEGY),
+ ValidationMessage.Type.ERROR));
+ }
+
+ try {
+ projectLevelConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_FALLBACK_CODE_OWNERS,
+ FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ "The value for fallback code owners '%s' that is configured in %s (parameter %s.%s) is invalid.",
+ projectLevelConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_FALLBACK_CODE_OWNERS),
+ fileName,
+ SECTION_CODE_OWNERS,
+ KEY_FALLBACK_CODE_OWNERS),
+ ValidationMessage.Type.ERROR));
+ }
+
+ try {
+ projectLevelConfig.getInt(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ "The value for max paths in change messages '%s' that is configured in %s"
+ + " (parameter %s.%s) is invalid.",
+ projectLevelConfig.getString(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ fileName,
+ SECTION_CODE_OWNERS,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ ValidationMessage.Type.ERROR));
+ }
+
+ return ImmutableList.copyOf(validationMessages);
+ }
+
+ /**
+ * Gets the file extension that should be used for code owner config files in the given project.
+ *
+ * @param pluginConfig the plugin config from which the file extension should be read.
+ * @return the file extension that should be used for code owner config files in the given
+ * project, {@link Optional#empty()} if no file extension should be used
+ */
+ Optional<String> getFileExtension(Config pluginConfig) {
+ return getStringValue(pluginConfig, KEY_FILE_EXTENSION);
+ }
+
+ /**
+ * Returns the email domains that are allowed to be used for code owners.
+ *
+ * @return the email domains that are allowed to be used for code owners, an empty set if all
+ * email domains are allowed (if {@code plugin.code-owners.allowedEmailDomain} is not set or
+ * set to an empty value)
+ */
+ ImmutableSet<String> getAllowedEmailDomains() {
+ return Arrays.stream(pluginConfigFromGerritConfig.getStringList(KEY_ALLOWED_EMAIL_DOMAIN))
+ .filter(emailDomain -> !Strings.isNullOrEmpty(emailDomain))
+ .distinct()
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * Gets the read-only configuration from the given plugin config with fallback to {@code
+ * gerrit.config}.
+ *
+ * <p>The read-only configuration controls whether code owner config files are read-only and all
+ * modifications of code owner config files should be rejected.
+ *
+ * @param project the project for which the read-only configuration should be read
+ * @param pluginConfig the plugin config from which the read-only configuration should be read.
+ * @return whether code owner config files are read-only
+ */
+ boolean getReadOnly(Project.NameKey project, Config pluginConfig) {
+ return getBooleanConfig(project, pluginConfig, KEY_READ_ONLY, /* defaultValue= */ false);
+ }
+
+ /**
+ * Gets the exempt-pure-reverts configuration from the given plugin config with fallback to {@code
+ * gerrit.config}.
+ *
+ * <p>The exempt-pure-reverts configuration controls whether pure revert changes are exempted from
+ * needing code owner approvals for submit.
+ *
+ * @param project the project for which the exempt-pure-revert configuration should be read
+ * @param pluginConfig the plugin config from which the read-only configuration should be read.
+ * @return whether pure reverts are exempted from needing code owner approvals for submit
+ */
+ boolean getExemptPureReverts(Project.NameKey project, Config pluginConfig) {
+ return getBooleanConfig(
+ project, pluginConfig, KEY_EXEMPT_PURE_REVERTS, /* defaultValue= */ false);
+ }
+
+ /**
+ * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the
+ * specified project with fallback to {@code gerrit.config}.
+ *
+ * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files
+ * with newly added non-resolvable code owners should be rejected on commit received and on
+ * submit.
+ *
+ * @param project the project for which the reject-non-resolvable-code-owners configuration should
+ * be read
+ * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners
+ * configuration should be read.
+ * @return whether code owner config files with newly added non-resolvable code owners should be
+ * rejected on commit received and on submit
+ */
+ boolean getRejectNonResolvableCodeOwners(Project.NameKey project, Config pluginConfig) {
+ return getBooleanConfig(
+ project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, /* defaultValue= */ true);
+ }
+
+ /**
+ * Gets the reject-non-resolvable-code-owners configuration from the given plugin config for the
+ * specified branch with fallback to {@code gerrit.config}.
+ *
+ * <p>If multiple branch-specific configurations match the specified branch, it is undefined which
+ * of the matching branch configurations takes precedence.
+ *
+ * <p>The reject-non-resolvable-code-owners configuration controls whether code owner config files
+ * with newly added non-resolvable code owners should be rejected on commit received and on
+ * submit.
+ *
+ * @param branchNameKey the branch and project for which the reject-non-resolvable-code-owners
+ * configuration should be read
+ * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners
+ * configuration should be read.
+ * @return whether code owner config files with newly added non-resolvable code owners should be
+ * rejected on commit received and on submit
+ */
+ Optional<Boolean> getRejectNonResolvableCodeOwnersForBranch(
+ BranchNameKey branchNameKey, Config pluginConfig) {
+ return getCodeOwnerConfigValidationFlagForBranch(
+ KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, branchNameKey, pluginConfig);
+ }
+
+ /**
+ * Gets the reject-non-resolvable-imports configuration from the given plugin config for the
+ * specified project with fallback to {@code gerrit.config}.
+ *
+ * <p>The reject-non-resolvable-imports configuration controls whether code owner config files
+ * with newly added non-resolvable imports should be rejected on commit received and on submit.
+ *
+ * @param project the project for which the reject-non-resolvable-imports configuration should be
+ * read
+ * @param pluginConfig the plugin config from which the reject-non-resolvable-imports
+ * configuration should be read.
+ * @return whether code owner config files with newly added non-resolvable imports should be
+ * rejected on commit received and on submit
+ */
+ boolean getRejectNonResolvableImports(Project.NameKey project, Config pluginConfig) {
+ return getBooleanConfig(
+ project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* defaultValue= */ true);
+ }
+
+ /**
+ * Gets the reject-non-resolvable-imports configuration from the given plugin config for the
+ * specified branch with fallback to {@code gerrit.config}.
+ *
+ * <p>The reject-non-resolvable-imports configuration controls whether code owner config files
+ * with newly added non-resolvable imports should be rejected on commit received and on submit.
+ *
+ * @param branchNameKey the branch and project for which the reject-non-resolvable-imports
+ * configuration should be read
+ * @param pluginConfig the plugin config from which the reject-non-resolvable-imports
+ * configuration should be read.
+ * @return whether code owner config files with newly added non-resolvable imports should be
+ * rejected on commit received and on submit
+ */
+ Optional<Boolean> getRejectNonResolvableImportsForBranch(
+ BranchNameKey branchNameKey, Config pluginConfig) {
+ return getCodeOwnerConfigValidationFlagForBranch(
+ KEY_REJECT_NON_RESOLVABLE_IMPORTS, branchNameKey, pluginConfig);
+ }
+
+ private boolean getBooleanConfig(
+ Project.NameKey project, Config pluginConfig, String key, boolean defaultValue) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+ requireNonNull(key, "key");
+
+ String value = pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key);
+ if (value != null) {
+ try {
+ return pluginConfig.getBoolean(
+ SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for %s in '%s.config' of project %s."
+ + " Falling back to global config.",
+ value, key, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getBoolean(key, defaultValue);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for %s in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(key), key, pluginName, key, defaultValue);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Gets the fallback code owners that own paths that have no defined code owners.
+ *
+ * @param project the project for which the fallback code owners should be read
+ * @param pluginConfig the plugin config from which the fallback code owners should be read
+ * @return the fallback code owners that own paths that have no defined code owners
+ */
+ FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String fallbackCodeOwnersString =
+ pluginConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_FALLBACK_CODE_OWNERS);
+ if (fallbackCodeOwnersString != null) {
+ try {
+ return pluginConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_FALLBACK_CODE_OWNERS,
+ FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for fallback code owners in '%s.config' of project %s."
+ + " Falling back to global config.",
+ fallbackCodeOwnersString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(
+ KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for fallback code owners in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_FALLBACK_CODE_OWNERS),
+ pluginName,
+ KEY_FALLBACK_CODE_OWNERS,
+ FallbackCodeOwners.NONE);
+ return FallbackCodeOwners.NONE;
+ }
+ }
+
+ /**
+ * Gets the maximum number of paths that should be incuded in change messages.
+ *
+ * @param project the project for which the maximum number of paths in change messages should be
+ * read
+ * @param pluginConfig the plugin config from which the maximum number of paths in change messages
+ * should be read
+ * @return the maximum number of paths in change messages
+ */
+ int getMaxPathsInChangeMessages(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String maxPathInChangeMessagesString =
+ pluginConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MAX_PATHS_IN_CHANGE_MESSAGES);
+ if (maxPathInChangeMessagesString != null) {
+ try {
+ return pluginConfig.getInt(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for max paths in change messages in '%s.config' of"
+ + " project %s. Falling back to global config.",
+ maxPathInChangeMessagesString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getInt(
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES, DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for max paths in change messages in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_MAX_PATHS_IN_CHANGE_MESSAGES),
+ pluginName,
+ KEY_MAX_PATHS_IN_CHANGE_MESSAGES,
+ DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES);
+ return DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES;
+ }
+ }
+
+ /**
+ * Gets the enable validation on commit received configuration from the given plugin config for
+ * the specified project with fallback to {@code gerrit.config} and default to {@code true}.
+ *
+ * <p>The enable validation on commit received controls whether code owner config files should be
+ * validated when a commit is received.
+ *
+ * @param project the project for which the enable validation on commit received configuration
+ * should be read
+ * @param pluginConfig the plugin config from which the enable validation on commit received
+ * configuration should be read
+ * @return whether code owner config files should be validated when a commit is received
+ */
+ CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
+ Project.NameKey project, Config pluginConfig) {
+ return getCodeOwnerConfigValidationPolicy(
+ KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED,
+ project,
+ pluginConfig,
+ CodeOwnerConfigValidationPolicy.TRUE);
+ }
+
+ /**
+ * Gets the enable validation on commit received configuration from the given plugin config for
+ * the specified branch.
+ *
+ * <p>If multiple branch-specific configurations match the specified branch, it is undefined which
+ * of the matching branch configurations takes precedence.
+ *
+ * <p>The enable validation on commit received controls whether code owner config files should be
+ * validated when a commit is received.
+ *
+ * @param branchNameKey the branch and project for which the enable validation on commit received
+ * configuration should be read
+ * @param pluginConfig the plugin config from which the enable validation on commit received
+ * configuration should be read
+ * @return the enable validation on commit received configuration that is configured for the
+ * branch, {@link Optional#empty()} if no branch specific configuration exists
+ */
+ Optional<CodeOwnerConfigValidationPolicy>
+ getCodeOwnerConfigValidationPolicyForCommitReceivedForBranch(
+ BranchNameKey branchNameKey, Config pluginConfig) {
+ return getCodeOwnerConfigValidationPolicyForBranch(
+ KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED, branchNameKey, pluginConfig);
+ }
+
+ /**
+ * Gets the enable validation on submit configuration from the given plugin config for the
+ * specified project with fallback to {@code gerrit.config} and default to {@code true}.
+ *
+ * <p>The enable validation on submit controls whether code owner config files should be validated
+ * when a change is submitted.
+ *
+ * @param project the project for which the enable validation on submit configuration should be
+ * read
+ * @param pluginConfig the plugin config from which the enable validation on submit configuration
+ * should be read
+ * @return whether code owner config files should be validated when a change is submitted
+ */
+ CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
+ Project.NameKey project, Config pluginConfig) {
+ return getCodeOwnerConfigValidationPolicy(
+ KEY_ENABLE_VALIDATION_ON_SUBMIT,
+ project,
+ pluginConfig,
+ CodeOwnerConfigValidationPolicy.FALSE);
+ }
+
+ /**
+ * Gets the enable validation on submit configuration from the given plugin config for the
+ * specified branch.
+ *
+ * <p>If multiple branch-specific configurations match the specified branch, it is undefined which
+ * of the matching branch configurations takes precedence.
+ *
+ * <p>The enable validation on submit controls whether code owner config files should be validated
+ * when a change is submitted.
+ *
+ * @param branchNameKey the branch and project for which the enable validation on submit
+ * configuration should be read
+ * @param pluginConfig the plugin config from which the enable validation on submit configuration
+ * should be read
+ * @return the enable validation on submit configuration that is configured for the branch, {@link
+ * Optional#empty()} if no branch specific configuration exists
+ */
+ Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForSubmitForBranch(
+ BranchNameKey branchNameKey, Config pluginConfig) {
+ return getCodeOwnerConfigValidationPolicyForBranch(
+ KEY_ENABLE_VALIDATION_ON_SUBMIT, branchNameKey, pluginConfig);
+ }
+
+ private Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForBranch(
+ String key, BranchNameKey branchNameKey, Config pluginConfig) {
+ requireNonNull(key, "key");
+ requireNonNull(branchNameKey, "branchNameKey");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ Optional<String> validationSectionForBranch =
+ getValidationSectionForBranch(branchNameKey, pluginConfig);
+ if (!validationSectionForBranch.isPresent()) {
+ return Optional.empty();
+ }
+
+ return getCodeOwnerConfigValidationPolicyForBranch(
+ validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig);
+ }
+
+ private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch(
+ String key, BranchNameKey branchNameKey, Config pluginConfig) {
+ requireNonNull(key, "key");
+ requireNonNull(branchNameKey, "branchNameKey");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ Optional<String> validationSectionForBranch =
+ getValidationSectionForBranch(branchNameKey, pluginConfig);
+ if (!validationSectionForBranch.isPresent()) {
+ return Optional.empty();
+ }
+
+ return getCodeOwnerConfigValidationFlagForBranch(
+ validationSectionForBranch.get(), key, branchNameKey.project(), pluginConfig);
+ }
+
+ private CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicy(
+ String key,
+ Project.NameKey project,
+ Config pluginConfig,
+ CodeOwnerConfigValidationPolicy defaultValue) {
+ requireNonNull(key, "key");
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String codeOwnerConfigValidationPolicyString =
+ pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key);
+ if (codeOwnerConfigValidationPolicyString != null) {
+ try {
+ return pluginConfig.getEnum(SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for the code owner config validation policy in '%s.config'"
+ + " of project %s (parameter %s.%s). Falling back to global config.",
+ codeOwnerConfigValidationPolicyString,
+ pluginName,
+ project.get(),
+ SECTION_CODE_OWNERS,
+ key);
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(key, defaultValue);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for the code owner config validation policy in gerrit.config"
+ + " (parameter plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(key), pluginName, key, defaultValue);
+ return defaultValue;
+ }
+ }
+
+ private Optional<String> getValidationSectionForBranch(
+ BranchNameKey branchNameKey, Config pluginConfig) {
+ ImmutableSet<String> matchingValidationSubsections =
+ pluginConfig.getSubsections(SECTION_VALIDATION).stream()
+ .filter(
+ refPattern -> {
+ try {
+ return RefPatternMatcher.getMatcher(refPattern)
+ .match(branchNameKey.branch(), /* user= */ null);
+ } catch (PatternSyntaxException e) {
+ logger.atWarning().withCause(e).log(
+ "invalid ref pattern %s for subsection %s.%s in %s.config of project %s",
+ refPattern,
+ SECTION_VALIDATION,
+ refPattern,
+ pluginName,
+ branchNameKey.project());
+ return false;
+ }
+ })
+ .collect(toImmutableSet());
+
+ if (matchingValidationSubsections.isEmpty()) {
+ return Optional.empty();
+ }
+
+ String matchingValidationSubsection = matchingValidationSubsections.asList().get(0);
+ if (matchingValidationSubsections.size() > 1) {
+ logger.atWarning().log(
+ "branch %s matches multiple %s subsections in %s.config of project %s: %s,"
+ + " subsection %s takes precedence",
+ branchNameKey.branch(),
+ SECTION_VALIDATION,
+ pluginName,
+ branchNameKey.project(),
+ matchingValidationSubsections,
+ matchingValidationSubsection);
+ }
+ return Optional.of(matchingValidationSubsection);
+ }
+
+ private Optional<CodeOwnerConfigValidationPolicy> getCodeOwnerConfigValidationPolicyForBranch(
+ String branchSubsection, String key, Project.NameKey project, Config pluginConfig) {
+ requireNonNull(branchSubsection, "branchSubsection");
+ requireNonNull(key, "key");
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String codeOwnerConfigValidationPolicyString =
+ pluginConfig.getString(SECTION_VALIDATION, branchSubsection, key);
+ if (codeOwnerConfigValidationPolicyString != null) {
+ try {
+ return Optional.of(
+ pluginConfig.getEnum(
+ SECTION_VALIDATION, branchSubsection, key, CodeOwnerConfigValidationPolicy.TRUE));
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for the code owner config validation policy in '%s.config'"
+ + " of project %s (parameter %s.%s.%s). Falling back to project-level setting.",
+ codeOwnerConfigValidationPolicyString,
+ pluginName,
+ project.get(),
+ SECTION_VALIDATION,
+ branchSubsection,
+ key);
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Optional<Boolean> getCodeOwnerConfigValidationFlagForBranch(
+ String branchSubsection, String key, Project.NameKey project, Config pluginConfig) {
+ requireNonNull(branchSubsection, "branchSubsection");
+ requireNonNull(key, "key");
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String codeOwnerConfigValidationFlagString =
+ pluginConfig.getString(SECTION_VALIDATION, branchSubsection, key);
+ if (codeOwnerConfigValidationFlagString != null) {
+ try {
+ return Optional.of(
+ pluginConfig.getBoolean(SECTION_VALIDATION, branchSubsection, key, true));
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for %s.%s.%s in '%s.config' of project %s."
+ + " Falling back to project-level setting.",
+ codeOwnerConfigValidationFlagString,
+ SECTION_VALIDATION,
+ branchSubsection,
+ key,
+ pluginName,
+ project.get());
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Gets the merge commit strategy from the given plugin config with fallback to {@code
+ * gerrit.config}.
+ *
+ * <p>The merge commit strategy defines for merge commits which files require code owner
+ * approvals.
+ *
+ * @param project the name of the project for which the merge commit strategy should be read
+ * @param pluginConfig the plugin config from which the merge commit strategy should be read
+ * @return the merge commit strategy that should be used
+ */
+ MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String mergeCommitStrategyString =
+ pluginConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_MERGE_COMMIT_STRATEGY);
+ if (mergeCommitStrategyString != null) {
+ try {
+ return pluginConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_MERGE_COMMIT_STRATEGY,
+ MergeCommitStrategy.ALL_CHANGED_FILES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for merge commit stategy in '%s.config' of project %s."
+ + " Falling back to global config or default value.",
+ mergeCommitStrategyString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(
+ KEY_MERGE_COMMIT_STRATEGY, MergeCommitStrategy.ALL_CHANGED_FILES);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for merge commit stategy in gerrit.config (parameter plugin.%s.%s)."
+ + " Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_MERGE_COMMIT_STRATEGY),
+ pluginName,
+ KEY_MERGE_COMMIT_STRATEGY,
+ MergeCommitStrategy.ALL_CHANGED_FILES);
+ return MergeCommitStrategy.ALL_CHANGED_FILES;
+ }
+ }
+
+ /**
+ * Gets whether an implicit code owner approvals are enabled from the given plugin config with
+ * fallback to {@code gerrit.config}.
+ *
+ * <p>If enabled, an implict code owner approval from the change owner is assumed if the last
+ * patch set was uploaded by the change owner.
+ *
+ * @param project the name of the project for which the configuration should be read
+ * @param pluginConfig the plugin config from which the configuration should be read.
+ * @return whether an implicit code owner approval from the last uploader is assumed
+ */
+ EnableImplicitApprovals getEnableImplicitApprovals(Project.NameKey project, Config pluginConfig) {
+ requireNonNull(project, "project");
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String enableImplicitApprovalsString =
+ pluginConfig.getString(
+ SECTION_CODE_OWNERS, /* subsection= */ null, KEY_ENABLE_IMPLICIT_APPROVALS);
+ if (enableImplicitApprovalsString != null) {
+ try {
+ return pluginConfig.getEnum(
+ SECTION_CODE_OWNERS,
+ /* subsection= */ null,
+ KEY_ENABLE_IMPLICIT_APPROVALS,
+ EnableImplicitApprovals.FALSE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for enabling implicit approvals in '%s.config' of project"
+ + " %s. Falling back to global config or default value.",
+ enableImplicitApprovalsString, pluginName, project.get());
+ }
+ }
+
+ try {
+ return pluginConfigFromGerritConfig.getEnum(
+ KEY_ENABLE_IMPLICIT_APPROVALS, EnableImplicitApprovals.FALSE);
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Ignoring invalid value %s for enabling implict approvals in gerrit.config (parameter"
+ + " plugin.%s.%s). Falling back to default value %s.",
+ pluginConfigFromGerritConfig.getString(KEY_ENABLE_IMPLICIT_APPROVALS),
+ pluginName,
+ KEY_ENABLE_IMPLICIT_APPROVALS,
+ EnableImplicitApprovals.FALSE);
+ return EnableImplicitApprovals.FALSE;
+ }
+ }
+
+ /**
+ * Gets the users which are configured as global code owners from the given plugin config with
+ * fallback to {@code gerrit.config}.
+ *
+ * @param pluginConfig the plugin config from which the global code owners should be read.
+ * @return the users which are configured as global code owners
+ */
+ ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Config pluginConfig) {
+ requireNonNull(pluginConfig, "pluginConfig");
+ return getMultiValue(pluginConfig, KEY_GLOBAL_CODE_OWNER)
+ .map(CodeOwnerReference::create)
+ .collect(toImmutableSet());
+ }
+
+ /**
+ * 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");
+ return getMultiValue(pluginConfig, KEY_EXEMPTED_USER).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
+ * gerrit.config}.
+ *
+ * @param pluginConfig the plugin config from which the override info URL should be read.
+ * @return URL that leads to an information page about overrides, {@link Optional#empty()} if no
+ * such URL is configured
+ */
+ Optional<String> getOverrideInfoUrl(Config pluginConfig) {
+ return getStringValue(pluginConfig, KEY_OVERRIDE_INFO_URL);
+ }
+
+ /**
+ * Gets an URL that leads to an information page about invalid code owner config files.
+ *
+ * <p>The URL is retrieved from the given plugin config, with fallback to the {@code
+ * gerrit.config}.
+ *
+ * @param pluginConfig the plugin config from which the invalid code owner config info URL should
+ * be read.
+ * @return URL that leads to an information page about invalid code owner config files, {@link
+ * Optional#empty()} if no such URL is configured
+ */
+ Optional<String> getInvalidCodeOwnerConfigInfoUrl(Config pluginConfig) {
+ return getStringValue(pluginConfig, KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL);
+ }
+
+ private Optional<String> getStringValue(Config pluginConfig, String key) {
+ requireNonNull(pluginConfig, "pluginConfig");
+
+ String value = pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key);
+ if (value != null) {
+ return Optional.of(value);
+ }
+
+ return Optional.ofNullable(pluginConfigFromGerritConfig.getString(key));
+ }
+
+ /**
+ * Gets the values for a parameter that can be set multiple times with taking inherited values
+ * from {@code gerrit.config} into account.
+ *
+ * <p>The inherited values from {@code gerrit.config} are included into the returned list at the
+ * first position. This matches the behavior in {@link Config#getStringList(String, String,
+ * String)} that includes inherited values from the base config into the result list at the first
+ * position too.
+ *
+ * <p>The returned stream contains duplicates if the exact same value is set for different
+ * projects in the line of parent projects.
+ */
+ private Stream<String> getMultiValue(Config pluginConfig, String key) {
+ return Streams.concat(
+ Arrays.stream(pluginConfigFromGerritConfig.getStringList(key)),
+ Arrays.stream(
+ pluginConfig.getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, key)))
+ .filter(Objects::nonNull)
+ .filter(value -> !value.trim().isEmpty());
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/InvalidPluginConfigurationException.java b/java/com/google/gerrit/plugins/codeowners/backend/config/InvalidPluginConfigurationException.java
similarity index 94%
rename from java/com/google/gerrit/plugins/codeowners/config/InvalidPluginConfigurationException.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/InvalidPluginConfigurationException.java
index dd78ada..d80b7cf 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/InvalidPluginConfigurationException.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/InvalidPluginConfigurationException.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
/**
* Exception that is thrown if a configuration parameter of the code-owners plugin has an invalid
diff --git a/java/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/OverrideApprovalConfig.java
similarity index 96%
rename from java/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfig.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/OverrideApprovalConfig.java
index 3b4f56c..2a815a8 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/OverrideApprovalConfig.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.extensions.annotations.PluginName;
diff --git a/java/com/google/gerrit/plugins/codeowners/config/RequiredApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
similarity index 94%
rename from java/com/google/gerrit/plugins/codeowners/config/RequiredApproval.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
index e361ea6..4cbdba0 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/RequiredApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -20,6 +20,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ComparisonChain;
import com.google.common.primitives.Ints;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSetApproval;
@@ -39,7 +40,7 @@
* </ul>
*/
@AutoValue
-public abstract class RequiredApproval {
+public abstract class RequiredApproval implements Comparable<RequiredApproval> {
/** The label on which an approval is required. */
public abstract LabelType labelType();
@@ -59,6 +60,11 @@
}
@Override
+ public final int compareTo(RequiredApproval other) {
+ return ComparisonChain.start().compare(toString(), other.toString()).result();
+ }
+
+ @Override
public final String toString() {
return labelType().getName() + "+" + value();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApprovalConfig.java
similarity index 94%
rename from java/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfig.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApprovalConfig.java
index e49904d..3902655 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApprovalConfig.java
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
-import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.extensions.annotations.PluginName;
diff --git a/java/com/google/gerrit/plugins/codeowners/config/StatusConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/StatusConfig.java
similarity index 83%
rename from java/com/google/gerrit/plugins/codeowners/config/StatusConfig.java
rename to java/com/google/gerrit/plugins/codeowners/backend/config/StatusConfig.java
index 4648271..c3d4dee 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/StatusConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/StatusConfig.java
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.config;
+package com.google.gerrit.plugins.codeowners.backend.config;
-import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
@@ -27,7 +28,6 @@
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.project.ProjectLevelConfig;
import com.google.gerrit.server.project.RefPatternMatcher;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -77,21 +77,21 @@
* validation errors
*/
ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
- String fileName, ProjectLevelConfig.Bare projectLevelConfig) {
+ String fileName, Config projectLevelConfig) {
requireNonNull(fileName, "fileName");
requireNonNull(projectLevelConfig, "projectLevelConfig");
List<CommitValidationMessage> validationMessages = new ArrayList<>();
try {
- projectLevelConfig.getConfig().getBoolean(SECTION_CODE_OWNERS, null, KEY_DISABLED, false);
+ projectLevelConfig.getBoolean(SECTION_CODE_OWNERS, null, KEY_DISABLED, false);
} catch (IllegalArgumentException e) {
validationMessages.add(
new CommitValidationMessage(
String.format(
"Disabled value '%s' that is configured in %s.config (parameter %s.%s) is"
+ " invalid.",
- projectLevelConfig.getConfig().getString(SECTION_CODE_OWNERS, null, KEY_DISABLED),
+ projectLevelConfig.getString(SECTION_CODE_OWNERS, null, KEY_DISABLED),
pluginName,
SECTION_CODE_OWNERS,
KEY_DISABLED),
@@ -99,9 +99,7 @@
}
for (String refPattern :
- projectLevelConfig
- .getConfig()
- .getStringList(SECTION_CODE_OWNERS, null, KEY_DISABLED_BRANCH)) {
+ projectLevelConfig.getStringList(SECTION_CODE_OWNERS, null, KEY_DISABLED_BRANCH)) {
try {
RefPatternMatcher.getMatcher(refPattern).match("refs/heads/master", null);
} catch (PatternSyntaxException e) {
@@ -182,31 +180,30 @@
requireNonNull(pluginConfig, "pluginConfig");
requireNonNull(branch, "branch");
- String disabledBranches =
- pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_DISABLED_BRANCH);
- if (disabledBranches != null) {
- // a value for KEY_DISABLED_BRANCH is set on project-level
- return isDisabledForBranch(
- pluginConfig.getStringList(SECTION_CODE_OWNERS, null, KEY_DISABLED_BRANCH),
- branch.branch(),
- "Disabled branch '%s' that is configured for project "
- + branch.project()
- + " in "
- + pluginName
- + ".config (parameter "
- + SECTION_CODE_OWNERS
- + "."
- + KEY_DISABLED_BRANCH
- + ") is invalid.");
+ // check if the branch is disabled in gerrit.config
+ boolean isDisabled =
+ isDisabledForBranch(
+ pluginConfigFromGerritConfig.getStringList(KEY_DISABLED_BRANCH),
+ branch.branch(),
+ "Disabled branch '%s' that is configured for in gerrit.config (parameter plugin."
+ + pluginName
+ + "."
+ + KEY_DISABLED_BRANCH
+ + ") is invalid.");
+ if (isDisabled) {
+ return true;
}
- // there is no project-level configuration for KEY_DISABLED_BRANCH, check if it's set in
- // gerrit.config
+ // check if the branch is disabled on project level
return isDisabledForBranch(
- pluginConfigFromGerritConfig.getStringList(KEY_DISABLED_BRANCH),
+ pluginConfig.getStringList(SECTION_CODE_OWNERS, null, KEY_DISABLED_BRANCH),
branch.branch(),
- "Disabled branch '%s' that is configured for in gerrit.config (parameter plugin."
+ "Disabled branch '%s' that is configured for project "
+ + branch.project()
+ + " in "
+ pluginName
+ + ".config (parameter "
+ + SECTION_CODE_OWNERS
+ "."
+ KEY_DISABLED_BRANCH
+ ") is invalid.");
@@ -215,7 +212,7 @@
private boolean isDisabledForBranch(
String[] refPatternList, String branch, String warningMsgForInvalidRefPattern) {
for (String refPattern : refPatternList) {
- if (refPattern == null) {
+ if (Strings.isNullOrEmpty(refPattern)) {
continue;
}
try {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
index ab08201..0e923d7 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
@@ -17,9 +17,10 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
-import com.google.gerrit.plugins.codeowners.backend.GlobMatcher;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
+import com.google.gerrit.plugins.codeowners.backend.FindOwnersGlobMatcher;
import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -44,6 +45,7 @@
@Inject
FindOwnersBackend(
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
FindOwnersCodeOwnerConfigParser codeOwnerConfigParser,
GitRepositoryManager repoManager,
@GerritPersonIdent PersonIdent serverIdent,
@@ -56,12 +58,13 @@
metaDataUpdateInternalFactory,
retryHelper,
CODE_OWNER_CONFIG_FILE_NAME,
+ codeOwnerConfigFileFactory,
codeOwnerConfigParser);
}
@Override
public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
- return Optional.of(GlobMatcher.INSTANCE);
+ return Optional.of(FindOwnersGlobMatcher.INSTANCE);
}
@Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
index 7a4261a..5ddbb9a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
@@ -48,7 +48,7 @@
* OWNERS} files as they are used by the {@code find-owners} plugin.
*
* <p>The syntax is described at in the {@code find-owners} plugin documentation at:
- * https://gerrit.googlesource.com/plugins/find-owners/+/master/src/main/resources/Documentation/syntax.md
+ * https://gerrit.googlesource.com/plugins/find-owners/+/HEAD/src/main/resources/Documentation/syntax.md
*
* <p>Comment lines are silently ignored.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
index e671c6f..b273ab2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
@@ -17,9 +17,10 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.plugins.codeowners.backend.AbstractFileBasedCodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFile;
import com.google.gerrit.plugins.codeowners.backend.PathExpressionMatcher;
import com.google.gerrit.plugins.codeowners.backend.SimplePathExpressionMatcher;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -52,6 +53,7 @@
@GerritPersonIdent PersonIdent serverIdent,
MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
RetryHelper retryHelper,
+ CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory,
ProtoCodeOwnerConfigParser codeOwnerConfigParser) {
super(
codeOwnersPluginConfiguration,
@@ -60,6 +62,7 @@
metaDataUpdateInternalFactory,
retryHelper,
CODE_OWNER_CONFIG_FILE_NAME,
+ codeOwnerConfigFileFactory,
codeOwnerConfigParser);
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoCodeOwnerConfigParser.java
index 8b15ae9..a42d760 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoCodeOwnerConfigParser.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoCodeOwnerConfigParser.java
@@ -133,7 +133,7 @@
for (CodeOwnerSet codeOwnerSet : codeOwnerConfig.codeOwnerSets()) {
checkState(
!codeOwnerSet.ignoreGlobalAndParentCodeOwners(),
- "ignoreGlobaleAndParentCodeOwners is not supported");
+ "ignoreGlobalAndParentCodeOwners is not supported");
OwnerSet.Builder ownerSetProtoBuilder = ownersConfigProtoBuilder.addOwnerSetsBuilder();
ownerSetProtoBuilder.addAllPathExpressions(codeOwnerSet.pathExpressions());
codeOwnerSet.codeOwners().stream()
diff --git a/java/com/google/gerrit/plugins/codeowners/common/BUILD b/java/com/google/gerrit/plugins/codeowners/common/BUILD
new file mode 100644
index 0000000..d4a7385
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/common/BUILD
@@ -0,0 +1,11 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "common",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK + [
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java b/java/com/google/gerrit/plugins/codeowners/common/ChangedFile.java
similarity index 74%
rename from java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java
rename to java/com/google/gerrit/plugins/codeowners/common/ChangedFile.java
index 82655cf..267e3c7 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/common/ChangedFile.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.backend;
+package com.google.gerrit.plugins.codeowners.common;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
@@ -22,8 +22,9 @@
import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch;
-import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import java.nio.file.Path;
import java.util.Optional;
import org.eclipse.jgit.diff.DiffEntry;
@@ -161,4 +162,58 @@
private static Optional<Path> convertPathFromPatchListEntry(@Nullable String path) {
return Optional.ofNullable(path).map(newName -> JgitPath.of(newName).getAsAbsolutePath());
}
+
+ /**
+ * Creates a {@link ChangedFile} instance from a {@link FileDiffOutput}.
+ *
+ * @param fileDiffOutput the file diff output
+ */
+ public static ChangedFile create(FileDiffOutput fileDiffOutput) {
+ requireNonNull(fileDiffOutput, "fileDiffOutput");
+
+ return new AutoValue_ChangedFile(
+ convertPathFromFileDiffOutput(fileDiffOutput.newPath()),
+ convertPathFromFileDiffOutput(fileDiffOutput.oldPath()),
+ CHANGE_TYPE.get(fileDiffOutput.changeType()));
+ }
+
+ /** Converts the given string path to an absolute path. */
+ private static Optional<Path> convertPathFromFileDiffOutput(Optional<String> path) {
+ requireNonNull(path, "path");
+ return path.map(p -> JgitPath.of(p).getAsAbsolutePath());
+ }
+
+ public static ChangedFile create(
+ Optional<String> newPath, Optional<String> oldPath, ChangeType changeType) {
+ requireNonNull(changeType, "changeType");
+
+ return new AutoValue_ChangedFile(
+ newPath.map(JgitPath::of).map(JgitPath::getAsAbsolutePath),
+ oldPath.map(JgitPath::of).map(JgitPath::getAsAbsolutePath),
+ changeType);
+ }
+
+ public static ChangedFile addition(Path newPath) {
+ requireNonNull(newPath, "newPath");
+
+ return new AutoValue_ChangedFile(Optional.of(newPath), Optional.empty(), ChangeType.ADD);
+ }
+
+ public static ChangedFile modification(Path path) {
+ requireNonNull(path, "path");
+
+ return new AutoValue_ChangedFile(Optional.of(path), Optional.of(path), ChangeType.MODIFY);
+ }
+
+ public static ChangedFile deletion(Path path) {
+ requireNonNull(path, "path");
+
+ return new AutoValue_ChangedFile(Optional.empty(), Optional.of(path), ChangeType.DELETE);
+ }
+
+ public static ChangedFile rename(Path newPath, Path oldPath) {
+ requireNonNull(newPath, "newPath");
+
+ return new AutoValue_ChangedFile(Optional.of(newPath), Optional.of(oldPath), ChangeType.RENAME);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/common/CodeOwnerConfigValidationPolicy.java b/java/com/google/gerrit/plugins/codeowners/common/CodeOwnerConfigValidationPolicy.java
new file mode 100644
index 0000000..7b35079
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/common/CodeOwnerConfigValidationPolicy.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.common;
+
+/** Policy that should be used to validate code owner config files. */
+public enum CodeOwnerConfigValidationPolicy {
+ /**
+ * The code owner config file validation is enabled and invalid code owner config files are
+ * rejected.
+ *
+ * <p>If the code owners functionality is disabled, no validation is performed.
+ */
+ TRUE,
+
+ /**
+ * The code owner config file validation is disabled. Invalid code owner config files are not
+ * rejected.
+ */
+ FALSE,
+
+ /**
+ * Code owner config files are validated, but invalid code owner config files are not rejected.
+ *
+ * <p>If the code owners functionality is disabled, no dry-run validation is performed.
+ */
+ DRY_RUN,
+
+ /**
+ * Code owner config files are validated even if the code owners functionality is disabled.
+ *
+ * <p>This option is useful when the code owner config validation should be enabled as preparation
+ * to enabling the code owners functionality.
+ */
+ FORCED,
+
+ /**
+ * Code owner config files are validated even if the code owners functionality is disabled, but
+ * invalid code owner config files are not rejected.
+ *
+ * <p>This option is useful when the code owner config validation should be enabled as preparation
+ * to enabling the code owners functionality.
+ */
+ FORCED_DRY_RUN;
+
+ public boolean isDryRun() {
+ return this == CodeOwnerConfigValidationPolicy.DRY_RUN
+ || this == CodeOwnerConfigValidationPolicy.FORCED_DRY_RUN;
+ }
+
+ public boolean runValidation() {
+ return this != CodeOwnerConfigValidationPolicy.FALSE;
+ }
+
+ public boolean isForced() {
+ return this == CodeOwnerConfigValidationPolicy.FORCED
+ || this == CodeOwnerConfigValidationPolicy.FORCED_DRY_RUN;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatus.java b/java/com/google/gerrit/plugins/codeowners/common/CodeOwnerStatus.java
similarity index 95%
rename from java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatus.java
rename to java/com/google/gerrit/plugins/codeowners/common/CodeOwnerStatus.java
index 1873b23..3af1c44 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerStatus.java
+++ b/java/com/google/gerrit/plugins/codeowners/common/CodeOwnerStatus.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.common;
/** Code owner status for a path in a change. */
public enum CodeOwnerStatus {
diff --git a/java/com/google/gerrit/plugins/codeowners/api/MergeCommitStrategy.java b/java/com/google/gerrit/plugins/codeowners/common/MergeCommitStrategy.java
similarity index 91%
rename from java/com/google/gerrit/plugins/codeowners/api/MergeCommitStrategy.java
rename to java/com/google/gerrit/plugins/codeowners/common/MergeCommitStrategy.java
index 96894a9..ca08938 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/MergeCommitStrategy.java
+++ b/java/com/google/gerrit/plugins/codeowners/common/MergeCommitStrategy.java
@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners.api;
+package com.google.gerrit.plugins.codeowners.common;
/** Strategy that defines for merge commits which files require code owner approvals. */
public enum MergeCommitStrategy {
/**
- * All files which differ between the merge commmit that is being reviewed and its first parent
+ * All files which differ between the merge commit that is being reviewed and its first parent
* commit (which is the HEAD of the destination branch) require code owner approvals.
*
* <p>Using this strategy is the safest option, but requires code owners to also approve files
@@ -30,7 +30,7 @@
ALL_CHANGED_FILES,
/**
- * Only files which differ between the merge commmit that is being reviewed and the auto merge
+ * Only files which differ between the merge commit that is being reviewed and the auto merge
* commit (the result of automatically merging the 2 parent commits, may contain Git conflict
* markers) require code owner approvals.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
deleted file mode 100644
index 1a7985d..0000000
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
+++ /dev/null
@@ -1,512 +0,0 @@
-// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.config;
-
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
-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.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigValidationPolicy;
-import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
-import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
-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.Singleton;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * The configuration of the code-owners plugin.
- *
- * <p>The global configuration of the code-owners plugin is stored in the {@code gerrit.config} file
- * in the {@code plugin.code-owners} subsection.
- *
- * <p>In addition there is configuration on project level that is stored in {@code
- * code-owners.config} files that are stored in the {@code refs/meta/config} branches of the
- * projects.
- *
- * <p>Parameters that are not set for a project are inherited from the parent project.
- */
-@Singleton
-public class CodeOwnersPluginConfiguration {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- @VisibleForTesting public static final String SECTION_CODE_OWNERS = "codeOwners";
-
- @VisibleForTesting
- static final String KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS = "enableExperimentalRestEndpoints";
-
- private final String pluginName;
- private final PluginConfigFactory pluginConfigFactory;
- private final ProjectCache projectCache;
- private final GeneralConfig generalConfig;
- private final StatusConfig statusConfig;
- private final BackendConfig backendConfig;
- private final RequiredApprovalConfig requiredApprovalConfig;
- private final OverrideApprovalConfig overrideApprovalConfig;
-
- @Inject
- CodeOwnersPluginConfiguration(
- @PluginName String pluginName,
- PluginConfigFactory pluginConfigFactory,
- ProjectCache projectCache,
- GeneralConfig generalConfig,
- StatusConfig statusConfig,
- BackendConfig backendConfig,
- RequiredApprovalConfig requiredApprovalConfig,
- OverrideApprovalConfig overrideApprovalConfig) {
- this.pluginName = pluginName;
- this.pluginConfigFactory = pluginConfigFactory;
- this.projectCache = projectCache;
- this.generalConfig = generalConfig;
- this.statusConfig = statusConfig;
- this.backendConfig = backendConfig;
- this.requiredApprovalConfig = requiredApprovalConfig;
- this.overrideApprovalConfig = overrideApprovalConfig;
- }
-
- /**
- * Gets the file extension that is configured for the given project.
- *
- * @param project the project for which the configured file extension should be returned
- * @return the file extension that is configured for the given project
- */
- public Optional<String> getFileExtension(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getFileExtension(getPluginConfig(project));
- }
-
- /**
- * Checks whether code owner configs in the given project are read-only.
- *
- * @param project the project for it should be checked whether code owner configs are read-only
- * @return whether code owner configs in the given project are read-only
- */
- public boolean areCodeOwnerConfigsReadOnly(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getReadOnly(getPluginConfig(project));
- }
-
- /**
- * Whether code owner configs should be validated when a commit is received.
- *
- * @param project the project for it should be checked whether code owner configs should be
- * validated when a commit is received
- * @return whether code owner configs should be validated when a commit is received
- */
- public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
- Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getCodeOwnerConfigValidationPolicyForCommitReceived(
- project, getPluginConfig(project));
- }
-
- /**
- * Whether code owner configs should be validated when a change is submitted.
- *
- * @param project the project for it should be checked whether code owner configs should be
- * validated when a change is submitted
- * @return whether code owner configs should be validated when a change is submitted
- */
- public CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
- Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getCodeOwnerConfigValidationPolicyForSubmit(
- project, getPluginConfig(project));
- }
-
- /**
- * Gets the merge commit strategy for the given project.
- *
- * @param project the project for which the merge commit strategy should be retrieved
- * @return the merge commit strategy for the given project
- */
- public MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getMergeCommitStrategy(project, getPluginConfig(project));
- }
-
- /**
- * Gets the fallback code owners for the given project.
- *
- * @param project the project for which the fallback code owners should be retrieved
- * @return the fallback code owners for the given project
- */
- public FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getFallbackCodeOwners(project, getPluginConfig(project));
- }
-
- /**
- * Checks whether an implicit code owner approval from the last uploader is assumed.
- *
- * @param project the project for it should be checked whether implict approvals are enabled
- * @return whether an implicit code owner approval from the last uploader is assumed
- */
- public boolean areImplicitApprovalsEnabled(Project.NameKey project) {
- requireNonNull(project, "project");
- LabelType requiredLabel = getRequiredApproval(project).labelType();
- if (requiredLabel.isIgnoreSelfApproval()) {
- logger.atFine().log(
- "ignoring implicit approval configuration on project %s since the label of the required"
- + " approval (%s) is configured to ignore self approvals",
- project, requiredLabel);
- return false;
- }
- return generalConfig.getEnableImplicitApprovals(getPluginConfig(project));
- }
-
- /**
- * Gets the global code owners of the given project.
- *
- * @param project the project for which the global code owners should be returned
- * @return the global code owners of the given project
- */
- public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getGlobalCodeOwners(getPluginConfig(project));
- }
-
- /**
- * Gets the override info URL that is configured for the given project.
- *
- * @param project the project for which the configured override info URL should be returned
- * @return the override info URL that is configured for the given project
- */
- public Optional<String> getOverrideInfoUrl(Project.NameKey project) {
- requireNonNull(project, "project");
- return generalConfig.getOverrideInfoUrl(getPluginConfig(project));
- }
-
- /**
- * Returns the email domains that are allowed to be used for code owners.
- *
- * @return the email domains that are allowed to be used for code owners, an empty set if all
- * email domains are allowed (if {@code plugin.code-owners.allowedEmailDomain} is not set or
- * set to an empty value)
- */
- public ImmutableSet<String> getAllowedEmailDomains() {
- return generalConfig.getAllowedEmailDomains();
- }
-
- /**
- * Whether the code owners functionality is disabled for the given branch.
- *
- * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
- * exist the call fails with {@link IllegalStateException}.
- *
- * <p>The configuration is evaluated in the following order:
- *
- * <ul>
- * <li>disabled configuration for the branch (with inheritance)
- * <li>disabled configuration for the project (with inheritance)
- * <li>hard-coded default (not disabled)
- * </ul>
- *
- * <p>The first disabled configuration that exists counts and the evaluation is stopped.
- *
- * @param branchNameKey the branch and project for which it should be checked whether the code
- * owners functionality is disabled
- * @return {@code true} if the code owners functionality is disabled for the given branch,
- * otherwise {@code false}
- */
- public boolean isDisabled(BranchNameKey branchNameKey) {
- requireNonNull(branchNameKey, "branchNameKey");
-
- Config pluginConfig = getPluginConfig(branchNameKey.project());
-
- boolean isDisabled = statusConfig.isDisabledForBranch(pluginConfig, branchNameKey);
- if (isDisabled) {
- return true;
- }
-
- return isDisabled(branchNameKey.project());
- }
-
- /**
- * Whether the code owners functionality is disabled for the given project.
- *
- * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
- * exist the call fails with {@link IllegalStateException}.
- *
- * <p>The configuration is evaluated in the following order:
- *
- * <ul>
- * <li>disabled configuration for the project (with inheritance)
- * <li>hard-coded default (not disabled)
- * </ul>
- *
- * <p>The first disabled configuration that exists counts and the evaluation is stopped.
- *
- * @param project the project for which it should be checked whether the code owners functionality
- * is disabled
- * @return {@code true} if the code owners functionality is disabled for the given project,
- * otherwise {@code false}
- */
- public boolean isDisabled(Project.NameKey project) {
- requireNonNull(project, "project");
-
- Config pluginConfig = getPluginConfig(project);
- return statusConfig.isDisabledForProject(pluginConfig, project);
- }
-
- /**
- * Returns the configured {@link CodeOwnerBackend} for the given branch.
- *
- * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
- * exist the call fails with {@link IllegalStateException}.
- *
- * <p>The code owner backend configuration is evaluated in the following order:
- *
- * <ul>
- * <li>backend configuration for branch (with inheritance, first by full branch name, then by
- * short branch name)
- * <li>backend configuration for project (with inheritance)
- * <li>default backend (first globally configured backend, then hard-coded default backend)
- * </ul>
- *
- * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
- *
- * @param branchNameKey project and branch for which the configured code owner backend should be
- * returned
- * @return the {@link CodeOwnerBackend} that should be used for the branch
- */
- public CodeOwnerBackend getBackend(BranchNameKey branchNameKey) {
- Config pluginConfig = getPluginConfig(branchNameKey.project());
-
- // check if a branch specific backend is configured
- Optional<CodeOwnerBackend> codeOwnerBackend =
- backendConfig.getBackendForBranch(pluginConfig, branchNameKey);
- if (codeOwnerBackend.isPresent()) {
- return codeOwnerBackend.get();
- }
-
- return getBackend(branchNameKey.project());
- }
-
- /**
- * Returns the configured {@link CodeOwnerBackend} for the given project.
- *
- * <p>Callers must ensure that the project exists. If the project doesn't exist the call fails
- * with {@link IllegalStateException}.
- *
- * <p>The code owner backend configuration is evaluated in the following order:
- *
- * <ul>
- * <li>backend configuration for project (with inheritance)
- * <li>default backend (first globally configured backend, then hard-coded default backend)
- * </ul>
- *
- * <p>The first code owner backend configuration that exists counts and the evaluation is stopped.
- *
- * @param project project for which the configured code owner backend should be returned
- * @return the {@link CodeOwnerBackend} that should be used for the project
- */
- public CodeOwnerBackend getBackend(Project.NameKey project) {
- Config pluginConfig = getPluginConfig(project);
-
- // check if a project specific backend is configured
- Optional<CodeOwnerBackend> codeOwnerBackend =
- backendConfig.getBackendForProject(pluginConfig, project);
- if (codeOwnerBackend.isPresent()) {
- return codeOwnerBackend.get();
- }
-
- // fall back to the default backend
- return backendConfig.getDefaultBackend();
- }
-
- /**
- * Returns the approval that is required from code owners to approve the files in a change of the
- * given project.
- *
- * <p>Defines which approval counts as code owner approval.
- *
- * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
- * exist the call fails with {@link IllegalStateException}.
- *
- * <p>The code owner required approval configuration is evaluated in the following order:
- *
- * <ul>
- * <li>required approval configuration for project (with inheritance)
- * <li>globally configured required approval
- * <li>hard-coded default required approval
- * </ul>
- *
- * <p>The first required code owner approval configuration that exists counts and the evaluation
- * is stopped.
- *
- * <p>If the code owner configuration contains multiple required approvals values, the last value
- * is used.
- *
- * @param project project for which the required approval should be returned
- * @return the required code owner approval that should be used for the given project
- */
- public RequiredApproval getRequiredApproval(Project.NameKey project) {
- ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
- getConfiguredRequiredApproval(requiredApprovalConfig, project);
- if (!configuredRequiredApprovalConfig.isEmpty()) {
- // There can be only one required approval. If multiple ones are configured just use the last
- // one, this is also what Config#getString(String, String, String) does.
- return Iterables.getLast(configuredRequiredApprovalConfig);
- }
-
- // fall back to hard-coded default required approval
- ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
- return requiredApprovalConfig.createDefault(projectState);
- }
-
- /**
- * Returns the approvals that are required to override the code owners submit check for a change
- * of the given project.
- *
- * <p>If multiple approvals are returned, any of them is sufficient to override the code owners
- * submit check.
- *
- * <p>Callers must ensure that the project of the specified branch exists. If the project doesn't
- * exist the call fails with {@link IllegalStateException}.
- *
- * <p>The override approval configuration is evaluated in the following order:
- *
- * <ul>
- * <li>override approval configuration for project (with inheritance)
- * <li>globally configured override approval
- * </ul>
- *
- * <p>The first override approval configuration that exists counts and the evaluation is stopped.
- *
- * @param project project for which the override approval should be returned
- * @return the override approvals that should be used for the given project, an empty set if no
- * override approval is configured, in this case the override functionality is disabled
- */
- public ImmutableSet<RequiredApproval> getOverrideApproval(Project.NameKey project) {
- try {
- return filterOutDuplicateRequiredApprovals(
- getConfiguredRequiredApproval(overrideApprovalConfig, project));
- } catch (InvalidPluginConfigurationException e) {
- logger.atWarning().withCause(e).log(
- "Ignoring invalid override approval configuration for project %s."
- + " Overrides are disabled.",
- project.get());
- }
-
- return ImmutableSet.of();
- }
-
- /**
- * Filters out duplicate required approvals from the input list.
- *
- * <p>The following entries are considered as duplicate:
- *
- * <ul>
- * <li>exact identical required approvals (e.g. "Code-Review+2" and "Code-Review+2")
- * <li>required approvals with the same label name and a higher value (e.g. "Code-Review+2" is
- * not needed if "Code-Review+1" is already contained, since "Code-Review+1" covers all
- * "Code-Review" approvals >= 1)
- * </ul>
- */
- private ImmutableSet<RequiredApproval> filterOutDuplicateRequiredApprovals(
- ImmutableList<RequiredApproval> requiredApprovals) {
- Map<String, RequiredApproval> requiredApprovalsByLabel = new HashMap<>();
- for (RequiredApproval requiredApproval : requiredApprovals) {
- String labelName = requiredApproval.labelType().getName();
- RequiredApproval otherRequiredApproval = requiredApprovalsByLabel.get(labelName);
- if (otherRequiredApproval != null
- && otherRequiredApproval.value() <= requiredApproval.value()) {
- continue;
- }
- requiredApprovalsByLabel.put(labelName, requiredApproval);
- }
- return ImmutableSet.copyOf(requiredApprovalsByLabel.values());
- }
-
- /**
- * Gets the required approvals that are configured for the given project.
- *
- * @param requiredApprovalConfig the config from which the required approvals should be read
- * @param project the project for which the configured required approvals should be returned
- * @return the required approvals that is configured for the given project, an empty list if no
- * required approvals are configured
- */
- private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
- AbstractRequiredApprovalConfig requiredApprovalConfig, Project.NameKey project) {
- Config pluginConfig = getPluginConfig(project);
- ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
- return requiredApprovalConfig.get(projectState, pluginConfig);
- }
-
- /**
- * Checks whether experimental REST endpoints are enabled.
- *
- * @throws MethodNotAllowedException thrown if experimental REST endpoints are disabled
- */
- public void checkExperimentalRestEndpointsEnabled() throws MethodNotAllowedException {
- if (!areExperimentalRestEndpointsEnabled()) {
- throw new MethodNotAllowedException("experimental code owners REST endpoints are disabled");
- }
- }
-
- /** Whether experimental REST endpoints are enabled. */
- public boolean areExperimentalRestEndpointsEnabled() {
- try {
- return pluginConfigFactory
- .getFromGerritConfig(pluginName)
- .getBoolean(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS, /* defaultValue= */ false);
- } catch (IllegalArgumentException e) {
- logger.atWarning().withCause(e).log(
- "Value '%s' in gerrit.config (parameter plugin.%s.%s) is invalid.",
- pluginConfigFactory
- .getFromGerritConfig(pluginName)
- .getString(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS),
- pluginName,
- KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS);
- return false;
- }
- }
-
- /**
- * Reads and returns the config from the {@code code-owners.config} file in {@code
- * refs/meta/config} branch of the given project.
- *
- * @param project the project for which the code owners configurations should be returned
- * @return the code owners configurations for the given project
- */
- private Config getPluginConfig(Project.NameKey project) {
- try {
- return pluginConfigFactory.getProjectPluginConfigWithInheritance(project, pluginName);
- } catch (NoSuchProjectException e) {
- throw new IllegalStateException(
- String.format(
- "cannot get %s plugin config for non-existing project %s", pluginName, project),
- e);
- }
- }
-}
diff --git a/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
deleted file mode 100644
index 85fd19a..0000000
--- a/java/com/google/gerrit/plugins/codeowners/config/GeneralConfig.java
+++ /dev/null
@@ -1,413 +0,0 @@
-// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.config;
-
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigValidationPolicy;
-import com.google.gerrit.plugins.codeowners.api.MergeCommitStrategy;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
-import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
-import com.google.gerrit.server.config.PluginConfig;
-import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.project.ProjectLevelConfig;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Class to read the general code owners configuration from {@code gerrit.config} and from {@code
- * code-owners.config} in {@code refs/meta/config}.
- *
- * <p>Default values are configured in {@code gerrit.config}.
- *
- * <p>The default values can be overridden on project-level in {@code code-owners.config} in {@code
- * refs/meta/config}.
- *
- * <p>Projects that have no configuration inherit the configuration from their parent projects.
- */
-@Singleton
-public class GeneralConfig {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- @VisibleForTesting public static final String KEY_ALLOWED_EMAIL_DOMAIN = "allowedEmailDomain";
- @VisibleForTesting public static final String KEY_FILE_EXTENSION = "fileExtension";
- @VisibleForTesting public static final String KEY_READ_ONLY = "readOnly";
- @VisibleForTesting public static final String KEY_FALLBACK_CODE_OWNERS = "fallbackCodeOwners";
-
- @VisibleForTesting
- public static final String KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED =
- "enableValidationOnCommitReceived";
-
- @VisibleForTesting
- public static final String KEY_ENABLE_VALIDATION_ON_SUBMIT = "enableValidationOnSubmit";
-
- @VisibleForTesting public static final String KEY_MERGE_COMMIT_STRATEGY = "mergeCommitStrategy";
- @VisibleForTesting public static final String KEY_GLOBAL_CODE_OWNER = "globalCodeOwner";
-
- @VisibleForTesting
- public static final String KEY_ENABLE_IMPLICIT_APPROVALS = "enableImplicitApprovals";
-
- @VisibleForTesting public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl";
-
- private final String pluginName;
- private final PluginConfig pluginConfigFromGerritConfig;
-
- @Inject
- GeneralConfig(@PluginName String pluginName, PluginConfigFactory pluginConfigFactory) {
- this.pluginName = pluginName;
- this.pluginConfigFromGerritConfig = pluginConfigFactory.getFromGerritConfig(pluginName);
- }
-
- /**
- * Validates the backend configuration in the given project level configuration.
- *
- * @param fileName the name of the config file
- * @param projectLevelConfig the project level plugin configuration
- * @return list of validation messages for validation errors, empty list if there are no
- * validation errors
- */
- ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
- String fileName, ProjectLevelConfig.Bare projectLevelConfig) {
- requireNonNull(fileName, "fileName");
- requireNonNull(projectLevelConfig, "projectLevelConfig");
-
- List<CommitValidationMessage> validationMessages = new ArrayList<>();
-
- try {
- projectLevelConfig
- .getConfig()
- .getEnum(
- SECTION_CODE_OWNERS,
- null,
- KEY_MERGE_COMMIT_STRATEGY,
- MergeCommitStrategy.ALL_CHANGED_FILES);
- } catch (IllegalArgumentException e) {
- validationMessages.add(
- new CommitValidationMessage(
- String.format(
- "Merge commit strategy '%s' that is configured in %s (parameter %s.%s) is invalid.",
- projectLevelConfig
- .getConfig()
- .getString(SECTION_CODE_OWNERS, null, KEY_MERGE_COMMIT_STRATEGY),
- fileName,
- SECTION_CODE_OWNERS,
- KEY_MERGE_COMMIT_STRATEGY),
- ValidationMessage.Type.ERROR));
- }
-
- try {
- projectLevelConfig
- .getConfig()
- .getEnum(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
- } catch (IllegalArgumentException e) {
- validationMessages.add(
- new CommitValidationMessage(
- String.format(
- "The value for fallback code owners '%s' that is configured in %s (parameter %s.%s) is invalid.",
- projectLevelConfig
- .getConfig()
- .getString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS),
- fileName,
- SECTION_CODE_OWNERS,
- KEY_FALLBACK_CODE_OWNERS),
- ValidationMessage.Type.ERROR));
- }
-
- return ImmutableList.copyOf(validationMessages);
- }
-
- /**
- * Gets the file extension that should be used for code owner config files in the given project.
- *
- * @param pluginConfig the plugin config from which the file extension should be read.
- * @return the file extension that should be used for code owner config files in the given
- * project, {@link Optional#empty()} if no file extension should be used
- */
- Optional<String> getFileExtension(Config pluginConfig) {
- requireNonNull(pluginConfig, "pluginConfig");
-
- String fileExtension = pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_FILE_EXTENSION);
- if (fileExtension != null) {
- return Optional.of(fileExtension);
- }
-
- return Optional.ofNullable(pluginConfigFromGerritConfig.getString(KEY_FILE_EXTENSION));
- }
-
- /**
- * Returns the email domains that are allowed to be used for code owners.
- *
- * @return the email domains that are allowed to be used for code owners, an empty set if all
- * email domains are allowed (if {@code plugin.code-owners.allowedEmailDomain} is not set or
- * set to an empty value)
- */
- ImmutableSet<String> getAllowedEmailDomains() {
- return Arrays.stream(pluginConfigFromGerritConfig.getStringList(KEY_ALLOWED_EMAIL_DOMAIN))
- .filter(emailDomain -> !Strings.isNullOrEmpty(emailDomain))
- .distinct()
- .collect(toImmutableSet());
- }
-
- /**
- * Gets the read-only configuration from the given plugin config with fallback to {@code
- * gerrit.config}.
- *
- * <p>The read-only controls whether code owner config files are read-only and all modifications
- * of code owner config files should be rejected.
- *
- * @param pluginConfig the plugin config from which the read-only configuration should be read.
- * @return whether code owner config files are read-only
- */
- boolean getReadOnly(Config pluginConfig) {
- requireNonNull(pluginConfig, "pluginConfig");
-
- if (pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_READ_ONLY) != null) {
- return pluginConfig.getBoolean(SECTION_CODE_OWNERS, null, KEY_READ_ONLY, false);
- }
-
- return pluginConfigFromGerritConfig.getBoolean(KEY_READ_ONLY, false);
- }
-
- /**
- * Gets the fallback code owners that own paths that have no defined code owners.
- *
- * @param project the project for which the fallback code owners should be read
- * @param pluginConfig the plugin config from which the fallback code owners should be read
- * @return the fallback code owners that own paths that have no defined code owners
- */
- FallbackCodeOwners getFallbackCodeOwners(Project.NameKey project, Config pluginConfig) {
- requireNonNull(project, "project");
- requireNonNull(pluginConfig, "pluginConfig");
-
- String fallbackCodeOwnersString =
- pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS);
- if (fallbackCodeOwnersString != null) {
- try {
- return pluginConfig.getEnum(
- SECTION_CODE_OWNERS, null, KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for fallback code owners in '%s.config' of project %s."
- + " Falling back to global config.",
- fallbackCodeOwnersString, pluginName, project.get());
- }
- }
-
- try {
- return pluginConfigFromGerritConfig.getEnum(
- KEY_FALLBACK_CODE_OWNERS, FallbackCodeOwners.NONE);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for fallback code owners in gerrit.config (parameter"
- + " plugin.%s.%s). Falling back to default value %s.",
- pluginConfigFromGerritConfig.getString(KEY_FALLBACK_CODE_OWNERS),
- pluginName,
- KEY_FALLBACK_CODE_OWNERS,
- FallbackCodeOwners.NONE);
- return FallbackCodeOwners.NONE;
- }
- }
-
- /**
- * Gets the enable validation on commit received configuration from the given plugin config with
- * fallback to {@code gerrit.config} and default to {@code true}.
- *
- * <p>The enable validation on commit received controls whether code owner config files should be
- * validated when a commit is received.
- *
- * @param pluginConfig the plugin config from which the enable validation on commit received
- * configuration should be read.
- * @return whether code owner config files should be validated when a commit is received
- */
- CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForCommitReceived(
- Project.NameKey project, Config pluginConfig) {
- return getCodeOwnerConfigValidationPolicy(
- KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED, project, pluginConfig);
- }
-
- /**
- * Gets the enable validation on submit configuration from the given plugin config with fallback
- * to {@code gerrit.config} and default to {@code true}.
- *
- * <p>The enable validation on submit controls whether code owner config files should be validated
- * when a change is submitted.
- *
- * @param pluginConfig the plugin config from which the enable validation on submit configuration
- * should be read.
- * @return whether code owner config files should be validated when a change is submitted
- */
- CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicyForSubmit(
- Project.NameKey project, Config pluginConfig) {
- return getCodeOwnerConfigValidationPolicy(
- KEY_ENABLE_VALIDATION_ON_SUBMIT, project, pluginConfig);
- }
-
- private CodeOwnerConfigValidationPolicy getCodeOwnerConfigValidationPolicy(
- String key, Project.NameKey project, Config pluginConfig) {
- requireNonNull(key, "key");
- requireNonNull(project, "project");
- requireNonNull(pluginConfig, "pluginConfig");
-
- String codeOwnerConfigValidationPolicyString =
- pluginConfig.getString(SECTION_CODE_OWNERS, null, key);
- if (codeOwnerConfigValidationPolicyString != null) {
- try {
- return pluginConfig.getEnum(
- SECTION_CODE_OWNERS, null, key, CodeOwnerConfigValidationPolicy.TRUE);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for the code owner config validation policy in '%s.config'"
- + " of project %s. Falling back to global config.",
- codeOwnerConfigValidationPolicyString, pluginName, project.get());
- }
- }
-
- try {
- return pluginConfigFromGerritConfig.getEnum(key, CodeOwnerConfigValidationPolicy.TRUE);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for the code owner config validation policy in gerrit.config"
- + " (parameter plugin.%s.%s). Falling back to default value %s.",
- pluginConfigFromGerritConfig.getString(key),
- pluginName,
- key,
- CodeOwnerConfigValidationPolicy.TRUE);
- return CodeOwnerConfigValidationPolicy.TRUE;
- }
- }
-
- /**
- * Gets the merge commit strategy from the given plugin config with fallback to {@code
- * gerrit.config}.
- *
- * <p>The merge commit strategy defines for merge commits which files require code owner
- * approvals.
- *
- * @param project the name of the project for which the merge commit strategy should be read
- * @param pluginConfig the plugin config from which the merge commit strategy should be read
- * @return the merge commit strategy that should be used
- */
- MergeCommitStrategy getMergeCommitStrategy(Project.NameKey project, Config pluginConfig) {
- requireNonNull(project, "project");
- requireNonNull(pluginConfig, "pluginConfig");
-
- String mergeCommitStrategyString =
- pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_MERGE_COMMIT_STRATEGY);
- if (mergeCommitStrategyString != null) {
- try {
- return pluginConfig.getEnum(
- SECTION_CODE_OWNERS,
- null,
- KEY_MERGE_COMMIT_STRATEGY,
- MergeCommitStrategy.ALL_CHANGED_FILES);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for merge commit stategy in '%s.config' of project %s."
- + " Falling back to global config or default value.",
- mergeCommitStrategyString, pluginName, project.get());
- }
- }
-
- try {
- return pluginConfigFromGerritConfig.getEnum(
- KEY_MERGE_COMMIT_STRATEGY, MergeCommitStrategy.ALL_CHANGED_FILES);
- } catch (IllegalArgumentException e) {
- logger.atWarning().log(
- "Ignoring invalid value %s for merge commit stategy in gerrit.config (parameter plugin.%s.%s)."
- + " Falling back to default value %s.",
- pluginConfigFromGerritConfig.getString(KEY_MERGE_COMMIT_STRATEGY),
- pluginName,
- KEY_MERGE_COMMIT_STRATEGY,
- MergeCommitStrategy.ALL_CHANGED_FILES);
- return MergeCommitStrategy.ALL_CHANGED_FILES;
- }
- }
-
- /**
- * Gets whether an implicit code owner approval from the last uploader is assumed from the given
- * plugin config with fallback to {@code gerrit.config}.
- *
- * @param pluginConfig the plugin config from which the configuration should be read.
- * @return whether an implicit code owner approval from the last uploader is assumed
- */
- boolean getEnableImplicitApprovals(Config pluginConfig) {
- requireNonNull(pluginConfig, "pluginConfig");
-
- return pluginConfig.getBoolean(
- SECTION_CODE_OWNERS,
- null,
- KEY_ENABLE_IMPLICIT_APPROVALS,
- pluginConfigFromGerritConfig.getBoolean(KEY_ENABLE_IMPLICIT_APPROVALS, false));
- }
-
- /**
- * Gets the users which are configured as global code owners from the given plugin config with
- * fallback to {@code gerrit.config}.
- *
- * @param pluginConfig the plugin config from which the global code owners should be read.
- * @return the users which are configured as global code owners
- */
- ImmutableSet<CodeOwnerReference> getGlobalCodeOwners(Config pluginConfig) {
- requireNonNull(pluginConfig, "pluginConfig");
-
- if (pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_GLOBAL_CODE_OWNER) != null) {
- return Arrays.stream(
- pluginConfig.getStringList(SECTION_CODE_OWNERS, null, KEY_GLOBAL_CODE_OWNER))
- .map(CodeOwnerReference::create)
- .collect(toImmutableSet());
- }
-
- return Arrays.stream(pluginConfigFromGerritConfig.getStringList(KEY_GLOBAL_CODE_OWNER))
- .map(CodeOwnerReference::create)
- .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
- * gerrit.config}.
- *
- * @param pluginConfig the plugin config from which the override info URL should be read.
- * @return URL that leads to an information page about overrides, {@link Optional#empty()} if no
- * such URL is configured
- */
- Optional<String> getOverrideInfoUrl(Config pluginConfig) {
- requireNonNull(pluginConfig, "pluginConfig");
-
- String fileExtension = pluginConfig.getString(SECTION_CODE_OWNERS, null, KEY_OVERRIDE_INFO_URL);
- if (fileExtension != null) {
- return Optional.of(fileExtension);
- }
-
- return Optional.ofNullable(pluginConfigFromGerritConfig.getString(KEY_OVERRIDE_INFO_URL));
- }
-}
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/BUILD b/java/com/google/gerrit/plugins/codeowners/metrics/BUILD
new file mode 100644
index 0000000..1f1ad14
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/BUILD
@@ -0,0 +1,9 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "metrics",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK,
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
new file mode 100644
index 0000000..89fac59
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -0,0 +1,254 @@
+// Copyright (C) 2021 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.google.gerrit.plugins.codeowners.metrics;
+
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram0;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Metrics of the code-owners plugin. */
+@Singleton
+public class CodeOwnerMetrics {
+ // latency metrics
+ public final Timer0 addChangeMessageOnAddReviewer;
+ public final Timer0 computeChangedFiles;
+ public final Timer0 computeFileStatus;
+ public final Timer0 computeFileStatuses;
+ public final Timer0 computeOwnedPaths;
+ public final Timer0 computePatchSetApprovals;
+ public final Timer0 extendChangeMessageOnPostReview;
+ public final Timer0 getAutoMerge;
+ public final Timer0 getChangedFiles;
+ public final Timer0 prepareFileStatusComputation;
+ public final Timer0 prepareFileStatusComputationForAccount;
+ public final Timer0 resolveCodeOwnerConfig;
+ public final Timer0 resolveCodeOwnerConfigImport;
+ public final Timer0 resolveCodeOwnerConfigImports;
+ public final Timer0 resolveCodeOwnerReferences;
+ public final Timer0 resolvePathCodeOwners;
+ public final Timer0 runCodeOwnerSubmitRule;
+
+ // code owner config metrics
+ public final Histogram0 codeOwnerConfigBackendReadsPerChange;
+ public final Histogram0 codeOwnerConfigCacheReadsPerChange;
+ public final Timer1<String> loadCodeOwnerConfig;
+ public final Timer0 readCodeOwnerConfig;
+ public final Timer1<String> parseCodeOwnerConfig;
+
+ // counter metrics
+ public final Counter0 countCodeOwnerConfigReads;
+ public final Counter0 countCodeOwnerConfigCacheReads;
+ public final Counter3<ValidationTrigger, ValidationResult, Boolean>
+ countCodeOwnerConfigValidations;
+ public final Counter1<String> countCodeOwnerSubmitRuleErrors;
+ public final Counter0 countCodeOwnerSubmitRuleRuns;
+ public final Counter1<Boolean> countCodeOwnerSuggestions;
+ public final Counter3<String, String, String> countInvalidCodeOwnerConfigFiles;
+
+ private final MetricMaker metricMaker;
+
+ @Inject
+ CodeOwnerMetrics(MetricMaker metricMaker) {
+ this.metricMaker = metricMaker;
+
+ // latency metrics
+ this.addChangeMessageOnAddReviewer =
+ createLatencyTimer(
+ "add_change_message_on_add_reviewer",
+ "Latency for adding a change message with the owned path when a code owner is added as"
+ + " a reviewer");
+ this.computeChangedFiles =
+ createLatencyTimer("compute_changed_files", "Latency for computing changed files");
+ this.computeFileStatus =
+ createLatencyTimer(
+ "compute_file_status", "Latency for computing the file status of one file");
+ this.computeFileStatuses =
+ createLatencyTimer(
+ "compute_file_statuses",
+ "Latency for computing file statuses for all files in a change");
+ this.computeOwnedPaths =
+ createLatencyTimer(
+ "compute_owned_paths",
+ "Latency for computing the files in a change that are owned by a user");
+ this.computePatchSetApprovals =
+ createLatencyTimer(
+ "compute_patch_set_approvals",
+ "Latency for computing the approvals of the current patch set");
+ this.extendChangeMessageOnPostReview =
+ createLatencyTimer(
+ "extend_change_message_on_post_review",
+ "Latency for extending the change message with the owned path when a code owner"
+ + " approval is applied");
+ this.getAutoMerge =
+ createLatencyTimer(
+ "get_auto_merge", "Latency for getting the auto merge commit of a merge commit");
+ this.getChangedFiles =
+ createLatencyTimer(
+ "get_changed_files", "Latency for getting changed files from diff cache");
+ this.prepareFileStatusComputation =
+ createLatencyTimer(
+ "prepare_file_status_computation", "Latency for preparing the file status computation");
+ this.prepareFileStatusComputationForAccount =
+ createLatencyTimer(
+ "compute_file_statuses_for_account",
+ "Latency for computing file statuses for an account");
+ this.resolveCodeOwnerConfig =
+ createLatencyTimer(
+ "resolve_code_owner_config", "Latency for resolving a code owner config file");
+ this.resolveCodeOwnerConfigImport =
+ createLatencyTimer(
+ "resolve_code_owner_config_import",
+ "Latency for resolving an import of a code owner config file");
+ this.resolveCodeOwnerConfigImports =
+ createLatencyTimer(
+ "resolve_code_owner_config_imports",
+ "Latency for resolving all imports of a code owner config file");
+ this.resolveCodeOwnerReferences =
+ createLatencyTimer(
+ "resolve_code_owner_references", "Latency for resolving the code owner references");
+ this.resolvePathCodeOwners =
+ createLatencyTimer(
+ "resolve_path_code_owners", "Latency for resolving the code owners of a path");
+ this.runCodeOwnerSubmitRule =
+ createLatencyTimer(
+ "run_code_owner_submit_rule", "Latency for running the code owner submit rule");
+
+ // code owner config metrics
+ this.codeOwnerConfigBackendReadsPerChange =
+ createHistogram(
+ "code_owner_config_backend_reads_per_change",
+ "Number of code owner config backend reads per change");
+ this.codeOwnerConfigCacheReadsPerChange =
+ createHistogram(
+ "code_owner_config_cache_reads_per_change",
+ "Number of code owner config cache reads per change");
+ this.loadCodeOwnerConfig =
+ createTimerWithClassField(
+ "load_code_owner_config",
+ "Latency for loading a code owner config file (read + parse)",
+ "backend");
+ this.parseCodeOwnerConfig =
+ createTimerWithClassField(
+ "parse_code_owner_config", "Latency for parsing a code owner config file", "parser");
+ this.readCodeOwnerConfig =
+ createLatencyTimer(
+ "read_code_owner_config", "Latency for reading a code owner config file");
+
+ // counter metrics
+ this.countCodeOwnerConfigReads =
+ createCounter(
+ "count_code_owner_config_reads",
+ "Total number of code owner config reads from backend");
+ this.countCodeOwnerConfigCacheReads =
+ createCounter(
+ "count_code_owner_config_cache_reads",
+ "Total number of code owner config reads from cache");
+ this.countCodeOwnerConfigValidations =
+ createCounter3(
+ "count_code_owner_config_validations",
+ "Total number of code owner config file validations",
+ Field.ofEnum(
+ ValidationTrigger.class, "trigger", (metadataBuilder, resolveAllUsers) -> {})
+ .description("The trigger of the validation.")
+ .build(),
+ Field.ofEnum(ValidationResult.class, "result", (metadataBuilder, resolveAllUsers) -> {})
+ .description("The result of the validation.")
+ .build(),
+ Field.ofBoolean("dry_run", (metadataBuilder, resolveAllUsers) -> {})
+ .description("Whether the validation was a dry run.")
+ .build());
+ this.countCodeOwnerSubmitRuleErrors =
+ createCounter1(
+ "count_code_owner_submit_rule_errors",
+ "Total number of code owner submit rule errors",
+ Field.ofString("cause", Metadata.Builder::cause)
+ .description("The cause of the submit rule error.")
+ .build());
+ this.countCodeOwnerSubmitRuleRuns =
+ createCounter(
+ "count_code_owner_submit_rule_runs", "Total number of code owner submit rule runs");
+ this.countCodeOwnerSuggestions =
+ createCounter1(
+ "count_code_owner_suggestions",
+ "Total number of code owner suggestions",
+ Field.ofBoolean("resolve_all_users", (metadataBuilder, resolveAllUsers) -> {})
+ .description(
+ "Whether code ownerships that are assigned to all users are resolved to random"
+ + " users.")
+ .build());
+ this.countInvalidCodeOwnerConfigFiles =
+ createCounter3(
+ "count_invalid_code_owner_config_files",
+ "Total number of failed requests caused by an invalid / non-parsable code owner config"
+ + " file",
+ Field.ofString("project", Metadata.Builder::projectName)
+ .description(
+ "The name of the project that contains the invalid code owner config file.")
+ .build(),
+ Field.ofString("branch", Metadata.Builder::branchName)
+ .description(
+ "The name of the branch that contains the invalid code owner config file.")
+ .build(),
+ Field.ofString("path", Metadata.Builder::filePath)
+ .description("The path of the invalid code owner config file.")
+ .build());
+ }
+
+ private Timer0 createLatencyTimer(String name, String description) {
+ return metricMaker.newTimer(
+ name, new Description(description).setCumulative().setUnit(Units.MILLISECONDS));
+ }
+
+ private Timer1<String> createTimerWithClassField(
+ String name, String description, String fieldName) {
+ Field<String> CODE_OWNER_BACKEND_FIELD =
+ Field.ofString(
+ fieldName, (metadataBuilder, fieldValue) -> metadataBuilder.className(fieldValue))
+ .build();
+
+ return metricMaker.newTimer(
+ name,
+ new Description(description).setCumulative().setUnit(Description.Units.MILLISECONDS),
+ CODE_OWNER_BACKEND_FIELD);
+ }
+
+ private Counter0 createCounter(String name, String description) {
+ return metricMaker.newCounter(name, new Description(description).setRate());
+ }
+
+ private <F1> Counter1<F1> createCounter1(String name, String description, Field<F1> field1) {
+ return metricMaker.newCounter(name, new Description(description).setRate(), field1);
+ }
+
+ private <F1, F2, F3> Counter3<F1, F2, F3> createCounter3(
+ String name, String description, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ return metricMaker.newCounter(
+ name, new Description(description).setRate(), field1, field2, field3);
+ }
+
+ private Histogram0 createHistogram(String name, String description) {
+ return metricMaker.newHistogram(name, new Description(description).setCumulative());
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/ValidationResult.java b/java/com/google/gerrit/plugins/codeowners/metrics/ValidationResult.java
new file mode 100644
index 0000000..5267736
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/ValidationResult.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.metrics;
+
+/** Enum that represents the result of a validation. */
+public enum ValidationResult {
+ /** The validation found issues that caused a rejection. */
+ REJECTED,
+
+ /** The validation passed without finding rejection reasons. */
+ PASSED,
+
+ /** The validation couldn't be performed due to a server error. */
+ FAILED;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/ValidationTrigger.java b/java/com/google/gerrit/plugins/codeowners/metrics/ValidationTrigger.java
new file mode 100644
index 0000000..4d28b70
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/metrics/ValidationTrigger.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.metrics;
+
+/** Enum to express which event triggered the validation. */
+public enum ValidationTrigger {
+ /** A new commit was received that should be validated. */
+ COMMIT_RECEIVED,
+
+ /** A commit is about to be merged and should be validated. */
+ PRE_MERGE;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/BatchModule.java b/java/com/google/gerrit/plugins/codeowners/module/BatchModule.java
similarity index 95%
rename from java/com/google/gerrit/plugins/codeowners/BatchModule.java
rename to java/com/google/gerrit/plugins/codeowners/module/BatchModule.java
index 3267d31..8295264 100644
--- a/java/com/google/gerrit/plugins/codeowners/BatchModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/module/BatchModule.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners;
+package com.google.gerrit.plugins.codeowners.module;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.plugins.codeowners.backend.BackendModule;
diff --git a/java/com/google/gerrit/plugins/codeowners/HttpModule.java b/java/com/google/gerrit/plugins/codeowners/module/HttpModule.java
similarity index 94%
rename from java/com/google/gerrit/plugins/codeowners/HttpModule.java
rename to java/com/google/gerrit/plugins/codeowners/module/HttpModule.java
index 117ffa2..c432d68 100644
--- a/java/com/google/gerrit/plugins/codeowners/HttpModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/module/HttpModule.java
@@ -11,7 +11,7 @@
// 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.google.gerrit.plugins.codeowners;
+package com.google.gerrit.plugins.codeowners.module;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
diff --git a/java/com/google/gerrit/plugins/codeowners/Module.java b/java/com/google/gerrit/plugins/codeowners/module/Module.java
similarity index 86%
rename from java/com/google/gerrit/plugins/codeowners/Module.java
rename to java/com/google/gerrit/plugins/codeowners/module/Module.java
index 341dfc8..c5447f6 100644
--- a/java/com/google/gerrit/plugins/codeowners/Module.java
+++ b/java/com/google/gerrit/plugins/codeowners/module/Module.java
@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.codeowners;
+package com.google.gerrit.plugins.codeowners.module;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.plugins.codeowners.api.ApiModule;
+import com.google.gerrit.plugins.codeowners.api.impl.ApiModule;
import com.google.gerrit.plugins.codeowners.backend.BackendModule;
-import com.google.gerrit.plugins.codeowners.config.ConfigModule;
+import com.google.gerrit.plugins.codeowners.backend.config.ConfigModule;
import com.google.gerrit.plugins.codeowners.restapi.RestApiModule;
import com.google.gerrit.plugins.codeowners.validation.ValidationModule;
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index a5f7b5d..85db8e3 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -19,12 +19,12 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.common.AccountVisibility;
@@ -32,14 +32,17 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolverResult;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScoring;
-import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScorings;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
@@ -52,10 +55,14 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
+import java.util.Random;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.kohsuke.args4j.Option;
@@ -73,6 +80,8 @@
private final Accounts accounts;
private final AccountControl.Factory accountControlFactory;
private final PermissionBackend permissionBackend;
+ private final CheckCodeOwnerCapability checkCodeOwnerCapability;
+ private final CodeOwnerMetrics codeOwnerMetrics;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
private final Provider<CodeOwnerResolver> codeOwnerResolver;
@@ -81,6 +90,10 @@
private final Set<String> hexOptions;
private int limit = DEFAULT_LIMIT;
+ private Optional<Long> seed = Optional.empty();
+ private boolean resolveAllUsers;
+ private boolean highestScoreOnly;
+ private boolean debug;
@Option(
name = "-o",
@@ -106,11 +119,45 @@
this.limit = limit;
}
+ @Option(
+ name = "--seed",
+ usage = "seed that should be used to shuffle code owners that have the same score")
+ public void setSeed(long seed) {
+ this.seed = Optional.of(seed);
+ }
+
+ @Option(
+ name = "--resolve-all-users",
+ usage =
+ "whether code ownerships that are assigned to all users should be resolved to random"
+ + " users")
+ public void setResolveAllUsers(boolean resolveAllUsers) {
+ this.resolveAllUsers = resolveAllUsers;
+ }
+
+ @Option(
+ name = "--highest-score-only",
+ usage = "whether only code owners with the highest score should be returned")
+ public void setHighestScoreOnly(boolean highestScoreOnly) {
+ this.highestScoreOnly = highestScoreOnly;
+ }
+
+ @Option(
+ name = "--debug",
+ usage =
+ "whether debug logs should be included into the response"
+ + " (requires the 'Check Code Owner' global capability)")
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
protected AbstractGetCodeOwnersForPath(
AccountVisibility accountVisibility,
Accounts accounts,
AccountControl.Factory accountControlFactory,
PermissionBackend permissionBackend,
+ CheckCodeOwnerCapability checkCodeOwnerCapability,
+ CodeOwnerMetrics codeOwnerMetrics,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
Provider<CodeOwnerResolver> codeOwnerResolver,
@@ -119,6 +166,8 @@
this.accounts = accounts;
this.accountControlFactory = accountControlFactory;
this.permissionBackend = permissionBackend;
+ this.checkCodeOwnerCapability = checkCodeOwnerCapability;
+ this.codeOwnerMetrics = codeOwnerMetrics;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
this.codeOwnerResolver = codeOwnerResolver;
@@ -127,11 +176,21 @@
this.hexOptions = new HashSet<>();
}
- protected Response<List<CodeOwnerInfo>> applyImpl(R rsrc)
+ protected Response<CodeOwnersInfo> applyImpl(R rsrc)
throws AuthException, BadRequestException, PermissionBackendException {
parseHexOptions();
validateLimit();
+ if (debug) {
+ permissionBackend.currentUser().check(checkCodeOwnerCapability.getPermission());
+ }
+
+ if (!seed.isPresent()) {
+ seed = getDefaultSeed(rsrc);
+ }
+
+ codeOwnerMetrics.countCodeOwnerSuggestions.increment(resolveAllUsers);
+
// The distance that applies to code owners that are defined in the root code owner
// configuration.
int rootDistance = rsrc.getPath().getNameCount();
@@ -143,6 +202,8 @@
CodeOwnerScoring.Builder distanceScoring = CodeOwnerScore.DISTANCE.createScoring(maxDistance);
Set<CodeOwner> codeOwners = new HashSet<>();
+ AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
+ List<String> debugLogs = new ArrayList<>();
codeOwnerConfigHierarchy.visit(
rsrc.getBranch(),
rsrc.getRevision(),
@@ -150,24 +211,13 @@
codeOwnerConfig -> {
CodeOwnerResolverResult pathCodeOwners =
codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath());
- codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners.codeOwners()));
- if (pathCodeOwners.ownedByAllUsers()) {
- fillUpWithRandomUsers(rsrc, codeOwners, limit);
-
- if (codeOwners.size() < limit) {
- logger.atFine().log(
- "tried to fill up the suggestion list with random users,"
- + " but didn't find enough visible accounts"
- + " (wanted number of suggestions = %d, got = %d",
- limit, codeOwners.size());
- }
-
- // We already found that the path is owned by all users. Hence we do not need to check
- // if there are further code owners in higher-level code owner configs.
- return false;
+ if (debug) {
+ debugLogs.addAll(pathCodeOwners.messages());
}
+ codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners.codeOwners()));
+
int distance =
codeOwnerConfig.key().branchNameKey().branch().equals(RefNames.REFS_CONFIG)
? defaultOwnersDistance
@@ -177,16 +227,38 @@
.forEach(
localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
- // If codeOwners.size() >= limit we have gathered enough code owners and do not need to
- // look at further code owner configs.
- // We can abort here, since all further code owners will have a lower distance scoring
- // and hence they would appear at the end of the sorted code owners list and be dropped
- // due to the limit.
- return codeOwners.size() < limit;
+ if (pathCodeOwners.ownedByAllUsers()) {
+ ownedByAllUsers.set(true);
+ ImmutableSet<CodeOwner> addedCodeOwners =
+ fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ addedCodeOwners.forEach(
+ localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
+
+ if (codeOwners.size() < limit) {
+ logger.atFine().log(
+ "tried to fill up the suggestion list with random users,"
+ + " but didn't find enough visible accounts"
+ + " (wanted number of suggestions = %d, got = %d",
+ limit, codeOwners.size());
+ }
+ }
+
+ // We always need to iterate over all relevant OWNERS files (even if the limit has already
+ // been reached).
+ // This is needed to collect distance scores for code owners that are mentioned in the
+ // more distant OWNERS files. Those become relevant if further scores are applied later
+ // (e.g. the score for current reviewers of the change).
+ return true;
});
- if (codeOwners.size() < limit) {
+ if (codeOwners.size() < limit || !ownedByAllUsers.get()) {
CodeOwnerResolverResult globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
+
+ if (debug) {
+ debugLogs.add("resolve global code owners");
+ debugLogs.addAll(globalCodeOwners.messages());
+ }
+
globalCodeOwners
.codeOwners()
.forEach(
@@ -194,26 +266,72 @@
codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners.codeOwners()));
if (globalCodeOwners.ownedByAllUsers()) {
- fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ ownedByAllUsers.set(true);
+ ImmutableSet<CodeOwner> addedCodeOwners = fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ addedCodeOwners.forEach(
+ codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance));
}
}
- return Response.ok(
- codeOwnerJsonFactory
- .create(getFillOptions())
- .format(sortAndLimit(distanceScoring.build(), ImmutableSet.copyOf(codeOwners))));
+ ImmutableSet<CodeOwner> immutableCodeOwners = ImmutableSet.copyOf(codeOwners);
+ CodeOwnerScorings codeOwnerScorings =
+ createScorings(rsrc, immutableCodeOwners, distanceScoring.build());
+ ImmutableMap<CodeOwner, Double> scoredCodeOwners =
+ codeOwnerScorings.getScorings(immutableCodeOwners);
+
+ ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(rsrc, scoredCodeOwners);
+
+ if (highestScoreOnly) {
+ Optional<Double> highestScore =
+ scoredCodeOwners.values().stream().max(Comparator.naturalOrder());
+ if (highestScore.isPresent()) {
+ sortedAndLimitedCodeOwners =
+ sortedAndLimitedCodeOwners.stream()
+ .filter(codeOwner -> scoredCodeOwners.get(codeOwner).equals(highestScore.get()))
+ .collect(toImmutableList());
+ }
+ }
+
+ CodeOwnersInfo codeOwnersInfo = new CodeOwnersInfo();
+ codeOwnersInfo.codeOwners =
+ codeOwnerJsonFactory.create(getFillOptions()).format(sortedAndLimitedCodeOwners);
+ codeOwnersInfo.ownedByAllUsers = ownedByAllUsers.get() ? true : null;
+ codeOwnersInfo.debugLogs = debug ? debugLogs : null;
+ return Response.ok(codeOwnersInfo);
+ }
+
+ private CodeOwnerScorings createScorings(
+ R rsrc, ImmutableSet<CodeOwner> codeOwners, CodeOwnerScoring distanceScoring) {
+ ImmutableSet.Builder<CodeOwnerScoring> codeOwnerScorings = ImmutableSet.builder();
+ codeOwnerScorings.add(distanceScoring);
+ codeOwnerScorings.addAll(getCodeOwnerScorings(rsrc, codeOwners));
+ return CodeOwnerScorings.create(codeOwnerScorings.build());
}
private CodeOwnerResolverResult getGlobalCodeOwners(Project.NameKey projectName) {
CodeOwnerResolverResult globalCodeOwners =
codeOwnerResolver
.get()
- .resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName));
+ .resolve(
+ codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners());
logger.atFine().log("including global code owners = %s", globalCodeOwners);
return globalCodeOwners;
}
/**
+ * Get further code owner scorings.
+ *
+ * <p>To be overridden by subclasses to include further scorings.
+ *
+ * @param rsrc resource on which the request is being performed
+ * @param codeOwners the code owners
+ */
+ protected ImmutableSet<CodeOwnerScoring> getCodeOwnerScorings(
+ R rsrc, ImmutableSet<CodeOwner> codeOwners) {
+ return ImmutableSet.of();
+ }
+
+ /**
* Filters out code owners that should not be suggested.
*
* <p>The following code owners are filtered out:
@@ -240,6 +358,18 @@
return codeOwners;
}
+ /**
+ * Returns the seed that should by default be used for sorting, if none was specified on the
+ * request.
+ *
+ * <p>If {@link Optional#empty()} is returned, a random seed will be used.
+ *
+ * @param rsrc resource on which the request is being performed
+ */
+ protected Optional<Long> getDefaultSeed(R rsrc) {
+ return Optional.empty();
+ }
+
private Stream<CodeOwner> getVisibleCodeOwners(R rsrc, ImmutableSet<CodeOwner> allCodeOwners) {
return allCodeOwners.stream()
.filter(
@@ -307,35 +437,39 @@
}
private ImmutableList<CodeOwner> sortAndLimit(
- CodeOwnerScoring distanceScoring, ImmutableSet<CodeOwner> codeOwners) {
- return sortCodeOwners(distanceScoring, codeOwners).limit(limit).collect(toImmutableList());
+ R rsrc, ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
+ return sortCodeOwners(rsrc, seed, scoredCodeOwners).limit(limit).collect(toImmutableList());
}
/**
* Sorts the code owners.
*
- * <p>Code owners with higher distance score are returned first.
+ * <p>Code owners with higher score are returned first.
*
- * <p>The order of code owners with the same distance score is random.
+ * <p>The order of code owners with the same score is random.
*
- * @param distanceScoring the distance scorings for the code owners
- * @param codeOwners the code owners that should be sorted
+ * @param rsrc resource on which this REST endpoint is invoked
+ * @param seed seed that should be used to randomize the order
+ * @param scoredCodeOwners the code owners with their scores
* @return the sorted code owners
*/
- private static Stream<CodeOwner> sortCodeOwners(
- CodeOwnerScoring distanceScoring, ImmutableSet<CodeOwner> codeOwners) {
- return randomizeOrder(codeOwners).sorted(distanceScoring.comparingByScoring());
+ private Stream<CodeOwner> sortCodeOwners(
+ R rsrc, Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) {
+ return randomizeOrder(seed, scoredCodeOwners.keySet())
+ .sorted(Comparator.comparingDouble(scoredCodeOwners::get).reversed());
}
/**
* Returns the entries from the given set in a random order.
*
+ * @param seed seed that should be used to randomize the order
* @param set the set for which the entries should be returned in a random order
* @return the entries from the given set in a random order
*/
- private static <T> Stream<T> randomizeOrder(Set<T> set) {
+ private static <T> Stream<T> randomizeOrder(Optional<Long> seed, Set<T> set) {
List<T> randomlyOrderedCodeOwners = new ArrayList<>(set);
- Collections.shuffle(randomlyOrderedCodeOwners);
+ Collections.shuffle(
+ randomlyOrderedCodeOwners, seed.isPresent() ? new Random(seed.get()) : new Random());
return randomlyOrderedCodeOwners.stream();
}
@@ -345,23 +479,35 @@
*
* <p>Must be only used to complete the suggestion list when it is found that the path is owned by
* all user.
+ *
+ * <p>No-op if code ownership for all users should not be resolved.
+ *
+ * @return the added code owners
*/
- private void fillUpWithRandomUsers(R rsrc, Set<CodeOwner> codeOwners, int limit) {
- if (codeOwners.size() >= limit) {
- // limit is already reach, we don't need to add further suggestions
- return;
+ private ImmutableSet<CodeOwner> fillUpWithRandomUsers(
+ R rsrc, Set<CodeOwner> codeOwners, int limit) {
+ if (!resolveAllUsers || codeOwners.size() >= limit) {
+ // code ownership for all users should not be resolved or the limit has already been reached
+ // so that we don't need to add further suggestions
+ return ImmutableSet.of();
}
logger.atFine().log("filling up with random users");
- codeOwners.addAll(
+ ImmutableSet<CodeOwner> codeOwnersToAdd =
filterCodeOwners(
- rsrc,
- // ask for 2 times the number of users that we need so that we still have enough
- // suggestions when some users are removed by the filterCodeOwners call or if the
- // returned users were already present in codeOwners
- getRandomVisibleUsers(2 * limit - codeOwners.size())
- .map(CodeOwner::create)
- .collect(toImmutableSet())));
+ rsrc,
+ // ask for 2 times the number of users that we need so that we still have enough
+ // suggestions when some users are removed by the filterCodeOwners call or if the
+ // returned users were already present in codeOwners
+ getRandomVisibleUsers(2 * limit - codeOwners.size())
+ .map(CodeOwner::create)
+ .collect(toImmutableSet()))
+ .stream()
+ .filter(codeOwner -> !codeOwners.contains(codeOwner))
+ .limit(limit - codeOwners.size())
+ .collect(toImmutableSet());
+ codeOwners.addAll(codeOwnersToAdd);
+ return codeOwnersToAdd;
}
/**
@@ -396,7 +542,7 @@
throw new IllegalStateException("unknown account visibility setting: " + accountVisibility);
} catch (IOException | PermissionBackendException e) {
- throw new StorageException("failed to get visible users", e);
+ throw new CodeOwnersInternalServerErrorException("failed to get visible users", e);
}
}
@@ -406,6 +552,6 @@
* <p>No visibility check is performed.
*/
private Stream<Account.Id> getRandomUsers(int limit) throws IOException {
- return randomizeOrder(accounts.allIds()).limit(limit);
+ return randomizeOrder(seed, accounts.allIds()).limit(limit);
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/BUILD b/java/com/google/gerrit/plugins/codeowners/restapi/BUILD
new file mode 100644
index 0000000..298b46b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/BUILD
@@ -0,0 +1,17 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK")
+
+java_library(
+ name = "restapi",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = PLUGIN_DEPS_NEVERLINK + [
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/api",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/metrics",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
+ "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/validation",
+ "//plugins/code-owners/proto:owners_metadata_java_proto",
+ ],
+)
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
new file mode 100644
index 0000000..f4e5c0f
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -0,0 +1,411 @@
+// Copyright (C) 2020 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.google.gerrit.plugins.codeowners.restapi;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
+import com.google.gerrit.plugins.codeowners.backend.UnresolvedImportFormatter;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
+import com.google.gerrit.plugins.codeowners.util.JgitPath;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.server.restapi.account.AccountsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+/**
+ * REST endpoint that checks the code ownership of a user for a path in a branch.
+ *
+ * <p>This REST endpoint handles {@code GET
+ * /projects/<project-name>/branches/<branch-name>/code_owners.check} requests.
+ */
+public class CheckCodeOwner implements RestReadView<BranchResource> {
+ private final CheckCodeOwnerCapability checkCodeOwnerCapability;
+ private final PermissionBackend permissionBackend;
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
+ private final PathCodeOwners.Factory pathCodeOwnersFactory;
+ private final Provider<CodeOwnerResolver> codeOwnerResolverProvider;
+ private final CodeOwners codeOwners;
+ private final AccountsCollection accountsCollection;
+ private final UnresolvedImportFormatter unresolvedImportFormatter;
+ private final ChangeFinder changeFinder;
+
+ private String email;
+ private String path;
+ private String change;
+ private ChangeNotes changeNotes;
+ private String user;
+ private IdentifiedUser identifiedUser;
+
+ @Inject
+ public CheckCodeOwner(
+ CheckCodeOwnerCapability checkCodeOwnerCapability,
+ PermissionBackend permissionBackend,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
+ PathCodeOwners.Factory pathCodeOwnersFactory,
+ Provider<CodeOwnerResolver> codeOwnerResolverProvider,
+ CodeOwners codeOwners,
+ AccountsCollection accountsCollection,