Merge "Add metrics to count the number of OWNERS cache/backend loads per change"
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
index 91b0f6f..ed65114 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -15,6 +15,7 @@
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 static java.util.stream.Collectors.toSet;
@@ -221,37 +222,43 @@
logger.atFine().log(
"resolve code owners for %s from code owner config %s", path, codeOwnerConfig.key());
+ List<String> messages = new ArrayList<>();
+
+ // 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());
- List<String> messages = new ArrayList<>();
-
- // 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(
- codeOwnerSet -> {
- messages.add(
- String.format(
- "per-file code owner set with path expressions %s matches",
- codeOwnerSet.pathExpressions()));
- resolvedCodeOwnerConfigBuilder.addCodeOwnerSet(codeOwnerSet);
- });
+ 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);
+ }
- OptionalResultWithMessages<List<UnresolvedImport>> unresolvedImports =
- resolveImports(codeOwnerConfig, resolvedCodeOwnerConfigBuilder);
+ // Resolve all global imports.
+ Set<CodeOwnerConfigImport> globalImports = getGlobalImports(0, codeOwnerConfig);
+ OptionalResultWithMessages<List<UnresolvedImport>> unresolvedGlobalImports =
+ resolveImports(codeOwnerConfig.key(), globalImports, resolvedCodeOwnerConfigBuilder);
+ messages.addAll(unresolvedGlobalImports.messages());
- 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.
+ // ignoreGlobalAndParentCodeOwners flags on per-file code owner sets again, but can just rely
+ // on the global ignoreParentCodeOwners flag.
Optional<CodeOwnerSet> matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners =
- getMatchingPerFileCodeOwnerSets(resolvedCodeOwnerConfig)
+ getMatchingPerFileCodeOwnerSets(resolvedCodeOwnerConfigBuilder.build())
.filter(CodeOwnerSet::ignoreGlobalAndParentCodeOwners)
.findAny();
if (matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners.isPresent()) {
@@ -263,21 +270,44 @@
matchingPerFileCodeOwnerSetThatIgnoresGlobalAndParentCodeOwners
.get()
.pathExpressions()));
- resolvedCodeOwnerConfig =
- resolvedCodeOwnerConfig
+ // 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()));
}
- messages.addAll(unresolvedImports.messages());
+ // Resolve per-file imports.
+ Set<CodeOwnerConfigImport> perFileImports =
+ getPerFileImports(
+ 0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets());
+ OptionalResultWithMessages<List<UnresolvedImport>> unresolvedPerFileImports =
+ resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder);
+ messages.addAll(unresolvedPerFileImports.messages());
+
this.pathCodeOwnersResult =
OptionalResultWithMessages.create(
- PathCodeOwnersResult.create(path, resolvedCodeOwnerConfig, unresolvedImports.get()),
+ PathCodeOwnersResult.create(
+ path,
+ resolvedCodeOwnerConfigBuilder.build(),
+ Stream.concat(
+ unresolvedGlobalImports.get().stream(),
+ unresolvedPerFileImports.get().stream())
+ .collect(toImmutableList())),
messages);
logger.atFine().log("path code owners result = %s", pathCodeOwnersResult);
return this.pathCodeOwnersResult;
@@ -287,17 +317,19 @@
/**
* 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 list of unresolved imports, empty list if all imports were successfully resolved
*/
private OptionalResultWithMessages<List<UnresolvedImport>> resolveImports(
- CodeOwnerConfig importingCodeOwnerConfig,
+ CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+ Set<CodeOwnerConfigImport> codeOwnerConfigImports,
CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) {
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", importingCodeOwnerConfig.key());
+ 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<>();
@@ -309,16 +341,12 @@
revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision());
Queue<CodeOwnerConfigImport> codeOwnerConfigsToImport = new ArrayDeque<>();
- codeOwnerConfigsToImport.addAll(getGlobalImports(0, importingCodeOwnerConfig));
- codeOwnerConfigsToImport.addAll(
- getPerFileImports(
- 0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets()));
-
+ codeOwnerConfigsToImport.addAll(codeOwnerConfigImports);
if (!codeOwnerConfigsToImport.isEmpty()) {
messageBuilder.append(
String.format(
"Code owner config %s imports:\n",
- importingCodeOwnerConfig.key().format(codeOwners)));
+ keyOfImportingCodeOwnerConfig.format(codeOwners)));
}
while (!codeOwnerConfigsToImport.isEmpty()) {
CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll();
@@ -328,7 +356,7 @@
codeOwnerConfigImport.referenceToImportedCodeOwnerConfig();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
createKeyForImportedCodeOwnerConfig(
- importingCodeOwnerConfig.key(), codeOwnerConfigReference);
+ keyOfImportingCodeOwnerConfig, codeOwnerConfigReference);
try (Timer0.Context ctx2 = codeOwnerMetrics.resolveCodeOwnerConfigImport.start()) {
logger.atFine().log(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
index 8929a72..d4ec968 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -38,6 +38,7 @@
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
+import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Key;
@@ -60,6 +61,7 @@
private PathCodeOwners.Factory pathCodeOwnersFactory;
private DynamicMap<CodeOwnerBackend> codeOwnerBackends;
private Provider<TransientCodeOwnerConfigCache> transientCodeOwnerConfigCacheProvider;
+ private TestPathExpressions testPathExpressions;
@Before
public void setUpCodeOwnersPlugin() throws Exception {
@@ -70,6 +72,7 @@
plugin.getSysInjector().getInstance(new Key<DynamicMap<CodeOwnerBackend>>() {});
transientCodeOwnerConfigCacheProvider =
plugin.getSysInjector().getInstance(new Key<Provider<TransientCodeOwnerConfigCache>>() {});
+ testPathExpressions = plugin.getSysInjector().getInstance(TestPathExpressions.class);
}
@Test
@@ -1952,6 +1955,57 @@
.isTrue();
}
+ @Test
+ public void perFileRuleThatIgnoresGlobalCodeOwnersCanImportGlobalCodeOwnersFromOtherFile()
+ throws Exception {
+ // create importing config that:
+ // * has a global code owner
+ // * has a per-file import for md files
+ // * ignores global and parent code owners for md files
+ CodeOwnerConfig.Key importingCodeOwnerConfigKey =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("md"))
+ .setIgnoreGlobalAndParentCodeOwners()
+ .addImport(
+ CodeOwnerConfigReference.create(
+ CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS"))
+ .build())
+ .create();
+
+ // create imported config with a global code owner
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/bar/")
+ .addCodeOwnerEmail(user.email())
+ .create();
+
+ Optional<PathCodeOwners> pathCodeOwners =
+ pathCodeOwnersFactory.create(
+ transientCodeOwnerConfigCacheProvider.get(),
+ importingCodeOwnerConfigKey,
+ projectOperations.project(project).getHead("master"),
+ Paths.get("/foo.md"));
+ assertThat(pathCodeOwners).isPresent();
+
+ // Expectation: we get the global code owner from the imported code owner config (since it is
+ // imported by a matching per-file rule), the global code owner from the importing code owner
+ // config is ignored (since the matching per-file rule ignores parent and global code owners)
+ assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners())
+ .comparingElementsUsing(hasEmail())
+ .containsExactly(user.email());
+ assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports())
+ .isFalse();
+ }
+
private CodeOwnerConfig.Builder createCodeOwnerBuilder() {
return CodeOwnerConfig.builder(
CodeOwnerConfig.Key.create(BranchNameKey.create(project, "master"), Paths.get("/")),
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 13f18d7..5f6bc92 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -109,7 +109,7 @@
listOwnersForPath(changeId, path) {
return this.restApi.get(
`/changes/${changeId}/revisions/current/code_owners` +
- `/${encodeURIComponent(path)}?limit=5&o=DETAILS`
+ `/${encodeURIComponent(path)}?limit=5&o=DETAILS&resolve-all-users=true`
);
}