Merge "Report logged in user role in logs"
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 2b054b3..8ff1222 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -14,6 +14,8 @@
package com.google.gerrit.plugins.codeowners.api;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
@@ -27,7 +29,36 @@
CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException;
abstract class CodeOwnerConfigFilesRequest {
+ private String email;
+
+ /**
+ * Limits the returned code owner config files to those that contain the given email.
+ *
+ * @param email the email that should appear in the returned code owner config files
+ */
+ public CodeOwnerConfigFilesRequest withEmail(String email) {
+ this.email = email;
+ return this;
+ }
+
+ /** Returns the email that should appear in the returned code owner config files/ */
+ @Nullable
+ public String getEmail() {
+ return email;
+ }
+
/** Executes the request and retrieves the paths of the requested code owner config file */
public abstract List<String> paths() throws RestApiException;
}
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements BranchCodeOwners {
+ @Override
+ public CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
index 3383eb1..189b0d1 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
@@ -18,6 +18,7 @@
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.util.List;
@@ -27,13 +28,14 @@
BranchCodeOwnersImpl create(BranchResource branchResource);
}
- private final GetCodeOwnerConfigFiles getCodeOwnerConfigFiles;
+ private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
private final BranchResource branchResource;
@Inject
public BranchCodeOwnersImpl(
- GetCodeOwnerConfigFiles getCodeOwnerConfigFiles, @Assisted BranchResource branchResource) {
- this.getCodeOwnerConfigFiles = getCodeOwnerConfigFiles;
+ Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
+ @Assisted BranchResource branchResource) {
+ this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
this.branchResource = branchResource;
}
@@ -42,6 +44,8 @@
return new CodeOwnerConfigFilesRequest() {
@Override
public List<String> paths() throws RestApiException {
+ GetCodeOwnerConfigFiles getCodeOwnerConfigFiles = getCodeOwnerConfigFilesProvider.get();
+ getCodeOwnerConfigFiles.setEmail(getEmail());
return getCodeOwnerConfigFiles.apply(branchResource).value();
}
};
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CheckCodeOwnerConfigFilesInput.java b/java/com/google/gerrit/plugins/codeowners/api/CheckCodeOwnerConfigFilesInput.java
new file mode 100644
index 0000000..fd865b8
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CheckCodeOwnerConfigFilesInput.java
@@ -0,0 +1,49 @@
+// 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;
+
+/**
+ * The input for the {@link com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles}
+ * REST endpoint.
+ */
+public class CheckCodeOwnerConfigFilesInput {
+ /**
+ * Whether code owner config files in branches for which the code owners functionality is disabled
+ * should be validated too.
+ *
+ * <p>By default unset, {@code false}.
+ */
+ public Boolean validateDisabledBranches;
+
+ /**
+ * Branches for which code owner config files should be validated.
+ *
+ * <p>The {@code refs/heads/} prefix may be omitted.
+ *
+ * <p>By default unset, which means that code owner config files in all branches should be
+ * validated.
+ */
+ public List<String> branches;
+
+ /**
+ * Glob that limits the validation to code owner config files that have a path that matches this
+ * glob.
+ *
+ * <p>By default unset, which means that all code owner config files should be validated.
+ */
+ public String path;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
index 8138287..4d5159f 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwners.java
@@ -14,7 +14,13 @@
package com.google.gerrit.plugins.codeowners.api;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.List;
+import java.util.Map;
/**
* Project-level Java API of the code-owners plugin.
@@ -25,6 +31,104 @@
/** Returns the code owner project configuration. */
CodeOwnerProjectConfigInfo getConfig() throws RestApiException;
+ /** Create a request to check the code owner config files in the project. */
+ CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException;
+
+ /** Request to check code owner config files. */
+ abstract class CheckCodeOwnerConfigFilesRequest {
+ private boolean validateDisabledBranches;
+ private ImmutableList<String> branches;
+ private String path;
+
+ /**
+ * Includes code owner config files in branches for which the code owners functionality is
+ * disabled into the validation.
+ */
+ public CheckCodeOwnerConfigFilesRequest validateDisabledBranches() {
+ return validateDisabledBranches(true);
+ }
+
+ /**
+ * Sets whether code owner config files in branches for which the code owners functionality is
+ * disabled should be validated.
+ */
+ public CheckCodeOwnerConfigFilesRequest validateDisabledBranches(
+ boolean validateDisabledBranches) {
+ this.validateDisabledBranches = validateDisabledBranches;
+ return this;
+ }
+
+ /**
+ * Whether code owner config files in branches for which the code owners functionality is
+ * disabled should be validated.
+ */
+ public boolean isValidateDisabledBranches() {
+ return validateDisabledBranches;
+ }
+
+ /** Sets the branches for which code owner config files should be validated. */
+ public CheckCodeOwnerConfigFilesRequest setBranches(List<String> branches) {
+ this.branches = ImmutableList.copyOf(branches);
+ return this;
+ }
+
+ /**
+ * Gets the branches for which code owner config files should be validated.
+ *
+ * <p>Returns {@code null} if no branches have been set.
+ */
+ @Nullable
+ public ImmutableList<String> getBranches() {
+ return branches;
+ }
+
+ /**
+ * Sets a glob that limits the validation to code owner config files that have a path that
+ * matches this glob.
+ */
+ public CheckCodeOwnerConfigFilesRequest setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Gets the glob that limits the validation to code owner config files that have a path that
+ * matches this glob.
+ */
+ @Nullable
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Executes the request to check the code owner config files and retrieves the result of the
+ * validation.
+ */
+ public abstract Map<String, Map<String, List<ConsistencyProblemInfo>>> check()
+ throws RestApiException;
+ }
+
/** Returns the branch-level code owners API for the given branch. */
BranchCodeOwners branch(String branchName) throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements ProjectCodeOwners {
+ @Override
+ public CodeOwnerProjectConfigInfo getConfig() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public BranchCodeOwners branch(String branchName) throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java
index 0454dc0..6011572 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/ProjectCodeOwnersImpl.java
@@ -16,14 +16,18 @@
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.restapi.CheckCodeOwnerConfigFiles;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerProjectConfig;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.BranchesCollection;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.util.List;
+import java.util.Map;
/** Implementation of the {@link ProjectCodeOwners} API. */
public class ProjectCodeOwnersImpl implements ProjectCodeOwners {
@@ -34,6 +38,7 @@
private final BranchesCollection branchesCollection;
private final BranchCodeOwnersImpl.Factory branchCodeOwnersApi;
private final GetCodeOwnerProjectConfig getCodeOwnerProjectConfig;
+ private final CheckCodeOwnerConfigFiles checkCodeOwnerConfigFiles;
private final ProjectResource projectResource;
@Inject
@@ -41,10 +46,12 @@
BranchesCollection branchesCollection,
BranchCodeOwnersImpl.Factory branchCodeOwnersApi,
GetCodeOwnerProjectConfig getCodeOwnerProjectConfig,
+ CheckCodeOwnerConfigFiles checkCodeOwnerConfigFiles,
@Assisted ProjectResource projectResource) {
this.branchesCollection = branchesCollection;
this.branchCodeOwnersApi = branchCodeOwnersApi;
this.getCodeOwnerProjectConfig = getCodeOwnerProjectConfig;
+ this.checkCodeOwnerConfigFiles = checkCodeOwnerConfigFiles;
this.projectResource = projectResource;
}
@@ -67,4 +74,23 @@
throw asRestApiException("Cannot get code owner project config", e);
}
}
+
+ @Override
+ public CheckCodeOwnerConfigFilesRequest checkCodeOwnerConfigFiles() throws RestApiException {
+ return new CheckCodeOwnerConfigFilesRequest() {
+ @Override
+ public Map<String, Map<String, List<ConsistencyProblemInfo>>> check()
+ throws RestApiException {
+ try {
+ CheckCodeOwnerConfigFilesInput input = new CheckCodeOwnerConfigFilesInput();
+ input.validateDisabledBranches = isValidateDisabledBranches();
+ input.branches = getBranches();
+ input.path = getPath();
+ return checkCodeOwnerConfigFiles.apply(projectResource, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check code owner config files", e);
+ }
+ }
+ };
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index f9c7027..5ae315b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -344,8 +344,7 @@
Path absolutePath) {
logger.atFine().log("computing path status for %s (bootstrapping mode)", absolutePath);
- AtomicReference<CodeOwnerStatus> codeOwnerStatus =
- new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ CodeOwnerStatus codeOwnerStatus = CodeOwnerStatus.INSUFFICIENT_REVIEWERS;
if (enableImplicitApprovalFromUploader && isProjectOwner(branch.project(), patchSetUploader)) {
// The uploader of the patch set is a project owner and thus a code owner. This means there
@@ -354,7 +353,7 @@
logger.atFine().log(
"the status for path %s is %s since the patch set uploader is a project owner",
absolutePath, CodeOwnerStatus.APPROVED.name());
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ codeOwnerStatus = CodeOwnerStatus.APPROVED;
} else if (enableImplicitApprovalFromUploader
&& globalCodeOwnerAccountIds.contains(patchSetUploader)) {
// If the uploader of the patch set is a global code owner, there is an implicit code owner
@@ -362,31 +361,31 @@
logger.atFine().log(
"the status for path %s is %s since the patch set uploader is a global code owner",
absolutePath, CodeOwnerStatus.APPROVED.name());
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ codeOwnerStatus = CodeOwnerStatus.APPROVED;
} else if (!Collections.disjoint(approverAccountIds, globalCodeOwnerAccountIds)) {
// At least one of the global code owners approved the change.
logger.atFine().log(
"the status for path %s is %s since at least one global code owner approved it",
absolutePath, CodeOwnerStatus.APPROVED.name());
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ codeOwnerStatus = CodeOwnerStatus.APPROVED;
} else if (approverAccountIds.stream()
.anyMatch(approverAccountId -> isProjectOwner(branch.project(), approverAccountId))) {
// At least one of the approvers is a project owner and thus a code owner.
logger.atFine().log(
"the status for path %s is %s since at least one approvers is a project owner",
absolutePath, CodeOwnerStatus.APPROVED.name());
- codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
+ codeOwnerStatus = CodeOwnerStatus.APPROVED;
} else if (reviewerAccountIds.stream()
.anyMatch(reviewerAccountId -> isProjectOwner(branch.project(), reviewerAccountId))) {
// At least one of the reviewers is a project owner and thus a code owner.
logger.atFine().log(
"the status for path %s is %s since at least one reviewer is a project owner",
absolutePath, CodeOwnerStatus.PENDING.name());
- codeOwnerStatus.set(CodeOwnerStatus.PENDING);
+ codeOwnerStatus = CodeOwnerStatus.PENDING;
}
PathCodeOwnerStatus pathCodeOwnerStatus =
- PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get());
+ PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus);
logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus);
return pathCodeOwnerStatus;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index b4be755..99ee653 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -15,8 +15,11 @@
package com.google.gerrit.plugins.codeowners.backend;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidConfigCause;
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.Project;
import com.google.gerrit.exceptions.StorageException;
@@ -26,10 +29,12 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -40,6 +45,8 @@
/** Class to scan a branch for code owner config files. */
@Singleton
public class CodeOwnerConfigScanner {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final GitRepositoryManager repoManager;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
@@ -66,7 +73,8 @@
codeOwnerConfig -> {
found.set(true);
return false;
- });
+ },
+ ignoreInvalidCodeOwnerConfigFiles());
return found.get();
}
@@ -76,12 +84,39 @@
* @param branchNameKey the project and branch for which the code owner config files should be
* visited
* @param codeOwnerConfigVisitor the callback that is invoked for each code owner config file
+ * @param invalidCodeOwnerConfigCallback callback that is invoked for invalid code owner config
+ * files
*/
- public void visit(BranchNameKey branchNameKey, CodeOwnerConfigVisitor codeOwnerConfigVisitor) {
+ public void visit(
+ BranchNameKey branchNameKey,
+ CodeOwnerConfigVisitor codeOwnerConfigVisitor,
+ InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback) {
+ visit(branchNameKey, codeOwnerConfigVisitor, invalidCodeOwnerConfigCallback, null);
+ }
+
+ /**
+ * 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
+ * visited
+ * @param codeOwnerConfigVisitor the callback that is invoked for each code owner config file
+ * @param invalidCodeOwnerConfigCallback callback that is invoked for invalid code owner config
+ * files
+ * @param pathGlob optional Java NIO glob that the paths of code owner config files must match
+ */
+ public void visit(
+ BranchNameKey branchNameKey,
+ CodeOwnerConfigVisitor codeOwnerConfigVisitor,
+ InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback,
+ @Nullable String pathGlob) {
requireNonNull(branchNameKey, "branchNameKey");
requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor");
+ requireNonNull(invalidCodeOwnerConfigCallback, "invalidCodeOwnerConfigCallback");
CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+ logger.atFine().log(
+ "scanning code owner files in branch %s of project %s (path glob = %s)",
+ branchNameKey.branch(), branchNameKey.project(), pathGlob);
try (Repository repository = repoManager.openRepository(branchNameKey.project());
RevWalk rw = new RevWalk(repository);
@@ -96,7 +131,8 @@
RevCommit revision = rw.parseCommit(ref.getObjectId());
treeWalk.addTree(revision.getTree());
treeWalk.setRecursive(true);
- treeWalk.setFilter(createCodeOwnerConfigFilter(codeOwnerBackend, branchNameKey.project()));
+ treeWalk.setFilter(
+ createCodeOwnerConfigFilter(codeOwnerBackend, branchNameKey.project(), pathGlob));
while (treeWalk.next()) {
Path filePath = Paths.get(treeWalk.getPathString());
@@ -107,8 +143,24 @@
String fileName = filePath.getFileName().toString();
CodeOwnerConfig.Key codeOwnerConfigKey =
CodeOwnerConfig.Key.create(branchNameKey, folderPath, fileName);
- Optional<CodeOwnerConfig> codeOwnerConfig =
- codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+ Optional<CodeOwnerConfig> codeOwnerConfig;
+
+ try {
+ codeOwnerConfig = codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+ } catch (StorageException storageException) {
+ Optional<ConfigInvalidException> configInvalidException =
+ getInvalidConfigCause(storageException);
+ if (!configInvalidException.isPresent()) {
+ // Propagate any failure that is not related to the contents of the code owner config.
+ throw storageException;
+ }
+
+ // The code owner config is invalid and cannot be parsed.
+ invalidCodeOwnerConfigCallback.onInvalidCodeOwnerConfig(
+ folderPath.resolve(fileName), configInvalidException.get());
+ continue;
+ }
+
checkState(codeOwnerConfig.isPresent(), "code owner config %s not found", codeOwnerConfig);
boolean visitFurtherCodeOwnerConfigFiles =
codeOwnerConfigVisitor.visit(codeOwnerConfig.get());
@@ -125,9 +177,16 @@
}
}
- /** Creates a {@link TreeFilter} that matches code owner config files in the given project. */
+ /**
+ * Creates a {@link TreeFilter} that matches code owner config files in the given project.
+ *
+ * @param codeOwnerBackend the code owner backend that is being used
+ * @param project the name of the project in which code owner config files should be matched
+ * @param pathGlob optional Java NIO glob that the paths of code owner config files must match
+ * @return the created {@link TreeFilter}
+ */
private static TreeFilter createCodeOwnerConfigFilter(
- CodeOwnerBackend codeOwnerBackend, Project.NameKey project) {
+ CodeOwnerBackend codeOwnerBackend, Project.NameKey project, @Nullable String pathGlob) {
return new TreeFilter() {
@Override
public boolean shouldBeRecursive() {
@@ -140,6 +199,14 @@
walker.enterSubtree();
return false;
}
+ if (pathGlob != null
+ && !FileSystems.getDefault()
+ .getPathMatcher("glob:" + pathGlob)
+ .matches(JgitPath.of(walker.getPathString()).getAsAbsolutePath())) {
+ logger.atFine().log(
+ "%s filtered out because it doesn't match the path glob", walker.getPathString());
+ return false;
+ }
String fileName = Paths.get(walker.getPathString()).getFileName().toString();
return codeOwnerBackend.isCodeOwnerConfigFile(project, fileName);
}
@@ -150,4 +217,13 @@
}
};
}
+
+ /**
+ * Returns an {@link InvalidCodeOwnerConfigCallback} instance that ignores invalid code owner
+ * config files.
+ */
+ public static InvalidCodeOwnerConfigCallback ignoreInvalidCodeOwnerConfigFiles() {
+ return (codeOwnerConfigFilePath, configInvalidException) ->
+ logger.atFine().log("ignoring invalid code owner config file %s", codeOwnerConfigFilePath);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 0c38ef4..519bc8f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -16,10 +16,12 @@
import static java.util.Objects.requireNonNull;
+import com.google.common.base.Throwables;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -69,4 +71,16 @@
codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, null);
}
+
+ /**
+ * 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.
+ */
+ public static Optional<ConfigInvalidException> getInvalidConfigCause(Exception e) {
+ return Throwables.getCausalChain(e).stream()
+ .filter(t -> t instanceof ConfigInvalidException)
+ .map(t -> (ConfigInvalidException) t)
+ .findFirst();
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java
new file mode 100644
index 0000000..fed838a
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigCallback.java
@@ -0,0 +1,30 @@
+// 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;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Callback interface to let callers handle invalid code owner config files. */
+public interface InvalidCodeOwnerConfigCallback {
+ /**
+ * 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
+ */
+ void onInvalidCodeOwnerConfig(
+ Path codeOwnerConfigFilePath, ConfigInvalidException configInvalidException);
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index 6a0c27c..dd4fd6a 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -21,6 +21,7 @@
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.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -32,6 +33,7 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
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.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.ServiceUserClassifier;
@@ -59,6 +61,7 @@
@VisibleForTesting public static final int DEFAULT_LIMIT = 10;
private final PermissionBackend permissionBackend;
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
private final Provider<CodeOwnerResolver> codeOwnerResolver;
private final ServiceUserClassifier serviceUserClassifier;
@@ -94,11 +97,13 @@
protected AbstractGetCodeOwnersForPath(
PermissionBackend permissionBackend,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
Provider<CodeOwnerResolver> codeOwnerResolver,
ServiceUserClassifier serviceUserClassifier,
CodeOwnerJson.Factory codeOwnerJsonFactory) {
this.permissionBackend = permissionBackend;
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
this.codeOwnerResolver = codeOwnerResolver;
this.serviceUserClassifier = serviceUserClassifier;
@@ -112,9 +117,14 @@
parseHexOptions();
validateLimit();
- // The maximal possible distance. This is the distance that applies to code owners that are
- // defined in the root code owner configuration.
- int maxDistance = rsrc.getPath().getNameCount();
+ // The distance that applies to code owners that are defined in the root code owner
+ // configuration.
+ int rootDistance = rsrc.getPath().getNameCount();
+
+ // The maximal possible distance. This is the distance that applies to global code owners and is
+ // by 1 greater than the distance that applies to code owners that are defined in the root code
+ // owner configuration.
+ int maxDistance = rootDistance + 1;
CodeOwnerScoring.Builder distanceScoring = CodeOwnerScore.DISTANCE.createScoring(maxDistance);
@@ -127,7 +137,7 @@
ImmutableSet<CodeOwner> pathCodeOwners =
codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath());
codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners));
- int distance = maxDistance - codeOwnerConfig.key().folderPath().getNameCount();
+ int distance = rootDistance - codeOwnerConfig.key().folderPath().getNameCount();
pathCodeOwners.forEach(
localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
@@ -139,12 +149,29 @@
return codeOwners.size() < limit;
});
+ if (codeOwners.size() < limit) {
+ ImmutableSet<CodeOwner> globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
+ globalCodeOwners.forEach(
+ codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, maxDistance));
+ codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners));
+ }
+
return Response.ok(
codeOwnerJsonFactory
.create(getFillOptions())
.format(sortAndLimit(distanceScoring.build(), ImmutableSet.copyOf(codeOwners))));
}
+ private ImmutableSet<CodeOwner> getGlobalCodeOwners(Project.NameKey projectName) {
+ ImmutableSet<CodeOwner> globalCodeOwners =
+ codeOwnerResolver
+ .get()
+ .resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName))
+ .collect(toImmutableSet());
+ logger.atFine().log("including global code owners = %s", globalCodeOwners);
+ return globalCodeOwners;
+ }
+
/**
* Filters out code owners that should not be suggested.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
new file mode 100644
index 0000000..09dc34d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -0,0 +1,236 @@
+// 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 com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.codeowners.api.CheckCodeOwnerConfigFilesInput;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigScanner;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.validation.CodeOwnerConfigValidator;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+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.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.ListBranches;
+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.Optional;
+
+/**
+ * REST endpoint that checks/validates the code owner config files in a project.
+ *
+ * <p>This REST endpoint handles {@code POST
+ * /projects/<project-name>/branches/<branch-name>/code_owners.check_config} requests.
+ *
+ * <p>The implementation of this REST endpoint iterates over all code owner config files in the
+ * project. This means the expected performance of this REST endpoint is rather low and it should
+ * not be used in any critical path where performance matters.
+ */
+@Singleton
+public class CheckCodeOwnerConfigFiles
+ implements RestModifyView<ProjectResource, CheckCodeOwnerConfigFilesInput> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CurrentUser currentUser;
+ private final PermissionBackend permissionBackend;
+ private final Provider<ListBranches> listBranches;
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+ private final CodeOwnerConfigValidator codeOwnerConfigValidator;
+
+ @Inject
+ public CheckCodeOwnerConfigFiles(
+ CurrentUser currentUser,
+ PermissionBackend permissionBackend,
+ Provider<ListBranches> listBranches,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerConfigScanner codeOwnerConfigScanner,
+ CodeOwnerConfigValidator codeOwnerConfigValidator) {
+ this.currentUser = currentUser;
+ this.permissionBackend = permissionBackend;
+ this.listBranches = listBranches;
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerConfigScanner = codeOwnerConfigScanner;
+ this.codeOwnerConfigValidator = codeOwnerConfigValidator;
+ }
+
+ @Override
+ public Response<Map<String, Map<String, List<ConsistencyProblemInfo>>>> apply(
+ ProjectResource projectResource, CheckCodeOwnerConfigFilesInput input)
+ throws RestApiException, PermissionBackendException, IOException {
+ if (!currentUser.isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ // This REST endpoint requires the caller to be a project owner.
+ permissionBackend
+ .currentUser()
+ .project(projectResource.getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ logger.atFine().log(
+ "checking code owner config files for project %s"
+ + " (validateDisabledBranches = %s, branches = %s, path = %s)",
+ projectResource.getNameKey(), input.validateDisabledBranches, input.branches, input.path);
+
+ ImmutableSet<BranchNameKey> branches = branches(projectResource);
+
+ validateInput(projectResource.getNameKey(), branches, input);
+
+ ImmutableMap.Builder<String, Map<String, List<ConsistencyProblemInfo>>> resultsByBranchBuilder =
+ ImmutableMap.builder();
+ branches.stream()
+ .filter(branchNameKey -> shouldValidateBranch(input, branchNameKey))
+ .filter(
+ branchNameKey ->
+ validateDisabledBranches(input)
+ || !codeOwnersPluginConfiguration.isDisabled(branchNameKey))
+ .forEach(
+ branchNameKey ->
+ resultsByBranchBuilder.put(
+ branchNameKey.branch(), checkBranch(input.path, branchNameKey)));
+ return Response.ok(resultsByBranchBuilder.build());
+ }
+
+ private ImmutableSet<BranchNameKey> branches(ProjectResource projectResource)
+ throws RestApiException, IOException, PermissionBackendException {
+ return listBranches.get().apply(projectResource).value().stream()
+ .filter(branchInfo -> !"HEAD".equals(branchInfo.ref))
+ .map(branchInfo -> BranchNameKey.create(projectResource.getNameKey(), branchInfo.ref))
+ .collect(toImmutableSet());
+ }
+
+ private Map<String, List<ConsistencyProblemInfo>> checkBranch(
+ String pathGlob, BranchNameKey branchNameKey) {
+ ListMultimap<String, ConsistencyProblemInfo> problemsByPath = LinkedListMultimap.create();
+ CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+ codeOwnerConfigScanner.visit(
+ branchNameKey,
+ codeOwnerConfig -> {
+ problemsByPath.putAll(
+ codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
+ checkCodeOwnerConfig(codeOwnerBackend, codeOwnerConfig));
+ return true;
+ },
+ (codeOwnerConfigFilePath, configInvalidException) -> {
+ problemsByPath.put(
+ codeOwnerConfigFilePath.toString(),
+ new ConsistencyProblemInfo(
+ ConsistencyProblemInfo.Status.ERROR, configInvalidException.getMessage()));
+ },
+ pathGlob);
+
+ return Multimaps.asMap(problemsByPath);
+ }
+
+ private ImmutableList<ConsistencyProblemInfo> checkCodeOwnerConfig(
+ CodeOwnerBackend codeOwnerBackend, CodeOwnerConfig codeOwnerConfig) {
+ return codeOwnerConfigValidator
+ .validateCodeOwnerConfig(currentUser.asIdentifiedUser(), codeOwnerBackend, codeOwnerConfig)
+ .map(this::createConsistencyProblemInfo)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(toImmutableList());
+ }
+
+ private Optional<ConsistencyProblemInfo> createConsistencyProblemInfo(
+ CommitValidationMessage commitValidationMessage) {
+ switch (commitValidationMessage.getType()) {
+ case ERROR:
+ return Optional.of(
+ new ConsistencyProblemInfo(
+ ConsistencyProblemInfo.Status.ERROR, commitValidationMessage.getMessage()));
+ case WARNING:
+ return Optional.of(
+ new ConsistencyProblemInfo(
+ ConsistencyProblemInfo.Status.WARNING, commitValidationMessage.getMessage()));
+ case HINT:
+ case OTHER:
+ return Optional.empty();
+ }
+
+ throw new IllegalStateException(
+ String.format(
+ "unknown message type %s for message %s",
+ commitValidationMessage.getType(), commitValidationMessage.getMessage()));
+ }
+
+ private void validateInput(
+ Project.NameKey projectName,
+ ImmutableSet<BranchNameKey> branches,
+ CheckCodeOwnerConfigFilesInput input)
+ throws RestApiException {
+ if (input.branches != null) {
+ for (String branchName : input.branches) {
+ BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName);
+ if (!branches.contains(branchNameKey)) {
+ throw new UnprocessableEntityException(String.format("branch %s not found", branchName));
+ }
+
+ if ((input.validateDisabledBranches == null || !input.validateDisabledBranches)
+ && codeOwnersPluginConfiguration.isDisabled(branchNameKey)) {
+ throw new BadRequestException(
+ String.format(
+ "code owners functionality for branch %s is disabled,"
+ + " set 'validate_disabled_braches' in the input to 'true' if code owner"
+ + " config files in this branch should be validated",
+ branchName));
+ }
+ }
+ }
+ }
+
+ private static boolean shouldValidateBranch(
+ CheckCodeOwnerConfigFilesInput input, BranchNameKey branchNameKey) {
+ boolean shouldValidateBranch =
+ input.branches == null
+ || input.branches.contains(branchNameKey.branch())
+ || input.branches.contains(branchNameKey.shortName());
+ if (!shouldValidateBranch) {
+ logger.atFine().log("skip validation for branch %s", branchNameKey.branch());
+ }
+ return shouldValidateBranch;
+ }
+
+ private static boolean validateDisabledBranches(CheckCodeOwnerConfigFilesInput input) {
+ return input.validateDisabledBranches != null && input.validateDisabledBranches;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
index 26f31fc..fe26037 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
@@ -15,18 +15,22 @@
package com.google.gerrit.plugins.codeowners.restapi;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigScanner;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
import java.nio.file.Path;
import java.util.List;
+import org.kohsuke.args4j.Option;
/**
* REST endpoint that lists the code owner config files in a branch.
@@ -38,11 +42,22 @@
* branch. This means the expected performance of this REST endpoint is rather low and it should not
* be used in any critical path where performance matters.
*/
-@Singleton
public class GetCodeOwnerConfigFiles implements RestReadView<BranchResource> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerConfigScanner codeOwnerConfigScanner;
+ private String email;
+
+ @Option(
+ name = "--email",
+ metaVar = "EMAIL",
+ usage = "limits the returned code owner config files to those that contain this email")
+ public void setEmail(@Nullable String email) {
+ this.email = email;
+ }
+
@Inject
public GetCodeOwnerConfigFiles(
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
@@ -56,13 +71,47 @@
CodeOwnerBackend codeOwnerBackend =
codeOwnersPluginConfiguration.getBackend(resource.getBranchKey());
ImmutableList.Builder<Path> codeOwnerConfigs = ImmutableList.builder();
+
+ if (email != null) {
+ logger.atFine().log(
+ "limiting the returned code owner config files to those that contain the email %s",
+ email);
+ }
+
codeOwnerConfigScanner.visit(
resource.getBranchKey(),
codeOwnerConfig -> {
- codeOwnerConfigs.add(codeOwnerBackend.getFilePath(codeOwnerConfig.key()));
+ Path codeOwnerConfigPath = codeOwnerBackend.getFilePath(codeOwnerConfig.key());
+ if (email == null || containsEmail(codeOwnerConfig, codeOwnerConfigPath, email)) {
+ codeOwnerConfigs.add(codeOwnerConfigPath);
+ }
return true;
- });
+ },
+ CodeOwnerConfigScanner.ignoreInvalidCodeOwnerConfigFiles());
return Response.ok(
codeOwnerConfigs.build().stream().map(Path::toString).collect(toImmutableList()));
}
+
+ /**
+ * Checks whether the given code owner config contains the given email.
+ *
+ * @param codeOwnerConfig the code owner config for which it should be checked if it contains the
+ * email
+ * @param codeOwnerConfigPath the path of the code owner config
+ * @param email the email
+ * @return whether the given code owner config contains the given email
+ */
+ private boolean containsEmail(
+ CodeOwnerConfig codeOwnerConfig, Path codeOwnerConfigPath, String email) {
+ requireNonNull(email, "email");
+ boolean containsEmail =
+ codeOwnerConfig.codeOwnerSets().stream()
+ .flatMap(codeOwnerSet -> codeOwnerSet.codeOwners().stream())
+ .anyMatch(codeOwnerReference -> email.equals(codeOwnerReference.email()));
+ if (!containsEmail) {
+ logger.atFine().log(
+ "Filtering out %s since it doesn't contain the email", codeOwnerConfigPath);
+ }
+ return containsEmail;
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
index 6c5a3a6..baf260d 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -25,6 +25,7 @@
import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.change.IncludedInResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -65,6 +66,7 @@
@Inject
GetCodeOwnersForPathInBranch(
PermissionBackend permissionBackend,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
Provider<CodeOwnerResolver> codeOwnerResolver,
ServiceUserClassifier serviceUserClassifier,
@@ -72,6 +74,7 @@
GitRepositoryManager repoManager) {
super(
permissionBackend,
+ codeOwnersPluginConfiguration,
codeOwnerConfigHierarchy,
codeOwnerResolver,
serviceUserClassifier,
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
index 4ad31aa..43a5d21 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -20,6 +20,7 @@
import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -40,12 +41,14 @@
@Inject
GetCodeOwnersForPathInChange(
PermissionBackend permissionBackend,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
Provider<CodeOwnerResolver> codeOwnerResolver,
ServiceUserClassifier serviceUserClassifier,
CodeOwnerJson.Factory codeOwnerJsonFactory) {
super(
permissionBackend,
+ codeOwnersPluginConfiguration,
codeOwnerConfigHierarchy,
codeOwnerResolver,
serviceUserClassifier,
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
index daf66ed..8599f39 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -43,5 +43,6 @@
get(CHANGE_KIND, "code_owners.status").to(GetCodeOwnerStatus.class);
get(PROJECT_KIND, "code_owners.project_config").to(GetCodeOwnerProjectConfig.class);
+ post(PROJECT_KIND, "code_owners.check_config").to(CheckCodeOwnerConfigFiles.class);
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index f78a016..54f8f09 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -17,10 +17,10 @@
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 com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidConfigCause;
import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
@@ -523,18 +523,6 @@
}
/**
- * 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.
- */
- private static Optional<ConfigInvalidException> getInvalidConfigCause(Exception e) {
- return Throwables.getCausalChain(e).stream()
- .filter(t -> t instanceof ConfigInvalidException)
- .map(t -> (ConfigInvalidException) t)
- .findFirst();
- }
-
- /**
* Validates the given code owner config and returns validation issues as stream.
*
* <p>Validation errors that exist in both code owner configs are returned as warning (because
@@ -575,7 +563,7 @@
* @return a stream of validation messages that describe issues with the code owner config, an
* empty stream if there are no issues
*/
- private Stream<CommitValidationMessage> validateCodeOwnerConfig(
+ public Stream<CommitValidationMessage> validateCodeOwnerConfig(
IdentifiedUser user, CodeOwnerBackend codeOwnerBackend, CodeOwnerConfig codeOwnerConfig) {
requireNonNull(codeOwnerConfig, "codeOwnerConfig");
return Streams.concat(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
index d0951ac..360e2a7 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -630,4 +630,95 @@
List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
}
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "global.owner@example.com")
+ public void getGlobalCodeOwners() throws Exception {
+ TestAccount globalOwner =
+ accountCreator.create("global_owner", "global.owner@example.com", "Global Owner", null);
+ assertThat(queryCodeOwners("/foo/bar/baz.md"))
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(globalOwner.id());
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.globalCodeOwner",
+ values = {"global.owner1@example.com", "global.owner2@example.com"})
+ public void getWithGlobalCodeOwnersAndLimit() throws Exception {
+ TestAccount globalOwner1 =
+ accountCreator.create(
+ "global_owner_1", "global.owner1@example.com", "Global Owner 1", null);
+ TestAccount globalOwner2 =
+ accountCreator.create(
+ "global_owner_2", "global.owner2@example.com", "Global Owner 2", null);
+
+ TestAccount user2 = accountCreator.user2();
+
+ // create some code owner configs
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .create();
+
+ // get code owners with different limits
+ List<CodeOwnerInfo> codeOwnerInfos =
+ queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).hasSize(1);
+ // the first 2 code owners have the same scoring, so their order is random and we don't know
+ // which of them we get when the limit is 1
+ assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+
+ codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id());
+
+ codeOwnerInfos = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id());
+
+ codeOwnerInfos = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).hasSize(4);
+ // the order of the first 2 code owners is random
+ assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+ assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+ assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+ // the order of the global code owners is random
+ assertThatList(codeOwnerInfos)
+ .element(3)
+ .hasAccountIdThat()
+ .isAnyOf(globalOwner1.id(), globalOwner2.id());
+
+ codeOwnerInfos = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id(), globalOwner1.id(), globalOwner2.id());
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "service.user@example.com")
+ public void globalCodeOwnersThatAreServiceUsersAreFilteredOut() throws Exception {
+ TestAccount serviceUser =
+ accountCreator.create("serviceUser", "service.user@example.com", "Service User", null);
+ groupOperations
+ .group(groupCache.get(AccountGroup.nameKey("Service Users")).get().getGroupUUID())
+ .forUpdate()
+ .addMember(serviceUser.id())
+ .update();
+ assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
+ }
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
new file mode 100644
index 0000000..a0f3384
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
@@ -0,0 +1,488 @@
+// 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.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.config.StatusConfig;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint.
+ *
+ * <p>Further tests for the {@link
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint that
+ * require using the REST API are implemented in {@link
+ * com.google.gerrit.plugins.codeowners.acceptance.restapi.CheckCodeOwnerConfigFilesRestIT}.
+ */
+public class CheckCodeOwnerConfigFilesIT extends AbstractCodeOwnersIT {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ private BackendConfig backendConfig;
+
+ @Before
+ public void setUpCodeOwnersPlugin() throws Exception {
+ backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+ }
+
+ @Test
+ public void requiresCallerToBeProjectOwner() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ AuthException authException =
+ assertThrows(AuthException.class, () -> checkCodeOwnerConfigFilesIn(project));
+ assertThat(authException).hasMessageThat().isEqualTo("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void noCodeOwnerConfigFile() throws Exception {
+ assertThat(checkCodeOwnerConfigFilesIn(project))
+ .containsExactly(
+ "refs/heads/master", ImmutableMap.of(),
+ "refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
+ public void disabledBranchesAreSkipped() throws Exception {
+ assertThat(checkCodeOwnerConfigFilesIn(project))
+ .containsExactly("refs/heads/master", ImmutableMap.of());
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
+ public void validateDisabledBranches() throws Exception {
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .validateDisabledBranches()
+ .check())
+ .containsExactly(
+ "refs/heads/master", ImmutableMap.of(),
+ "refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ public void noIssuesInCodeOwnerConfigFile() throws Exception {
+ // Create some code owner config files.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail(user.email())
+ .create();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/baz/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ assertThat(checkCodeOwnerConfigFilesIn(project))
+ .containsExactly(
+ "refs/heads/master", ImmutableMap.of(),
+ "refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ public void nonParseableCodeOwnerConfigFile() throws Exception {
+ String codeOwnerConfigPath = "/" + getCodeOwnerConfigFileName();
+ createInvalidCodeOwnerConfig(codeOwnerConfigPath);
+
+ assertThat(checkCodeOwnerConfigFilesIn(project))
+ .containsExactly(
+ "refs/heads/master",
+ ImmutableMap.of(
+ codeOwnerConfigPath,
+ ImmutableList.of(
+ error(
+ String.format(
+ "invalid code owner config file '%s':\n %s",
+ codeOwnerConfigPath,
+ getParsingErrorMessage(
+ ImmutableMap.of(
+ FindOwnersBackend.class,
+ "invalid line: INVALID",
+ ProtoBackend.class,
+ "1:8: Expected \"{\".")))))),
+ "refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ public void issuesInCodeOwnerConfigFile() throws Exception {
+ // imports are not supported for the proto backend
+ assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+ // Create some code owner config files with issues.
+ CodeOwnerConfig.Key keyOfInvalidConfig1 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addImport(
+ CodeOwnerConfigReference.create(
+ CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
+ .create();
+
+ CodeOwnerConfig.Key keyOfInvalidConfig2 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail("unknown1@example.com")
+ .addCodeOwnerEmail("unknown2@example.com")
+ .create();
+
+ // Also create a code owner config files without issues.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/baz")
+ .addCodeOwnerEmail(user.email())
+ .create();
+
+ assertThat(checkCodeOwnerConfigFilesIn(project))
+ .containsExactly(
+ "refs/heads/master",
+ ImmutableMap.of(
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig1),
+ ImmutableList.of(
+ error(
+ String.format(
+ "invalid global import in '%s': '/not-a-code-owner-config' is"
+ + " not a code owner config file",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig1)))),
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2),
+ ImmutableList.of(
+ error(
+ String.format(
+ "code owner email 'unknown1@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2))),
+ error(
+ String.format(
+ "code owner email 'unknown2@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2))))),
+ "refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ public void validateSpecifiedBranches() throws Exception {
+ gApi.projects().name(project.get()).branch("stable-1.0").create(new BranchInput());
+ gApi.projects().name(project.get()).branch("stable-1.1").create(new BranchInput());
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("refs/heads/stable-1.0", "refs/heads/stable-1.1"))
+ .check())
+ .containsExactly(
+ "refs/heads/stable-1.0", ImmutableMap.of(),
+ "refs/heads/stable-1.1", ImmutableMap.of());
+ }
+
+ @Test
+ public void validateSpecifiedBranches_shortNames() throws Exception {
+ gApi.projects().name(project.get()).branch("stable-1.0").create(new BranchInput());
+ gApi.projects().name(project.get()).branch("stable-1.1").create(new BranchInput());
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("stable-1.0", "stable-1.1"))
+ .check())
+ .containsExactly(
+ "refs/heads/stable-1.0", ImmutableMap.of(),
+ "refs/heads/stable-1.1", ImmutableMap.of());
+ }
+
+ @Test
+ public void cannotValidateNonExistingBranch() throws Exception {
+ UnprocessableEntityException exception =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () ->
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("refs/heads/non-existing"))
+ .check());
+ assertThat(exception).hasMessageThat().isEqualTo("branch refs/heads/non-existing not found");
+ }
+
+ @Test
+ public void cannotValidateNonVisibleBranch() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ UnprocessableEntityException exception =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () ->
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("refs/heads/master"))
+ .check());
+ assertThat(exception).hasMessageThat().isEqualTo("branch refs/heads/master not found");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
+ public void cannotValidateDisabledBranchWithoutEnablingValidationForDisabledBranches()
+ throws Exception {
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("refs/meta/config"))
+ .check());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ "code owners functionality for branch refs/meta/config is disabled,"
+ + " set 'validate_disabled_braches' in the input to 'true' if code owner config"
+ + " files in this branch should be validated");
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
+ public void validateSpecifiedBranchThatIsDisabled() throws Exception {
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .validateDisabledBranches()
+ .setBranches(ImmutableList.of("refs/meta/config"))
+ .check())
+ .containsExactly("refs/meta/config", ImmutableMap.of());
+ }
+
+ @Test
+ public void validateExactFile() throws Exception {
+ // imports are not supported for the proto backend
+ assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+ // Create some code owner config files with issues.
+ CodeOwnerConfig.Key keyOfInvalidConfig1 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addImport(
+ CodeOwnerConfigReference.create(
+ CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
+ .create();
+
+ CodeOwnerConfig.Key keyOfInvalidConfig2 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail("unknown1@example.com")
+ .addCodeOwnerEmail("unknown2@example.com")
+ .create();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("master"))
+ .setPath(getCodeOwnerConfigFilePath(keyOfInvalidConfig1))
+ .check())
+ .containsExactly(
+ "refs/heads/master",
+ ImmutableMap.of(
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig1),
+ ImmutableList.of(
+ error(
+ String.format(
+ "invalid global import in '%s': '/not-a-code-owner-config' is"
+ + " not a code owner config file",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig1))))));
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("master"))
+ .setPath(getCodeOwnerConfigFilePath(keyOfInvalidConfig2))
+ .check())
+ .containsExactly(
+ "refs/heads/master",
+ ImmutableMap.of(
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2),
+ ImmutableList.of(
+ error(
+ String.format(
+ "code owner email 'unknown1@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2))),
+ error(
+ String.format(
+ "code owner email 'unknown2@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2))))));
+ }
+
+ @Test
+ public void validateFilesMatchingGlob() throws Exception {
+ // imports are not supported for the proto backend
+ assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+ // Create some code owner config files with issues.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addImport(
+ CodeOwnerConfigReference.create(
+ CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
+ .create();
+
+ CodeOwnerConfig.Key keyOfInvalidConfig2 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail("unknown1@example.com")
+ .create();
+
+ CodeOwnerConfig.Key keyOfInvalidConfig3 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail("unknown2@example.com")
+ .create();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .checkCodeOwnerConfigFiles()
+ .setBranches(ImmutableList.of("master"))
+ .setPath("/foo/**")
+ .check())
+ .containsExactly(
+ "refs/heads/master",
+ ImmutableMap.of(
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2),
+ ImmutableList.of(
+ error(
+ String.format(
+ "code owner email 'unknown1@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig2)))),
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig3),
+ ImmutableList.of(
+ error(
+ String.format(
+ "code owner email 'unknown2@example.com' in '%s' cannot be"
+ + " resolved for admin",
+ getCodeOwnerConfigFilePath(keyOfInvalidConfig3))))));
+ }
+
+ private ConsistencyProblemInfo error(String message) {
+ return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
+ }
+
+ private Map<String, Map<String, List<ConsistencyProblemInfo>>> checkCodeOwnerConfigFilesIn(
+ Project.NameKey projectName) throws RestApiException {
+ return projectCodeOwnersApiFactory.project(projectName).checkCodeOwnerConfigFiles().check();
+ }
+
+ private String getCodeOwnerConfigFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
+ return backendConfig.getDefaultBackend().getFilePath(codeOwnerConfigKey).toString();
+ }
+
+ private 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());
+ }
+
+ private void createInvalidCodeOwnerConfig(String path) throws Exception {
+ disableCodeOwnersForProject(project);
+ String changeId =
+ createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID")
+ .getChangeId();
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+ setCodeOwnersConfig(project, null, StatusConfig.KEY_DISABLED, "false");
+ }
+
+ private String getParsingErrorMessage(
+ ImmutableMap<Class<? extends CodeOwnerBackend>, String> messagesByBackend) {
+ CodeOwnerBackend codeOwnerBackend = backendConfig.getDefaultBackend();
+ assertThat(messagesByBackend).containsKey(codeOwnerBackend.getClass());
+ return messagesByBackend.get(codeOwnerBackend.getClass());
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
index 1c0c145..88b8421 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
@@ -16,12 +16,16 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.config.StatusConfig;
import org.junit.Before;
import org.junit.Test;
@@ -179,6 +183,129 @@
.isEmpty();
}
+ @Test
+ public void getCodeOwnerConfigFilesIfInvalidCodeOwnerConfigFilesExist() throws Exception {
+ createInvalidCodeOwnerConfig(getCodeOwnerConfigFileName());
+
+ CodeOwnerConfig.Key codeOwnerConfigKey =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .codeOwnerConfigFiles()
+ .paths())
+ .containsExactly(getCodeOwnerConfigFilePath(codeOwnerConfigKey));
+ }
+
+ @Test
+ public void filterByEmail() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
+
+ CodeOwnerConfig.Key codeOwnerConfigKey1 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(user.email())
+ .create();
+
+ CodeOwnerConfig.Key codeOwnerConfigKey2 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ CodeOwnerConfig.Key codeOwnerConfigKey3 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression("foo")
+ .addCodeOwnerEmail(user.email())
+ .build())
+ .create();
+
+ CodeOwnerConfig.Key codeOwnerConfigKey4 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/baz/")
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerEmail(user.email())
+ .create();
+
+ CodeOwnerConfig.Key codeOwnerConfigKey5 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/xyz/")
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerEmail(user2.email())
+ .create();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .codeOwnerConfigFiles()
+ .withEmail(admin.email())
+ .paths())
+ .containsExactly(
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey2),
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey4),
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey5))
+ .inOrder();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .codeOwnerConfigFiles()
+ .withEmail(user.email())
+ .paths())
+ .containsExactly(
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey1),
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey3),
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey4))
+ .inOrder();
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .codeOwnerConfigFiles()
+ .withEmail(user2.email())
+ .paths())
+ .containsExactly(getCodeOwnerConfigFilePath(codeOwnerConfigKey5));
+
+ assertThat(
+ projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .codeOwnerConfigFiles()
+ .withEmail(user3.email())
+ .paths())
+ .isEmpty();
+ }
+
private String getCodeOwnerConfigFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
return backendConfig.getDefaultBackend().getFilePath(codeOwnerConfigKey).toString();
}
@@ -192,4 +319,14 @@
}
throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
}
+
+ private void createInvalidCodeOwnerConfig(String path) throws Exception {
+ disableCodeOwnersForProject(project);
+ String changeId =
+ createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID")
+ .getChangeId();
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+ setCodeOwnersConfig(project, null, StatusConfig.KEY_DISABLED, "false");
+ }
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerConfigFilesRestIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerConfigFilesRestIT.java
new file mode 100644
index 0000000..eaf3ef1
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerConfigFilesRestIT.java
@@ -0,0 +1,64 @@
+// 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.acceptance.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint that
+ * require using REST.
+ *
+ * <p>Acceptance test for the {@link
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint that can
+ * use the Java API are implemented in {@link
+ * com.google.gerrit.plugins.codeowners.acceptance.api.CheckCodeOwnerConfigFilesIT}.
+ *
+ * <p>The tests in this class do not depend on the used code owner backend, hence we do not need to
+ * extend {@link AbstractCodeOwnersIT}.
+ */
+public class CheckCodeOwnerConfigFilesRestIT extends AbstractCodeOwnersTest {
+ @Test
+ @GerritConfig(name = "plugin.code-owners.backend", value = "non-existing-backend")
+ public void cannotCheckCodeOwnerConfigFilesIfPluginConfigurationIsInvalid() throws Exception {
+ RestResponse r =
+ adminRestSession.post(
+ String.format(
+ "/projects/%s/code_owners.check_config", IdString.fromDecoded(project.get())));
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains(
+ "Invalid configuration of the code-owners plugin. Code owner backend"
+ + " 'non-existing-backend' that is configured in gerrit.config (parameter"
+ + " plugin.code-owners.backend) not found.");
+ }
+
+ @Test
+ public void cannotCheckCodeOwnerConfigFilesAnonymously() throws Exception {
+ RestResponse r =
+ anonymousRestSession.post(
+ String.format(
+ "/projects/%s/code_owners.check_config", IdString.fromDecoded(project.get())));
+ r.assertForbidden();
+ assertThat(r.getEntityContent()).contains("Authentication required");
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
index ae1aee8..7d90176 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
@@ -41,7 +41,9 @@
ImmutableList.of(RestCall.get("/changes/%s/code-owners~code_owners.status"));
private static final ImmutableList<RestCall> PROJECT_ENDPOINTS =
- ImmutableList.of(RestCall.get("/projects/%s/code-owners~code_owners.project_config"));
+ ImmutableList.of(
+ RestCall.get("/projects/%s/code-owners~code_owners.project_config"),
+ RestCall.post("/projects/%s/code_owners.check_config"));
private static final ImmutableList<RestCall> BRANCH_ENDPOINTS =
ImmutableList.of(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
index feaa728..ea0eab7 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
@@ -17,14 +17,19 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
+import com.google.gerrit.plugins.codeowners.config.StatusConfig;
+import java.nio.file.Paths;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -44,6 +49,7 @@
@Rule public final MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Mock private CodeOwnerConfigVisitor visitor;
+ @Mock private InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback;
private CodeOwnerConfigOperations codeOwnerConfigOperations;
private CodeOwnerConfigScanner codeOwnerConfigScanner;
@@ -58,7 +64,9 @@
@Test
public void cannotVisitCodeOwnerConfigsForNullBranch() throws Exception {
NullPointerException npe =
- assertThrows(NullPointerException.class, () -> codeOwnerConfigScanner.visit(null, visitor));
+ assertThrows(
+ NullPointerException.class,
+ () -> codeOwnerConfigScanner.visit(null, visitor, invalidCodeOwnerConfigCallback));
assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
}
@@ -67,17 +75,31 @@
BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
NullPointerException npe =
assertThrows(
- NullPointerException.class, () -> codeOwnerConfigScanner.visit(branchNameKey, null));
+ NullPointerException.class,
+ () ->
+ codeOwnerConfigScanner.visit(branchNameKey, null, invalidCodeOwnerConfigCallback));
assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigVisitor");
}
@Test
+ public void cannotVisitCodeOwnerConfigsWithNullCallback() throws Exception {
+ BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () -> codeOwnerConfigScanner.visit(branchNameKey, visitor, null));
+ assertThat(npe).hasMessageThat().isEqualTo("invalidCodeOwnerConfigCallback");
+ }
+
+ @Test
public void cannotVisitCodeOwnerConfigsForNonExistingBranch() throws Exception {
BranchNameKey branchNameKey = BranchNameKey.create(project, "non-existing");
IllegalStateException exception =
assertThrows(
IllegalStateException.class,
- () -> codeOwnerConfigScanner.visit(branchNameKey, visitor));
+ () ->
+ codeOwnerConfigScanner.visit(
+ branchNameKey, visitor, invalidCodeOwnerConfigCallback));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
@@ -89,6 +111,7 @@
public void visitorNotInvokedIfNoCodeOwnerConfigFilesExists() throws Exception {
visit();
verifyZeroInteractions(visitor);
+ verifyZeroInteractions(invalidCodeOwnerConfigCallback);
}
@Test
@@ -112,6 +135,50 @@
visit();
verifyZeroInteractions(visitor);
+ verifyZeroInteractions(invalidCodeOwnerConfigCallback);
+ }
+
+ @Test
+ public void visitorNotInvokedForInvalidCodeOwnerConfigFiles() throws Exception {
+ createInvalidCodeOwnerConfig("/OWNERS");
+
+ visit();
+ verifyZeroInteractions(visitor);
+
+ // Verify that we received the expected callbacks for the invalid code onwer config.
+ Mockito.verify(invalidCodeOwnerConfigCallback)
+ .onInvalidCodeOwnerConfig(eq(Paths.get("/OWNERS")), any(ConfigInvalidException.class));
+ verifyNoMoreInteractions(invalidCodeOwnerConfigCallback);
+ }
+
+ @Test
+ public void visitorInvokedForValidCodeOwnerConfigFilesEvenIfInvalidCodeOwnerConfigFileExist()
+ throws Exception {
+ createInvalidCodeOwnerConfig("/OWNERS");
+
+ // Create a valid code owner config file.
+ CodeOwnerConfig.Key codeOwnerConfigKey =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .fileName("OWNERS")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
+ visit();
+
+ // Verify that we received the expected callbacks.
+ Mockito.verify(visitor)
+ .visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
+ verifyNoMoreInteractions(visitor);
+
+ // Verify that we received the expected callbacks for the invalid code onwer config.
+ Mockito.verify(invalidCodeOwnerConfigCallback)
+ .onInvalidCodeOwnerConfig(eq(Paths.get("/OWNERS")), any(ConfigInvalidException.class));
+ verifyNoMoreInteractions(invalidCodeOwnerConfigCallback);
}
@Test
@@ -200,6 +267,8 @@
Mockito.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
+
+ verifyZeroInteractions(invalidCodeOwnerConfigCallback);
}
@Test
@@ -249,6 +318,8 @@
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooBarCodeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
+
+ verifyZeroInteractions(invalidCodeOwnerConfigCallback);
}
@Test
@@ -297,6 +368,8 @@
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
+
+ verifyZeroInteractions(invalidCodeOwnerConfigCallback);
}
@Test
@@ -324,7 +397,37 @@
.isTrue();
}
+ @Test
+ public void containsACodeOwnerConfigFile_invalidCodeOwnerConfigFileExists() throws Exception {
+ createInvalidCodeOwnerConfig("/OWNERS");
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .fileName("OWNERS")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ assertThat(
+ codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
+ BranchNameKey.create(project, "master")))
+ .isTrue();
+ }
+
private void visit() {
- codeOwnerConfigScanner.visit(BranchNameKey.create(project, "master"), visitor);
+ codeOwnerConfigScanner.visit(
+ BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback);
+ }
+
+ private void createInvalidCodeOwnerConfig(String path) throws Exception {
+ disableCodeOwnersForProject(project);
+ String changeId =
+ createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID")
+ .getChangeId();
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+ setCodeOwnersConfig(project, null, StatusConfig.KEY_DISABLED, "false");
}
}
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index b3ea9b9..84e36a1 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -56,6 +56,73 @@
}
```
+### <a id="check-code-owner-config-files">Check Code Owner Config Files
+_'POST /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/code_owners.check_config'_
+
+Checks/validates the code owner config files in a project.
+
+Requires that the caller is an owner of the project.
+
+Input options can be set in the request body as a
+[CheckCodeOwnerConfigFilesInput](#check-code-owner-config-files-input) entity.
+
+No validation is done for branches for which the code owner functionality is
+[disabled](config.html#codeOwnersDisabledBranch), unless
+`validate_disabled_branches` is set to `true` in the
+[input](#check-code-owner-config-files-input).
+
+As a response a map is returned that maps a branch name to a map that maps an
+owner configuration file path to a list of
+[ConsistencyProblemInfo](../../../Documentation/rest-api-config.html#consistency-problem-info)
+entities.
+
+Code owner config files that have no issues are omitted from the response.
+
+#### Request
+
+```
+ POST /projects/foo%2Fbar/code_owners.check_config HTTP/1.0
+```
+
+#### Response
+
+```
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "refs/heads/master": {
+ "/OWNERS": [
+ {
+ "status": "ERROR",
+ "message": "code owner email 'foo@example.com' in '/OWNERS' cannot be resolved for John Doe"
+ },
+ {
+ "status": "ERROR",
+ "message": "code owner email 'bar@example.com' in '/OWNERS' cannot be resolved for John Doe"
+ }
+ ],
+ "/foo/OWNERS": [
+ {
+ "status": "ERROR",
+ "message": "invalid global import in '/foo/OWNERS': '/not-a-code-owner-config' is not a code owner config file"
+ }
+ ]
+ },
+ "refs/heads/stable-1.0" {},
+ "refs/heads/stable-1.1" {
+ "/foo/OWNERS": [
+ {
+ "status": "ERROR",
+ "message": "invalid global import in '/foo/OWNERS': '/not-a-code-owner-config' is not a code owner config file"
+ }
+ ]
+ }
+ }
+```
+
## <a id="branch-endpoints">Branch Endpoints
### <a id="list-code-owner-config-files">List Code Owner Config Files
@@ -68,6 +135,13 @@
why this REST endpoint must not be used in any critical paths where performance
matters.
+The following request parameters can be specified:
+
+| Field Name | | Description |
+| ----------- | -------- | ----------- |
+| `email` | optional | Code owner email that must appear in the returned
+code owner config files.
+
#### Request
```
@@ -78,6 +152,8 @@
result also includes code owner config that use name prefixes
('\<prefix\>_OWNERS') or name extensions ('OWNERS_\<extension\>').
+Non-parseable code owner config files are omitted from the response.
+
#### Response
```
@@ -299,6 +375,18 @@
---
+### <a id="check-code-owner-config-files-input"> CheckCodeOwnerConfigFilesInput
+The `CheckCodeOwnerConfigFilesInput` allows to set options for the [Check Code
+Owner Config Files REST endpoint](#check-code-owner-config-files).
+
+| Field Name | | Description |
+| ---------------------------- | -------- | ----------- |
+| `validate_disabled_branches` | optional | Whether code owner config files in branches for which the code owners functionality is disabled should be validated too. By default unset, `false`.
+| `branches` | optional | List of branches for which code owner config files should be validated. The `refs/heads/` prefix may be omitted. By default unset, which means that code owner config files in all branches should be validated.
+| `path` | optional | Glob that limits the validation to code owner config files that have a path that matches this glob. By default unset, which means that all code owner config files should be validated.
+
+---
+
### <a id="code-owner-config-info"> CodeOwnerConfigInfo
The `CodeOwnerConfigInfo` entity contains information about a code owner config
for a path.
diff --git a/resources/Documentation/setup-guide.md b/resources/Documentation/setup-guide.md
index 62cd076..507f50c 100644
--- a/resources/Documentation/setup-guide.md
+++ b/resources/Documentation/setup-guide.md
@@ -349,6 +349,11 @@
submitting an invalid code owner config that may block the submission of all
changes (e.g. if it is not parseable).
+**NOTE** If the repository contains pre-existing code owner config files, it is
+recommended to validate them via the [Check code owners files REST
+endpoint](rest-api.html#check-code-owner-config-files) and fix the reported
+issues.
+
### <a id="faq">FAQ's
##### <a id="updateCodeOwnersConfig">How to update the code-owners.config file for a project
diff --git a/resources/Documentation/validation.md b/resources/Documentation/validation.md
index 4d3165d..9966f09 100644
--- a/resources/Documentation/validation.md
+++ b/resources/Documentation/validation.md
@@ -9,7 +9,9 @@
configuration of the `@PLUGIN@` plugin
To reduce the risk that these files become invalid, they are validated when
-they are modified and invalid modifications are rejected.
+they are modified and invalid modifications are rejected. In addition code owner
+config files in a repository can be validated on demand by the [Check code
+owners files REST endpoint](rest-api.html#check-code-owner-config-files).
**NOTE:** Most configuration issues are gracefully handled and do not break the
code owners functionality (e.g. non-resolveable code owners or non-resolveable