Merge changes I477eda7b,I29539813,I12bb9d13
* changes:
Add REST endpoint to check if an email is a code owner of a path
BranchCodeOwners: JavaDoc fixes
CodeOwnerResolverResult: Include debug messages
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 22bc543..0508bc7 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -31,6 +31,7 @@
/** Create a request to retrieve code owner config files from the branch. */
CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException;
+ /** Request to retrieve code owner config files from the branch. */
abstract class CodeOwnerConfigFilesRequest {
private boolean includeNonParsableFiles;
private String email;
@@ -57,7 +58,7 @@
return this;
}
- /** Returns the email that should appear in the returned code owner config files/ */
+ /** Returns the email that should appear in the returned code owner config files. */
@Nullable
public String getEmail() {
return email;
@@ -74,13 +75,13 @@
return this;
}
- /** Returns the path glob that should be matched by the returned code owner config files/ */
+ /** Returns the path glob that should be matched by the returned code owner config files. */
@Nullable
public String getPath() {
return path;
}
- /** Executes the request and retrieves the paths of the requested code owner config file */
+ /** Executes the request and retrieves the paths of the requested code owner config file. */
public abstract List<String> paths() throws RestApiException;
}
@@ -88,6 +89,69 @@
RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
throws RestApiException;
+ /** Checks the code ownership of a user for a path in a branch. */
+ CodeOwnerCheckRequest checkCodeOwner() throws RestApiException;
+
+ /** Request for checking the code ownership of a user for a path in a branch. */
+ abstract class CodeOwnerCheckRequest {
+ private String email;
+ private String path;
+ private String user;
+
+ /**
+ * Sets the email for which the code ownership should be checked.
+ *
+ * @param email the email for which the code ownership should be checked
+ */
+ public CodeOwnerCheckRequest email(String email) {
+ this.email = email;
+ return this;
+ }
+
+ /** Returns the email for which the code ownership should be checked. */
+ @Nullable
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Sets the path for which the code ownership should be checked.
+ *
+ * @param path the path for which the code ownership should be checked
+ */
+ public CodeOwnerCheckRequest path(String path) {
+ this.path = path;
+ return this;
+ }
+
+ /** Returns the path for which the code ownership should be checked. */
+ @Nullable
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the user for which the code owner visibility should be checked.
+ *
+ * <p>If not specified the code owner visibility is not checked.
+ *
+ * @param user the user for which the code owner visibility should be checked
+ */
+ public CodeOwnerCheckRequest user(@Nullable String user) {
+ this.user = user;
+ return this;
+ }
+
+ /** Returns the user for which the code owner visibility should be checked. */
+ @Nullable
+ public String getUser() {
+ return user;
+ }
+
+ /** Executes the request and retrieves the result. */
+ public abstract CodeOwnerCheckInfo check() throws RestApiException;
+ }
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -108,5 +172,10 @@
throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
index 7052a1b..beae1f6 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerBranchConfig;
import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
import com.google.gerrit.plugins.codeowners.restapi.RenameEmail;
@@ -35,6 +36,7 @@
private final GetCodeOwnerBranchConfig getCodeOwnerBranchConfig;
private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
private final RenameEmail renameEmail;
+ private final Provider<CheckCodeOwner> checkCodeOwnerProvider;
private final BranchResource branchResource;
@Inject
@@ -42,10 +44,12 @@
GetCodeOwnerBranchConfig getCodeOwnerBranchConfig,
Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
RenameEmail renameEmail,
+ Provider<CheckCodeOwner> checkCodeOwnerProvider,
@Assisted BranchResource branchResource) {
this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
this.getCodeOwnerBranchConfig = getCodeOwnerBranchConfig;
this.renameEmail = renameEmail;
+ this.checkCodeOwnerProvider = checkCodeOwnerProvider;
this.branchResource = branchResource;
}
@@ -81,4 +85,22 @@
throw asRestApiException("Cannot rename email", e);
}
}
+
+ @Override
+ public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+ return new CodeOwnerCheckRequest() {
+ @Override
+ public CodeOwnerCheckInfo check() throws RestApiException {
+ CheckCodeOwner checkCodeOwner = checkCodeOwnerProvider.get();
+ checkCodeOwner.setEmail(getEmail());
+ checkCodeOwner.setPath(getPath());
+ checkCodeOwner.setUser(getUser());
+ try {
+ return checkCodeOwner.apply(branchResource).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check code owner", e);
+ }
+ }
+ };
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
new file mode 100644
index 0000000..4b33e11
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * REST API representation of the result of checking a code owner via {code
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner}.
+ *
+ * <p>This class determines the JSON format of check result in the REST API.
+ */
+public class CodeOwnerCheckInfo {
+ /**
+ * Whether the given email owns the specified path in the branch.
+ *
+ * <p>True if:
+ *
+ * <ul>
+ * <li>the given email is resolvable (see {@link #isResolvable}) and
+ * <li>any code owner config file assigns codeownership to the email for the path (see {@link
+ * #codeOwnerConfigFilePaths}) or the email is configured as default code owner (see {@link
+ * CodeOwnerCheckInfo#isDefaultCodeOwner} field) or the email is configured as global code
+ * owner (see {@link #isGlobalCodeOwner} field)
+ * </ul>
+ */
+ public boolean isCodeOwner;
+
+ /**
+ * Whether the given email is resolvable for the specified user or the calling user if no user was
+ * specified.
+ */
+ public boolean isResolvable;
+
+ /**
+ * Paths of the code owner config files that assign code ownership to the given email for the
+ * specified path.
+ *
+ * <p>If code ownership is assigned to the email via a code owner config files, but the email is
+ * not resolvable (see {@link #isResolvable} field), the user is not a code owner.
+ */
+ public List<String> codeOwnerConfigFilePaths;
+
+ /**
+ * Whether the given email is configured as a default code owner.
+ *
+ * <p>If the email is configured as default code owner, but the email is not resolvable (see
+ * {@link #isResolvable} field), the user is not a code owner.
+ */
+ public boolean isDefaultCodeOwner;
+
+ /**
+ * Whether the given email is configured as a global code owner.
+ *
+ * <p>If the email is configured as global code owner, but the email is not resolvable (see {@link
+ * #isResolvable} field), the user is not a code owner.
+ */
+ public boolean isGlobalCodeOwner;
+
+ /** 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/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index da32379..8e1d906 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
@@ -42,6 +43,8 @@
import com.google.inject.Provider;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -156,7 +159,7 @@
PathCodeOwnersResult pathCodeOwnersResult =
pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
return resolve(
- pathCodeOwnersResult.getPathCodeOwners(), pathCodeOwnersResult.hasUnresolvedImports());
+ pathCodeOwnersResult.getPathCodeOwners(), pathCodeOwnersResult.unresolvedImports());
}
}
@@ -178,22 +181,26 @@
* @see #resolve(CodeOwnerReference)
*/
public CodeOwnerResolverResult resolve(Set<CodeOwnerReference> codeOwnerReferences) {
- return resolve(codeOwnerReferences, /* hasUnresolvedImports= */ false);
+ return resolve(codeOwnerReferences, /* unresolvedImports= */ ImmutableList.of());
}
/**
* Resolves the given {@link CodeOwnerReference}s to {@link CodeOwner}s.
*
* @param codeOwnerReferences the code owner references that should be resolved
- * @param hasUnresolvedImports whether there are unresolved imports
+ * @param unresolvedImports list of unresolved imports
* @return the {@link CodeOwner} for the given code owner references
* @see #resolve(CodeOwnerReference)
*/
private CodeOwnerResolverResult resolve(
- Set<CodeOwnerReference> codeOwnerReferences, boolean hasUnresolvedImports) {
+ Set<CodeOwnerReference> codeOwnerReferences, List<UnresolvedImport> unresolvedImports) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
+ requireNonNull(unresolvedImports, "unresolvedImports");
AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
+ List<String> messages = new ArrayList<>();
+ unresolvedImports.forEach(
+ unresolvedImport -> messages.add(unresolvedImport.format(codeOwnersPluginConfiguration)));
ImmutableSet<CodeOwner> codeOwners =
codeOwnerReferences.stream()
.filter(
@@ -204,19 +211,27 @@
}
return true;
})
- .map(this::resolve)
+ .map(this::resolveWithMessages)
.filter(
- codeOwner -> {
- if (!codeOwner.isPresent()) {
+ resolveResult -> {
+ messages.addAll(resolveResult.messages());
+ if (!resolveResult.isPresent()) {
hasUnresolvedCodeOwners.set(true);
return false;
}
return true;
})
- .map(Optional::get)
+ .map(OptionalResultWithMessages::get)
.collect(toImmutableSet());
- return CodeOwnerResolverResult.create(
- codeOwners, ownedByAllUsers.get(), hasUnresolvedCodeOwners.get(), hasUnresolvedImports);
+ CodeOwnerResolverResult codeOwnerResolverResult =
+ CodeOwnerResolverResult.create(
+ codeOwners,
+ ownedByAllUsers.get(),
+ hasUnresolvedCodeOwners.get(),
+ !unresolvedImports.isEmpty(),
+ messages);
+ logger.atFine().log("resolve result = %s", codeOwnerResolverResult);
+ return codeOwnerResolverResult;
}
/**
@@ -261,33 +276,55 @@
* Optional#empty()}
*/
public Optional<CodeOwner> resolve(CodeOwnerReference codeOwnerReference) {
+ OptionalResultWithMessages<CodeOwner> resolveResult = resolveWithMessages(codeOwnerReference);
+ logger.atFine().log("resolve result = %s", resolveResult);
+ return resolveResult.result();
+ }
+
+ public OptionalResultWithMessages<CodeOwner> resolveWithMessages(
+ CodeOwnerReference codeOwnerReference) {
String email = requireNonNull(codeOwnerReference, "codeOwnerReference").email();
- logger.atFine().log("resolving code owner reference %s", codeOwnerReference);
- if (!isEmailDomainAllowed(email)) {
- logger.atFine().log("domain of email %s is not allowed", email);
- return Optional.empty();
+ List<String> messages = new ArrayList<>();
+ messages.add(String.format("resolving code owner reference %s", codeOwnerReference));
+
+ OptionalResultWithMessages<Boolean> emailDomainAllowedResult = isEmailDomainAllowed(email);
+ messages.addAll(emailDomainAllowedResult.messages());
+ if (!emailDomainAllowedResult.get()) {
+ return OptionalResultWithMessages.createEmpty(messages);
}
- Optional<AccountState> accountState =
- lookupEmail(email).flatMap(accountId -> lookupAccount(accountId, email));
- if (!accountState.isPresent()) {
- logger.atFine().log("no account for email %s", email);
- return Optional.empty();
- }
- if (!accountState.get().account().isActive()) {
- logger.atFine().log("account for email %s is inactive", email);
- return Optional.empty();
- }
- if (enforceVisibility && !isVisible(accountState.get(), email)) {
- logger.atFine().log(
- "account %d of email %s is not visible", accountState.get().account().id().get(), email);
- return Optional.empty();
+ OptionalResultWithMessages<Account.Id> lookupEmailResult = lookupEmail(email);
+ messages.addAll(lookupEmailResult.messages());
+ if (lookupEmailResult.isEmpty()) {
+ return OptionalResultWithMessages.createEmpty(messages);
}
- CodeOwner codeOwner = CodeOwner.create(accountState.get().account().id());
- logger.atFine().log("resolved to code owner %s", codeOwner);
- return Optional.of(codeOwner);
+ Account.Id accountId = lookupEmailResult.get();
+ OptionalResultWithMessages<AccountState> lookupAccountResult = lookupAccount(accountId, email);
+ messages.addAll(lookupAccountResult.messages());
+ if (lookupAccountResult.isEmpty()) {
+ return OptionalResultWithMessages.createEmpty(messages);
+ }
+
+ AccountState accountState = lookupAccountResult.get();
+ if (!accountState.account().isActive()) {
+ messages.add(String.format("account %s for email %s is inactive", accountId, email));
+ return OptionalResultWithMessages.createEmpty(messages);
+ }
+ if (enforceVisibility) {
+ OptionalResultWithMessages<Boolean> isVisibleResult = isVisible(accountState, email);
+ messages.addAll(isVisibleResult.messages());
+ if (!isVisibleResult.get()) {
+ return OptionalResultWithMessages.createEmpty(messages);
+ }
+ } else {
+ messages.add("code owner visibility is not checked");
+ }
+
+ CodeOwner codeOwner = CodeOwner.create(accountState.account().id());
+ messages.add(String.format("resolved to account %s", codeOwner.accountId()));
+ return OptionalResultWithMessages.create(codeOwner, messages);
}
/** Whether the given account can be seen. */
@@ -306,7 +343,7 @@
* @param email the email that should be looked up
* @return the ID of the account to which the email belongs if was found
*/
- private Optional<Account.Id> lookupEmail(String email) {
+ private OptionalResultWithMessages<Account.Id> lookupEmail(String email) {
ImmutableSet<ExternalId> extIds;
try {
extIds = externalIds.byEmail(email);
@@ -315,17 +352,17 @@
}
if (extIds.isEmpty()) {
- logger.atFine().log(
- "cannot resolve code owner email %s: no account with this email exists", email);
- return Optional.empty();
+ return OptionalResultWithMessages.createEmpty(
+ String.format(
+ "cannot resolve code owner email %s: no account with this email exists", email));
}
if (extIds.stream().map(ExternalId::accountId).distinct().count() > 1) {
- logger.atFine().log("cannot resolve code owner email %s: email is ambiguous", email);
- return Optional.empty();
+ return OptionalResultWithMessages.createEmpty(
+ String.format("cannot resolve code owner email %s: email is ambiguous", email));
}
- return Optional.of(extIds.stream().findFirst().get().accountId());
+ return OptionalResultWithMessages.create(extIds.stream().findFirst().get().accountId());
}
/**
@@ -336,16 +373,17 @@
* @param email the email that was resolved to the account ID
* @return the {@link AccountState} of the account with the given account ID, if it exists
*/
- private Optional<AccountState> lookupAccount(Account.Id accountId, String email) {
+ private OptionalResultWithMessages<AccountState> lookupAccount(
+ Account.Id accountId, String email) {
Optional<AccountState> accountState = accountCache.get(accountId);
if (!accountState.isPresent()) {
- logger.atFine().log(
- "cannot resolve code owner email %s: email belongs to account %s,"
- + " but no account with this ID exists",
- email, accountId);
- return Optional.empty();
+ return OptionalResultWithMessages.createEmpty(
+ String.format(
+ "cannot resolve code owner email %s: email belongs to account %s,"
+ + " but no account with this ID exists",
+ email, accountId));
}
- return accountState;
+ return OptionalResultWithMessages.create(accountState.get());
}
/**
@@ -365,14 +403,15 @@
* @return {@code true} if the given account and email are visible to the user, otherwise {@code
* false}
*/
- private boolean isVisible(AccountState accountState, String email) {
+ private OptionalResultWithMessages<Boolean> isVisible(AccountState accountState, String email) {
if (!canSee(accountState)) {
- logger.atFine().log(
- "cannot resolve code owner email %s: account %s is not visible to user %s",
- email,
- accountState.account().id(),
- user != null ? user.getLoggableName() : currentUser.get().getLoggableName());
- return false;
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is not visible to user %s",
+ email,
+ accountState.account().id(),
+ user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
}
if (!email.equals(accountState.account().preferredEmail())) {
@@ -380,14 +419,23 @@
if (user != null) {
if (user.hasEmailAddress(email)) {
- // it's a secondary email of the user, users can always see their own secondary emails
- return true;
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "email %s is visible to user %s: email is a secondary email that is owned by this"
+ + " user",
+ email, user.getLoggableName()));
}
} else if (currentUser.get().isIdentifiedUser()
&& currentUser.get().asIdentifiedUser().hasEmailAddress(email)) {
// it's a secondary email of the calling user, users can always see their own secondary
// emails
- return true;
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "email %s is visible to the calling user %s: email is a secondary email that is"
+ + " owned by this user",
+ email, currentUser.get().getLoggableName()));
}
// the email is a secondary email of another account, check if the user can see secondary
@@ -395,18 +443,33 @@
try {
if (user != null) {
if (!permissionBackend.user(user).test(GlobalPermission.MODIFY_ACCOUNT)) {
- logger.atFine().log(
- "cannot resolve code owner email %s: account %s is referenced by secondary email,"
- + " but user %s cannot see secondary emails",
- email, accountState.account().id(), user.getLoggableName());
- return false;
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email"
+ + " but user %s cannot see secondary emails",
+ email, accountState.account().id(), user.getLoggableName()));
}
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email"
+ + " and user %s can see secondary emails",
+ email, accountState.account().id(), user.getLoggableName()));
} else if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
- logger.atFine().log(
- "cannot resolve code owner email %s: account %s is referenced by secondary email,"
- + " but the calling user %s cannot see secondary emails",
- email, accountState.account().id(), currentUser.get().getLoggableName());
- return false;
+ return OptionalResultWithMessages.create(
+ false,
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email"
+ + " but the calling user %s cannot see secondary emails",
+ email, accountState.account().id(), currentUser.get().getLoggableName()));
+ } else {
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email"
+ + " and the calling user %s can see secondary emails",
+ email, accountState.account().id(), currentUser.get().getLoggableName()));
}
} catch (PermissionBackendException e) {
throw new StorageException(
@@ -415,8 +478,12 @@
e);
}
}
-
- return true;
+ return OptionalResultWithMessages.create(
+ true,
+ String.format(
+ "account %s is visible to user %s",
+ accountState.account().id(),
+ user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
}
/**
@@ -426,29 +493,30 @@
* @return {@code true} if the domain of the given email is allowed for code owners, otherwise
* {@code false}
*/
- public boolean isEmailDomainAllowed(String email) {
+ public OptionalResultWithMessages<Boolean> isEmailDomainAllowed(String email) {
requireNonNull(email, "email");
ImmutableSet<String> allowedEmailDomains =
codeOwnersPluginConfiguration.getAllowedEmailDomains();
if (allowedEmailDomains.isEmpty()) {
- // all domains are allowed
- return true;
+ return OptionalResultWithMessages.create(true, "all domains are allowed");
}
if (email.equals(ALL_USERS_WILDCARD)) {
- return true;
+ return OptionalResultWithMessages.create(true, "all users wildcard is allowed");
}
int emailAtIndex = email.lastIndexOf('@');
if (emailAtIndex >= 0 && emailAtIndex < email.length() - 1) {
String emailDomain = email.substring(emailAtIndex + 1);
- logger.atFine().log("email domain = %s", emailDomain);
- return allowedEmailDomains.contains(emailDomain);
+ boolean isEmailDomainAllowed = allowedEmailDomains.contains(emailDomain);
+ return OptionalResultWithMessages.create(
+ isEmailDomainAllowed,
+ String.format(
+ "domain %s of email %s is %s",
+ emailDomain, email, isEmailDomainAllowed ? "allowed" : "not allowed"));
}
- // email has no domain
- logger.atFine().log("email %s has no domain", email);
- return false;
+ return OptionalResultWithMessages.create(false, String.format("email %s has no domain", email));
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index b5b71c0..4a9e372 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -18,8 +18,10 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
+import java.util.List;
/** The result of resolving code owner references via {@link CodeOwnerResolver}. */
@AutoValue
@@ -49,6 +51,9 @@
/** Whether there are imports which couldn't be resolved. */
public abstract boolean hasUnresolvedImports();
+ /** Gets messages that were collected while resolving the code owners. */
+ public abstract ImmutableList<String> messages();
+
/**
* Whether there are any code owners defined for the path, regardless of whether they can be
* resolved or not.
@@ -67,6 +72,7 @@
.add("ownedByAllUsers", ownedByAllUsers())
.add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners())
.add("hasUnresolvedImports", hasUnresolvedImports())
+ .add("messages", messages())
.toString();
}
@@ -75,9 +81,14 @@
ImmutableSet<CodeOwner> codeOwners,
boolean ownedByAllUsers,
boolean hasUnresolvedCodeOwners,
- boolean hasUnresolvedImports) {
+ boolean hasUnresolvedImports,
+ List<String> messages) {
return new AutoValue_CodeOwnerResolverResult(
- codeOwners, ownedByAllUsers, hasUnresolvedCodeOwners, hasUnresolvedImports);
+ codeOwners,
+ ownedByAllUsers,
+ hasUnresolvedCodeOwners,
+ hasUnresolvedImports,
+ ImmutableList.copyOf(messages));
}
/** Creates a empty {@link CodeOwnerResolverResult} instance. */
@@ -86,6 +97,7 @@
/* codeOwners= */ ImmutableSet.of(),
/* ownedByAllUsers= */ false,
/* hasUnresolvedCodeOwners= */ false,
- /* hasUnresolvedImports= */ false);
+ /* hasUnresolvedImports= */ false,
+ /* messages= */ ImmutableList.of());
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
new file mode 100644
index 0000000..66fce67
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * An optional result of an operation with optional messages.
+ *
+ * @param <T> type of the optional result
+ */
+@AutoValue
+public abstract class OptionalResultWithMessages<T> {
+ /** Gets the result. */
+ public abstract Optional<T> result();
+
+ /** Whether the result is present. */
+ public boolean isPresent() {
+ return result().isPresent();
+ }
+
+ /** Whether the result is empty. */
+ public boolean isEmpty() {
+ return !result().isPresent();
+ }
+
+ /** Returns the result value, if present. Fails if the result is not present. */
+ public T get() {
+ return result().get();
+ }
+
+ /** Gets the messages. */
+ public abstract ImmutableList<String> messages();
+
+ /** Creates a {@link OptionalResultWithMessages} instance without messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result) {
+ return create(result, ImmutableList.of());
+ }
+
+ /** Creates an empty {@link OptionalResultWithMessages} instance with a single message. */
+ public static <T> OptionalResultWithMessages<T> createEmpty(String message) {
+ requireNonNull(message, "message");
+ return createEmpty(ImmutableList.of(message));
+ }
+
+ /** Creates an empty {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> createEmpty(List<String> messages) {
+ requireNonNull(messages, "messages");
+ return new AutoValue_OptionalResultWithMessages<>(
+ Optional.empty(), ImmutableList.copyOf(messages));
+ }
+
+ /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result, String message) {
+ requireNonNull(message, "message");
+ return create(result, ImmutableList.of(message));
+ }
+
+ /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+ public static <T> OptionalResultWithMessages<T> create(T result, List<String> messages) {
+ requireNonNull(result, "result");
+ requireNonNull(messages, "messages");
+ return new AutoValue_OptionalResultWithMessages<>(
+ Optional.of(result), ImmutableList.copyOf(messages));
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
new file mode 100644
index 0000000..1bc41d7
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -0,0 +1,240 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.restapi;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.server.restapi.account.AccountsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+/**
+ * REST endpoint that checks the code ownership of a user for a path in a branch.
+ *
+ * <p>This REST endpoint handles {@code GET
+ * /projects/<project-name>/branches/<branch-name>/code_owners.check} requests.
+ */
+public class CheckCodeOwner implements RestReadView<BranchResource> {
+ private final PermissionBackend permissionBackend;
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
+ private final PathCodeOwners.Factory pathCodeOwnersFactory;
+ private final Provider<CodeOwnerResolver> codeOwnerResolverProvider;
+ private final CodeOwners codeOwners;
+ private final AccountsCollection accountsCollection;
+
+ private String email;
+ private String path;
+ private String user;
+ private IdentifiedUser identifiedUser;
+
+ @Inject
+ public CheckCodeOwner(
+ PermissionBackend permissionBackend,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
+ PathCodeOwners.Factory pathCodeOwnersFactory,
+ Provider<CodeOwnerResolver> codeOwnerResolverProvider,
+ CodeOwners codeOwners,
+ AccountsCollection accountsCollection) {
+ this.permissionBackend = permissionBackend;
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
+ this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+ this.codeOwnerResolverProvider = codeOwnerResolverProvider;
+ this.codeOwners = codeOwners;
+ this.accountsCollection = accountsCollection;
+ }
+
+ @Option(name = "--email", usage = "email for which the code ownership should be checked")
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ @Option(name = "--path", usage = "path for which the code ownership should be checked")
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ @Option(
+ name = "--user",
+ usage =
+ "user for which the code owner visibility should be checked,"
+ + " if not specified the code owner visibility is not checked")
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ @Override
+ public Response<CodeOwnerCheckInfo> apply(BranchResource branchResource)
+ throws BadRequestException, AuthException, IOException, ConfigInvalidException,
+ PermissionBackendException {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+
+ validateInput();
+
+ Path absolutePath = JgitPath.of(path).getAsAbsolutePath();
+ List<String> messages = new ArrayList<>();
+ List<Path> codeOwnerConfigFilePaths = new ArrayList<>();
+ AtomicBoolean isCodeOwnershipAssignedToEmail = new AtomicBoolean(false);
+ AtomicBoolean isDefaultCodeOwner = new AtomicBoolean(false);
+ codeOwnerConfigHierarchy.visit(
+ branchResource.getBranchKey(),
+ ObjectId.fromString(branchResource.getRevision()),
+ absolutePath,
+ codeOwnerConfig -> {
+ Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
+ messages.add(
+ String.format(
+ "checking code owner config file %s:%s:%s",
+ codeOwnerConfig.key().project(),
+ codeOwnerConfig.key().shortBranchName(),
+ codeOwnerConfigFilePath));
+ PathCodeOwnersResult pathCodeOwnersResult =
+ pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
+ pathCodeOwnersResult
+ .unresolvedImports()
+ .forEach(
+ unresolvedImport ->
+ messages.add(unresolvedImport.format(codeOwnersPluginConfiguration)));
+ Optional<CodeOwnerReference> codeOwnerReference =
+ pathCodeOwnersResult.getPathCodeOwners().stream()
+ .filter(cor -> cor.email().equals(email))
+ .findAny();
+ if (codeOwnerReference.isPresent()) {
+ isCodeOwnershipAssignedToEmail.set(true);
+
+ if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
+ messages.add(
+ String.format(
+ "found email %s as code owner in default code owner config", email));
+ isDefaultCodeOwner.set(true);
+ } else {
+ messages.add(
+ String.format(
+ "found email %s as code owner in %s", email, codeOwnerConfigFilePath));
+ codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
+ }
+ }
+
+ if (pathCodeOwnersResult.ignoreParentCodeOwners()) {
+ messages.add("parent code owners are ignored");
+ }
+
+ return !pathCodeOwnersResult.ignoreParentCodeOwners();
+ });
+
+ boolean isGlobalCodeOwner = isGlobalCodeOwner(branchResource.getNameKey());
+ if (isGlobalCodeOwner) {
+ messages.add(String.format("found email %s as global code owner", email));
+ isCodeOwnershipAssignedToEmail.set(true);
+ }
+
+ OptionalResultWithMessages<Boolean> isResolvableResult = isResolvable();
+ boolean isResolvable = isResolvableResult.get();
+ messages.addAll(isResolvableResult.messages());
+
+ CodeOwnerCheckInfo codeOwnerCheckInfo = new CodeOwnerCheckInfo();
+ codeOwnerCheckInfo.isCodeOwner = isCodeOwnershipAssignedToEmail.get() && isResolvable;
+ codeOwnerCheckInfo.isResolvable = isResolvable;
+ codeOwnerCheckInfo.codeOwnerConfigFilePaths =
+ codeOwnerConfigFilePaths.stream().map(Path::toString).collect(toList());
+ codeOwnerCheckInfo.isDefaultCodeOwner = isDefaultCodeOwner.get();
+ codeOwnerCheckInfo.isGlobalCodeOwner = isGlobalCodeOwner;
+ codeOwnerCheckInfo.debugLogs = messages;
+ return Response.ok(codeOwnerCheckInfo);
+ }
+
+ private void validateInput()
+ throws BadRequestException, AuthException, IOException, ConfigInvalidException {
+ if (email == null) {
+ throw new BadRequestException("email required");
+ }
+ if (path == null) {
+ throw new BadRequestException("path required");
+ }
+ if (user != null) {
+ try {
+ identifiedUser =
+ accountsCollection
+ .parse(TopLevelResource.INSTANCE, IdString.fromDecoded(user))
+ .getUser();
+ } catch (ResourceNotFoundException e) {
+ throw new BadRequestException(String.format("user %s not found", user), e);
+ }
+ }
+ }
+
+ private boolean isGlobalCodeOwner(Project.NameKey projectName) {
+ return codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName).stream()
+ .filter(cor -> cor.email().equals(email))
+ .findAny()
+ .isPresent();
+ }
+
+ private OptionalResultWithMessages<Boolean> isResolvable() {
+ if (email.equals(CodeOwnerResolver.ALL_USERS_WILDCARD)) {
+ return OptionalResultWithMessages.create(true);
+ }
+
+ CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get();
+ if (identifiedUser != null) {
+ codeOwnerResolver.forUser(identifiedUser);
+ } else {
+ codeOwnerResolver.enforceVisibility(false);
+ }
+ OptionalResultWithMessages<CodeOwner> resolveResult =
+ codeOwnerResolver.resolveWithMessages(CodeOwnerReference.create(email));
+
+ List<String> messages = new ArrayList<>();
+ messages.add(String.format("trying to resolve email %s", email));
+ messages.addAll(resolveResult.messages());
+ return OptionalResultWithMessages.create(resolveResult.isPresent(), messages);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
index 03d8858..68d057e 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -32,6 +32,7 @@
get(BRANCH_KIND, "code_owners.config_files").to(GetCodeOwnerConfigFiles.class);
get(BRANCH_KIND, "code_owners.branch_config").to(GetCodeOwnerBranchConfig.class);
post(BRANCH_KIND, "code_owners.rename").to(RenameEmail.class);
+ get(BRANCH_KIND, "code_owners.check").to(CheckCodeOwner.class);
factory(CodeOwnerJson.Factory.class);
DynamicMap.mapOf(binder(), CodeOwnersInBranchCollection.PathResource.PATH_KIND);
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
new file mode 100644
index 0000000..b060198
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+
+/** {@link Subject} for doing assertions on {@link CodeOwnerCheckInfo}s. */
+public class CodeOwnerCheckInfoSubject extends Subject {
+ /**
+ * Starts fluent chain to do assertions on a {@link CodeOwnerCheckInfo}.
+ *
+ * @param codeOwnerCheckInfo the code owner check info on which assertions should be done
+ * @return the created {@link CodeOwnerCheckInfoSubject}
+ */
+ public static CodeOwnerCheckInfoSubject assertThat(CodeOwnerCheckInfo codeOwnerCheckInfo) {
+ return assertAbout(codeOwnerCheckInfos()).that(codeOwnerCheckInfo);
+ }
+
+ private static Factory<CodeOwnerCheckInfoSubject, CodeOwnerCheckInfo> codeOwnerCheckInfos() {
+ return CodeOwnerCheckInfoSubject::new;
+ }
+
+ private final CodeOwnerCheckInfo codeOwnerCheckInfo;
+
+ private CodeOwnerCheckInfoSubject(
+ FailureMetadata metadata, CodeOwnerCheckInfo codeOwnerCheckInfo) {
+ super(metadata, codeOwnerCheckInfo);
+ this.codeOwnerCheckInfo = codeOwnerCheckInfo;
+ }
+
+ public void isCodeOwner() {
+ check("isCodeOwner").that(codeOwnerCheckInfo().isCodeOwner).isTrue();
+ }
+
+ public void isNotCodeOwner() {
+ check("isCodeOwner").that(codeOwnerCheckInfo().isCodeOwner).isFalse();
+ }
+
+ public void isResolvable() {
+ check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isTrue();
+ }
+
+ public void isNotResolvable() {
+ check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isFalse();
+ }
+
+ public IterableSubject hasCodeOwnerConfigFilePathsThat() {
+ return check("codeOwnerConfigFilePaths").that(codeOwnerCheckInfo().codeOwnerConfigFilePaths);
+ }
+
+ public void isDefaultCodeOwner() {
+ check("isDefaultCodeOwner").that(codeOwnerCheckInfo().isDefaultCodeOwner).isTrue();
+ }
+
+ public void isNotDefaultCodeOwner() {
+ check("isDefaultCodeOwner").that(codeOwnerCheckInfo().isDefaultCodeOwner).isFalse();
+ }
+
+ public void isGlobalCodeOwner() {
+ check("isGlobalCodeOwner").that(codeOwnerCheckInfo().isGlobalCodeOwner).isTrue();
+ }
+
+ public void isNotGlobalCodeOwner() {
+ check("isGlobalCodeOwner").that(codeOwnerCheckInfo().isGlobalCodeOwner).isFalse();
+ }
+
+ public void hasDebugLogsThatContainAllOf(String... expectedMessages) {
+ for (String expectedMessage : expectedMessages) {
+ check("debugLogs").that(codeOwnerCheckInfo().debugLogs).contains(expectedMessage);
+ }
+ }
+
+ public IterableSubject hasDebugLogsThat() {
+ return check("debugLogs").that(codeOwnerCheckInfo().debugLogs);
+ }
+
+ private CodeOwnerCheckInfo codeOwnerCheckInfo() {
+ isNotNull();
+ return codeOwnerCheckInfo;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java
new file mode 100644
index 0000000..a0a9082
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.OptionalSubject.optionals;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
+
+/** {@link Subject} for doing assertions on {@link OptionalResultWithMessages}s. */
+public class OptionalResultWithMessagesSubject extends Subject {
+ /**
+ * Starts fluent chain to do assertions on a {@link OptionalResultWithMessages}.
+ *
+ * @param optionalResultWithMessages the optionalResultWithMessages instance on which assertions
+ * should be done
+ * @return the created {@link OptionalResultWithMessagesSubject}
+ */
+ public static OptionalResultWithMessagesSubject assertThat(
+ OptionalResultWithMessages<?> optionalResultWithMessages) {
+ return assertAbout(optionalResultsWithMessages()).that(optionalResultWithMessages);
+ }
+
+ /**
+ * Creates subject factory for mapping {@link OptionalResultWithMessages}s to {@link
+ * OptionalResultWithMessagesSubject}s.
+ */
+ private static Subject.Factory<OptionalResultWithMessagesSubject, OptionalResultWithMessages<?>>
+ optionalResultsWithMessages() {
+ return OptionalResultWithMessagesSubject::new;
+ }
+
+ private final OptionalResultWithMessages<?> optionalResultWithMessages;
+
+ private OptionalResultWithMessagesSubject(
+ FailureMetadata metadata, OptionalResultWithMessages<?> optionalResultWithMessages) {
+ super(metadata, optionalResultWithMessages);
+ this.optionalResultWithMessages = optionalResultWithMessages;
+ }
+
+ public void isPresent() {
+ check("result()").about(optionals()).that(optionalResultWithMessages().result()).isPresent();
+ }
+
+ public void isEmpty() {
+ check("result()").about(optionals()).that(optionalResultWithMessages().result()).isEmpty();
+ }
+
+ public IterableSubject hasMessagesThat() {
+ return check("messages()").that(optionalResultWithMessages().messages());
+ }
+
+ private OptionalResultWithMessages<?> optionalResultWithMessages() {
+ isNotNull();
+ return optionalResultWithMessages;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index e659580..b2dd664 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -734,7 +734,7 @@
private Optional<CommitValidationMessage> validateCodeOwnerReference(
IdentifiedUser user, Path codeOwnerConfigFilePath, CodeOwnerReference codeOwnerReference) {
CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().forUser(user);
- if (!codeOwnerResolver.isEmailDomainAllowed(codeOwnerReference.email())) {
+ if (!codeOwnerResolver.isEmailDomainAllowed(codeOwnerReference.email()).get()) {
return error(
String.format(
"the domain of the code owner email '%s' in '%s' is not allowed for code owners",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
new file mode 100644
index 0000000..ea4fc3a
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -0,0 +1,682 @@
+// 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.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerCheckInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner} REST
+ * endpoint.
+ */
+public class CheckCodeOwnerIT extends AbstractCodeOwnersIT {
+ private static final String ROOT_PATH = "/";
+
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ProjectOperations projectOperations;
+ @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdate;
+ @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+
+ private BackendConfig backendConfig;
+
+ @Before
+ public void setUpCodeOwnersPlugin() throws Exception {
+ backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+ }
+
+ @Test
+ public void requiresEmail() throws Exception {
+ BadRequestException exception =
+ assertThrows(BadRequestException.class, () -> checkCodeOwner("/", /* email= */ null));
+ assertThat(exception).hasMessageThat().isEqualTo("email required");
+ }
+
+ @Test
+ public void requiresPath() throws Exception {
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class, () -> checkCodeOwner(/* path= */ null, user.email()));
+ assertThat(exception).hasMessageThat().isEqualTo("path required");
+ }
+
+ @Test
+ public void requiresCallerToBeAdmin() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ AuthException authException =
+ assertThrows(AuthException.class, () -> checkCodeOwner(ROOT_PATH, user.email()));
+ assertThat(authException).hasMessageThat().isEqualTo("administrate server not permitted");
+ }
+
+ @Test
+ public void checkCodeOwner() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsCodeOwners("/foo/", codeOwner);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+ String.format("resolved to account %s", codeOwner.id()));
+ }
+
+ @Test
+ public void checkCodeOwnerThatHasCodeOwnershipThroughMultipleFiles() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsRootCodeOwners(codeOwner);
+ setAsCodeOwners("/foo/", codeOwner);
+ setAsCodeOwners("/foo/bar/", codeOwner);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(
+ getCodeOwnerConfigFilePath("/foo/bar/"),
+ getCodeOwnerConfigFilePath("/foo/"),
+ getCodeOwnerConfigFilePath(ROOT_PATH))
+ .inOrder();
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format("resolved to account %s", codeOwner.id()));
+ }
+
+ @Test
+ public void checkCodeOwnerWithParentCodeOwnersIgnored() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsRootCodeOwners(codeOwner);
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .ignoreParentCodeOwners()
+ .addCodeOwnerEmail(codeOwner.email())
+ .create();
+
+ setAsCodeOwners("/foo/bar/", codeOwner);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(
+ getCodeOwnerConfigFilePath("/foo/bar/"), getCodeOwnerConfigFilePath("/foo/"))
+ .inOrder();
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+ "parent code owners are ignored",
+ String.format("resolved to account %s", codeOwner.id()));
+ }
+
+ @Test
+ public void checkCodeOwner_secondaryEmail() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ String secondaryEmail = "codeOwnerSecondary@example.com";
+ accountOperations
+ .account(codeOwner.id())
+ .forUpdate()
+ .addSecondaryEmail(secondaryEmail)
+ .update();
+
+ setAsRootCodeOwner(secondaryEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail);
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format("resolved to account %s", codeOwner.id()));
+ }
+
+ @Test
+ public void checkNonCodeOwner() throws Exception {
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(String.format("resolved to account %s", user.id()));
+ }
+
+ @Test
+ public void checkNonExistingEmail() throws Exception {
+ String nonExistingEmail = "non-exiting@example.com";
+
+ setAsRootCodeOwner(nonExistingEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, nonExistingEmail);
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ nonExistingEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "cannot resolve code owner email %s: no account with this email exists",
+ nonExistingEmail));
+ }
+
+ @Test
+ public void checkAmbiguousExistingEmail() throws Exception {
+ String ambiguousEmail = "ambiguous@example.com";
+
+ setAsRootCodeOwner(ambiguousEmail);
+
+ // Add the email to 2 accounts to make it ambiguous.
+ addEmail(user.id(), ambiguousEmail);
+ addEmail(admin.id(), ambiguousEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, ambiguousEmail);
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ ambiguousEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "cannot resolve code owner email %s: email is ambiguous", ambiguousEmail));
+ }
+
+ @Test
+ public void checkOrphanedEmail() throws Exception {
+ // Create an external ID with an email for a non-existing account.
+ String orphanedEmail = "orphaned@example.com";
+ Account.Id accountId = Account.id(999999);
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ extIdNotes.upsert(ExternalId.createEmail(accountId, orphanedEmail));
+ extIdNotes.commit(md);
+ }
+
+ setAsRootCodeOwner(orphanedEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, orphanedEmail);
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ orphanedEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "cannot resolve code owner email %s: email belongs to account %s,"
+ + " but no account with this ID exists",
+ orphanedEmail, accountId));
+ }
+
+ @Test
+ public void checkInactiveAccount() throws Exception {
+ TestAccount inactiveUser =
+ accountCreator.create(
+ "inactiveUser", "inactiveUser@example.com", "Inactive User", /* displayName= */ null);
+ accountOperations.account(inactiveUser.id()).forUpdate().inactive().update();
+
+ setAsRootCodeOwners(inactiveUser);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, inactiveUser.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ inactiveUser.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "account %s for email %s is inactive", inactiveUser.id(), inactiveUser.email()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.net")
+ public void checkEmailWithAllowedDomain() throws Exception {
+ String emailWithAllowedEmailDomain = "foo@example.net";
+ TestAccount userWithAllowedEmail =
+ accountCreator.create(
+ "userWithAllowedEmail",
+ emailWithAllowedEmailDomain,
+ "User with allowed emil",
+ /* displayName= */ null);
+
+ setAsRootCodeOwners(userWithAllowedEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, emailWithAllowedEmailDomain);
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ emailWithAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "domain %s of email %s is allowed",
+ emailWithAllowedEmailDomain.substring(emailWithAllowedEmailDomain.indexOf('@') + 1),
+ emailWithAllowedEmailDomain));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.net")
+ public void checkEmailWithNonAllowedDomain() throws Exception {
+ String emailWithNonAllowedEmailDomain = "foo@example.com";
+ TestAccount userWithAllowedEmail =
+ accountCreator.create(
+ "userWithNonAllowedEmail",
+ emailWithNonAllowedEmailDomain,
+ "User with non-allowed emil",
+ /* displayName= */ null);
+
+ setAsRootCodeOwners(userWithAllowedEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo =
+ checkCodeOwner(ROOT_PATH, emailWithNonAllowedEmailDomain);
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ emailWithNonAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "domain %s of email %s is not allowed",
+ emailWithNonAllowedEmailDomain.substring(
+ emailWithNonAllowedEmailDomain.indexOf('@') + 1),
+ emailWithNonAllowedEmailDomain));
+ }
+
+ @Test
+ public void checkAllUsersWildcard() throws Exception {
+ CodeOwnerCheckInfo checkCodeOwnerInfo =
+ checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ }
+
+ @Test
+ public void checkAllUsersWildcard_ownedByAllUsers() throws Exception {
+ setAsRootCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo =
+ checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+ }
+
+ @Test
+ public void checkDefaultCodeOwner() throws Exception {
+ TestAccount defaultCodeOwner =
+ accountCreator.create(
+ "defaultCodeOwner",
+ "defaultCodeOwner@example.com",
+ "Default Code Owner",
+ /* displayName= */ null);
+ setAsDefaultCodeOwners(defaultCodeOwner);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, defaultCodeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+ assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in default code owner config",
+ defaultCodeOwner.email()),
+ String.format("resolved to account %s", defaultCodeOwner.id()));
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "globalCodeOwner@example.com")
+ public void checkGlobalCodeOwner() throws Exception {
+ TestAccount globalCodeOwner =
+ accountCreator.create(
+ "globalCodeOwner",
+ "globalCodeOwner@example.com",
+ "Global Code Owner",
+ /* displayName= */ null);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, globalCodeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format("found email %s as global code owner", globalCodeOwner.email()),
+ String.format("resolved to account %s", globalCodeOwner.id()));
+ }
+
+ @Test
+ public void checkCodeOwnerForOtherUser() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsCodeOwners("/foo/", codeOwner);
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email(), user.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+ String.format("account %s is visible to user %s", codeOwner.id(), user.username()),
+ String.format("resolved to account %s", codeOwner.id()));
+ }
+
+ @Test
+ public void cannotCheckForNonExistingUser() throws Exception {
+ String nonExistingEmail = "non-existing@example.com";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class, () -> checkCodeOwner("/", user.email(), nonExistingEmail));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(String.format("user %s not found", nonExistingEmail));
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void checkNonVisibleCodeOwnerForOtherUser() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+ setAsRootCodeOwners(codeOwner);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo =
+ checkCodeOwner(ROOT_PATH, codeOwner.email(), user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as 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",
+ codeOwner.email(), codeOwner.id(), user.username()));
+ }
+
+ @Test
+ public void checkNonVisibleCodeOwnerForOtherUser_secondaryEmail() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ String secondaryEmail = "codeOwnerSecondary@example.com";
+ accountOperations
+ .account(codeOwner.id())
+ .forUpdate()
+ .addSecondaryEmail(secondaryEmail)
+ .update();
+
+ setAsRootCodeOwner(secondaryEmail);
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotResolvable();
+ assertThat(checkCodeOwnerInfo)
+ .hasCodeOwnerConfigFilePathsThat()
+ .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+ assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThatContainAllOf(
+ String.format(
+ "found email %s as code owner in %s",
+ secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email"
+ + " but user %s cannot see secondary emails",
+ secondaryEmail, codeOwner.id(), user.username()));
+ }
+
+ @Test
+ public void debugLogsContainUnresolvedImports() throws Exception {
+ // imports are not supported for the proto backend
+ assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+ CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
+ CodeOwnerConfigReference.create(
+ CodeOwnerConfigImportMode.ALL, "non-existing/" + getCodeOwnerConfigFileName());
+ CodeOwnerConfig.Key codeOwnerConfigKey =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath(ROOT_PATH)
+ .addImport(unresolvableCodeOwnerConfigReference)
+ .create();
+
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+ assertThat(checkCodeOwnerInfo)
+ .hasDebugLogsThat()
+ .contains(
+ String.format(
+ "The import of %s:%s:%s in %s:%s:%s cannot be resolved:"
+ + " code owner config does not exist (revision = %s)",
+ project,
+ "master",
+ JgitPath.of(unresolvableCodeOwnerConfigReference.filePath()).getAsAbsolutePath(),
+ project,
+ "master",
+ getCodeOwnerConfigFilePath(codeOwnerConfigKey.folderPath().toString()),
+ projectOperations.project(project).getHead("master").name()));
+ }
+
+ private CodeOwnerCheckInfo checkCodeOwner(String path, String email) throws RestApiException {
+ return checkCodeOwner(path, email, null);
+ }
+
+ private CodeOwnerCheckInfo checkCodeOwner(String path, String email, @Nullable String user)
+ throws RestApiException {
+ return projectCodeOwnersApiFactory
+ .project(project)
+ .branch("master")
+ .checkCodeOwner()
+ .path(path)
+ .email(email)
+ .user(user)
+ .check();
+ }
+
+ private String getCodeOwnerConfigFilePath(String folderPath) {
+ assertThat(folderPath).startsWith("/");
+ assertThat(folderPath).endsWith("/");
+ return folderPath + getCodeOwnerConfigFileName();
+ }
+
+ private void setAsRootCodeOwner(String email) {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath(ROOT_PATH)
+ .addCodeOwnerEmail(email)
+ .create();
+ }
+
+ private String getCodeOwnerConfigFileName() {
+ CodeOwnerBackend backend = backendConfig.getDefaultBackend();
+ if (backend instanceof FindOwnersBackend) {
+ return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ } else if (backend instanceof ProtoBackend) {
+ return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
+ }
+ throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
+ }
+
+ private void addEmail(Account.Id accountId, String email) throws Exception {
+ accountsUpdate
+ .get()
+ .update(
+ "Test update",
+ accountId,
+ (a, u) ->
+ u.addExternalId(
+ ExternalId.create(
+ "foo",
+ "bar" + accountId.get(),
+ accountId,
+ email,
+ /* hashedPassword= */ null)));
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
index d70be9f..7d2bd66 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
@@ -53,7 +53,8 @@
ImmutableList.of(
RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config_files"),
RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.branch_config"),
- RestCall.post("/projects/%s/branches/%s/code-owners~code_owners.rename"));
+ RestCall.post("/projects/%s/branches/%s/code-owners~code_owners.rename"),
+ RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.check"));
private static final ImmutableList<RestCall> BRANCH_CODE_OWNER_CONFIGS_ENDPOINTS =
ImmutableList.of(RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config/%s"));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 1210ae5..0297426 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerSubject.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.OptionalResultWithMessagesSubject.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
@@ -82,25 +83,41 @@
@Test
public void resolveCodeOwnerReferenceForNonExistingEmail() throws Exception {
- assertThat(
- codeOwnerResolver.get().resolve(CodeOwnerReference.create("non-existing@example.com")))
- .isEmpty();
+ String nonExistingEmail = "non-existing@example.com";
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(nonExistingEmail));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: no account with this email exists",
+ nonExistingEmail));
}
@Test
public void resolveCodeOwnerReferenceForEmail() throws Exception {
- Optional<CodeOwner> codeOwner =
- codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()));
- assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(admin.id());
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+ assertThat(result.get()).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(String.format("account %s is visible to user %s", admin.id(), admin.username()));
}
@Test
public void cannotResolveCodeOwnerReferenceForStarAsEmail() throws Exception {
- Optional<CodeOwner> codeOwner =
+ OptionalResultWithMessages<CodeOwner> result =
codeOwnerResolver
.get()
- .resolve(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
- assertThat(codeOwner).isEmpty();
+ .resolveWithMessages(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: no account with this email exists",
+ CodeOwnerResolver.ALL_USERS_WILDCARD));
}
@Test
@@ -116,27 +133,47 @@
ExternalId.create(
"foo", "bar", user.id(), admin.email(), /* hashedPassword= */ null)));
- assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()))).isEmpty();
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format("cannot resolve code owner email %s: email is ambiguous", admin.email()));
}
@Test
public void resolveCodeOwnerReferenceForOrphanedEmail() throws Exception {
// Create an external ID with an email for a non-existing account.
String email = "foo.bar@example.com";
+ Account.Id accountId = Account.id(999999);
try (Repository allUsersRepo = repoManager.openRepository(allUsers);
MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
- extIdNotes.upsert(ExternalId.createEmail(Account.id(999999), email));
+ extIdNotes.upsert(ExternalId.createEmail(accountId, email));
extIdNotes.commit(md);
}
- assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(email))).isEmpty();
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(email));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: email belongs to account %s, but no account with this ID exists",
+ email, accountId));
}
@Test
public void resolveCodeOwnerReferenceForInactiveUser() throws Exception {
accountOperations.account(user.id()).forUpdate().inactive().update();
- assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(user.email()))).isEmpty();
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(user.email()));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(String.format("account %s for email %s is inactive", user.id(), user.email()));
}
@Test
@@ -149,7 +186,15 @@
// user2 cannot see the admin account since they do not share any group and
// "accounts.visibility" is set to "SAME_GROUP".
- assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()))).isEmpty();
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: account %s is not visible to user %s",
+ admin.email(), admin.id(), user2.username()));
}
@Test
@@ -162,33 +207,57 @@
// admin has the "Modify Account" global capability and hence can see the secondary email of the
// user account.
- Optional<CodeOwner> codeOwner =
- codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail));
- assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email and the calling user %s can see secondary emails",
+ secondaryEmail, user.id(), admin.username()));
// admin has the "Modify Account" global capability and hence can see the secondary email of the
// user account if another user is the calling user
requestScopeOperations.setApiUser(user2.id());
- codeOwner =
+ result =
codeOwnerResolver
.get()
.forUser(identifiedUserFactory.create(admin.id()))
- .resolve(CodeOwnerReference.create(secondaryEmail));
- assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+ .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "resolved code owner email %s: account %s is referenced by secondary email and user %s can see secondary emails",
+ secondaryEmail, user.id(), admin.username()));
// user can see its own secondary email.
requestScopeOperations.setApiUser(user.id());
- codeOwner = codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail));
- assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+ result = codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "email %s is visible to the calling user %s: email is a secondary email that is owned by this user",
+ secondaryEmail, user.username()));
// user can see its own secondary email if another user is the calling user.
requestScopeOperations.setApiUser(user2.id());
- codeOwner =
+ result =
codeOwnerResolver
.get()
.forUser(identifiedUserFactory.create(user.id()))
- .resolve(CodeOwnerReference.create(secondaryEmail));
- assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+ .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "email %s is visible to user %s: email is a secondary email that is owned by this user",
+ secondaryEmail, user.username()));
}
@Test
@@ -200,18 +269,31 @@
// user doesn't have the "Modify Account" global capability and hence cannot see the secondary
// email of the admin account.
requestScopeOperations.setApiUser(user.id());
- assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail)))
- .isEmpty();
+ OptionalResultWithMessages<CodeOwner> result =
+ codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email but the calling user %s cannot see secondary emails",
+ secondaryEmail, admin.id(), user.username()));
// user doesn't have the "Modify Account" global capability and hence cannot see the secondary
// email of the admin account if another user is the calling user
requestScopeOperations.setApiUser(admin.id());
- assertThat(
- codeOwnerResolver
- .get()
- .forUser(identifiedUserFactory.create(user.id()))
- .resolve(CodeOwnerReference.create(secondaryEmail)))
- .isEmpty();
+ result =
+ codeOwnerResolver
+ .get()
+ .forUser(identifiedUserFactory.create(user.id()))
+ .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+ assertThat(result).isEmpty();
+ assertThat(result)
+ .hasMessagesThat()
+ .contains(
+ String.format(
+ "cannot resolve code owner email %s: account %s is referenced by secondary email but user %s cannot see secondary emails",
+ secondaryEmail, admin.id(), user.username()));
}
@Test
@@ -400,30 +482,43 @@
name = "plugin.code-owners.allowedEmailDomain",
values = {"example.com", "example.net"})
public void configuredEmailDomainsAreAllowed() throws Exception {
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.net")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org@example.com"))
- .isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org")).isFalse();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo")).isFalse();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com@example.org"))
- .isFalse();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD))
- .isTrue();
+ assertIsEmailDomainAllowed(
+ "foo@example.com", true, "domain example.com of email foo@example.com is allowed");
+ assertIsEmailDomainAllowed(
+ "foo@example.net", true, "domain example.net of email foo@example.net is allowed");
+ assertIsEmailDomainAllowed(
+ "foo@example.org@example.com",
+ true,
+ "domain example.com of email foo@example.org@example.com is allowed");
+ assertIsEmailDomainAllowed(
+ "foo@example.org", false, "domain example.org of email foo@example.org is not allowed");
+ assertIsEmailDomainAllowed("foo", false, "email foo has no domain");
+ assertIsEmailDomainAllowed(
+ "foo@example.com@example.org",
+ false,
+ "domain example.org of email foo@example.com@example.org is not allowed");
+ assertIsEmailDomainAllowed(
+ CodeOwnerResolver.ALL_USERS_WILDCARD, true, "all users wildcard is allowed");
}
@Test
public void allEmailDomainsAreAllowed() throws Exception {
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.net")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org@example.com"))
- .isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo")).isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com@example.org"))
- .isTrue();
- assertThat(codeOwnerResolver.get().isEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD))
- .isTrue();
+ String expectedMessage = "all domains are allowed";
+ assertIsEmailDomainAllowed("foo@example.com", true, expectedMessage);
+ assertIsEmailDomainAllowed("foo@example.net", true, expectedMessage);
+ assertIsEmailDomainAllowed("foo@example.org@example.com", true, expectedMessage);
+ assertIsEmailDomainAllowed("foo@example.org", true, expectedMessage);
+ assertIsEmailDomainAllowed("foo", true, expectedMessage);
+ assertIsEmailDomainAllowed("foo@example.com@example.org", true, expectedMessage);
+ assertIsEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD, true, expectedMessage);
+ }
+
+ private void assertIsEmailDomainAllowed(
+ String email, boolean expectedResult, String expectedMessage) {
+ OptionalResultWithMessages<Boolean> isEmailDomainAllowedResult =
+ codeOwnerResolver.get().isEmailDomainAllowed(email);
+ assertThat(isEmailDomainAllowedResult.get()).isEqualTo(expectedResult);
+ assertThat(isEmailDomainAllowedResult.messages()).containsExactly(expectedMessage);
}
@Test
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index f6cd213..8902d36 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -219,6 +219,66 @@
]
```
+### <a id="check-code-owner">Check Code Owner
+_'GET /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.check/'_
+
+Checks the code ownership of a user for a path in a branch.
+
+The following request parameters can be specified:
+
+| Field Name | | Description |
+| ----------- | --------- | ----------- |
+| `email` | mandatory | Email for which the code ownership should be checked.
+| `path` | mandatory | Path for which the code ownership should be checked.
+| `user` | optional | User for which the code owner visibility should be checked. If not specified the code owner visibility is not checked. Can be used to investigate why a code owner is not shown/suggested to this user.
+
+Requires that the caller has the [Administrate
+Server](../../../Documentation/access-control.html#capability_administrateServer)
+global capability.
+
+This REST endpoint is intended to investigate code owner configurations that do
+not work as intended. The response contains debug logs that may point out issues
+with the code owner configuration. For example, with this REST endpoint it is
+possible to find out why a certain email that is listed as code owner in a code
+owner config file is ignored (e.g. because it is ambiguous or because it belongs
+to an inactive account).
+
+#### Request
+
+```
+ GET /projects/foo%2Fbar/branches/master/code_owners.check?email=xyz@example.com&path=/foo/bar/baz.md HTTP/1.0
+```
+
+#### Response
+
+As response a [CodeOwnerCheckInfo](#code-owner-check-info) entity is returned.
+
+```
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "is_code_owner": "false",
+ "is_resolvable": "false",
+ "code_owner_config_file_paths": [
+ "/OWNERS",
+ ],
+ "is_default_code_owner": "false",
+ "is_global_code_owner": "false",
+ "debug_logs": [
+ "checking code owner config file foo/bar:master:/OWNERS",
+ "found email xyz@example.com as code owner in /OWNERS",
+ "trying to resolve email xyz@example.com",
+ "resolving code owner reference CodeOwnerReference{email=xyz@example.com}",
+ "all domains are allowed",
+ "cannot resolve code owner email xyz@example.com: email is ambiguous",
+ "email xyz@example.com is not a code owner of path '/foo/bar/baz.md'"
+ ]
+ }
+```
+
### <a id="rename-email-in-code-owner-config-files">Rename Email In Code Owner Config Files
_'POST /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.rename/'_
@@ -617,11 +677,26 @@
---
+### <a id="code-owner-check-info"> CodeOwnerCheckInfo
+The `CodeOwnerCheckInfo` entity contains the result of checking the code
+ownership of a user for a path in a branch.
+
+| Field Name | Description |
+| --------------- | ----------- |
+| `is_code_owner` | Whether the given email owns the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) any code owner config file assigns codeownership to the email for the path (see field `code_owner_config_file_paths`) or the email is configured as default code owner (see field `is_default_code_owner` or the email is configured as global code owner (see field `is_global_code_owner`).
+| `is_resolvable` | Whether the given email is resolvable for the specified user or the calling user if no user was specified.
+| `code_owner_config_file_paths` | Paths of the code owner config files that assign code ownership to the specified email and path as a list. Note that if code ownership is assigned to the email via a code owner config files, but the email is not resolvable (see field `is_resolvable` field), the user is not a code owner.
+| `is_default_code_owner` | Whether the given email is configured as a default code owner in the code owner config file in `refs/meta/config`. Note that if the email is configured as default code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
+| `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.
+| `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner.
+
+---
+
### <a id="code-owner-config-info"> CodeOwnerConfigInfo
The `CodeOwnerConfigInfo` entity contains information about a code owner config
for a path.
-
| Field Name | | Description |
| ----------- | -------- | ----------- |
| `ignore_parent_code_owners` | optional, not set if `false` | Whether code owners from parent code owner configs (code owner configs in parent folders) should be ignored.