blob: a61c913e43521e60e510737de31b1a72f9fdc3bd [file] [log] [blame]
// 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 static com.google.common.collect.ImmutableSet.toImmutableSet;
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.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;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.ObjectId;
/**
* Class to compute the code owners for a path from a {@link CodeOwnerConfig}.
*
* <p>Code owners from inherited code owner configs are not considered.
*/
public class PathCodeOwners {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@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 createWithoutCache(CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
requireNonNull(codeOwnerConfig, "codeOwnerConfig");
return new PathCodeOwners(
codeOwnerMetrics,
projectCache,
/* transientCodeOwnerConfigCache= */ null,
codeOwners,
codeOwnerConfig,
absolutePath,
getMatcher(codeOwnerConfig.key()));
}
public Optional<PathCodeOwners> create(
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,
getMatcher(codeOwnerConfigKey)));
}
/**
* Gets the {@link PathExpressionMatcher} that should be used for the specified code owner
* config.
*
* <p>Checks which {@link CodeOwnerBackend} is responsible for the specified code owner config
* and retrieves the {@link PathExpressionMatcher} from it.
*
* <p>If the {@link CodeOwnerBackend} doesn't support path expressions and doesn't provide a
* {@link PathExpressionMatcher} a {@link PathExpressionMatcher} that never matches is returned.
* This way {@link CodeOwnerSet}s that have path expressions are ignored and will not have any
* effect.
*
* @param codeOwnerConfigKey the key of the code owner config for which the path expression
* matcher should be returned
* @return the {@link PathExpressionMatcher} that should be used for the specified code owner
* config
*/
private PathExpressionMatcher getMatcher(CodeOwnerConfig.Key codeOwnerConfigKey) {
CodeOwnerBackend codeOwnerBackend =
codeOwnersPluginConfiguration
.getProjectConfig(codeOwnerConfigKey.project())
.getBackend(codeOwnerConfigKey.branchNameKey().branch());
return codeOwnerBackend
.getPathExpressionMatcher(codeOwnerConfigKey.branchNameKey())
.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 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");
this.pathExpressionMatcher = requireNonNull(pathExpressionMatcher, "pathExpressionMatcher");
checkState(path.isAbsolute(), "path %s must be absolute", path);
}
/** Returns the local code owner config. */
public CodeOwnerConfig getCodeOwnerConfig() {
return codeOwnerConfig;
}
/** Returns the absolute path for which code owners were computed. */
public Path getPath() {
return path;
}
/**
* Resolves the {@link #codeOwnerConfig}.
*
* <p>Resolving means that:
*
* <ul>
* <li>non-matching per-file code owner sets are dropped (since code owner sets that do not
* match the {@link #path} are not relevant)
* <li>imported code owner configs are loaded and replaced with the parts of them which should
* be imported (depends on the {@link CodeOwnerConfigImportMode}) and that are relevant for
* the {@link #path}
* <li>global code owner sets are dropped if any matching per-file code owner set has the
* ignoreGlobalAndParentCodeOwners flag set to {@code true} (since in this case global code
* owners should be ignored and then the global code owner sets are not relevant)
* <li>the ignoreParentCodeOwners flag is set to {@code true} if any matching per-file code
* owner set has the ignoreGlobalAndParentCodeOwners flag set to true (since in this case
* code owners from parent configurations should be ignored)
* </ul>
*
* <p>When resolving imports, cycles are detected and code owner configs that have been seen
* already are not evaluated again.
*
* <p>Non-resolvable imports are silently ignored.
*
* <p>Imports that are loaded from the same project/branch as {@link #codeOwnerConfig} are
* imported from the same revision from which {@link #codeOwnerConfig} was loaded. Imports that
* are loaded from other projects/branches are imported from the current revision. If several
* imports are loaded from the same project/branch we guarantee that they are all loaded from the
* same revision, even if the current revision is changed by a concurrent request while the
* resolution is being performed.
*
* <p>Imports from other projects are always loaded from the same branch from which the importing
* code owner config was loaded.
*
* @return the resolved code owner config
*/
public OptionalResultWithMessages<PathCodeOwnersResult> resolveCodeOwnerConfig() {
if (this.pathCodeOwnersResult != null) {
return this.pathCodeOwnersResult;
}
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 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);
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;
}
}
// 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());
// 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 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(
resolvedCodeOwnerConfigBuilder.codeOwnerSets().stream()
.filter(codeOwnerSet -> !codeOwnerSet.pathExpressions().isEmpty())
.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 =
OptionalResultWithMessages.create(
PathCodeOwnersResult.create(
path, resolvedCodeOwnerConfigBuilder.build(), unresolvedImports.build()),
messages);
logger.atFine().log("path code owners result = %s", pathCodeOwnersResult);
return this.pathCodeOwnersResult;
}
}
/**
* Resolve the imports of the given code owner config.
*
* @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.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", keyOfImportingCodeOwnerConfig);
// To detect cyclic dependencies we keep track of all seen code owner configs.
Set<CodeOwnerConfig.Key> seenCodeOwnerConfigs = new HashSet<>();
seenCodeOwnerConfigs.add(codeOwnerConfig.key());
// To ensure that code owner configs from the same project/branch are imported from the same
// revision we keep track of the revisions.
Map<BranchNameKey, ObjectId> revisionMap = new HashMap<>();
revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision());
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()) {
CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll();
messageBuilder.append(codeOwnerConfigImport.format());
CodeOwnerConfigReference codeOwnerConfigReference =
codeOwnerConfigImport.referenceToImportedCodeOwnerConfig();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
createKeyForImportedCodeOwnerConfig(
codeOwnerConfigImport.importingCodeOwnerConfig(), 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()) {
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()) {
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;
}
Optional<ObjectId> revision =
Optional.ofNullable(revisionMap.get(keyOfImportedCodeOwnerConfig.branchNameKey()));
logger.atFine().log(
"import from %s",
revision.isPresent() ? "revision " + revision.get().name() : "current revision");
Optional<CodeOwnerConfig> mayBeImportedCodeOwnerConfig =
revision.isPresent()
? codeOwnerConfigLoader.get(keyOfImportedCodeOwnerConfig, revision.get())
: codeOwnerConfigLoader.getFromCurrentRevision(keyOfImportedCodeOwnerConfig);
if (!mayBeImportedCodeOwnerConfig.isPresent()) {
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;
}
CodeOwnerConfig importedCodeOwnerConfig = mayBeImportedCodeOwnerConfig.get();
CodeOwnerConfigImportMode importMode = codeOwnerConfigReference.importMode();
logger.atFine().log("import mode = %s", importMode.name());
revisionMap.putIfAbsent(
keyOfImportedCodeOwnerConfig.branchNameKey(), importedCodeOwnerConfig.revision());
if (importMode.importIgnoreParentCodeOwners()
&& importedCodeOwnerConfig.ignoreParentCodeOwners()) {
logger.atFine().log("import ignoreParentCodeOwners flag");
resolvedCodeOwnerConfigBuilder.setIgnoreParentCodeOwners();
}
if (importMode.importGlobalCodeOwnerSets()) {
logger.atFine().log("import global code owners");
getGlobalCodeOwnerSets(importedCodeOwnerConfig)
.forEach(resolvedCodeOwnerConfigBuilder::addCodeOwnerSet);
}
ImmutableSet<CodeOwnerSet> matchingPerFileCodeOwnerSets =
getMatchingPerFileCodeOwnerSets(importedCodeOwnerConfig).collect(toImmutableSet());
if (importMode.importPerFileCodeOwnerSets()) {
logger.atFine().log("import per-file code owners");
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<CodeOwnerConfigImport> transitiveImports = new HashSet<>();
transitiveImports.addAll(
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
// import global code owners, no matter which import mode is specified in the imported
// code owner configs.
logger.atFine().log(
"import transitive imports with mode %s",
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY);
transitiveImports =
transitiveImports.stream()
.map(
codeOwnerCfgImport ->
CodeOwnerConfigImport.create(
codeOwnerCfgImport.importLevel(),
codeOwnerCfgImport.importingCodeOwnerConfig(),
CodeOwnerConfigReference.copyWithNewImportMode(
codeOwnerCfgImport.referenceToImportedCodeOwnerConfig(),
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY),
codeOwnerCfgImport.codeOwnerSet()))
.collect(toSet());
}
logger.atFine().log("transitive imports = %s", transitiveImports);
codeOwnerConfigsToImport.addAll(transitiveImports);
}
}
}
}
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(
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
CodeOwnerConfigReference codeOwnerConfigReference) {
// if the code owner config reference doesn't have a project, the imported code owner config
// file is contained in the same project as the importing code owner config
Project.NameKey project =
codeOwnerConfigReference.project().orElse(keyOfImportingCodeOwnerConfig.project());
// if the code owner config reference doesn't have a branch, the imported code owner config file
// is imported from the same branch in which the importing code owner config is stored
String branch =
codeOwnerConfigReference
.branch()
.orElse(keyOfImportingCodeOwnerConfig.branchNameKey().branch());
// if the path of the imported code owner config is relative, it should be resolved against
// the folder path of the importing code owner config
Path folderPath =
keyOfImportingCodeOwnerConfig
.folderPath()
.resolve(codeOwnerConfigReference.path())
.normalize();
return CodeOwnerConfig.Key.create(
BranchNameKey.create(project, branch), folderPath, codeOwnerConfigReference.fileName());
}
private static Stream<CodeOwnerSet> getGlobalCodeOwnerSets(CodeOwnerConfig codeOwnerConfig) {
return codeOwnerConfig.codeOwnerSets().stream()
.filter(codeOwnerSet -> codeOwnerSet.pathExpressions().isEmpty());
}
private Stream<CodeOwnerSet> getMatchingPerFileCodeOwnerSets(CodeOwnerConfig codeOwnerConfig) {
return codeOwnerConfig.codeOwnerSets().stream()
.filter(codeOwnerSet -> !codeOwnerSet.pathExpressions().isEmpty())
.filter(codeOwnerSet -> matches(codeOwnerSet, getRelativePath(), pathExpressionMatcher));
}
private Path getRelativePath() {
return codeOwnerConfig.relativize(path);
}
/**
* Whether the given code owner set matches the given path.
*
* <p>A path matches the code owner set, if any of its path expressions matches the path.
*
* <p>The passed in code owner set must have at least one path expression.
*
* @param codeOwnerSet the code owner set for which it should be checked if it matches the given
* path, must have at least one path expression
* @param relativePath path for which it should be checked whether it matches the given owner set;
* the path must be relative to the path in which the {@link CodeOwnerConfig} is stored that
* contains the code owner set; can be the path of a file or folder; the path may or may not
* exist
* @param matcher the {@link PathExpressionMatcher} that should be used to match path expressions
* against the given path
* @return whether this owner set matches the given path
*/
@VisibleForTesting
static boolean matches(
CodeOwnerSet codeOwnerSet, Path relativePath, PathExpressionMatcher matcher) {
requireNonNull(codeOwnerSet, "codeOwnerSet");
requireNonNull(relativePath, "relativePath");
requireNonNull(matcher, "matcher");
checkState(!relativePath.isAbsolute(), "path %s must be relative", relativePath);
checkState(
!codeOwnerSet.pathExpressions().isEmpty(), "code owner set must have path expressions");
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);
}
}
}