Merge changes I9bc8a28d,I4352392a,Ic685a93d
* changes:
CodeOwnerApprovalCheck: Do not check twice if a global owner is reviewer
CodeOwnerApprovalCheck: Drop unreachable code
Simplify assertions in CodeOwnerApprovalCheck*Test
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
index 4e381d7..412aebc 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
@@ -102,6 +102,11 @@
assumeThatCodeOwnersBackendIsNotProtoBackend();
}
+ protected void skipTestIfAnnotationsNotSupportedByCodeOwnersBackend() {
+ // the proto backend doesn't support annotations on code owners
+ assumeThatCodeOwnersBackendIsNotProtoBackend();
+ }
+
protected void assumeThatCodeOwnersBackendIsNotProtoBackend() {
assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
}
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/CodeOwnerAnnotation.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotation.java
new file mode 100644
index 0000000..bcb4d8c
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotation.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * An annotation on a {@link CodeOwnerReference} in a {@link CodeOwnerConfig}.
+ *
+ * <p>This is a class rather than string so that we can easily support values on annotations later.
+ */
+@AutoValue
+public abstract class CodeOwnerAnnotation {
+ public abstract String key();
+
+ public static CodeOwnerAnnotation create(String key) {
+ return new AutoValue_CodeOwnerAnnotation(key);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java
new file mode 100644
index 0000000..a9d3edb
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerAnnotations.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Class that defines all known/supported {@link CodeOwnerAnnotation}s on code owners. */
+public class CodeOwnerAnnotations {
+ /**
+ * Code owners with this annotation are omitted when suggesting code owners (see {@link
+ * com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInChange}).
+ */
+ 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.
+ *
+ * <p>This class contains only static method and hence never needs to be instantiated.
+ */
+ private CodeOwnerAnnotations() {}
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index b363dd2..6ed577e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -22,8 +22,10 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
@@ -228,6 +230,7 @@
pathCodeOwners.resolveCodeOwnerConfig();
return resolve(
pathCodeOwnersResult.get().getPathCodeOwners(),
+ pathCodeOwnersResult.get().getAnnotations(),
pathCodeOwnersResult.get().unresolvedImports(),
pathCodeOwnersResult.messages());
}
@@ -253,6 +256,7 @@
public CodeOwnerResolverResult resolve(Set<CodeOwnerReference> codeOwnerReferences) {
return resolve(
codeOwnerReferences,
+ /* annotations= */ ImmutableMultimap.of(),
/* unresolvedImports= */ ImmutableList.of(),
/* pathCodeOwnersMessages= */ ImmutableList.of());
}
@@ -264,12 +268,14 @@
* parallel (via {@link AccountCache#get(Set)}.
*
* @param codeOwnerReferences the code owner references that should be resolved
+ * @param annotationsByCodeOwnerReference annotations by code owner reference
* @param unresolvedImports list of unresolved imports
* @param pathCodeOwnersMessages messages that were collected when resolving path code owners
* @return the resolved code owner references as a {@link CodeOwnerResolverResult}
*/
private CodeOwnerResolverResult resolve(
Set<CodeOwnerReference> codeOwnerReferences,
+ ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotationsByCodeOwnerReference,
List<UnresolvedImport> unresolvedImports,
ImmutableList<String> pathCodeOwnersMessages) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
@@ -285,12 +291,23 @@
AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
- ImmutableSet<CodeOwner> codeOwners =
- resolve(messageBuilder, ownedByAllUsers, hasUnresolvedCodeOwners, codeOwnerReferences);
+ ImmutableMap<CodeOwner, ImmutableSet<CodeOwnerAnnotation>> codeOwnersWithAnnotations =
+ resolve(
+ messageBuilder,
+ ownedByAllUsers,
+ hasUnresolvedCodeOwners,
+ codeOwnerReferences,
+ annotationsByCodeOwnerReference);
+
+ ImmutableMultimap.Builder<CodeOwner, CodeOwnerAnnotation> annotationsByCodeOwner =
+ ImmutableMultimap.builder();
+ codeOwnersWithAnnotations.forEach(
+ (codeOwner, annotations) -> annotationsByCodeOwner.putAll(codeOwner, annotations));
CodeOwnerResolverResult codeOwnerResolverResult =
CodeOwnerResolverResult.create(
- codeOwners,
+ codeOwnersWithAnnotations.keySet(),
+ annotationsByCodeOwner.build(),
ownedByAllUsers.get(),
hasUnresolvedCodeOwners.get(),
!unresolvedImports.isEmpty(),
@@ -341,17 +358,19 @@
ImmutableList.Builder<String> messageBuilder = ImmutableList.builder();
AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
- ImmutableSet<CodeOwner> codeOwners =
+ ImmutableMap<CodeOwner, ImmutableSet<CodeOwnerAnnotation>> codeOwnersWithAnnotations =
resolve(
messageBuilder,
ownedByAllUsers,
hasUnresolvedCodeOwners,
- ImmutableSet.of(codeOwnerReference));
+ ImmutableSet.of(codeOwnerReference),
+ /* annotations= */ ImmutableMultimap.of());
ImmutableList<String> messages = messageBuilder.build();
- if (codeOwners.isEmpty()) {
+ if (codeOwnersWithAnnotations.isEmpty()) {
return OptionalResultWithMessages.createEmpty(messages);
}
- return OptionalResultWithMessages.create(Iterables.getOnlyElement(codeOwners), messages);
+ return OptionalResultWithMessages.create(
+ Iterables.getOnlyElement(codeOwnersWithAnnotations.keySet()), messages);
}
/**
@@ -366,13 +385,17 @@
* @param hasUnresolvedCodeOwners a flag that is set any of the given {@link CodeOwnerReference}s
* cannot be resolved
* @param codeOwnerReferences the code owner references that should be resolved
- * @return the resolved code owner references as a {@link CodeOwner}s
+ * @param annotations annotations by code owner reference
+ * @return map that maps the resolved {@link CodeOwner}s to their annotations (note: we cannot
+ * return a {@code Multimap<CodeOwner, CodeOwnerAnnotation>} here since there may be code
+ * owners without annotations and Multimap doesn't store keys for which no values are stored)
*/
- private ImmutableSet<CodeOwner> resolve(
+ private ImmutableMap<CodeOwner, ImmutableSet<CodeOwnerAnnotation>> resolve(
ImmutableList.Builder<String> messages,
AtomicBoolean ownedByAllUsers,
AtomicBoolean hasUnresolvedCodeOwners,
- Set<CodeOwnerReference> codeOwnerReferences) {
+ Set<CodeOwnerReference> codeOwnerReferences,
+ ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
ImmutableSet<String> emailsToResolve =
@@ -416,16 +439,30 @@
hasUnresolvedCodeOwners.set(true);
}
- ImmutableSet<CodeOwner> cachedCodeOwners =
- cachedCodeOwnersByEmail.values().stream()
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(toImmutableSet());
+ // Merge code owners that have been newly resolved with code owners which have been looked up
+ // from cache and return them with their annotations.
+ Stream<Pair<String, CodeOwner>> newlyResolvedCodeOwnersStream =
+ codeOwnersByEmail.entrySet().stream().map(e -> Pair.of(e.getKey(), e.getValue()));
+ Stream<Pair<String, CodeOwner>> cachedCodeOwnersStream =
+ cachedCodeOwnersByEmail.entrySet().stream()
+ .filter(e -> e.getValue().isPresent())
+ .map(e -> Pair.of(e.getKey(), e.getValue().get()));
+ return Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream)
+ .collect(
+ toImmutableMap(
+ Pair::value,
+ p -> {
+ ImmutableSet.Builder<CodeOwnerAnnotation> annotationBuilder =
+ ImmutableSet.builder();
- ImmutableSet.Builder<CodeOwner> codeOwners = ImmutableSet.builder();
- codeOwners.addAll(cachedCodeOwners);
- codeOwners.addAll(codeOwnersByEmail.values());
- return codeOwners.build();
+ annotationBuilder.addAll(annotations.get(CodeOwnerReference.create(p.key())));
+
+ // annotations for the all users wildcard (aka '*') apply to all code owners
+ annotationBuilder.addAll(
+ annotations.get(CodeOwnerReference.create(ALL_USERS_WILDCARD)));
+
+ return annotationBuilder.build();
+ }));
}
/**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index 0744e5b..7dd9e96 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import java.util.List;
@@ -39,6 +40,9 @@
return codeOwners().stream().map(CodeOwner::accountId).collect(toImmutableSet());
}
+ /** Returns the annotations for the {@link #codeOwners()}. */
+ public abstract ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations();
+
/**
* Whether the code ownership was assigned to all users by using the {@link
* CodeOwnerResolver#ALL_USERS_WILDCARD}.
@@ -69,6 +73,7 @@
public final String toString() {
return MoreObjects.toStringHelper(this)
.add("codeOwners", codeOwners())
+ .add("annotations", annotations())
.add("ownedByAllUsers", ownedByAllUsers())
.add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners())
.add("hasUnresolvedImports", hasUnresolvedImports())
@@ -79,12 +84,14 @@
/** Creates a {@link CodeOwnerResolverResult} instance. */
public static CodeOwnerResolverResult create(
ImmutableSet<CodeOwner> codeOwners,
+ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
boolean ownedByAllUsers,
boolean hasUnresolvedCodeOwners,
boolean hasUnresolvedImports,
List<String> messages) {
return new AutoValue_CodeOwnerResolverResult(
codeOwners,
+ annotations,
ownedByAllUsers,
hasUnresolvedCodeOwners,
hasUnresolvedImports,
@@ -95,6 +102,7 @@
public static CodeOwnerResolverResult createEmpty() {
return new AutoValue_CodeOwnerResolverResult(
/* codeOwners= */ ImmutableSet.of(),
+ /* annotations= */ ImmutableMultimap.of(),
/* ownedByAllUsers= */ false,
/* hasUnresolvedCodeOwners= */ false,
/* hasUnresolvedImports= */ false,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
index 523c2ee..40a4a3b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScore.java
@@ -53,7 +53,21 @@
* <p>The IS_REVIEWER score has a higher weight than the {@link #DISTANCE} score so that it takes
* precedence and code owners that are reviewers are always returned first.
*/
- IS_REVIEWER(Kind.GREATER_VALUE_IS_BETTER, /* weight= */ 2, /* maxValue= */ 1);
+ IS_REVIEWER(Kind.GREATER_VALUE_IS_BETTER, /* weight= */ 2, /* maxValue= */ 1),
+
+ /**
+ * Score to take into account when a user is explicitly mentioned as a code owner
+ *
+ * <p>Users that are explicitly mentioned as code owner in a code owner config file get scored
+ * with 1 (see {@link #IS_EXPLICITLY_MENTIONED_SCORING_VALUE}), while users that are not
+ * explicitly mentioned as code owners in the code owner config file, and are only code owners
+ * because the code ownership is assigned to all users aka {@code *}, get scored with 0 (see
+ * {@link #NOT_EXPLICITLY_MENTIONED_SCORING_VALUE}).
+ *
+ * <p>The IS_EXPLICITLY_MENTIONED score has a lower weight than the {@link #DISTANCE} score so
+ * that the {@link #DISTANCE} score takes precedence.
+ */
+ IS_EXPLICITLY_MENTIONED(Kind.GREATER_VALUE_IS_BETTER, /* weight= */ 0.5, /* maxValue= */ 1);
/**
* Scoring value for the {@link #IS_REVIEWER} score for users that are not a reviewer of the
@@ -67,6 +81,19 @@
public static int IS_REVIEWER_SCORING_VALUE = 1;
/**
+ * Scoring value for the {@link #IS_EXPLICITLY_MENTIONED} score for users that are not explicitly
+ * mentioned as code owners in the code owner config file and are only code owners because the
+ * code ownership is assigned to all users aka {@code *}.
+ */
+ public static int NOT_EXPLICITLY_MENTIONED_SCORING_VALUE = 0;
+
+ /**
+ * Scoring value for the {@link #IS_EXPLICITLY_MENTIONED} score for users that are explicitly
+ * mentioned as code owners in the code owner config file.
+ */
+ public static int IS_EXPLICITLY_MENTIONED_SCORING_VALUE = 1;
+
+ /**
* Score kind.
*
* <p>Whether a greater value as scoring is better than a lower value ({@code
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSet.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSet.java
index 02b6524..43f49d5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSet.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSet.java
@@ -19,8 +19,10 @@
import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
+import java.util.Set;
/**
* A code owner set defines a set of code owners for a set of path expressions.
@@ -71,6 +73,9 @@
/** Gets the code owners of this code owner set. */
public abstract ImmutableSet<CodeOwnerReference> codeOwners();
+ /** Gets the annotations of the {@link #codeOwners()}. */
+ public abstract ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations();
+
/**
* Creates a builder from this code owner set.
*
@@ -199,6 +204,37 @@
return this;
}
+ /** Gets a builder to add code owner annotations. */
+ abstract ImmutableMultimap.Builder<CodeOwnerReference, CodeOwnerAnnotation>
+ annotationsBuilder();
+
+ /**
+ * Adds an annotation for a code owner.
+ *
+ * @param email email of the code owner for which the annotation should be added
+ * @param annotation annotation that should be added
+ * @return the Builder instance for chaining calls
+ */
+ public Builder addAnnotation(String email, CodeOwnerAnnotation annotation) {
+ return addAnnotations(CodeOwnerReference.create(email), ImmutableSet.of(annotation));
+ }
+
+ /**
+ * Adds annotations for a code owner.
+ *
+ * @param codeOwnerReference reference to the code owner for which the annotations should be
+ * added
+ * @param annotations annotations that should be added
+ * @return the Builder instance for chaining calls
+ */
+ public Builder addAnnotations(
+ CodeOwnerReference codeOwnerReference, Set<CodeOwnerAnnotation> annotations) {
+ requireNonNull(codeOwnerReference, "codeOwnerReference");
+ requireNonNull(annotations, "annotations");
+ annotationsBuilder().putAll(codeOwnerReference, annotations);
+ return this;
+ }
+
/**
* Adds a code owner for the given email.
*
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
index bc45ded..93a36be 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import java.nio.file.Path;
@@ -52,7 +53,7 @@
*/
public ImmutableSet<CodeOwnerReference> getPathCodeOwners() {
logger.atFine().log(
- "computing path code owners for %s from %s", path(), codeOwnerConfig().key());
+ "retrieving path code owners for %s from %s", path(), codeOwnerConfig().key());
ImmutableSet<CodeOwnerReference> pathCodeOwners =
codeOwnerConfig().codeOwnerSets().stream()
.flatMap(codeOwnerSet -> codeOwnerSet.codeOwners().stream())
@@ -62,6 +63,34 @@
}
/**
+ * Gets the annotations for all path code owners that are returned by {@link
+ * #getPathCodeOwners()}.
+ *
+ * @return annotations by code owner
+ */
+ public ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> getAnnotations() {
+ logger.atFine().log(
+ "retrieving path code owner annotations for %s from %s", path(), codeOwnerConfig().key());
+ ImmutableMultimap.Builder<CodeOwnerReference, CodeOwnerAnnotation> annotationsBuilder =
+ ImmutableMultimap.builder();
+ codeOwnerConfig()
+ .codeOwnerSets()
+ .forEach(codeOwnerSet -> annotationsBuilder.putAll(codeOwnerSet.annotations()));
+
+ ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations =
+ annotationsBuilder.build();
+ logger.atFine().log("annotations = %s", annotations);
+ 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.
*
* @return whether parent code owners should be ignored for the path
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
index 788e921..e94875a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
@@ -15,7 +15,10 @@
package com.google.gerrit.plugins.codeowners.backend.findowners;
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.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
@@ -25,7 +28,12 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SortedSetMultimap;
+import com.google.common.collect.TreeMultimap;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigParseException;
@@ -37,7 +45,11 @@
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@@ -161,6 +173,7 @@
// Simple input lines with 0 or 1 sub-pattern.
private static final Pattern PAT_COMMENT = Pattern.compile(BOL + EOL);
private static final Pattern PAT_EMAIL = Pattern.compile(BOL + EMAIL_OR_STAR + EOL);
+ private static final Pattern PAT_ANNOTATION = Pattern.compile("#\\{([A-Za-z_]+)\\}");
private static final Pattern PAT_INCLUDE =
Pattern.compile(BOL + INCLUDE_OR_FILE + PROJECT_BRANCH_AND_FILE + EOL);
private static final Pattern PAT_NO_PARENT = Pattern.compile(BOL + SET_NOPARENT + EOL);
@@ -206,16 +219,18 @@
CodeOwnerSet.Builder globalCodeOwnerSetBuilder,
List<CodeOwnerSet> perFileCodeOwnerSets,
String line) {
- String email;
+ ParsedEmailLine parsedEmailLine;
CodeOwnerSet codeOwnerSet;
CodeOwnerConfigReference codeOwnerConfigReference;
if (isNoParent(line)) {
codeOwnerConfigBuilder.setIgnoreParentCodeOwners();
} else if (isComment(line)) {
// ignore comment lines and empty lines
- } else if ((email = parseEmail(line)) != null) {
- globalCodeOwnerSetBuilder.addCodeOwner(CodeOwnerReference.create(email));
- } else if ((codeOwnerSet = parsePerFile(line)) != null) {
+ } else if ((parsedEmailLine = parseEmailLine(line)) != null) {
+ globalCodeOwnerSetBuilder.addCodeOwner(parsedEmailLine.codeOwnerReference());
+ globalCodeOwnerSetBuilder.addAnnotations(
+ parsedEmailLine.codeOwnerReference(), parsedEmailLine.annotations());
+ } else if ((codeOwnerSet = parsePerFileLine(line)) != null) {
perFileCodeOwnerSets.add(codeOwnerSet);
} else if ((codeOwnerConfigReference = parseInclude(line)) != null) {
codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
@@ -224,13 +239,13 @@
}
}
- private static CodeOwnerSet parsePerFile(String line) {
- Matcher m = PAT_PER_FILE.matcher(line);
- if (!m.matches() || !isGlobs(m.group(1).trim())) {
+ private static CodeOwnerSet parsePerFileLine(String line) {
+ Matcher perFileMatcher = PAT_PER_FILE.matcher(line);
+ if (!perFileMatcher.matches() || !isGlobs(perFileMatcher.group(1).trim())) {
return null;
}
- String matchedGroup2 = m.group(2).trim();
+ String matchedGroup2 = perFileMatcher.group(2).trim();
if (!PAT_PER_FILE_OWNERS.matcher(matchedGroup2).matches()) {
checkState(
!PAT_PER_FILE_INCLUDE.matcher(matchedGroup2).matches(),
@@ -241,7 +256,9 @@
}
String[] globsAndOwners =
- new String[] {removeExtraSpaces(m.group(1)), removeExtraSpaces(m.group(2))};
+ new String[] {
+ removeExtraSpaces(perFileMatcher.group(1)), removeExtraSpaces(perFileMatcher.group(2))
+ };
String[] dirGlobs = splitGlobs(globsAndOwners[0]);
String directive = globsAndOwners[1];
if (directive.equals(TOK_SET_NOPARENT)) {
@@ -260,11 +277,27 @@
}
List<String> ownerEmails = Arrays.asList(directive.split(COMMA, -1));
- return CodeOwnerSet.builder()
- .setPathExpressions(ImmutableSet.copyOf(dirGlobs))
- .setCodeOwners(
- ownerEmails.stream().map(CodeOwnerReference::create).collect(toImmutableSet()))
- .build();
+
+ // Get the comment part of the line (the first '#' and everything that follows).
+ String comment = perFileMatcher.group(3);
+ Set<CodeOwnerAnnotation> annotations = new HashSet<>();
+ if (comment != null) {
+ Matcher annotationMatcher = PAT_ANNOTATION.matcher(comment);
+ while (annotationMatcher.find()) {
+ String annotation = annotationMatcher.group(1);
+ annotations.add(CodeOwnerAnnotation.create(annotation));
+ }
+ }
+
+ CodeOwnerSet.Builder codeOwnerSet =
+ CodeOwnerSet.builder()
+ .setPathExpressions(ImmutableSet.copyOf(dirGlobs))
+ .setCodeOwners(
+ ownerEmails.stream().map(CodeOwnerReference::create).collect(toImmutableSet()));
+ ownerEmails.stream()
+ .forEach(
+ email -> codeOwnerSet.addAnnotations(CodeOwnerReference.create(email), annotations));
+ return codeOwnerSet.build();
}
/**
@@ -327,9 +360,25 @@
return PAT_NO_PARENT.matcher(line).matches();
}
- private static String parseEmail(String line) {
- Matcher m = PAT_EMAIL.matcher(line);
- return m.matches() ? m.group(1).trim() : null;
+ private static ParsedEmailLine parseEmailLine(String line) {
+ Matcher emailMatcher = PAT_EMAIL.matcher(line);
+ if (!emailMatcher.matches()) {
+ return null;
+ }
+ String email = emailMatcher.group(1).trim();
+ ParsedEmailLine.Builder parsedEmailLine = ParsedEmailLine.builder(email);
+
+ // Get the comment part of the line (the first '#' and everything that follows).
+ String comment = emailMatcher.group(2);
+ if (comment != null) {
+ Matcher annotationMatcher = PAT_ANNOTATION.matcher(comment);
+ while (annotationMatcher.find()) {
+ String annotation = annotationMatcher.group(1);
+ parsedEmailLine.addAnnotation(annotation);
+ }
+ }
+
+ return parsedEmailLine.build();
}
private static CodeOwnerConfigReference parseInclude(String line) {
@@ -403,7 +452,7 @@
static String formatAsString(CodeOwnerConfig codeOwnerConfig) {
return formatIgnoreParentCodeOwners(codeOwnerConfig)
+ formatImports(codeOwnerConfig)
- + formatGlobalCodeOwners(codeOwnerConfig)
+ + formatFolderCodeOwners(codeOwnerConfig)
+ formatPerFileCodeOwners(codeOwnerConfig);
}
@@ -411,22 +460,37 @@
return codeOwnerConfig.ignoreParentCodeOwners() ? SET_NOPARENT_LINE : "";
}
- private static String formatGlobalCodeOwners(CodeOwnerConfig codeOwnerConfig) {
- String emails =
+ private static String formatFolderCodeOwners(CodeOwnerConfig codeOwnerConfig) {
+ ImmutableSet<CodeOwnerSet> folderCodeOwnerSets =
codeOwnerConfig.codeOwnerSets().stream()
// Filter out code owner sets with path expressions. If path expressions are present
// the code owner set defines per-file code owners and is handled in
// formatPerFileCodeOwners(CodeOwnerConfig).
.filter(codeOwnerSet -> codeOwnerSet.pathExpressions().isEmpty())
+ .collect(toImmutableSet());
+ ImmutableList<String> emails =
+ folderCodeOwnerSets.stream()
.flatMap(codeOwnerSet -> codeOwnerSet.codeOwners().stream())
.map(CodeOwnerReference::email)
.sorted()
.distinct()
- .collect(joining("\n"));
- if (!emails.isEmpty()) {
- return emails + "\n";
+ .collect(toImmutableList());
+ SortedSetMultimap<String, String> annotations = TreeMultimap.create();
+ folderCodeOwnerSets.forEach(
+ codeOwnerSet ->
+ codeOwnerSet
+ .annotations()
+ .forEach(
+ (codeOwnerReference, annotation) ->
+ annotations.put(codeOwnerReference.email(), annotation.key())));
+
+ StringBuilder b = new StringBuilder();
+ for (String email : emails) {
+ b.append(email);
+ annotations.get(email).forEach(annotation -> b.append(" #{" + annotation + "}"));
+ b.append('\n');
}
- return emails;
+ return b.toString();
}
private static String formatPerFileCodeOwners(CodeOwnerConfig codeOwnerConfig) {
@@ -458,20 +522,48 @@
}
if (!codeOwnerSet.codeOwners().isEmpty()) {
- b.append(
- String.format(
- PER_FILE_LINE_FORMAT,
- formattedPathExpressions,
- formatCodeOwnerReferencesAsList(codeOwnerSet.codeOwners())));
+ // group code owners that have the same annotations
+ ListMultimap<SortedSet<String>, CodeOwnerReference> codeOwnersByAnnotations =
+ MultimapBuilder.hashKeys().arrayListValues().build();
+ codeOwnerSet
+ .codeOwners()
+ .forEach(
+ codeOwnerReference ->
+ codeOwnersByAnnotations.put(
+ codeOwnerSet.annotations().get(codeOwnerReference).stream()
+ .map(CodeOwnerAnnotation::key)
+ .collect(toImmutableSortedSet(naturalOrder())),
+ codeOwnerReference));
+
+ codeOwnersByAnnotations
+ .asMap()
+ .forEach(
+ (annotations, codeOwners) ->
+ b.append(
+ String.format(
+ PER_FILE_LINE_FORMAT,
+ formattedPathExpressions,
+ formatCodeOwnerReferencesAsList(codeOwners)
+ + formatAnnotations(annotations))));
}
return b.toString();
}
private static String formatCodeOwnerReferencesAsList(
- ImmutableSet<CodeOwnerReference> codeOwnerReferences) {
+ Collection<CodeOwnerReference> codeOwnerReferences) {
return formatValuesAsList(codeOwnerReferences.stream().map(CodeOwnerReference::email));
}
+ private static String formatAnnotations(SortedSet<String> annotations) {
+ if (annotations.isEmpty()) {
+ return "";
+ }
+
+ return annotations.stream()
+ .map(annotation -> "#{" + annotation + "}")
+ .collect(joining(" ", " ", ""));
+ }
+
private static String formatValuesAsList(ImmutableSet<String> values) {
return formatValuesAsList(values.stream());
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/ParsedEmailLine.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/ParsedEmailLine.java
new file mode 100644
index 0000000..19d5004
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/ParsedEmailLine.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend.findowners;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+
+/**
+ * A parsed email line of an {@code OWNERS} file, consisting out of an email and optionally
+ * annotations for that email.
+ */
+@AutoValue
+abstract class ParsedEmailLine {
+ abstract CodeOwnerReference codeOwnerReference();
+
+ abstract ImmutableSet<CodeOwnerAnnotation> annotations();
+
+ static ParsedEmailLine.Builder builder(String email) {
+ requireNonNull(email, "email");
+ return new AutoValue_ParsedEmailLine.Builder()
+ .codeOwnerReference(CodeOwnerReference.create(email));
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder codeOwnerReference(CodeOwnerReference codeOwnerReference);
+
+ abstract ImmutableSet.Builder<CodeOwnerAnnotation> annotationsBuilder();
+
+ Builder addAnnotation(String annotation) {
+ requireNonNull(annotation, "annotation");
+ annotationsBuilder().add(CodeOwnerAnnotation.create(annotation));
+ return this;
+ }
+
+ abstract ParsedEmailLine build();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index 85db8e3..56aa627 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -16,11 +16,16 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.IS_EXPLICITLY_MENTIONED_SCORING_VALUE;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.NOT_EXPLICITLY_MENTIONED_SCORING_VALUE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
@@ -34,6 +39,7 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolverResult;
@@ -200,10 +206,13 @@
int maxDistance = globalOwnersDistance;
CodeOwnerScoring.Builder distanceScoring = CodeOwnerScore.DISTANCE.createScoring(maxDistance);
+ CodeOwnerScoring.Builder isExplicitlyMentionedScoring =
+ CodeOwnerScore.IS_EXPLICITLY_MENTIONED.createScoring();
Set<CodeOwner> codeOwners = new HashSet<>();
+ ListMultimap<CodeOwner, CodeOwnerAnnotation> annotations = LinkedListMultimap.create();
AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
- List<String> debugLogs = new ArrayList<>();
+ ImmutableList.Builder<String> debugLogsBuilder = ImmutableList.builder();
codeOwnerConfigHierarchy.visit(
rsrc.getBranch(),
rsrc.getRevision(),
@@ -212,11 +221,9 @@
CodeOwnerResolverResult pathCodeOwners =
codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath());
- if (debug) {
- debugLogs.addAll(pathCodeOwners.messages());
- }
-
- codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners.codeOwners()));
+ debugLogsBuilder.addAll(pathCodeOwners.messages());
+ codeOwners.addAll(pathCodeOwners.codeOwners());
+ annotations.putAll(pathCodeOwners.annotations());
int distance =
codeOwnerConfig.key().branchNameKey().branch().equals(RefNames.REFS_CONFIG)
@@ -225,14 +232,21 @@
pathCodeOwners
.codeOwners()
.forEach(
- localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
+ localCodeOwner -> {
+ distanceScoring.putValueForCodeOwner(localCodeOwner, distance);
+ isExplicitlyMentionedScoring.putValueForCodeOwner(
+ localCodeOwner, IS_EXPLICITLY_MENTIONED_SCORING_VALUE);
+ });
if (pathCodeOwners.ownedByAllUsers()) {
ownedByAllUsers.set(true);
- ImmutableSet<CodeOwner> addedCodeOwners =
- fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ ImmutableSet<CodeOwner> addedCodeOwners = fillUpWithRandomUsers(codeOwners, limit);
addedCodeOwners.forEach(
- localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
+ localCodeOwner -> {
+ distanceScoring.putValueForCodeOwner(localCodeOwner, distance);
+ isExplicitlyMentionedScoring.putValueForCodeOwner(
+ localCodeOwner, NOT_EXPLICITLY_MENTIONED_SCORING_VALUE);
+ });
if (codeOwners.size() < limit) {
logger.atFine().log(
@@ -251,33 +265,48 @@
return true;
});
- if (codeOwners.size() < limit || !ownedByAllUsers.get()) {
+ if (!ownedByAllUsers.get()) {
CodeOwnerResolverResult globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
- if (debug) {
- debugLogs.add("resolve global code owners");
- debugLogs.addAll(globalCodeOwners.messages());
- }
+ debugLogsBuilder.add("resolve global code owners");
+ debugLogsBuilder.addAll(globalCodeOwners.messages());
globalCodeOwners
.codeOwners()
.forEach(
- codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance));
- codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners.codeOwners()));
+ codeOwner -> {
+ distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance);
+ isExplicitlyMentionedScoring.putValueForCodeOwner(
+ codeOwner, IS_EXPLICITLY_MENTIONED_SCORING_VALUE);
+ });
+ codeOwners.addAll(globalCodeOwners.codeOwners());
if (globalCodeOwners.ownedByAllUsers()) {
ownedByAllUsers.set(true);
- ImmutableSet<CodeOwner> addedCodeOwners = fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ ImmutableSet<CodeOwner> addedCodeOwners = fillUpWithRandomUsers(codeOwners, limit);
addedCodeOwners.forEach(
- codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance));
+ codeOwner -> {
+ distanceScoring.putValueForCodeOwner(codeOwner, globalOwnersDistance);
+ isExplicitlyMentionedScoring.putValueForCodeOwner(
+ codeOwner, NOT_EXPLICITLY_MENTIONED_SCORING_VALUE);
+ });
}
}
- ImmutableSet<CodeOwner> immutableCodeOwners = ImmutableSet.copyOf(codeOwners);
+ ImmutableSet<CodeOwner> filteredCodeOwners =
+ filterCodeOwners(
+ rsrc,
+ ImmutableMultimap.copyOf(annotations),
+ ImmutableSet.copyOf(codeOwners),
+ debugLogsBuilder);
CodeOwnerScorings codeOwnerScorings =
- createScorings(rsrc, immutableCodeOwners, distanceScoring.build());
+ createScorings(
+ rsrc,
+ filteredCodeOwners,
+ distanceScoring.build(),
+ isExplicitlyMentionedScoring.build());
ImmutableMap<CodeOwner, Double> scoredCodeOwners =
- codeOwnerScorings.getScorings(immutableCodeOwners);
+ codeOwnerScorings.getScorings(filteredCodeOwners);
ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(rsrc, scoredCodeOwners);
@@ -296,14 +325,18 @@
codeOwnersInfo.codeOwners =
codeOwnerJsonFactory.create(getFillOptions()).format(sortedAndLimitedCodeOwners);
codeOwnersInfo.ownedByAllUsers = ownedByAllUsers.get() ? true : null;
+
+ ImmutableList<String> debugLogs = debugLogsBuilder.build();
codeOwnersInfo.debugLogs = debug ? debugLogs : null;
+ logger.atFine().log("debug logs: %s", debugLogs);
+
return Response.ok(codeOwnersInfo);
}
private CodeOwnerScorings createScorings(
- R rsrc, ImmutableSet<CodeOwner> codeOwners, CodeOwnerScoring distanceScoring) {
+ R rsrc, ImmutableSet<CodeOwner> codeOwners, CodeOwnerScoring... scorings) {
ImmutableSet.Builder<CodeOwnerScoring> codeOwnerScorings = ImmutableSet.builder();
- codeOwnerScorings.add(distanceScoring);
+ codeOwnerScorings.addAll(ImmutableSet.copyOf(scorings));
codeOwnerScorings.addAll(getCodeOwnerScorings(rsrc, codeOwners));
return CodeOwnerScorings.create(codeOwnerScorings.build());
}
@@ -343,18 +376,29 @@
* normally doesn't make sense since they will not react to review requests.
* </ul>
*/
- private ImmutableSet<CodeOwner> filterCodeOwners(R rsrc, ImmutableSet<CodeOwner> codeOwners) {
- return filterCodeOwners(rsrc, getVisibleCodeOwners(rsrc, codeOwners)).collect(toImmutableSet());
+ private ImmutableSet<CodeOwner> filterCodeOwners(
+ R rsrc,
+ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
+ ImmutableSet<CodeOwner> codeOwners,
+ ImmutableList.Builder<String> debugLogs) {
+ return filterCodeOwners(rsrc, annotations, getVisibleCodeOwners(rsrc, codeOwners), debugLogs)
+ .collect(toImmutableSet());
}
/**
* To be overridden by subclasses to filter out additional code owners.
*
* @param rsrc resource on which the request is being performed
+ * @param annotations annotations that were set on the code owners
* @param codeOwners stream of code owners that should be filtered
+ * @param debugLogs builder to collect debug logs that may be returned to the caller
* @return the filtered stream of code owners
*/
- protected Stream<CodeOwner> filterCodeOwners(R rsrc, Stream<CodeOwner> codeOwners) {
+ protected Stream<CodeOwner> filterCodeOwners(
+ R rsrc,
+ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
+ Stream<CodeOwner> codeOwners,
+ ImmutableList.Builder<String> debugLogs) {
return codeOwners;
}
@@ -484,8 +528,7 @@
*
* @return the added code owners
*/
- private ImmutableSet<CodeOwner> fillUpWithRandomUsers(
- R rsrc, Set<CodeOwner> codeOwners, int limit) {
+ private ImmutableSet<CodeOwner> fillUpWithRandomUsers(Set<CodeOwner> codeOwners, int limit) {
if (!resolveAllUsers || codeOwners.size() >= limit) {
// code ownership for all users should not be resolved or the limit has already been reached
// so that we don't need to add further suggestions
@@ -494,17 +537,12 @@
logger.atFine().log("filling up with random users");
ImmutableSet<CodeOwner> codeOwnersToAdd =
- filterCodeOwners(
- rsrc,
- // ask for 2 times the number of users that we need so that we still have enough
- // suggestions when some users are removed by the filterCodeOwners call or if the
- // returned users were already present in codeOwners
- getRandomVisibleUsers(2 * limit - codeOwners.size())
- .map(CodeOwner::create)
- .collect(toImmutableSet()))
- .stream()
+ // ask for 2 times the number of users that we need so that we still have enough
+ // suggestions when some users are removed on the filter step later or if the returned users
+ // were already present in codeOwners
+ getRandomVisibleUsers(2 * limit - codeOwners.size()).map(CodeOwner::create)
+ .collect(toImmutableSet()).stream()
.filter(codeOwner -> !codeOwners.contains(codeOwner))
- .limit(limit - codeOwners.size())
.collect(toImmutableSet());
codeOwners.addAll(codeOwnersToAdd);
return codeOwnersToAdd;
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index f4e5c0f..9c4e144 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()),
@@ -187,15 +195,23 @@
if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
messages.add(
String.format(
- "found email %s as code owner in default code owner config", email));
+ "found email %s as a code owner in the default code owner config", email));
isDefaultCodeOwner.set(true);
} else {
Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
messages.add(
String.format(
- "found email %s as code owner in %s", email, codeOwnerConfigFilePath));
+ "found email %s as a 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()
@@ -205,19 +221,31 @@
if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
messages.add(
String.format(
- "found email %s as code owner in default code owner config",
- CodeOwnerResolver.ALL_USERS_WILDCARD));
+ "found the all users wildcard ('%s') as a code owner in the default code"
+ + " owner config which makes %s a code owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, email));
isDefaultCodeOwner.set(true);
} else {
Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
messages.add(
String.format(
- "found email %s as code owner in %s",
- CodeOwnerResolver.ALL_USERS_WILDCARD, codeOwnerConfigFilePath));
+ "found the all users wildcard ('%s') as a code owner in %s which makes %s a"
+ + " code owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, codeOwnerConfigFilePath, email));
if (!codeOwnerConfigFilePaths.contains(codeOwnerConfigFilePath)) {
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 +310,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 +344,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 +448,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/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
index e2aac82..cdab23f 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -17,14 +17,17 @@
import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.IS_REVIEWER_SCORING_VALUE;
import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.NO_REVIEWER_SCORING_VALUE;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore;
@@ -53,7 +56,6 @@
*/
public class GetCodeOwnersForPathInChange
extends AbstractGetCodeOwnersForPath<CodeOwnersInChangeCollection.PathResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ServiceUserClassifier serviceUserClassifier;
@@ -123,31 +125,57 @@
@Override
protected Stream<CodeOwner> filterCodeOwners(
- CodeOwnersInChangeCollection.PathResource rsrc, Stream<CodeOwner> codeOwners) {
- return codeOwners.filter(filterOutChangeOwner(rsrc)).filter(filterOutServiceUsers());
+ CodeOwnersInChangeCollection.PathResource rsrc,
+ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
+ Stream<CodeOwner> codeOwners,
+ ImmutableList.Builder<String> debugLogs) {
+ return codeOwners
+ .filter(filterOutChangeOwner(rsrc, debugLogs))
+ .filter(filterOutCodeOwnersThatAreAnnotatedWithNeverSuggest(annotations, debugLogs))
+ .filter(filterOutServiceUsers(debugLogs));
}
private Predicate<CodeOwner> filterOutChangeOwner(
- CodeOwnersInChangeCollection.PathResource rsrc) {
+ CodeOwnersInChangeCollection.PathResource rsrc, ImmutableList.Builder<String> debugLogs) {
return codeOwner -> {
if (!codeOwner.accountId().equals(rsrc.getRevisionResource().getChange().getOwner())) {
// Returning true from the Predicate here means that the code owner should be kept.
return true;
}
- logger.atFine().log(
- "Filtering out %s because this code owner is the change owner", codeOwner);
+ debugLogs.add(
+ String.format("filtering out %s because this code owner is the change owner", codeOwner));
// Returning false from the Predicate here means that the code owner should be filtered out.
return false;
};
}
- private Predicate<CodeOwner> filterOutServiceUsers() {
+ private Predicate<CodeOwner> filterOutCodeOwnersThatAreAnnotatedWithNeverSuggest(
+ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations,
+ ImmutableList.Builder<String> debugLogs) {
+ return codeOwner -> {
+ boolean neverSuggest =
+ annotations.containsEntry(codeOwner, CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION);
+ if (!neverSuggest) {
+ // Returning true from the Predicate here means that the code owner should be kept.
+ return true;
+ }
+ debugLogs.add(
+ String.format(
+ "filtering out %s because this code owner is annotated with %s",
+ codeOwner, CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION.key()));
+ // Returning false from the Predicate here means that the code owner should be filtered out.
+ return false;
+ };
+ }
+
+ private Predicate<CodeOwner> filterOutServiceUsers(ImmutableList.Builder<String> debugLogs) {
return codeOwner -> {
if (!isServiceUser(codeOwner)) {
// Returning true from the Predicate here means that the code owner should be kept.
return true;
}
- logger.atFine().log("Filtering out %s because this code owner is a service user", codeOwner);
+ debugLogs.add(
+ String.format("filtering out %s because this code owner is a service user", codeOwner));
// Returning false from the Predicate here means that the code owner should be filtered out.
return false;
};
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/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerSetSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerSetSubject.java
index 974289a..8c7d014 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerSetSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerSetSubject.java
@@ -14,6 +14,8 @@
package com.google.gerrit.plugins.codeowners.testing;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigReferenceSubject.codeOwnerConfigReferences;
import static com.google.gerrit.truth.ListSubject.elements;
@@ -22,7 +24,9 @@
import com.google.common.truth.Correspondence;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.IterableSubject;
+import com.google.common.truth.MapSubject;
import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
@@ -85,6 +89,19 @@
return hasCodeOwnersThat().comparingElementsUsing(hasEmail());
}
+ public MapSubject hasAnnotationsThat() {
+ return check("codeOwners()")
+ .that(
+ codeOwnerSet().annotations().asMap().entrySet().stream()
+ .collect(
+ toImmutableMap(
+ e -> e.getKey().email(),
+ e ->
+ e.getValue().stream()
+ .map(CodeOwnerAnnotation::key)
+ .collect(toImmutableSet()))));
+ }
+
/**
* Returns an {@link ListSubject} for the imports in the code owner set.
*
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 b7f856a..85f52b9 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -77,7 +77,7 @@
@Inject private GroupOperations groupOperations;
@Inject private ProjectOperations projectOperations;
- private TestPathExpressions testPathExpressions;
+ protected TestPathExpressions testPathExpressions;
@Before
public void setup() throws Exception {
@@ -1472,7 +1472,7 @@
}
@Test
- public void debugRequireCallerToBeAdminOrHaveTheCheckCodeOwnerCapability() throws Exception {
+ public void debugRequiresCallerToBeAdminOrHaveTheCheckCodeOwnerCapability() throws Exception {
requestScopeOperations.setApiUser(user.id());
AuthException authException =
assertThrows(
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..4baef37 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,10 +147,11 @@
assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
+ assertThat(checkCodeOwnerInfo).hasAnnotationsThat().isEmpty();
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
}
@@ -178,13 +182,13 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
}
@@ -222,10 +226,10 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
"parent code owners are ignored",
String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
@@ -257,7 +261,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format("resolved email %s to account %s", secondaryEmail, codeOwner.id()));
}
@@ -282,8 +286,11 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
- CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+ "found the all users wildcard ('%s') as a code owner in %s which makes %s a code"
+ + " owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ getCodeOwnerConfigFilePath(ROOT_PATH),
+ codeOwner.email()));
}
@Test
@@ -306,11 +313,14 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
- "found email %s as code owner in %s",
- CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+ "found the all users wildcard ('%s') as a code owner in %s which makes %s a code"
+ + " owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ getCodeOwnerConfigFilePath(ROOT_PATH),
+ codeOwner.email()));
}
@Test
@@ -345,7 +355,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
nonExistingEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"cannot resolve code owner email %s: no account with this email exists",
@@ -374,7 +384,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
ambiguousEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"cannot resolve code owner email %s: email is ambiguous", ambiguousEmail));
@@ -406,7 +416,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
orphanedEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"cannot resolve account %s for email %s: account does not exists",
@@ -437,7 +447,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
inactiveUser.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"ignoring inactive account %s for email %s",
@@ -469,7 +479,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
emailWithAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"domain %s of email %s is allowed",
@@ -503,7 +513,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
emailWithNonAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"domain %s of email %s is not allowed",
@@ -544,8 +554,10 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
- CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+ "found the all users wildcard ('%s') as a code owner in %s which makes %s a code owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ getCodeOwnerConfigFilePath(ROOT_PATH),
+ CodeOwnerResolver.ALL_USERS_WILDCARD));
}
@Test
@@ -569,7 +581,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in default code owner config",
+ "found email %s as a code owner in the default code owner config",
defaultCodeOwner.email()),
String.format(
"resolved email %s to account %s",
@@ -597,8 +609,9 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in default code owner config",
- CodeOwnerResolver.ALL_USERS_WILDCARD),
+ "found the all users wildcard ('%s') as a code owner in the default code owner"
+ + " config which makes %s a code owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, defaultCodeOwner.email()),
String.format(
"resolved email %s to account %s",
defaultCodeOwner.email(), defaultCodeOwner.id()));
@@ -677,7 +690,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format("account %s is visible to user %s", codeOwner.id(), user.username()),
String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
@@ -716,7 +729,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"cannot resolve code owner email %s: account %s is not visible to user %s",
@@ -749,7 +762,7 @@
assertThat(checkCodeOwnerInfo)
.hasDebugLogsThatContainAllOf(
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
String.format(
"cannot resolve code owner email %s: account %s is referenced by secondary email"
@@ -935,7 +948,7 @@
"per-file code owner set with path expressions [%s] matches",
testPathExpressions.matchFileType("md")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
mdOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format("resolved email %s to account %s", mdOwner.email(), mdOwner.id()));
assertThat(checkCodeOwnerInfo)
@@ -1006,7 +1019,7 @@
+ " parent code owners, hence ignoring the folder code owners",
testPathExpressions.matchFileType("md")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
fileCodeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format(
"resolved email %s to account %s", fileCodeOwner.email(), fileCodeOwner.id()));
@@ -1061,7 +1074,7 @@
getCodeOwnerConfigFileName(),
getCodeOwnerConfigFileName()),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format("resolved email %s to account %s", codeOwner.email(), codeOwner.id()));
}
@@ -1125,7 +1138,7 @@
getCodeOwnerConfigFileName(),
testPathExpressions.matchFileType("md")),
String.format(
- "found email %s as code owner in %s",
+ "found email %s as a code owner in %s",
mdCodeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
String.format(
"resolved email %s to account %s", mdCodeOwner.email(), mdCodeOwner.id()));
@@ -1454,6 +1467,92 @@
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 a 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 the all users wildcard ('%s') as a code owner in %s which makes %s a code"
+ + " owner",
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ getCodeOwnerConfigFilePath("/foo/"),
+ codeOwner.email()),
+ 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 a 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/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
index c260776..82ca271 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
@@ -32,7 +32,9 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.codeowners.api.CodeOwners;
import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSetModification;
import com.google.inject.Inject;
import java.util.List;
@@ -243,4 +245,63 @@
.comparingElementsUsing(hasAccountId())
.containsExactly(serviceUser.id());
}
+
+ @Test
+ public void codeOwnersWithNeverSuggestAnnotationAreIncluded() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ .create();
+
+ // Expectation: admin is included because GetCodeOwnersForPathInBranch ignores the NEVER_SUGGEST
+ // suggestion.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id());
+ }
+
+ @Test
+ public void perFileCodeOwnersWithNeverSuggestAnnotationAreIncluded() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("md"))
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ .create();
+
+ // Expectation: admin is included because GetCodeOwnersForPathInBranch ignores the NEVER_SUGGEST
+ // suggestion.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id());
+ }
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
index 1feefc6..d5eeb0c 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
@@ -36,7 +36,10 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.codeowners.api.CodeOwners;
import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.util.JgitPath;
import com.google.inject.Inject;
import java.util.List;
@@ -327,6 +330,211 @@
}
@Test
+ public void codeOwnersWithNeverSuggestAnnotationAreFilteredOut() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ .create();
+
+ // Expectation: admin is filtered out because it is annotated with NEVER_SUGGEST.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id());
+ }
+
+ @Test
+ public void codeOwnersWithNeverSuggestAnnotationAreFilteredOut_annotationSetForAllUsersWildcard()
+ throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(CodeOwnerResolver.ALL_USERS_WILDCARD)
+ .addAnnotation(
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerEmail(user.email())
+ .build())
+ .create();
+
+ // Expectation: none of the code owners is suggested since NEVER_SUGGEST was set for the all
+ // users wildcard.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
+ }
+
+ @Test
+ public void perFileCodeOwnersWithNeverSuggestAnnotationAreFilteredOut() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("md"))
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ .create();
+
+ // Expectation: admin is filtered out because it is annotated with NEVER_SUGGEST.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id());
+ }
+
+ @Test
+ public void
+ perFileCodeOwnersWithNeverSuggestAnnotationAreFilteredOut_annotationSetForAllUsersWildcard()
+ throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("md"))
+ .addCodeOwnerEmail(CodeOwnerResolver.ALL_USERS_WILDCARD)
+ .addAnnotation(
+ CodeOwnerResolver.ALL_USERS_WILDCARD,
+ CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerEmail(user.email())
+ .build())
+ .create();
+
+ // Expectation: none of the code owners is suggested since NEVER_SUGGEST was set for the all
+ // users wildcard.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
+ }
+
+ @Test
+ public void neverSuggestTakesEffectEvenIfCodeOwnerIsAlsoSpecifiedWithoutThisAnnotation()
+ throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ // Code owner config with admin as code owner, but without annotation.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ // Code owner config that specifies admin multiple times as code owner, but only once with the
+ // NEVER_SUGGEST annotation.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ // Another code owner set with admin as folder code owner, but without annotation.
+ .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
+ // A per-file code owner with admin as file code owner, but without annotation.
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("md"))
+ .addCodeOwnerEmail(admin.email())
+ .build())
+ .create();
+
+ // Another code owner config with admin as code owner, but without annotation.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/bar/")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+
+ // Expectation: admin is filtered out because at once place it is annotated with NEVER_SUGGEST.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id());
+ }
+
+ @Test
+ public void neverSuggestOnNonMatchingPerFileRuleDoesntHaveAnyEffect() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ TestAccount user2 = accountCreator.user2();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addCodeOwnerEmail(user.email())
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ // Non-matching per-file code owner with NEVER_SUGGEST annotation.
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addPathExpression(testPathExpressions.matchFileType("txt"))
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .build())
+ .create();
+
+ // Expectation: admin is suggested since the NEVER_SUGGEST annotation is set on the per-file
+ // rule which doesn't match.
+ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id());
+ }
+
+ @Test
public void codeOwnersThatAreReviewersAreReturnedFirst() throws Exception {
TestAccount user2 = accountCreator.user2();
@@ -429,4 +637,52 @@
.inOrder();
assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isTrue();
}
+
+ @Test
+ public void filteredOutCodeOwnersAreMentionedInDebugLogs() throws Exception {
+ skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
+
+ // Create a service user.
+ 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();
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(changeOwner.email())
+ .addCodeOwnerEmail(serviceUser.email())
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION)
+ .addCodeOwnerEmail(user.email())
+ .build())
+ .create();
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnersInfo codeOwnersInfo =
+ queryCodeOwners(getCodeOwnersApi().query().withDebug(/* debug= */ true), path);
+ assertThat(codeOwnersInfo)
+ .hasCodeOwnersThat()
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id());
+ assertThat(codeOwnersInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "filtering out %s because this code owner is the change owner",
+ CodeOwner.create(changeOwner.id())),
+ String.format(
+ "filtering out %s because this code owner is a service user",
+ CodeOwner.create(serviceUser.id())),
+ String.format(
+ "filtering out %s because this code owner is annotated with %s",
+ CodeOwner.create(admin.id()), CodeOwnerAnnotations.NEVER_SUGGEST_ANNOTATION.key()));
+ }
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
index d64f5bf..18ea017 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
@@ -15,6 +15,7 @@
package com.google.gerrit.plugins.codeowners.backend;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import org.junit.Test;
@@ -25,6 +26,7 @@
CodeOwnerResolverResult codeOwnerResolverResult =
CodeOwnerResolverResult.create(
ImmutableSet.of(CodeOwner.create(admin.id())),
+ /* annotations= */ ImmutableMultimap.of(),
/* ownedByAllUsers= */ false,
/* hasUnresolvedCodeOwners= */ false,
/* hasUnresolvedImports= */ false,
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 629f42c..6e91160 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -426,6 +426,65 @@
}
@Test
+ public void resolvePathCodeOwnersWithAnnotations() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ CodeOwnerConfig codeOwnerConfig =
+ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotation.create("FOO"))
+ .addAnnotation(admin.email(), CodeOwnerAnnotation.create("BAR"))
+ .addCodeOwnerEmail(user.email())
+ .addAnnotation(user.email(), CodeOwnerAnnotation.create("BAZ"))
+ .addCodeOwnerEmail(user2.email())
+ .build())
+ .build();
+
+ CodeOwnerResolverResult result =
+ codeOwnerResolverProvider
+ .get()
+ .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id(), user.id(), user2.id());
+ assertThat(result.annotations().keySet())
+ .containsExactly(CodeOwner.create(admin.id()), CodeOwner.create(user.id()));
+ assertThat(result.annotations().get(CodeOwner.create(admin.id())))
+ .containsExactly(CodeOwnerAnnotation.create("FOO"), CodeOwnerAnnotation.create("BAR"));
+ assertThat(result.annotations().get(CodeOwner.create(user.id())))
+ .containsExactly(CodeOwnerAnnotation.create("BAZ"));
+ }
+
+ @Test
+ public void resolvePathCodeOwnersWithAnnotations_annotationOnAllUsersWildcard()
+ throws Exception {
+ CodeOwnerConfig codeOwnerConfig =
+ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+ .addCodeOwnerSet(
+ CodeOwnerSet.builder()
+ .addCodeOwnerEmail(admin.email())
+ .addAnnotation(admin.email(), CodeOwnerAnnotation.create("FOO"))
+ .addCodeOwnerEmail(CodeOwnerResolver.ALL_USERS_WILDCARD)
+ .addAnnotation(
+ CodeOwnerResolver.ALL_USERS_WILDCARD, CodeOwnerAnnotation.create("BAR"))
+ .addCodeOwnerEmail(user.email())
+ .build())
+ .build();
+
+ CodeOwnerResolverResult result =
+ codeOwnerResolverProvider
+ .get()
+ .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id(), user.id());
+ assertThat(result.annotations().keySet())
+ .containsExactly(CodeOwner.create(admin.id()), CodeOwner.create(user.id()));
+ assertThat(result.annotations().get(CodeOwner.create(admin.id())))
+ .containsExactly(CodeOwnerAnnotation.create("FOO"), CodeOwnerAnnotation.create("BAR"));
+ assertThat(result.annotations().get(CodeOwner.create(user.id())))
+ .containsExactly(CodeOwnerAnnotation.create("BAR"));
+ }
+
+ @Test
public void cannotResolvePathCodeOwnersOfNullCodeOwnerConfig() throws Exception {
NullPointerException npe =
assertThrows(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
index 53c986d..cc9cefc 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
@@ -22,6 +22,7 @@
import static java.util.stream.Collectors.joining;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.plugins.codeowners.backend.AbstractCodeOwnerConfigParserTest;
@@ -181,6 +182,47 @@
}
@Test
+ public void codeOwnerConfigWithAnnotations() throws Exception {
+ assertParseAndFormat(
+ getCodeOwnerConfig(
+ EMAIL_1,
+ EMAIL_2 + " #{FOO_BAR}#{BAR_BAZ} #NO_ANNOTATION, #{FOO} #{bar} #{bAz} other comment",
+ EMAIL_3),
+ codeOwnerConfig -> {
+ CodeOwnerSetSubject codeOwnerSetSubject =
+ assertThat(codeOwnerConfig).hasCodeOwnerSetsThat().onlyElement();
+ codeOwnerSetSubject.hasCodeOwnersEmailsThat().containsExactly(EMAIL_1, EMAIL_2, EMAIL_3);
+ codeOwnerSetSubject
+ .hasAnnotationsThat()
+ .containsExactly(EMAIL_2, ImmutableSet.of("FOO_BAR", "BAR_BAZ", "FOO", "bar", "bAz"));
+ },
+ // annotations are sorted alphabetically, the normal comment is dropped
+ EMAIL_1
+ + "\n"
+ + EMAIL_2
+ + " #{BAR_BAZ} #{FOO} #{FOO_BAR} #{bAz} #{bar}\n"
+ + EMAIL_3
+ + "\n");
+ }
+
+ @Test
+ public void codeOwnerConfigWithAnnotationsOnAllUsersWildcard() throws Exception {
+ assertParseAndFormat(
+ getCodeOwnerConfig(
+ "* #{FOO_BAR}#{BAR_BAZ} #NO_ANNOTATION, #{FOO} #{bar} #{bAz} other comment"),
+ codeOwnerConfig -> {
+ CodeOwnerSetSubject codeOwnerSetSubject =
+ assertThat(codeOwnerConfig).hasCodeOwnerSetsThat().onlyElement();
+ codeOwnerSetSubject.hasCodeOwnersEmailsThat().containsExactly("*");
+ codeOwnerSetSubject
+ .hasAnnotationsThat()
+ .containsExactly("*", ImmutableSet.of("FOO_BAR", "BAR_BAZ", "FOO", "bar", "bAz"));
+ },
+ // annotations are sorted alphabetically, the normal comment is dropped
+ "* #{BAR_BAZ} #{FOO} #{FOO_BAR} #{bAz} #{bar}\n");
+ }
+
+ @Test
public void codeOwnerConfigWithNonSortedEmails() throws Exception {
assertParseAndFormat(
String.join("\n", EMAIL_3, EMAIL_2, EMAIL_1) + "\n",
@@ -428,6 +470,41 @@
}
@Test
+ public void perFileCodeOwnerConfigWithAnnotations() throws Exception {
+ assertParseAndFormat(
+ "per-file foo="
+ + EMAIL_1
+ + ","
+ + EMAIL_2
+ + ","
+ + EMAIL_3
+ + " #{FOO_BAR}#{BAR_BAZ} #NO_ANNOTATION, #{FOO} #{bar} #{bAz} other comment",
+ codeOwnerConfig -> {
+ CodeOwnerSetSubject codeOwnerSetSubject =
+ assertThat(codeOwnerConfig).hasCodeOwnerSetsThat().onlyElement();
+ codeOwnerSetSubject.hasPathExpressionsThat().containsExactly("foo");
+ codeOwnerSetSubject.hasCodeOwnersEmailsThat().containsExactly(EMAIL_1, EMAIL_2, EMAIL_3);
+ codeOwnerSetSubject
+ .hasAnnotationsThat()
+ .containsExactly(
+ EMAIL_1,
+ ImmutableSet.of("FOO_BAR", "BAR_BAZ", "FOO", "bar", "bAz"),
+ EMAIL_2,
+ ImmutableSet.of("FOO_BAR", "BAR_BAZ", "FOO", "bar", "bAz"),
+ EMAIL_3,
+ ImmutableSet.of("FOO_BAR", "BAR_BAZ", "FOO", "bar", "bAz"));
+ },
+ // annotations are sorted alphabetically, the normal comment is dropped, a newline is added
+ "per-file foo="
+ + EMAIL_1
+ + ","
+ + EMAIL_2
+ + ","
+ + EMAIL_3
+ + " #{BAR_BAZ} #{FOO} #{FOO_BAR} #{bAz} #{bar}\n");
+ }
+
+ @Test
public void perFileCodeOwnerConfigImportFromSameProjectAndBranch() throws Exception {
Path path = Paths.get("/foo/bar/OWNERS");
CodeOwnerConfigReference codeOwnerConfigReference =
diff --git a/resources/Documentation/backend-find-owners.md b/resources/Documentation/backend-find-owners.md
index a1af71d..122b598 100644
--- a/resources/Documentation/backend-find-owners.md
+++ b/resources/Documentation/backend-find-owners.md
@@ -291,6 +291,41 @@
per-file docs.config,*.md=richard.roe@example.com
```
+### <a id="anotations">Annotations
+
+Lines representing [access grants](#accessGrants) can be annotated. Annotations
+have the format `#{ANNOTATION_NAME}` and can appear at the end of the line.
+E.g.:
+
+```
+ john.doe@example.com #{NEVER_SUGGEST}
+ per-file docs.config,*.md=richard.roe@example.com #{NEVER_SUGGEST}
+```
+\
+Annotations can be mixed with [comments](#comments) that can appear before and
+after annotations, E.g.:
+
+```
+ jane.roe@example.com # foo bar #{NEVER_SUGGEST} baz
+```
+\
+The following annotations are supported:
+
+#### <a id="neverSuggest">
+* `NEVER_SUGGEST`:
+ Code owners with this annotation are omitted when [suggesting code
+ owners](rest-api.html#list-code-owners-for-path-in-change). If code ownership
+ is assigned to the same code owner through multiple relevant access grants in
+ the same code owner config file or in other relevant code owner config files
+ the code owner gets omitted from the suggestion if it has the `NEVER_SUGGEST`
+ set on any of the access grants.
+
+Unknown annotations are silently ignored.
+
+**NOTE:** If an access grant line that assigns code ownership to multiple users
+has an annotation, this annotation applies to all these users. E.g. if an
+annotation is set for the all users wildcard (aka `*`) it applies to all users.
+
### <a id="comments">Comments
The '#' character indicates the beginning of a comment. Arbitrary text may be
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 461232e..90d6055 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -483,6 +483,9 @@
* distance of the code owner config file that defines the code owner to the
path for which code owners are listed (the lower the distance the better the
code owner)
+* whether the user is explicitly mentioned as a code owner in the code owner
+ config file vs. the user being a code owner only because the code ownership
+ has been assigned to all users (aka `*`)
* whether the code owner is a reviewer of the change (only when listing code
owners for a change)
@@ -673,6 +676,8 @@
* [service users](#serviceUsers) (members of the `Service Users` group)
* the change owner (since the change owner cannot be added as reviewer)
+* code owners that are annotated with
+ [NEVER_SUGGEST](backend-find-owners.html#neverSuggest)
In addition, by default the change number is used as seed if none was specified.
This way the sort order on a change is always the same for files that have the
@@ -842,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.
---