Merge changes Ia856d964,I99fd4433,Ibd694fc2,I896edd3a
* changes:
CodeOwnerConfigScanner#containsAnyCodeOwnerConfigFile: Handle invalid files
CodeOwnerApprovalCheck: Fix exception when no old path is set for deletion
CodeOwnerApprovalCheck: Return 409 Conflict if destination branch was deleted
Avoid loading all accounts when code ownership is assigned to all users
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java
index 3ead395..82655cf 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFile.java
@@ -137,6 +137,15 @@
public static ChangedFile create(PatchListEntry patchListEntry) {
requireNonNull(patchListEntry, "patchListEntry");
+ if (patchListEntry.getChangeType() == Patch.ChangeType.DELETED) {
+ // For deletions PatchListEntry sets the old path as new name and the old name is unset (see
+ // PatchListEntry constructor). This means to get the old path we need to read the new name.
+ return new AutoValue_ChangedFile(
+ Optional.empty(),
+ convertPathFromPatchListEntry(patchListEntry.getNewName()),
+ CHANGE_TYPE.get(patchListEntry.getChangeType()));
+ }
+
return new AutoValue_ChangedFile(
convertPathFromPatchListEntry(patchListEntry.getNewName()),
convertPathFromPatchListEntry(patchListEntry.getOldName()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 4ed19ce..5f55f62 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -14,7 +14,6 @@
package com.google.gerrit.plugins.codeowners.backend;
-import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.Objects.requireNonNull;
@@ -27,6 +26,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.plugins.codeowners.config.RequiredApproval;
@@ -106,7 +106,7 @@
* @return whether the given change has sufficient code owner approvals to be submittable
*/
public boolean isSubmittable(ChangeNotes changeNotes)
- throws IOException, PatchListNotAvailableException {
+ throws ResourceConflictException, IOException, PatchListNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
logger.atFine().log(
"checking if change %d in project %s is submittable",
@@ -156,7 +156,7 @@
* returned
*/
public Stream<FileCodeOwnerStatus> getFileStatuses(ChangeNotes changeNotes)
- throws IOException, PatchListNotAvailableException {
+ throws ResourceConflictException, IOException, PatchListNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
try (TraceTimer traceTimer =
TraceContext.newTimer(
@@ -187,15 +187,13 @@
"patchSetUploader = %d, implicit approval from uploader is %s",
patchSetUploader.get(), enableImplicitApprovalFromUploader ? "enabled" : "disabled");
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds =
+ CodeOwnerResolverResult globalCodeOwners =
codeOwnerResolver
.get()
.enforceVisibility(false)
.resolve(
- codeOwnersPluginConfiguration.getGlobalCodeOwners(changeNotes.getProjectName()))
- .map(CodeOwner::accountId)
- .collect(toImmutableSet());
- logger.atFine().log("global code owner accounts = %s", globalCodeOwnerAccountIds);
+ codeOwnersPluginConfiguration.getGlobalCodeOwners(changeNotes.getProjectName()));
+ logger.atFine().log("global code owners = %s", globalCodeOwners);
// If the branch doesn't contain any code owner config file yet, we apply special logic
// (project
@@ -217,7 +215,7 @@
getFileStatus(
branch,
revision,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
enableImplicitApprovalFromUploader,
patchSetUploader,
reviewerAccountIds,
@@ -231,7 +229,7 @@
private FileCodeOwnerStatus getFileStatus(
BranchNameKey branch,
ObjectId revision,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader,
ImmutableSet<Account.Id> reviewerAccountIds,
@@ -250,7 +248,7 @@
getPathCodeOwnerStatus(
branch,
revision,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
enableImplicitApprovalFromUploader,
patchSetUploader,
reviewerAccountIds,
@@ -271,7 +269,7 @@
getPathCodeOwnerStatus(
branch,
revision,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
enableImplicitApprovalFromUploader,
patchSetUploader,
reviewerAccountIds,
@@ -290,7 +288,7 @@
private PathCodeOwnerStatus getPathCodeOwnerStatus(
BranchNameKey branch,
ObjectId revision,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader,
ImmutableSet<Account.Id> reviewerAccountIds,
@@ -310,7 +308,7 @@
return isBootstrapping
? getPathCodeOwnerStatusBootstrappingMode(
branch,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
enableImplicitApprovalFromUploader,
patchSetUploader,
reviewerAccountIds,
@@ -318,7 +316,7 @@
absolutePath)
: getPathCodeOwnerStatusRegularMode(
branch,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
enableImplicitApprovalFromUploader,
patchSetUploader,
revision,
@@ -336,7 +334,7 @@
*/
private PathCodeOwnerStatus getPathCodeOwnerStatusBootstrappingMode(
BranchNameKey branch,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader,
ImmutableSet<Account.Id> reviewerAccountIds,
@@ -348,13 +346,13 @@
if (isApprovedBootstrappingMode(
branch.project(),
absolutePath,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
approverAccountIds,
enableImplicitApprovalFromUploader,
patchSetUploader)) {
codeOwnerStatus = CodeOwnerStatus.APPROVED;
} else if (isPendingBootstrappingMode(
- branch.project(), absolutePath, globalCodeOwnerAccountIds, reviewerAccountIds)) {
+ branch.project(), absolutePath, globalCodeOwners, reviewerAccountIds)) {
codeOwnerStatus = CodeOwnerStatus.PENDING;
}
@@ -367,21 +365,21 @@
private boolean isApprovedBootstrappingMode(
Project.NameKey projectName,
Path absolutePath,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
ImmutableSet<Account.Id> approverAccountIds,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader) {
return (enableImplicitApprovalFromUploader
&& isImplicitlyApprovedBootstrappingMode(
- projectName, absolutePath, globalCodeOwnerAccountIds, patchSetUploader))
+ projectName, absolutePath, globalCodeOwners, patchSetUploader))
|| isExplicitlyApprovedBootstrappingMode(
- projectName, absolutePath, globalCodeOwnerAccountIds, approverAccountIds);
+ projectName, absolutePath, globalCodeOwners, approverAccountIds);
}
private boolean isImplicitlyApprovedBootstrappingMode(
Project.NameKey projectName,
Path absolutePath,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
Account.Id patchSetUploader) {
if (isProjectOwner(projectName, patchSetUploader)) {
// The uploader of the patch set is a project owner and thus a code owner. This means there
@@ -393,7 +391,8 @@
return true;
}
- if (globalCodeOwnerAccountIds.contains(patchSetUploader)) {
+ if (globalCodeOwners.ownedByAllUsers()
+ || globalCodeOwners.codeOwnersAccountIds().contains(patchSetUploader)) {
// If the uploader of the patch set is a global code owner, there is an implicit code owner
// approval from the patch set uploader so that the path is automatically approved.
logger.atFine().log(
@@ -408,9 +407,10 @@
private boolean isExplicitlyApprovedBootstrappingMode(
Project.NameKey projectName,
Path absolutePath,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
ImmutableSet<Account.Id> approverAccountIds) {
- if (!Collections.disjoint(approverAccountIds, globalCodeOwnerAccountIds)) {
+ if (!Collections.disjoint(approverAccountIds, globalCodeOwners.codeOwnersAccountIds())
+ || (globalCodeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) {
// At least one of the global code owners approved the change.
logger.atFine().log("%s was approved by a global code owner", absolutePath);
return true;
@@ -429,7 +429,7 @@
private boolean isPendingBootstrappingMode(
Project.NameKey projectName,
Path absolutePath,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
ImmutableSet<Account.Id> reviewerAccountIds) {
if (reviewerAccountIds.stream()
.anyMatch(reviewerAccountId -> isProjectOwner(projectName, reviewerAccountId))) {
@@ -438,7 +438,7 @@
return true;
}
- if (isPending(absolutePath, globalCodeOwnerAccountIds, reviewerAccountIds)) {
+ if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) {
// At least one of the reviewers is a global code owner.
logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath);
return true;
@@ -453,7 +453,7 @@
*/
private PathCodeOwnerStatus getPathCodeOwnerStatusRegularMode(
BranchNameKey branch,
- ImmutableSet<Account.Id> globalCodeOwnerAccountIds,
+ CodeOwnerResolverResult globalCodeOwners,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader,
ObjectId revision,
@@ -467,7 +467,7 @@
if (isApproved(
absolutePath,
- globalCodeOwnerAccountIds,
+ globalCodeOwners,
approverAccountIds,
enableImplicitApprovalFromUploader,
patchSetUploader)) {
@@ -476,7 +476,7 @@
} else {
logger.atFine().log("%s was not approved by a global code owner", absolutePath);
- if (isPending(absolutePath, globalCodeOwnerAccountIds, reviewerAccountIds)) {
+ if (isPending(absolutePath, globalCodeOwners, reviewerAccountIds)) {
logger.atFine().log("%s is owned by a reviewer who is a global owner", absolutePath);
codeOwnerStatus.set(CodeOwnerStatus.PENDING);
}
@@ -486,23 +486,22 @@
revision,
absolutePath,
codeOwnerConfig -> {
- ImmutableSet<Account.Id> codeOwnerAccountIds =
- getCodeOwnerAccountIds(codeOwnerConfig, absolutePath);
+ CodeOwnerResolverResult codeOwners = getCodeOwners(codeOwnerConfig, absolutePath);
logger.atFine().log(
"code owners = %s (code owner config folder path = %s, file name = %s)",
- codeOwnerAccountIds,
+ codeOwners,
codeOwnerConfig.key().folderPath(),
codeOwnerConfig.key().fileName().orElse("<default>"));
if (isApproved(
absolutePath,
- codeOwnerAccountIds,
+ codeOwners,
approverAccountIds,
enableImplicitApprovalFromUploader,
patchSetUploader)) {
codeOwnerStatus.set(CodeOwnerStatus.APPROVED);
return false;
- } else if (isPending(absolutePath, codeOwnerAccountIds, reviewerAccountIds)) {
+ } else if (isPending(absolutePath, codeOwners, reviewerAccountIds)) {
codeOwnerStatus.set(CodeOwnerStatus.PENDING);
// We need to continue to check if any of the higher-level code owners approved the
@@ -524,18 +523,21 @@
private boolean isApproved(
Path absolutePath,
- ImmutableSet<Account.Id> codeOwnerAccountIds,
+ CodeOwnerResolverResult codeOwners,
ImmutableSet<Account.Id> approverAccountIds,
boolean enableImplicitApprovalFromUploader,
Account.Id patchSetUploader) {
- if (enableImplicitApprovalFromUploader && codeOwnerAccountIds.contains(patchSetUploader)) {
+ if (enableImplicitApprovalFromUploader
+ && (codeOwners.codeOwnersAccountIds().contains(patchSetUploader)
+ || codeOwners.ownedByAllUsers())) {
// If the uploader of the patch set owns the path, there is an implicit code owner
// approval from the patch set uploader so that the path is automatically approved.
logger.atFine().log("%s was implicitly approved by the patch set uploader", absolutePath);
return true;
}
- if (!Collections.disjoint(approverAccountIds, codeOwnerAccountIds)) {
+ if (!Collections.disjoint(approverAccountIds, codeOwners.codeOwnersAccountIds())
+ || (codeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) {
// At least one of the global code owners approved the change.
logger.atFine().log("%s was explicitly approved by a code owner", absolutePath);
return true;
@@ -546,9 +548,10 @@
private boolean isPending(
Path absolutePath,
- ImmutableSet<Account.Id> codeOwnerAccountIds,
+ CodeOwnerResolverResult codeOwners,
ImmutableSet<Account.Id> reviewerAccountIds) {
- if (!Collections.disjoint(codeOwnerAccountIds, reviewerAccountIds)) {
+ if (!Collections.disjoint(codeOwners.codeOwnersAccountIds(), reviewerAccountIds)
+ || (codeOwners.ownedByAllUsers() && !reviewerAccountIds.isEmpty())) {
logger.atFine().log("%s is owned by a reviewer", absolutePath);
return true;
}
@@ -578,17 +581,17 @@
}
/**
- * Gets the IDs of the accounts that own the given path according to the given code owner config.
+ * Gets the code owners that own the given path according to the given code owner config.
*
* @param codeOwnerConfig the code owner config from which the code owners should be retrieved
* @param absolutePath the path for which the code owners should be retrieved
*/
- private ImmutableSet<Account.Id> getCodeOwnerAccountIds(
+ private CodeOwnerResolverResult getCodeOwners(
CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
- return codeOwnerResolver.get().enforceVisibility(false)
- .resolvePathCodeOwners(codeOwnerConfig, absolutePath).stream()
- .map(CodeOwner::accountId)
- .collect(toImmutableSet());
+ return codeOwnerResolver
+ .get()
+ .enforceVisibility(false)
+ .resolvePathCodeOwners(codeOwnerConfig, absolutePath);
}
/**
@@ -633,16 +636,18 @@
*
* <p>This is the revision from which the code owner configs should be read when computing code
* owners for the files that are touched in the change.
+ *
+ * @throws ResourceConflictException thrown if the destination branch is not found, e.g. when the
+ * branch got deleted after the change was created
*/
- private ObjectId getDestBranchRevision(Change change) throws IOException {
+ private ObjectId getDestBranchRevision(Change change)
+ throws IOException, ResourceConflictException {
try (Repository repository = repoManager.openRepository(change.getProject());
RevWalk rw = new RevWalk(repository)) {
Ref ref = repository.exactRef(change.getDest().branch());
- checkNotNull(
- ref,
- "branch %s in repository %s not found",
- change.getDest().branch(),
- change.getProject().get());
+ if (ref == null) {
+ throw new ResourceConflictException("destination branch not found");
+ }
return rw.parseCommit(ref.getObjectId());
}
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index 99ee653..06bd4ef 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -74,7 +74,7 @@
found.set(true);
return false;
},
- ignoreInvalidCodeOwnerConfigFiles());
+ (codeOwnerConfigFilePath, configInvalidException) -> found.set(true));
return found.get();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index b53afb2..fdd4da2 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -29,7 +29,6 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.logging.Metadata;
@@ -44,6 +43,7 @@
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
/** Class to resolve {@link CodeOwnerReference}s to {@link CodeOwner}s. */
@@ -57,7 +57,6 @@
private final Provider<CurrentUser> currentUser;
private final ExternalIds externalIds;
private final AccountCache accountCache;
- private final Accounts accounts;
private final AccountControl.Factory accountControlFactory;
private final PathCodeOwners.Factory pathCodeOwnersFactory;
@@ -76,7 +75,6 @@
Provider<CurrentUser> currentUser,
ExternalIds externalIds,
AccountCache accountCache,
- Accounts accounts,
AccountControl.Factory accountControlFactory,
PathCodeOwners.Factory pathCodeOwnersFactory) {
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
@@ -84,7 +82,6 @@
this.currentUser = currentUser;
this.externalIds = externalIds;
this.accountCache = accountCache;
- this.accounts = accounts;
this.accountControlFactory = accountControlFactory;
this.pathCodeOwnersFactory = pathCodeOwnersFactory;
}
@@ -123,6 +120,12 @@
return this;
}
+ /** Whether the given code owner reference can be resolved. */
+ public boolean isResolvable(CodeOwnerReference codeOwnerReference) {
+ CodeOwnerResolverResult result = resolve(ImmutableSet.of(codeOwnerReference));
+ return !result.codeOwners().isEmpty() || result.ownedByAllUsers();
+ }
+
/**
* Resolves the code owners from the given code owner config for the given path from {@link
* CodeOwnerReference}s to a {@link CodeOwner}s.
@@ -135,7 +138,7 @@
* absolute; can be the path of a file or folder; the path may or may not exist
* @return the resolved code owners
*/
- public ImmutableSet<CodeOwner> resolvePathCodeOwners(
+ public CodeOwnerResolverResult resolvePathCodeOwners(
CodeOwnerConfig codeOwnerConfig, Path absolutePath) {
requireNonNull(codeOwnerConfig, "codeOwnerConfig");
requireNonNull(absolutePath, "absolutePath");
@@ -150,9 +153,7 @@
.filePath(codeOwnerConfig.key().fileName().orElse("<default>"))
.build())) {
logger.atFine().log("resolving path code owners for path %s", absolutePath);
- return pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).get().stream()
- .flatMap(this::resolve)
- .collect(toImmutableSet());
+ return resolve(pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).get());
}
}
@@ -163,9 +164,22 @@
* @return the {@link CodeOwner} for the given code owner references
* @see #resolve(CodeOwnerReference)
*/
- public Stream<CodeOwner> resolve(Set<CodeOwnerReference> codeOwnerReferences) {
+ public CodeOwnerResolverResult resolve(Set<CodeOwnerReference> codeOwnerReferences) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
- return codeOwnerReferences.stream().flatMap(this::resolve);
+ AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
+ ImmutableSet<CodeOwner> codeOwners =
+ codeOwnerReferences.stream()
+ .filter(
+ codeOwnerReference -> {
+ if (ALL_USERS_WILDCARD.equals(codeOwnerReference.email())) {
+ ownedByAllUsers.set(true);
+ return false;
+ }
+ return true;
+ })
+ .flatMap(this::resolve)
+ .collect(toImmutableSet());
+ return CodeOwnerResolverResult.create(codeOwners, ownedByAllUsers.get());
}
/**
@@ -201,18 +215,18 @@
* all accounts
* </ul>
*
+ * <p>This method does not resolve {@link CodeOwnerReference}s that assign the code ownership to
+ * all user by using {@link #ALL_USERS_WILDCARD} as email.
+ *
* @param codeOwnerReference the code owner reference that should be resolved
* @return the {@link CodeOwner} for the code owner reference if it was resolved, otherwise {@link
* Optional#empty()}
*/
+ @VisibleForTesting
public Stream<CodeOwner> resolve(CodeOwnerReference codeOwnerReference) {
String email = requireNonNull(codeOwnerReference, "codeOwnerReference").email();
logger.atFine().log("resolving code owner reference %s", codeOwnerReference);
- if (ALL_USERS_WILDCARD.equals(email)) {
- return resolveAllUsersWildcard();
- }
-
if (!isEmailDomainAllowed(email)) {
logger.atFine().log("domain of email %s is not allowed", email);
return Stream.of();
@@ -239,18 +253,6 @@
return Stream.of(codeOwner);
}
- private Stream<CodeOwner> resolveAllUsersWildcard() {
- try (TraceTimer traceTimer =
- TraceContext.newTimer("Resolve all users wildcard", Metadata.builder().build())) {
- return accounts.all().stream()
- .filter(accountState -> !enforceVisibility || canSee(accountState))
- .map(accountState -> CodeOwner.create(accountState.account().id()));
- } catch (IOException e) {
- throw new StorageException(
- String.format("cannot resolve code owner email %s", ALL_USERS_WILDCARD), e);
- }
- }
-
/** Whether the given account can be seen. */
private boolean canSee(AccountState accountState) {
AccountControl accountControl =
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
new file mode 100644
index 0000000..226d541
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+
+/** The result of resolving code owner references via {@link CodeOwnerResolver}. */
+@AutoValue
+public abstract class CodeOwnerResolverResult {
+ /**
+ * Returns the resolved code owners as stream.
+ *
+ * <p>Doesn't include code owners to which the code ownership was assigned by using the {@link
+ * CodeOwnerResolver#ALL_USERS_WILDCARD}.
+ */
+ public abstract ImmutableSet<CodeOwner> codeOwners();
+
+ /** Returns the account IDs of the resolved code owners as set. */
+ public ImmutableSet<Account.Id> codeOwnersAccountIds() {
+ return codeOwners().stream().map(CodeOwner::accountId).collect(toImmutableSet());
+ }
+
+ /**
+ * Whether the code ownership was assigned to all users by using the {@link
+ * CodeOwnerResolver#ALL_USERS_WILDCARD}.
+ */
+ public abstract boolean ownedByAllUsers();
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("codeOwners", codeOwners())
+ .add("ownedByAllUsers", ownedByAllUsers())
+ .toString();
+ }
+
+ /** Creates a {@link CodeOwnerResolverResult} instance. */
+ public static CodeOwnerResolverResult create(
+ ImmutableSet<CodeOwner> codeOwners, boolean ownedByAllUsers) {
+ return new AutoValue_CodeOwnerResolverResult(codeOwners, ownedByAllUsers);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
index c1a21a1..f179a9c 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
@@ -22,6 +22,7 @@
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
@@ -98,7 +99,7 @@
}
private SubmitRecord getSubmitRecord(ChangeNotes changeNotes)
- throws IOException, PatchListNotAvailableException {
+ throws ResourceConflictException, IOException, PatchListNotAvailableException {
requireNonNull(changeNotes, "changeNotes");
return codeOwnerApprovalCheck.isSubmittable(changeNotes) ? ok() : notReady();
}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index dd4fd6a..fcc5543 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -21,9 +21,12 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ListOption;
+import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
@@ -31,17 +34,21 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolverResult;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScoring;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.ServiceUserClassifier;
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.permissions.RefPermission;
import com.google.inject.Provider;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -60,6 +67,9 @@
@VisibleForTesting public static final int DEFAULT_LIMIT = 10;
+ private final AccountVisibility accountVisibility;
+ private final Accounts accounts;
+ private final AccountControl.Factory accountControlFactory;
private final PermissionBackend permissionBackend;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
@@ -96,12 +106,18 @@
}
protected AbstractGetCodeOwnersForPath(
+ AccountVisibility accountVisibility,
+ Accounts accounts,
+ AccountControl.Factory accountControlFactory,
PermissionBackend permissionBackend,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
Provider<CodeOwnerResolver> codeOwnerResolver,
ServiceUserClassifier serviceUserClassifier,
CodeOwnerJson.Factory codeOwnerJsonFactory) {
+ this.accountVisibility = accountVisibility;
+ this.accounts = accounts;
+ this.accountControlFactory = accountControlFactory;
this.permissionBackend = permissionBackend;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
@@ -134,12 +150,31 @@
rsrc.getRevision(),
rsrc.getPath(),
codeOwnerConfig -> {
- ImmutableSet<CodeOwner> pathCodeOwners =
+ CodeOwnerResolverResult pathCodeOwners =
codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath());
- codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners));
+ codeOwners.addAll(filterCodeOwners(rsrc, pathCodeOwners.codeOwners()));
+
+ if (pathCodeOwners.ownedByAllUsers()) {
+ fillUpWithRandomUsers(rsrc, codeOwners, limit);
+
+ if (codeOwners.size() < limit) {
+ logger.atFine().log(
+ "tried to fill up the suggestion list with random users,"
+ + " but didn't find enough visible accounts"
+ + " (wanted number of suggestions = %d, got = %d",
+ limit, codeOwners.size());
+ }
+
+ // We already found that the path is owned by all users. Hence we do not need to check
+ // if there are further code owners in higher-level code owner configs.
+ return false;
+ }
+
int distance = rootDistance - codeOwnerConfig.key().folderPath().getNameCount();
- pathCodeOwners.forEach(
- localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
+ pathCodeOwners
+ .codeOwners()
+ .forEach(
+ localCodeOwner -> distanceScoring.putValueForCodeOwner(localCodeOwner, distance));
// If codeOwners.size() >= limit we have gathered enough code owners and do not need to
// look at further code owner configs.
@@ -150,10 +185,15 @@
});
if (codeOwners.size() < limit) {
- ImmutableSet<CodeOwner> globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
- globalCodeOwners.forEach(
- codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, maxDistance));
- codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners));
+ CodeOwnerResolverResult globalCodeOwners = getGlobalCodeOwners(rsrc.getBranch().project());
+ globalCodeOwners
+ .codeOwners()
+ .forEach(codeOwner -> distanceScoring.putValueForCodeOwner(codeOwner, maxDistance));
+ codeOwners.addAll(filterCodeOwners(rsrc, globalCodeOwners.codeOwners()));
+
+ if (globalCodeOwners.ownedByAllUsers()) {
+ fillUpWithRandomUsers(rsrc, codeOwners, limit);
+ }
}
return Response.ok(
@@ -162,12 +202,11 @@
.format(sortAndLimit(distanceScoring.build(), ImmutableSet.copyOf(codeOwners))));
}
- private ImmutableSet<CodeOwner> getGlobalCodeOwners(Project.NameKey projectName) {
- ImmutableSet<CodeOwner> globalCodeOwners =
+ private CodeOwnerResolverResult getGlobalCodeOwners(Project.NameKey projectName) {
+ CodeOwnerResolverResult globalCodeOwners =
codeOwnerResolver
.get()
- .resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName))
- .collect(toImmutableSet());
+ .resolve(codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName));
logger.atFine().log("including global code owners = %s", globalCodeOwners);
return globalCodeOwners;
}
@@ -288,14 +327,85 @@
}
/**
- * Returns the given code owners in a random order.
+ * Returns the entries from the given set in a random order.
*
- * @param codeOwners the code owners that should be returned in a random order
- * @return the given code owners in a random order
+ * @param set the set for which the entries should be returned in a random order
+ * @return the entries from the given set in a random order
*/
- private static Stream<CodeOwner> randomizeOrder(ImmutableSet<CodeOwner> codeOwners) {
- List<CodeOwner> randomlyOrderedCodeOwners = new ArrayList<>(codeOwners);
+ private static <T> Stream<T> randomizeOrder(Set<T> set) {
+ List<T> randomlyOrderedCodeOwners = new ArrayList<>(set);
Collections.shuffle(randomlyOrderedCodeOwners);
return randomlyOrderedCodeOwners.stream();
}
+
+ /**
+ * If the limit is not reached yet, add random visible users as code owners to the given code
+ * owner set.
+ *
+ * <p>Must be only used to complete the suggestion list when it is found that the path is owned by
+ * all user.
+ */
+ private void fillUpWithRandomUsers(
+ AbstractPathResource rsrc, Set<CodeOwner> codeOwners, int limit) {
+ if (codeOwners.size() >= limit) {
+ // limit is already reach, we don't need to add further suggestions
+ return;
+ }
+
+ logger.atFine().log("filling up with random users");
+ codeOwners.addAll(
+ filterCodeOwners(
+ rsrc,
+ // ask for 2 times the number of users that we need so that we still have enough
+ // suggestions when some users are removed by the filterCodeOwners call or if the
+ // returned users were already present in codeOwners
+ getRandomVisibleUsers(2 * limit - codeOwners.size())
+ .map(CodeOwner::create)
+ .collect(toImmutableSet())));
+ }
+
+ /**
+ * Returns random visible users, at most as many as specified by the limit.
+ *
+ * <p>It's possible that this method returns less users than the limit although further visible
+ * users exist. This is because we may inspect only a random set of users, instead of all users,
+ * for performance reasons.
+ *
+ * @param limit the max number of users that should be returned
+ * @return random visible users
+ */
+ private Stream<Account.Id> getRandomVisibleUsers(int limit) {
+ try {
+ if (permissionBackend.currentUser().test(GlobalPermission.VIEW_ALL_ACCOUNTS)) {
+ return getRandomUsers(limit);
+ }
+
+ switch (accountVisibility) {
+ case ALL:
+ return getRandomUsers(limit);
+ case SAME_GROUP:
+ case VISIBLE_GROUP:
+ // We cannot afford to inspect all relevant users and test their visibility for
+ // performance reasons, hence we use a random sample of users that is 3 times the limit.
+ return getRandomUsers(3 * limit)
+ .filter(accountId -> accountControlFactory.get().canSee(accountId))
+ .limit(limit);
+ case NONE:
+ return Stream.of();
+ }
+
+ throw new IllegalStateException("unknown account visibility setting: " + accountVisibility);
+ } catch (IOException | PermissionBackendException e) {
+ throw new StorageException("failed to get visible users", e);
+ }
+ }
+
+ /**
+ * Returns random users, at most as many as specified by the limit.
+ *
+ * <p>No visibility check is performed.
+ */
+ private Stream<Account.Id> getRandomUsers(int limit) throws IOException {
+ return randomizeOrder(accounts.allIds()).limit(limit);
+ }
}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
index baf260d..6e3d054 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +27,8 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.change.IncludedInResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -65,6 +68,9 @@
@Inject
GetCodeOwnersForPathInBranch(
+ AccountVisibility accountVisibility,
+ Accounts accounts,
+ AccountControl.Factory accountControlFactory,
PermissionBackend permissionBackend,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
@@ -73,6 +79,9 @@
CodeOwnerJson.Factory codeOwnerJsonFactory,
GitRepositoryManager repoManager) {
super(
+ accountVisibility,
+ accounts,
+ accountControlFactory,
permissionBackend,
codeOwnersPluginConfiguration,
codeOwnerConfigHierarchy,
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
index 43a5d21..3483614 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -14,6 +14,7 @@
package com.google.gerrit.plugins.codeowners.restapi;
+import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -21,6 +22,8 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -40,6 +43,9 @@
implements RestReadView<CodeOwnersInChangeCollection.PathResource> {
@Inject
GetCodeOwnersForPathInChange(
+ AccountVisibility accountVisibility,
+ Accounts accounts,
+ AccountControl.Factory accountControlFactory,
PermissionBackend permissionBackend,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
@@ -47,6 +53,9 @@
ServiceUserClassifier serviceUserClassifier,
CodeOwnerJson.Factory codeOwnerJsonFactory) {
super(
+ accountVisibility,
+ accounts,
+ accountControlFactory,
permissionBackend,
codeOwnersPluginConfiguration,
codeOwnerConfigHierarchy,
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 4e883c5..f0023ff 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -674,7 +674,7 @@
}
// Check if the code owner reference is resolvable.
- if (codeOwnerResolver.resolve(codeOwnerReference).findAny().isPresent()) {
+ if (codeOwnerResolver.isResolvable(codeOwnerReference)) {
// The code owner reference was successfully resolved to at least one code owner.
return Optional.empty();
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
index 360e2a7..4048684 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -15,10 +15,12 @@
package com.google.gerrit.plugins.codeowners.acceptance.api;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.assertThatList;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountName;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
@@ -28,6 +30,7 @@
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.client.ListAccountsOption;
@@ -721,4 +724,178 @@
.update();
assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
}
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "ALL")
+ public void getAllUsersAsCodeOwners_allVisible() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ // Add a code owner config that makes all users code owners.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id(), admin.id());
+
+ // Query code owners with a limit.
+ codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).hasSize(2);
+ assertThatList(codeOwnerInfos)
+ .element(0)
+ .hasAccountIdThat()
+ .isAnyOf(user.id(), user2.id(), admin.id());
+ assertThatList(codeOwnerInfos)
+ .element(1)
+ .hasAccountIdThat()
+ .isAnyOf(user.id(), user2.id(), admin.id());
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void getAllUsersAsCodeOwners_sameGroupVisibility() throws Exception {
+ // Create 2 accounts that share a group.
+ TestAccount user2 = accountCreator.user2();
+ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
+ groupOperations.newGroup().addMember(user2.id()).addMember(user3.id()).create();
+
+ // Add a code owner config that makes all users code owners.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // user can only see itself
+ requestScopeOperations.setApiUser(user.id());
+ List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(user.id());
+
+ // user2 can see user3 and itself
+ requestScopeOperations.setApiUser(user2.id());
+ codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user2.id(), user3.id());
+
+ // admin can see all users
+ requestScopeOperations.setApiUser(admin.id());
+ codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id(), user3.id());
+
+ // Query code owners with a limit, user2 can see user3 and itself
+ requestScopeOperations.setApiUser(user2.id());
+ codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).hasSize(1);
+ assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user2.id(), user3.id());
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "VISIBLE_GROUP")
+ public void getAllUsersAsCodeOwners_visibleGroupVisibility() throws Exception {
+ // create a group that until contains user
+ AccountGroup.UUID userGroup = groupOperations.newGroup().addMember(user.id()).create();
+
+ // create user2 account and a group that only contains user2, but which is visible to user
+ // (since user owns the group)
+ TestAccount user2 = accountCreator.user2();
+ groupOperations.newGroup().addMember(user2.id()).ownerGroupUuid(userGroup).create();
+
+ // create user3 account and a group that only contains user3, but which is visible to all users
+ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
+ groupOperations.newGroup().addMember(user3.id()).visibleToAll(true).create();
+
+ // Add a code owner config that makes all users code owners.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // user can only see itself, user2 (because user is owner of a group that contains user2) and
+ // user3 (because user3 is member of a group that is visible to all users)
+ requestScopeOperations.setApiUser(user.id());
+ List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user.id(), user2.id(), user3.id());
+
+ // user2 can see user3 and itself
+ requestScopeOperations.setApiUser(user2.id());
+ codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(user2.id(), user3.id());
+
+ // admin can see all users
+ requestScopeOperations.setApiUser(admin.id());
+ codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id(), user3.id());
+
+ // Query code owners with a limit, user2 can see user3 and itself
+ requestScopeOperations.setApiUser(user2.id());
+ codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).hasSize(1);
+ assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user2.id(), user3.id());
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "NONE")
+ public void getAllUsersAsCodeOwners_noneVisible() throws Exception {
+ accountCreator.user2();
+
+ // Add a code owner config that makes all users code owners.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // Use user, since admin is allowed to view all accounts.
+ requestScopeOperations.setApiUser(user.id());
+ List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "NONE")
+ public void getAllUsersAsCodeOwners_withViewAllAccounts() throws Exception {
+ // Allow all users to view all accounts.
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allowCapability(GlobalCapability.VIEW_ALL_ACCOUNTS).group(REGISTERED_USERS))
+ .update();
+
+ TestAccount user2 = accountCreator.user2();
+
+ // Add a code owner config that makes all users code owners.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
+ assertThat(codeOwnerInfos)
+ .comparingElementsUsing(hasAccountId())
+ .containsExactly(admin.id(), user.id(), user2.id());
+ }
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/BUILD b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/BUILD
index 861a20f..4821121 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/BUILD
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/BUILD
@@ -25,8 +25,10 @@
srcs = glob(["Abstract*.java"]),
deps = [
"//java/com/google/gerrit/acceptance:framework-lib",
+ "//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server",
"//lib:guava",
"//lib/guice",
"//plugins/code-owners:code-owners__plugin",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFileTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFileTest.java
index 95fef12..a05799d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFileTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFileTest.java
@@ -316,8 +316,14 @@
private void setupPatchListEntry(
@Nullable String newPath, @Nullable String oldPath, Patch.ChangeType changeType) {
- when(patchListEntry.getNewName()).thenReturn(newPath);
- when(patchListEntry.getOldName()).thenReturn(oldPath);
- when(patchListEntry.getChangeType()).thenReturn(changeType);
+ if (Patch.ChangeType.DELETED == changeType) {
+ // for deletions PatchListEntry sets the oldPath as new name
+ when(patchListEntry.getNewName()).thenReturn(oldPath);
+ when(patchListEntry.getChangeType()).thenReturn(changeType);
+ } else {
+ when(patchListEntry.getNewName()).thenReturn(newPath);
+ when(patchListEntry.getOldName()).thenReturn(oldPath);
+ when(patchListEntry.getChangeType()).thenReturn(changeType);
+ }
}
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
index 8694f92..19d5fdd 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/ChangedFilesTest.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
@@ -243,6 +244,74 @@
}
}
+ @Test
+ @GerritConfig(name = "plugin.code-owners.mergeCommitStrategy", value = "ALL_CHANGED_FILES")
+ public void computeForMergeChangeThatContainsADeletedFileAsConflictResolution_allChangedFiles()
+ throws Exception {
+ testComputeForMergeChangeThatContainsADeletedFileAsConflictResolution();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "plugin.code-owners.mergeCommitStrategy",
+ value = "FILES_WITH_CONFLICT_RESOLUTION")
+ public void
+ computeForMergeChangeThatContainsADeletedFileAsConflictResolution_filesWithConflictResolution()
+ throws Exception {
+ testComputeForMergeChangeThatContainsADeletedFileAsConflictResolution();
+ }
+
+ private void testComputeForMergeChangeThatContainsADeletedFileAsConflictResolution()
+ throws Exception {
+ String file = "foo/a.txt";
+
+ // Create a base change.
+ Change.Id baseChange =
+ changeOperations.newChange().branch("master").file(file).content("base content").create();
+ approveAndSubmit(baseChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches file1.
+ Change.Id changeInMaster =
+ changeOperations.newChange().branch("master").file(file).content("master content").create();
+ approveAndSubmit(changeInMaster);
+
+ // Create a change in the other branch and that deleted file1.
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "Change Deleting A File", file, "");
+ Result r = push.rm("refs/for/master");
+ r.assertOkStatus();
+ approveAndSubmit(r.getChange().getId());
+
+ // Create a merge change with resolving the conflict on file between the edit in master and the
+ // deletion in the other branch by deleting the file.
+ Change.Id mergeChange =
+ changeOperations
+ .newChange()
+ .branch("master")
+ .mergeOf()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file)
+ .delete()
+ .create();
+
+ ImmutableSet<ChangedFile> changedFilesSet =
+ changedFiles.compute(getRevisionResource(Integer.toString(mergeChange.get())));
+ ImmutableSet<String> oldPaths =
+ changedFilesSet.stream()
+ .map(changedFile -> JgitPath.of(changedFile.oldPath().get()).get())
+ .collect(toImmutableSet());
+ assertThat(oldPaths).containsExactly(file);
+ }
+
private void approveAndSubmit(Change.Id changeId) throws Exception {
approve(Integer.toString(changeId.get()));
gApi.changes().id(changeId.get()).current().submit();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index 69e5f97..1249301 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -22,13 +22,17 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
@@ -892,6 +896,136 @@
}
@Test
+ public void approvedByAnyoneWhenEveryoneIsCodeOwner() throws Exception {
+ // Create a code owner config file that makes everyone a code owner.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // Create a change.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(user, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Verify that the file is not approved yet (the change owner is a code owner, but
+ // implicit approvals are disabled).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add an approval by a user that is a code owner only through the global code ownership.
+ approve(changeId);
+
+ // Check that the file is approved now.
+ requestScopeOperations.setApiUser(admin.id());
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.APPROVED);
+ }
+
+ @Test
+ public void everyoneIsCodeOwner_noImplicitApproval() throws Exception {
+ testImplicitlyApprovedWhenEveryoneIsCodeOwner(false);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void everyoneIsCodeOwner_withImplicitApproval() throws Exception {
+ testImplicitlyApprovedWhenEveryoneIsCodeOwner(true);
+ }
+
+ private void testImplicitlyApprovedWhenEveryoneIsCodeOwner(boolean implicitApprovalsEnabled)
+ throws Exception {
+ // Create a code owner config file that makes everyone a code owner.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // Create a change as a user that is a code owner.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(
+ implicitApprovalsEnabled
+ ? CodeOwnerStatus.APPROVED
+ : CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ public void anyReviewerWhenEveryoneIsCodeOwner() throws Exception {
+ // Create a code owner config file that makes everyone a code owner.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .addCodeOwnerEmail("*")
+ .create();
+
+ // Create a change as a user that is a code owner.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the status of the file is INSUFFICIENT_REVIEWERS (since there is no implicit
+ // approval by default).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a user as reviewer that is a code owner.
+ gApi.changes().id(changeId).addReviewer(user.email());
+
+ // Check that the status of the file is PENDING now.
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.PENDING);
+ }
+
+ @Test
@GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
public void approvedByGlobalCodeOwner() throws Exception {
testApprovedByGlobalCodeOwner(false);
@@ -1114,6 +1248,198 @@
}
@Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void approvedByAnyoneWhenEveryoneIsGlobalCodeOwner() throws Exception {
+ testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(false);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void approvedByAnyoneWhenEveryoneIsGlobalCodeOwner_bootstrappingMode() throws Exception {
+ testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(true);
+ }
+
+ private void testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(boolean bootstrappingMode)
+ throws Exception {
+ if (!bootstrappingMode) {
+ // Create a code owner config file so that we are not in the bootstrapping mode.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(user.email())
+ .create();
+ }
+
+ // Create a change.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange(user, "Change Adding A File", JgitPath.of(path).get(), "file content")
+ .getChangeId();
+
+ // Verify that the file is not approved yet (the change owner is a global code owner, but
+ // implicit approvals are disabled).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add an approval by a user that is a code owner only through the global code ownership.
+ approve(changeId);
+
+ // Check that the file is approved now.
+ requestScopeOperations.setApiUser(admin.id());
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.APPROVED);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void everyoneIsGlobalCodeOwner_noImplicitApproval() throws Exception {
+ testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
+ /** implicitApprovalsEnabled = */
+ false,
+ /** bootstrappingMode = */
+ false);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void everyoneIsGlobalCodeOwner_withImplicitApproval() throws Exception {
+ testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
+ /** implicitApprovalsEnabled = */
+ true,
+ /** bootstrappingMode = */
+ false);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void everyoneIsGlobalCodeOwner_noImplicitApproval_bootstrappingMode() throws Exception {
+ testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
+ /** implicitApprovalsEnabled = */
+ false,
+ /** bootstrappingMode = */
+ true);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
+ public void everyoneIsGlobalCodeOwner_withImplicitApproval_bootstrappingMode() throws Exception {
+ testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
+ /** implicitApprovalsEnabled = */
+ true,
+ /** bootstrappingMode = */
+ true);
+ }
+
+ private void testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
+ boolean implicitApprovalsEnabled, boolean bootstrappingMode) throws Exception {
+ if (!bootstrappingMode) {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(user.email())
+ .create();
+ }
+
+ // Create a change as a user that is a code owner only through the global code ownership.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(
+ implicitApprovalsEnabled
+ ? CodeOwnerStatus.APPROVED
+ : CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void anyReviewerWhenEveryoneIsGlobalCodeOwner() throws Exception {
+ testAnyReviewerWhenEveryoneIsGlobalCodeOwner(false);
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
+ public void anyReviewerWhenEveryoneIsGlobalCodeOwner_bootstrappingMode() throws Exception {
+ testAnyReviewerWhenEveryoneIsGlobalCodeOwner(true);
+ }
+
+ private void testAnyReviewerWhenEveryoneIsGlobalCodeOwner(boolean bootstrappingMode)
+ throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ if (!bootstrappingMode) {
+ // Create a code owner config file so that we are not in the bootstrapping mode.
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(user2.email())
+ .create();
+ }
+
+ // Create a change as a user that is a code owner only through the global code ownership.
+ Path path = Paths.get("/foo/bar.baz");
+ String changeId =
+ createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+
+ // Verify that the status of the file is INSUFFICIENT_REVIEWERS (since there is no implicit
+ // approval by default).
+ Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+ codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+ assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+
+ // Add a user as reviewer that is a code owner only through the global code ownership.
+ gApi.changes().id(changeId).addReviewer(user.email());
+
+ // Check that the status of the file is PENDING now.
+ fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId));
+ fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement();
+ fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+ fileCodeOwnerStatusSubject
+ .hasNewPathStatus()
+ .value()
+ .hasStatusThat()
+ .isEqualTo(CodeOwnerStatus.PENDING);
+ }
+
+ @Test
public void parentCodeOwnerConfigsAreConsidered() throws Exception {
TestAccount user2 = accountCreator.user2();
TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
@@ -1526,6 +1852,24 @@
}
}
+ @Test
+ public void getStatus_branchDeleted() throws Exception {
+ String branchName = "tempBranch";
+ createBranch(BranchNameKey.create(project, branchName));
+
+ String changeId = createChange("refs/for/" + branchName).getChangeId();
+
+ DeleteBranchesInput input = new DeleteBranchesInput();
+ input.branches = ImmutableList.of(branchName);
+ gApi.projects().name(project.get()).deleteBranches(input);
+
+ ResourceConflictException exception =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)));
+ assertThat(exception).hasMessageThat().isEqualTo("destination branch not found");
+ }
+
private ChangeNotes getChangeNotes(String changeId) throws Exception {
return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number));
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
index ea0eab7..14bf652 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
@@ -416,6 +416,16 @@
.isTrue();
}
+ @Test
+ public void containsOnlyInvalidCodeOwnerConfigFiles() throws Exception {
+ createInvalidCodeOwnerConfig("/OWNERS");
+
+ assertThat(
+ codeOwnerConfigScanner.containsAnyCodeOwnerConfigFile(
+ BranchNameKey.create(project, "master")))
+ .isTrue();
+ }
+
private void visit() {
codeOwnerConfigScanner.visit(
BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 8737bc6..f390336 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -16,7 +16,6 @@
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.CodeOwnerSubject.hasAccountId;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
@@ -90,53 +89,12 @@
}
@Test
- public void resolveCodeOwnerReferenceForStarAsEmail() throws Exception {
- TestAccount user2 = accountCreator.user2();
+ public void cannotResolveCodeOwnerReferenceForStarAsEmail() throws Exception {
Stream<CodeOwner> codeOwner =
codeOwnerResolver
.get()
.resolve(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
- assertThat(codeOwner)
- .comparingElementsUsing(hasAccountId())
- .containsExactly(admin.id(), user.id(), user2.id());
- }
-
- @Test
- @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
- public void resolveCodeOwnerReferenceForStarAsEmailChecksAccountVisibility() throws Exception {
- // Create a new user that is not a member of any group. This means 'user' and 'admin' are not
- // visible to this user since they do not share any group.
- TestAccount user2 = accountCreator.user2();
-
- // Set user2 as current user.
- requestScopeOperations.setApiUser(user2.id());
-
- Stream<CodeOwner> codeOwner =
- codeOwnerResolver
- .get()
- .resolve(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
- assertThat(codeOwner).comparingElementsUsing(hasAccountId()).containsExactly(user2.id());
- }
-
- @Test
- @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
- public void nonVisibleCodeOwnerCanBeResolvedForStarAsEmailIfVisibilityIsNotEnforced()
- throws Exception {
- // Create a new user that is not a member of any group. This means 'user' and 'admin' are not
- // visible to this user since they do not share any group.
- TestAccount user2 = accountCreator.user2();
-
- // Set user2 as current user.
- requestScopeOperations.setApiUser(user2.id());
-
- Stream<CodeOwner> codeOwner =
- codeOwnerResolver
- .get()
- .enforceVisibility(false)
- .resolve(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
- assertThat(codeOwner)
- .comparingElementsUsing(hasAccountId())
- .containsExactly(admin.id(), user.id(), user2.id());
+ assertThat(codeOwner).isEmpty();
}
@Test
@@ -222,9 +180,10 @@
CodeOwnerConfig codeOwnerConfig =
CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
.build();
- assertThat(
- codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")))
- .isEmpty();
+ CodeOwnerResolverResult result =
+ codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwners()).isEmpty();
+ assertThat(result.ownedByAllUsers()).isFalse();
}
@Test
@@ -233,10 +192,25 @@
CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email(), user.email()))
.build();
- assertThat(
- codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")))
- .comparingElementsUsing(hasAccountId())
- .containsExactly(admin.id(), user.id());
+
+ CodeOwnerResolverResult result =
+ codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id(), user.id());
+ assertThat(result.ownedByAllUsers()).isFalse();
+ }
+
+ @Test
+ public void resolvePathCodeOwnersWhenStarIsUsedAsEmail() throws Exception {
+ CodeOwnerConfig codeOwnerConfig =
+ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
+ .addCodeOwnerSet(
+ CodeOwnerSet.createWithoutPathExpressions(CodeOwnerResolver.ALL_USERS_WILDCARD))
+ .build();
+
+ CodeOwnerResolverResult result =
+ codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwnersAccountIds()).isEmpty();
+ assertThat(result.ownedByAllUsers()).isTrue();
}
@Test
@@ -247,10 +221,10 @@
CodeOwnerSet.createWithoutPathExpressions(
admin.email(), "non-existing@example.com"))
.build();
- assertThat(
- codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")))
- .comparingElementsUsing(hasAccountId())
- .containsExactly(admin.id());
+ CodeOwnerResolverResult result =
+ codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
+ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id());
+ assertThat(result.ownedByAllUsers()).isFalse();
}
@Test
@@ -376,14 +350,27 @@
@Test
public void resolveCodeOwnerReferences() throws Exception {
+ CodeOwnerResolverResult result =
+ codeOwnerResolver
+ .get()
+ .resolve(
+ ImmutableSet.of(
+ CodeOwnerReference.create(admin.email()),
+ CodeOwnerReference.create(user.email())));
+ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id(), user.id());
+ assertThat(result.ownedByAllUsers()).isFalse();
+ }
+
+ @Test
+ public void isResolvable() throws Exception {
+ assertThat(codeOwnerResolver.get().isResolvable(CodeOwnerReference.create(admin.email())))
+ .isTrue();
+ }
+
+ @Test
+ public void isNotResolvable() throws Exception {
assertThat(
- codeOwnerResolver
- .get()
- .resolve(
- ImmutableSet.of(
- CodeOwnerReference.create(admin.email()),
- CodeOwnerReference.create(user.email()))))
- .comparingElementsUsing(hasAccountId())
- .containsExactly(admin.id(), user.id());
+ codeOwnerResolver.get().isResolvable(CodeOwnerReference.create("unknown@example.com")))
+ .isFalse();
}
}
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index abc4bfe..fdcd4da 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -340,6 +340,11 @@
}
```
+If the destination branch of a change no longer exists (e.g. because it was
+deleted), `409 Conflict` is returned. Since the code owners are retrieved from
+the destination branch, computing the code owner status is not possible, if the
+destination branch is missing.
+
## <a id="revision-endpoints"> Revision Endpoints
### <a id="list-code-owners-for-path-in-change"> List Code Owners for path in change