CheckCodeOwner: Include annotations into the response
This REST endpoint is used to debug code ownerships. Since annotations
are important they should be returned as well.
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I687494d8a1e1654809d09014a207c2361a4515de
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
index c96645c..19b02a0 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -131,6 +131,16 @@
/** Whether the the specified path in the branch is owned by all users (aka {@code *}). */
public boolean isOwnedByAllUsers;
+ /**
+ * Annotations that were set for the user.
+ *
+ * <p>Contains only supported annotations (unsupported annotations are reported in the {@link
+ * #debugLogs}).
+ *
+ * <p>Sorted alphabetically.
+ */
+ public List<String> annotations;
+
/** Debug logs that may help to understand why the user is or isn't a code owner. */
public List<String> debugLogs;
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java
index b3977ff..a9d3edb 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java
@@ -14,6 +14,9 @@
package com.google.gerrit.plugins.codeowners.backend;
+import java.util.ArrayList;
+import java.util.List;
+
/** Class that defines all known/supported {@link CodeOwnerAnnotation}s on code owners. */
public class CodeOwnerAnnotations {
/**
@@ -23,6 +26,18 @@
public static final CodeOwnerAnnotation NEVER_SUGGEST_ANNOTATION =
CodeOwnerAnnotation.create("NEVER_SUGGEST");
+ private static final List<String> KEYS_ALL;
+
+ static {
+ KEYS_ALL = new ArrayList<>();
+ KEYS_ALL.add(NEVER_SUGGEST_ANNOTATION.key());
+ }
+
+ /** Whether the given annotation is known and supported. */
+ public static boolean isSupported(String annotation) {
+ return KEYS_ALL.contains(annotation);
+ }
+
/**
* Private constructor to prevent instantiation of this class.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
index 5b2ef36..93a36be 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -83,6 +83,13 @@
return annotations;
}
+ /** Gets the annotations for the given email. */
+ public ImmutableSet<String> getAnnotationsFor(String email) {
+ return getAnnotations().get(CodeOwnerReference.create(email)).stream()
+ .map(CodeOwnerAnnotation::key)
+ .collect(toImmutableSet());
+ }
+
/**
* Whether parent code owners should be ignored for the path.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index f4e5c0f..4f65355 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -14,8 +14,12 @@
package com.google.gerrit.plugins.codeowners.restapi;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.stream.Collectors.toList;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,6 +31,7 @@
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
@@ -57,8 +62,10 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -157,6 +164,7 @@
AtomicBoolean isDefaultCodeOwner = new AtomicBoolean(false);
AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false);
+ Set<String> annotations = new HashSet<>();
codeOwnerConfigHierarchy.visit(
branchResource.getBranchKey(),
ObjectId.fromString(branchResource.getRevision()),
@@ -196,6 +204,14 @@
"found email %s as code owner in %s", email, codeOwnerConfigFilePath));
codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
}
+
+ ImmutableSet<String> localAnnotations =
+ pathCodeOwnersResult.get().getAnnotationsFor(email);
+ if (!localAnnotations.isEmpty()) {
+ messages.add(
+ String.format("email %s is annotated with %s", email, sort(localAnnotations)));
+ annotations.addAll(localAnnotations);
+ }
}
if (pathCodeOwnersResult.get().getPathCodeOwners().stream()
@@ -218,6 +234,16 @@
codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
}
}
+
+ ImmutableSet<String> localAnnotations =
+ pathCodeOwnersResult.get().getAnnotationsFor(CodeOwnerResolver.ALL_USERS_WILDCARD);
+ if (!localAnnotations.isEmpty()) {
+ messages.add(
+ String.format(
+ "found annotations for the all users wildcard ('%s') which apply to %s: %s",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, email, sort(localAnnotations)));
+ annotations.addAll(localAnnotations);
+ }
}
if (codeOwnerResolverProvider
@@ -282,6 +308,17 @@
}
}
+ ImmutableSet<String> unsupportedAnnotations =
+ annotations.stream()
+ .filter(annotation -> !CodeOwnerAnnotations.isSupported(annotation))
+ .collect(toImmutableSet());
+ if (!unsupportedAnnotations.isEmpty()) {
+ messages.add(
+ String.format(
+ "dropping unsupported annotations for %s: %s", email, sort(unsupportedAnnotations)));
+ annotations.removeAll(unsupportedAnnotations);
+ }
+
boolean isFallbackCodeOwner =
!isCodeOwnershipAssignedToEmail.get()
&& !isCodeOwnershipAssignedToAllUsers.get()
@@ -305,6 +342,7 @@
codeOwnerCheckInfo.isDefaultCodeOwner = isDefaultCodeOwner.get();
codeOwnerCheckInfo.isGlobalCodeOwner = isGlobalCodeOwner;
codeOwnerCheckInfo.isOwnedByAllUsers = isCodeOwnershipAssignedToAllUsers.get();
+ codeOwnerCheckInfo.annotations = sort(annotations);
codeOwnerCheckInfo.debugLogs = messages;
return Response.ok(codeOwnerCheckInfo);
}
@@ -408,4 +446,8 @@
}
return OptionalResultWithMessages.createEmpty(messages);
}
+
+ private ImmutableList<String> sort(Set<String> set) {
+ return set.stream().sorted().collect(toImmutableList());
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
index 31fa4e0..3128b7f 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -133,6 +133,10 @@
check("isOwnedByAllUsers").that(codeOwnerCheckInfo().isOwnedByAllUsers).isFalse();
}
+ public IterableSubject hasAnnotationsThat() {
+ return check("annotations").that(codeOwnerCheckInfo().annotations);
+ }
+
public void hasDebugLogsThatContainAllOf(String... expectedMessages) {
for (String expectedMessage : expectedMessages) {
check("debugLogs").that(codeOwnerCheckInfo().debugLogs).contains(expectedMessage);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
index 0f12736..582617a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -23,6 +23,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -44,6 +45,8 @@
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
@@ -144,6 +147,7 @@
assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
+ assertThat(checkCodeOwnerInfo).hasAnnotationsThat().isEmpty();
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
@@ -1454,6 +1458,89 @@
assertThat(checkCodeOwnerInfo).cannotApproveChange();
}
+ @Test
+ public void checkCodeOwnerWithAnnotations() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(codeOwner.email())
+ .addAnnotation(codeOwner.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .build())
+ .create();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(CodeOwnerResolver.ALL_USERS_WILDCARD)
+ .addAnnotation(
+ CodeOwnerResolver.ALL_USERS_WILDCARD, CodeOwnerAnnotation.create("ANNOTATION"))
+ .build())
+ .create();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(codeOwner.email())
+ .addAnnotation(codeOwner.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addAnnotation(codeOwner.email(), CodeOwnerAnnotation.create("OTHER_ANNOTATION"))
+ .build())
+ .create();
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasAnnotationsThat()
+ .containsExactly(CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION.key())
+ .inOrder();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
+ String.format(
+ "email %s is annotated with %s",
+ codeOwner.email(),
+ ImmutableSet.of(
+ CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION.key(), "OTHER_ANNOTATION")),
+ String.format(
+ "found email %s as code owner in %s",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath("/foo/")),
+ String.format(
+ "found annotations for the all users wildcard ('%s') which apply to %s: %s",
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ codeOwner.email(),
+ ImmutableSet.of("ANNOTATION")),
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/")),
+ String.format(
+ "email %s is annotated with %s",
+ codeOwner.email(),
+ ImmutableSet.of(CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION.key())),
+ String.format(
+ "dropping unsupported annotations for %s: %s",
+ codeOwner.email(), ImmutableSet.of("ANNOTATION", "OTHER_ANNOTATION")));
+ }
+
private CodeOwnerCheckInfo checkCodeOwner(String path, String email) throws RestApiException {
return checkCodeOwner(path, email, /* user= */ null);
}
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 0ab7345..90d6055 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -847,6 +847,7 @@
| `is_global_code_owner` | Whether the given email is configured as a global
code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
| `is_owned_by_all_users` | Whether the the specified path in the branch is owned by all users (aka `*`).
+| `annotation` | Annotations that were set for the user. Contains only supported annotations (unsupported annotations are reported in the `debugs_logs`). Sorted alphabetically.
| `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner.
---