Merge branch 'stable-3.7' * stable-3.7: Enable CheckReturnValue Don't show the Owners Column if the plugin is disabled. Change-Id: I5f84619f459f6d456521a5267a1a1c7cc9e8bf0f
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java index 8ea5a5b..f7a2943 100644 --- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java +++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
@@ -25,7 +25,6 @@ import com.google.gerrit.plugins.codeowners.api.impl.ProjectCodeOwnersFactory; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId; import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig; -import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend; import com.google.gerrit.testing.ConfigSuite; import java.util.Arrays; @@ -45,26 +44,6 @@ */ public class AbstractCodeOwnersIT extends AbstractCodeOwnersTest { /** - * Returns a {@code gerrit.config} without code owner backend configuration to test the default - * setup. - */ - @ConfigSuite.Default - public static Config defaultConfig() { - Config cfg = new Config(); - - // Disable asynchronous posting of change messages during tests to avoid parallel updates to - // NoteDb and hence risking LOCK_FAILURES (especially needed since the test API does not retry - // on LOCK_FAILURES). - cfg.setBoolean( - "plugin", - "code-owners", - GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER, - /* value= */ false); - - return cfg; - } - - /** * Returns a {@code gerrit.config} for every code owner backend so that all code owner backends * are tested. */
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java index 32c82a1..1283932 100644 --- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java +++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -38,12 +38,17 @@ import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation.Builder; 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.config.BackendConfig; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; +import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig; import com.google.gerrit.plugins.codeowners.backend.config.StatusConfig; import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend; import com.google.gerrit.plugins.codeowners.util.JgitPath; +import com.google.gerrit.testing.ConfigSuite; import com.google.inject.Inject; import java.nio.file.Path; import java.util.Map; @@ -69,6 +74,31 @@ name = "code-owners", sysModule = "com.google.gerrit.plugins.codeowners.acceptance.TestModule") public class AbstractCodeOwnersTest extends LightweightPluginDaemonTest { + /** + * Returns a {@code gerrit.config} without code owner backend configuration to test the default + * setup. + */ + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + + // Disable asynchronous posting of change messages during tests to avoid parallel updates to + // NoteDb and hence risking LOCK_FAILURES (especially needed since the test API does not retry + // on LOCK_FAILURES). + cfg.setBoolean( + "plugin", + "code-owners", + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER, + /* value= */ false); + cfg.setBoolean( + "plugin", + "code-owners", + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + /* value= */ false); + + return cfg; + } + @Inject private ProjectOperations projectOperations; private CodeOwnerConfigOperations codeOwnerConfigOperations; @@ -341,4 +371,13 @@ } throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName()); } + + protected CodeOwnerConfigReference createCodeOwnerConfigReference( + CodeOwnerConfigImportMode importMode, CodeOwnerConfig.Key codeOwnerConfigKey) { + return CodeOwnerConfigReference.builder( + importMode, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()) + .setProject(codeOwnerConfigKey.project()) + .setBranch(codeOwnerConfigKey.branchNameKey().branch()) + .build(); + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java new file mode 100644 index 0000000..41fa62b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
@@ -0,0 +1,65 @@ +// Copyright (C) 2023 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 com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import java.util.List; + +/** + * Representation of a code owner config file in the REST API. + * + * <p>This class determines the JSON format of code owner config files in the REST API. + * + * <p>The JSON only contains the location of the code owner config file (project, branch, path) and + * information about the imported code owner config files. It's not a representation of the code + * owner config file content (the content of a code owner config file is represented by {@link + * CodeOwnerConfigInfo}). + */ +public class CodeOwnerConfigFileInfo { + /** + * The name of the project from which the code owner config was loaded, or for unresolved imports, + * from which the code owner config was supposed to be loaded. + */ + public String project; + + /** + * The name of the branch from which the code owner config was loaded, or for unresolved imports, + * from which the code owner config was supposed to be loaded. + */ + public String branch; + + /** The path of the code owner config file. */ + public String path; + + /** Imported code owner config files. */ + public List<CodeOwnerConfigFileInfo> imports; + + /** Imported code owner config files that couldn't be resolved. */ + public List<CodeOwnerConfigFileInfo> unresolvedImports; + + /** + * Message explaining why this code owner config couldn't be resolved. + * + * <p>Only set for unresolved imports. + */ + public String unresolvedErrorMessage; + + /** + * The import mode. + * + * <p>Only set for imports. + */ + public CodeOwnerConfigImportMode importMode; +}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java index d699029..763dff2 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java +++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
@@ -34,6 +34,9 @@ */ public Boolean ownedByAllUsers; + /** The code owner config files that have been inspected to gather the code owners. */ + public List<CodeOwnerConfigFileInfo> codeOwnerConfigs; + /** * Debug logs that may help to understand why a user is or isn't suggested as a code owner. *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java index a0df99d..5dc8c9c 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -15,6 +15,8 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; @@ -29,7 +31,9 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -52,6 +56,7 @@ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final GitRepositoryManager repoManager; + private final ProjectCache projectCache; private final PersonIdent serverIdent; private final MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory; private final RetryHelper retryHelper; @@ -62,6 +67,7 @@ protected AbstractFileBasedCodeOwnerBackend( CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, GitRepositoryManager repoManager, + ProjectCache projectCache, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, RetryHelper retryHelper, @@ -70,6 +76,7 @@ CodeOwnerConfigParser codeOwnerConfigParser) { this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.repoManager = repoManager; + this.projectCache = projectCache; this.serverIdent = serverIdent; this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory; this.retryHelper = retryHelper; @@ -129,7 +136,7 @@ @Override public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { - return codeOwnerConfigKey.filePath(defaultFileName); + return codeOwnerConfigKey.filePath(getFileName(codeOwnerConfigKey.project())); } @Override @@ -188,9 +195,13 @@ private String getFileName(Project.NameKey project) { return defaultFileName - + codeOwnersPluginConfiguration - .getProjectConfig(project) - .getFileExtension() + + projectCache + .get(project) + .flatMap( + projectState -> + codeOwnersPluginConfiguration + .getProjectConfig(projectState.getNameKey()) + .getFileExtension()) .map(ext -> "." + ext) .orElse(""); } @@ -251,8 +262,10 @@ codeOwnerConfigKey) .setCodeOwnerConfigUpdate(codeOwnerConfigUpdate); - try (MetaDataUpdate metaDataUpdate = - createMetaDataUpdate(codeOwnerConfigKey.project(), repository, currentUser)) { + try (RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext ctx = RefUpdateContext.open(VERSIONED_META_DATA_CHANGE); + MetaDataUpdate metaDataUpdate = + createMetaDataUpdate(codeOwnerConfigKey.project(), repository, currentUser)) { codeOwnerConfigFile.commit(metaDataUpdate); }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java index cba9409..55421e9 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -16,6 +16,7 @@ import com.google.gerrit.extensions.annotations.Exports; import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.extensions.events.CommentAddedListener; import com.google.gerrit.extensions.events.ReviewerAddedListener; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicSet; @@ -61,6 +62,7 @@ DynamicSet.bind(binder(), ExceptionHook.class).to(CodeOwnersExceptionHook.class); DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerApproval.class); + DynamicSet.bind(binder(), CommentAddedListener.class).to(OnCodeOwnerApproval.class); DynamicSet.bind(binder(), OnPostReview.class).to(OnCodeOwnerOverride.class); DynamicSet.bind(binder(), ReviewerAddedListener.class).to(CodeOwnersOnAddReviewer.class); }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java index 90fd878..0669b68 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; @@ -25,6 +26,7 @@ import com.google.gerrit.plugins.codeowners.util.JgitPath; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.git.meta.VersionedMetaData; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.inject.Inject; import java.io.IOException; import java.util.Optional; @@ -260,8 +262,11 @@ update.getRepository().exactRef(getRefName()) != null, "branch %s does not exist", getRefName()); - - return super.commit(update); + // The commit goes to an ordinary branch (e.g. refs/heads/main). PLUGIN context is enough for + // such cases. + try(RefUpdateContext ctx = RefUpdateContext.open(PLUGIN)) { + return super.commit(update); + } } @Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java index d0bd003..0fa31e1 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; @@ -24,7 +25,9 @@ import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -57,6 +60,7 @@ private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final GitRepositoryManager repoManager; + private final GitReferenceUpdated gitRefUpdated; private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final Provider<PersonIdent> serverIdentProvider; private final Provider<IdentifiedUser> identifiedUser; @@ -64,10 +68,12 @@ @Inject CodeOwnerConfigFileUpdateScanner( GitRepositoryManager repoManager, + GitReferenceUpdated gitRefUpdated, CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, @GerritPersonIdent Provider<PersonIdent> serverIdentProvider, Provider<IdentifiedUser> identifiedUser) { this.repoManager = repoManager; + this.gitRefUpdated = gitRefUpdated; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.serverIdentProvider = serverIdentProvider; this.identifiedUser = identifiedUser; @@ -101,7 +107,8 @@ "updating code owner files in branch %s of project %s", branchNameKey.branch(), branchNameKey.project()); - try (Repository repository = repoManager.openRepository(branchNameKey.project()); + try (RefUpdateContext ctx = RefUpdateContext.open(PLUGIN); + Repository repository = repoManager.openRepository(branchNameKey.project()); RevWalk rw = new RevWalk(repository); ObjectInserter oi = repository.newObjectInserter(); CodeOwnerConfigTreeWalk treeWalk = @@ -113,7 +120,7 @@ /** pathGlob */ null)) { RevCommit revision = treeWalk.getRevision(); - DirCache newTree = DirCache.newInCore(); + DirCache newTree = DirCache.read(rw.getObjectReader(), revision.getTree()); DirCacheEditor editor = newTree.editor(); boolean dirty = false; @@ -138,7 +145,7 @@ editor.finish(); ObjectId treeId = newTree.writeTree(oi); ObjectId commitId = createCommit(oi, commitMessage, revision, treeId); - updateBranch(branchNameKey.branch(), repository, revision, commitId); + updateBranch(repository, branchNameKey, revision, commitId); return Optional.of(rw.parseCommit(commitId)); } catch (IOException e) { throw newInternalServerError( @@ -194,18 +201,22 @@ /** * Update the given branch. * - * @param branchName the name of the branch that should be updated * @param repository the repository in which the branch should be updated + * @param branchNameKey the project and branch that should be updated * @param oldObjectId the expected old object ID of the branch * @param newObjectId the new object ID that should be set for the branch */ private void updateBranch( - String branchName, Repository repository, ObjectId oldObjectId, ObjectId newObjectId) + Repository repository, + BranchNameKey branchNameKey, + ObjectId oldObjectId, + ObjectId newObjectId) throws IOException { - RefUpdate ru = repository.updateRef(branchName); + RefUpdate ru = repository.updateRef(branchNameKey.branch()); ru.setExpectedOldObjectId(oldObjectId); ru.setNewObjectId(newObjectId); ru.update(); RefUpdateUtil.checkResult(ru); + gitRefUpdated.fire(branchNameKey.project(), ru, identifiedUser.get().state()); } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java new file mode 100644 index 0000000..a75c15b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
@@ -0,0 +1,86 @@ +// 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 com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import java.util.Optional; + +/** + * Information about an import of a {@link CodeOwnerConfig}. + * + * <p>Contains the keys of the importing and the imported code owner config, as well as the + * reference that the importing code owner config uses to reference the imported code owner config + * (contains the import mode). + * + * <p>It's possible that this class represents non-resolveable imports (e.g. an import of a + * non-existing code owner config). In this case an error message is contained that explains why the + * import couldn't be resolved. + */ +@AutoValue +public abstract class CodeOwnerConfigImport { + /** Key of the importing code owner config. */ + public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig(); + + /** Key of the imported code owner config. */ + public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig(); + + /** The code owner config reference that references the imported code owner config. */ + public abstract CodeOwnerConfigReference codeOwnerConfigReference(); + + /** + * If the import couldn't be resolved, a message explaining why the code owner config reference + * couldn't be resolved. + */ + public abstract Optional<String> errorMessage(); + + @Override + public final String toString() { + return MoreObjects.toStringHelper(this) + .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig()) + .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig()) + .add("codeOwnerConfigReference", codeOwnerConfigReference()) + .add("errorMessage", errorMessage()) + .toString(); + } + + /** Creates a {@link CodeOwnerConfigImport} instance for an unresolved import. */ + @VisibleForTesting + public static CodeOwnerConfigImport createUnresolvedImport( + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, + CodeOwnerConfigReference codeOwnerConfigReference, + String errorMessage) { + return new AutoValue_CodeOwnerConfigImport( + keyOfImportingCodeOwnerConfig, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + Optional.of(errorMessage)); + } + + /** Creates a {@link CodeOwnerConfigImport} instance for a resolved import. */ + @VisibleForTesting + public static CodeOwnerConfigImport createResolvedImport( + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, + CodeOwnerConfigReference codeOwnerConfigReference) { + return new AutoValue_CodeOwnerConfigImport( + keyOfImportingCodeOwnerConfig, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + Optional.empty()); + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java index 07894ce..7000959 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -39,7 +39,7 @@ import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.externalids.ExternalId; -import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdCache; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -50,7 +50,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -106,7 +105,7 @@ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final PermissionBackend permissionBackend; private final Provider<CurrentUser> currentUser; - private final ExternalIds externalIds; + private final ExternalIdCache externalIdCache; private final AccountCache accountCache; private final AccountControl.Factory accountControlFactory; private final PathCodeOwners.Factory pathCodeOwnersFactory; @@ -127,7 +126,7 @@ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, PermissionBackend permissionBackend, Provider<CurrentUser> currentUser, - ExternalIds externalIds, + ExternalIdCache externalIdCache, AccountCache accountCache, AccountControl.Factory accountControlFactory, PathCodeOwners.Factory pathCodeOwnersFactory, @@ -137,7 +136,7 @@ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.permissionBackend = permissionBackend; this.currentUser = currentUser; - this.externalIds = externalIds; + this.externalIdCache = externalIdCache; this.accountCache = accountCache; this.accountControlFactory = accountControlFactory; this.pathCodeOwnersFactory = pathCodeOwnersFactory; @@ -234,6 +233,7 @@ return resolve( pathCodeOwnersResult.get().getPathCodeOwners(), pathCodeOwnersResult.get().getAnnotations(), + pathCodeOwnersResult.get().resolvedImports(), pathCodeOwnersResult.get().unresolvedImports(), pathCodeOwnersResult.messages()); } @@ -260,6 +260,7 @@ return resolve( codeOwnerReferences, /* annotationsByCodeOwnerReference= */ ImmutableMultimap.of(), + /* resolvedImports= */ ImmutableList.of(), /* unresolvedImports= */ ImmutableList.of(), /* pathCodeOwnersMessages= */ ImmutableList.of()); } @@ -279,9 +280,11 @@ private CodeOwnerResolverResult resolve( Set<CodeOwnerReference> codeOwnerReferences, ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotationsByCodeOwnerReference, - List<UnresolvedImport> unresolvedImports, + ImmutableList<CodeOwnerConfigImport> resolvedImports, + ImmutableList<CodeOwnerConfigImport> unresolvedImports, ImmutableList<String> pathCodeOwnersMessages) { requireNonNull(codeOwnerReferences, "codeOwnerReferences"); + requireNonNull(resolvedImports, "resolvedImports"); requireNonNull(unresolvedImports, "unresolvedImports"); requireNonNull(pathCodeOwnersMessages, "pathCodeOwnersMessages"); @@ -313,7 +316,8 @@ annotationsByCodeOwner.build(), ownedByAllUsers.get(), hasUnresolvedCodeOwners.get(), - !unresolvedImports.isEmpty(), + resolvedImports, + unresolvedImports, messageBuilder.build()); logger.atFine().log("resolve result = %s", codeOwnerResolverResult); return codeOwnerResolverResult; @@ -573,7 +577,7 @@ * Looks up the external IDs for the given emails. * * <p>Looks up all emails from the external ID cache at once, which is more efficient than looking - * up external IDs for emails one by one (see {@link ExternalIds#byEmails(String...)}). + * up external IDs for emails one by one (see {@link ExternalIdCache#byEmail(String)}). * * @param messages builder to which debug messages are added * @param emails the emails for which the external IDs should be looked up @@ -583,7 +587,7 @@ ImmutableList.Builder<String> messages, ImmutableSet<String> emails) { try { ImmutableMap<String, Collection<ExternalId>> extIdsByEmail = - externalIds.byEmails(emails.toArray(new String[0])).asMap(); + externalIdCache.byEmails(emails.toArray(new String[0])).asMap(); emails.stream() .filter(email -> !extIdsByEmail.containsKey(email)) .forEach( @@ -837,12 +841,12 @@ // emails try { if (user != null) { - if (!permissionBackend.user(user).test(GlobalPermission.MODIFY_ACCOUNT)) { + if (!permissionBackend.user(user).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { transientCodeOwnerCache.cacheNonResolvable(email); messages.add( String.format( - "cannot resolve code owner email %s: account %s is referenced by secondary email" - + " but user %s cannot see secondary emails", + "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; } @@ -852,7 +856,7 @@ + " and user %s can see secondary emails", email, accountState.account().id(), user.getLoggableName())); return true; - } else if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) { + } else if (!permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { transientCodeOwnerCache.cacheNonResolvable(email); messages.add( String.format( @@ -871,7 +875,7 @@ } catch (PermissionBackendException ex) { throw newInternalServerError( String.format( - "failed to test the %s global capability", GlobalPermission.MODIFY_ACCOUNT), + "failed to test the %s global capability", GlobalPermission.VIEW_SECONDARY_EMAILS), ex); } };
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java index 7dd9e96..4db3a4c 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -52,8 +52,16 @@ /** Whether there are code owner references which couldn't be resolved. */ public abstract boolean hasUnresolvedCodeOwners(); + /** Imports which were successfully resolved. */ + public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports(); + + /** Imports which couldn't be resolved. */ + public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports(); + /** Whether there are imports which couldn't be resolved. */ - public abstract boolean hasUnresolvedImports(); + public boolean hasUnresolvedImports() { + return !unresolvedImports().isEmpty(); + } /** Gets messages that were collected while resolving the code owners. */ public abstract ImmutableList<String> messages(); @@ -76,7 +84,8 @@ .add("annotations", annotations()) .add("ownedByAllUsers", ownedByAllUsers()) .add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners()) - .add("hasUnresolvedImports", hasUnresolvedImports()) + .add("resolvedImports", resolvedImports()) + .add("unresolvedImports", unresolvedImports()) .add("messages", messages()) .toString(); } @@ -87,25 +96,16 @@ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations, boolean ownedByAllUsers, boolean hasUnresolvedCodeOwners, - boolean hasUnresolvedImports, + ImmutableList<CodeOwnerConfigImport> resolvedImports, + ImmutableList<CodeOwnerConfigImport> unresolvedImports, List<String> messages) { return new AutoValue_CodeOwnerResolverResult( codeOwners, annotations, ownedByAllUsers, hasUnresolvedCodeOwners, - hasUnresolvedImports, + resolvedImports, + unresolvedImports, ImmutableList.copyOf(messages)); } - - /** Creates a empty {@link CodeOwnerResolverResult} instance. */ - public static CodeOwnerResolverResult createEmpty() { - return new AutoValue_CodeOwnerResolverResult( - /* codeOwners= */ ImmutableSet.of(), - /* annotations= */ ImmutableMultimap.of(), - /* ownedByAllUsers= */ false, - /* hasUnresolvedCodeOwners= */ false, - /* hasUnresolvedImports= */ false, - /* messages= */ ImmutableList.of()); - } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersChangeMessageUtil.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersChangeMessageUtil.java new file mode 100644 index 0000000..ee0e9ec --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersChangeMessageUtil.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import com.google.gerrit.entities.ChangeMessage; +import com.google.gerrit.plugins.codeowners.util.JgitPath; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** Utility functions to write code owners {@link ChangeMessage}s. */ +public class CodeOwnersChangeMessageUtil { + /** + * Appends the given paths to the given message builder. + * + * @param message message builder to which the paths should be appended + * @param pathsToAppend the paths to append to the message builder + */ + public static void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) { + pathsToAppend.forEach( + path -> message.append( + String.format("* `%s`\n", JgitPath.of(path).get().replace("`", "\\`")))); + } + + /** + * Private constructor to prevent instantiation of this class. + * + * <p>The class only contains static methods, hence the class never needs to be instantiated. + */ + private CodeOwnersChangeMessageUtil() {} +}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java index 5052edb..3ce8e01 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -14,6 +14,9 @@ package com.google.gerrit.plugins.codeowners.backend; +import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersChangeMessageUtil.appendPaths; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.util.stream.Collectors.joining; import com.google.common.collect.ImmutableList; @@ -27,7 +30,6 @@ import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; -import com.google.gerrit.plugins.codeowners.util.JgitPath; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.WorkQueue; @@ -36,6 +38,7 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.OneOffRequestContext; @@ -46,7 +49,6 @@ import java.time.Instant; import java.util.List; import java.util.Optional; -import java.util.stream.Stream; /** * Callback that is invoked when a user is added as a reviewer. @@ -157,7 +159,9 @@ "addCodeOwnersMessageOnAddReviewer", updateFactory -> { try (BatchUpdate batchUpdate = - updateFactory.create(projectName, currentUser, when)) { + updateFactory.create(projectName, currentUser, when); + RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) { batchUpdate.addOp(changeId, new Op(reviewers, maxPathsInChangeMessages)); batchUpdate.execute(); } @@ -244,10 +248,5 @@ return Optional.of(message.toString()); } - - private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) { - pathsToAppend.forEach( - path -> message.append(String.format("* %s\n", JgitPath.of(path).get()))); - } } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java index ee93f09..ab700ec 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -15,27 +15,46 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersChangeMessageUtil.appendPaths; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.common.ApprovalInfo; +import com.google.gerrit.extensions.events.CommentAddedListener; import com.google.gerrit.metrics.Timer0; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval; import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; -import com.google.gerrit.plugins.codeowners.util.JgitPath; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.restapi.change.OnPostReview; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.server.util.LabelVote; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import java.nio.file.Path; +import java.time.Instant; import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; /** * Callback that is invoked on post review and that extends the change message if a code owner @@ -51,25 +70,47 @@ * </ul> */ @Singleton -class OnCodeOwnerApproval implements OnPostReview { +class OnCodeOwnerApproval implements OnPostReview, CommentAddedListener { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final String TAG_ADD_CODE_OWNER_APPROVAL = + ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addCodeOwnerApproval"; + + private final WorkQueue workQueue; + private final OneOffRequestContext oneOffRequestContext; private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final CodeOwnerApprovalCheck codeOwnerApprovalCheck; private final CodeOwnerMetrics codeOwnerMetrics; + private final ChangeNotes.Factory changeNotesFactory; + private final ChangeMessagesUtil changeMessageUtil; + private final Provider<CurrentUser> userProvider; + private final RetryHelper retryHelper; @Inject OnCodeOwnerApproval( + WorkQueue workQueue, + OneOffRequestContext oneOffRequestContext, CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, CodeOwnerApprovalCheck codeOwnerApprovalCheck, - CodeOwnerMetrics codeOwnerMetrics) { + CodeOwnerMetrics codeOwnerMetrics, + ChangeNotes.Factory changeNotesFactory, + ChangeMessagesUtil changeMessageUtil, + Provider<CurrentUser> userProvider, + RetryHelper retryHelper) { + this.workQueue = workQueue; + this.oneOffRequestContext = oneOffRequestContext; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.codeOwnerApprovalCheck = codeOwnerApprovalCheck; this.codeOwnerMetrics = codeOwnerMetrics; + this.changeNotesFactory = changeNotesFactory; + this.changeMessageUtil = changeMessageUtil; + this.userProvider = userProvider; + this.retryHelper = retryHelper; } @Override public Optional<String> getChangeMessageAddOn( + Instant when, IdentifiedUser user, ChangeNotes changeNotes, PatchSet patchSet, @@ -80,11 +121,20 @@ int maxPathsInChangeMessage = codeOwnersConfig.getMaxPathsInChangeMessages(); if (codeOwnersConfig.isDisabled(changeNotes.getChange().getDest().branch()) || maxPathsInChangeMessage <= 0) { + logger.atFine().log("skip extending the change message since message posting is disabled"); + return Optional.empty(); + } + if (codeOwnersConfig.enableAsyncMessageOnCodeOwnerApproval()) { + // To avoid adding latency to PostReview post the change message asynchronously from + // #onCommentAdded(Event) after PostReview is done. + logger.atFine().log( + "skip extending the change message since async message posting is enabled"); return Optional.empty(); } // code owner approvals are only computed for the current patch set if (!changeNotes.getChange().currentPatchSetId().equals(patchSet.id())) { + logger.atFine().log("skip extending the change message on non-current patch set"); return Optional.empty(); } @@ -97,6 +147,7 @@ return Optional.empty(); } + logger.atFine().log("post change message synchronously"); try (Timer0.Context ctx = codeOwnerMetrics.extendChangeMessageOnPostReview.start()) { return buildMessageForCodeOwnerApproval( user, @@ -126,6 +177,127 @@ } } + @Override + public void onCommentAdded(Event event) { + Project.NameKey projectName = Project.nameKey(event.getChange().project); + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig = + codeOwnersPluginConfiguration.getProjectConfig(projectName); + int maxPathsInChangeMessage = codeOwnersConfig.getMaxPathsInChangeMessages(); + if (codeOwnersConfig.isDisabled(event.getChange().branch) || maxPathsInChangeMessage <= 0) { + logger.atFine().log("skip posting the change message since message posting is disabled"); + return; + } + if (!codeOwnersConfig.enableAsyncMessageOnCodeOwnerApproval()) { + // The change message has already been synchronously extended by #getChangeMessageAddOn(...). + logger.atFine().log( + "skip posting the change message since async message posting is disabled"); + return; + } + + // post change message asynchronously to avoid adding latency to PostReview + logger.atFine().log("schedule asynchronous posting of the change message"); + + CurrentUser user = userProvider.get(); + if (!user.isIdentifiedUser()) { + // cannot compute owned paths for non-identified user + logger.atFine().log( + "skip posting the change message for non-identified user %s", user.getLoggableName()); + return; + } + + Change.Id changeId = Change.id(event.getChange()._number); + ChangeNotes changeNotes = changeNotesFactory.create(projectName, changeId); + PatchSet.Id patchSetId = PatchSet.id(changeId, event.getRevision()._number); + RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); + + // code owner approvals are only computed for the current patch set + PatchSet currentPatchSet = changeNotes.getCurrentPatchSet(); + if (!currentPatchSet.id().equals(patchSetId)) { + logger.atFine().log("skip posting the change message on non-current patch set"); + return; + } + + if (event.getOldApprovals().get(requiredApproval.labelType().getName()) == null) { + // If oldApprovals doesn't contain the label or if the labels value in it is null, the label + // was not changed. + // This means that the user only voted on unrelated labels. + return; + } + + @SuppressWarnings("unused") + WorkQueue.Task<?> possiblyIgnoredError = + (WorkQueue.Task<?>) + workQueue + .getDefaultQueue() + .submit( + () -> { + try (ManualRequestContext ignored = + oneOffRequestContext.openAs(user.getAccountId())) { + postChangeMessage( + event.getWhen(), + user.asIdentifiedUser(), + changeNotes, + currentPatchSet, + mapApprovalInfosToVotingValues(event.getOldApprovals()), + mapApprovalInfosToVotingValues(event.getApprovals()), + requiredApproval, + maxPathsInChangeMessage); + } + }); + } + + private void postChangeMessage( + Instant when, + IdentifiedUser user, + ChangeNotes changeNotes, + PatchSet patchSet, + Map<String, Short> oldApprovals, + Map<String, Short> approvals, + RequiredApproval requiredApproval, + int maxPathsInChangeMessages) { + try (Timer0.Context ctx = codeOwnerMetrics.addChangeMessageOnCodeOwnerApproval.start()) { + retryHelper + .changeUpdate( + "addCodeOwnersMessageOnCodeOwnerApproval", + updateFactory -> { + try (BatchUpdate batchUpdate = + updateFactory.create(changeNotes.getProjectName(), user, when); + RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) { + batchUpdate.addOp( + changeNotes.getChangeId(), + new Op( + user, + changeNotes, + patchSet, + oldApprovals, + approvals, + requiredApproval, + maxPathsInChangeMessages)); + batchUpdate.execute(); + } + return null; + }) + .call(); + } catch (Exception e) { + Optional<? extends Exception> configurationError = + CodeOwnersExceptionHook.getCauseOfConfigurationError(e); + if (configurationError.isPresent()) { + logger.atWarning().log( + "Failed to post code-owners change message for code owner approval on change %s in" + + " project %s: %s", + changeNotes.getChangeId(), + changeNotes.getProjectName(), + configurationError.get().getMessage()); + } else { + logger.atSevere().withCause(e).log( + "Failed to post code-owners change message for code owner approval on change %s in" + + " project %s.", + changeNotes.getChangeId(), changeNotes.getProjectName()); + } + } + } + private Optional<String> buildMessageForCodeOwnerApproval( IdentifiedUser user, ChangeNotes changeNotes, @@ -250,10 +422,6 @@ return Optional.of(message.toString()); } - private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) { - pathsToAppend.forEach(path -> message.append(String.format("* %s\n", JgitPath.of(path).get()))); - } - private boolean isIgnoredDueToSelfApproval( IdentifiedUser user, PatchSet patchSet, RequiredApproval requiredApproval) { return patchSet.uploader().equals(user.getAccountId()) @@ -290,4 +458,54 @@ approvals); return LabelVote.create(labelName, approvals.get(labelName)); } + + private static ImmutableMap<String, Short> mapApprovalInfosToVotingValues( + Map<String, ApprovalInfo> approvals) { + return approvals.entrySet().stream() + .collect( + toImmutableMap( + Map.Entry::getKey, + e -> e.getValue().value != null ? e.getValue().value.shortValue() : null)); + } + + private class Op implements BatchUpdateOp { + private final IdentifiedUser user; + private final ChangeNotes changeNotes; + private final PatchSet patchSet; + private final Map<String, Short> oldApprovals; + private final Map<String, Short> approvals; + private final RequiredApproval requiredApproval; + private final int limit; + + Op( + IdentifiedUser user, + ChangeNotes changeNotes, + PatchSet patchSet, + Map<String, Short> oldApprovals, + Map<String, Short> approvals, + RequiredApproval requiredApproval, + int limit) { + this.user = user; + this.changeNotes = changeNotes; + this.patchSet = patchSet; + this.oldApprovals = oldApprovals; + this.approvals = approvals; + this.requiredApproval = requiredApproval; + this.limit = limit; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws Exception { + Optional<String> message = + buildMessageForCodeOwnerApproval( + user, changeNotes, patchSet, oldApprovals, approvals, requiredApproval, limit); + + if (message.isEmpty()) { + return false; + } + + changeMessageUtil.setChangeMessage(ctx, message.get(), TAG_ADD_CODE_OWNER_APPROVAL); + return true; + } + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java index ea9fa59..270b1ed 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -33,6 +33,7 @@ import com.google.gerrit.server.util.LabelVote; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -60,6 +61,7 @@ @Override public Optional<String> getChangeMessageAddOn( + Instant when, IdentifiedUser user, ChangeNotes changeNotes, PatchSet patchSet,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java index a61c913..5e7a11f 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -252,17 +252,18 @@ } // Resolve global imports. - ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder(); - ImmutableSet<CodeOwnerConfigImport> globalImports = getGlobalImports(0, codeOwnerConfig); - OptionalResultWithMessages<List<UnresolvedImport>> unresolvedGlobalImports; + ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder(); + ImmutableSet<CodeOwnerImport> globalImports = getGlobalImports(0, codeOwnerConfig); + OptionalResultWithMessages<CodeOwnerConfigImports> globalImportedCodeOwnerConfigs; if (!globalCodeOwnersIgnored) { - unresolvedGlobalImports = + globalImportedCodeOwnerConfigs = resolveImports(codeOwnerConfig.key(), globalImports, resolvedCodeOwnerConfigBuilder); } else { // skip global import with mode GLOBAL_CODE_OWNER_SETS_ONLY, // since we already know that global code owners will be ignored, we do not need to resolve // these imports - unresolvedGlobalImports = + globalImportedCodeOwnerConfigs = resolveImports( codeOwnerConfig.key(), globalImports.stream() @@ -273,8 +274,9 @@ .collect(toImmutableSet()), resolvedCodeOwnerConfigBuilder); } - messages.addAll(unresolvedGlobalImports.messages()); - unresolvedImports.addAll(unresolvedGlobalImports.get()); + messages.addAll(globalImportedCodeOwnerConfigs.messages()); + resolvedImports.addAll(globalImportedCodeOwnerConfigs.get().resolved()); + unresolvedImports.addAll(globalImportedCodeOwnerConfigs.get().unresolved()); // Remove all global code owner sets if any per-file code owner set has the // ignoreGlobalAndParentCodeOwners flag set to true (as in this case they are ignored and @@ -317,18 +319,22 @@ } // Resolve per-file imports. - ImmutableSet<CodeOwnerConfigImport> perFileImports = + ImmutableSet<CodeOwnerImport> perFileImports = getPerFileImports( 0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets()); - OptionalResultWithMessages<List<UnresolvedImport>> unresolvedPerFileImports = + OptionalResultWithMessages<CodeOwnerConfigImports> perFileImportedCodeOwnerConfigs = resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder); - messages.addAll(unresolvedPerFileImports.messages()); - unresolvedImports.addAll(unresolvedPerFileImports.get()); + messages.addAll(perFileImportedCodeOwnerConfigs.messages()); + resolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().resolved()); + unresolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().unresolved()); this.pathCodeOwnersResult = OptionalResultWithMessages.create( PathCodeOwnersResult.create( - path, resolvedCodeOwnerConfigBuilder.build(), unresolvedImports.build()), + path, + resolvedCodeOwnerConfigBuilder.build(), + resolvedImports.build(), + unresolvedImports.build()), messages); logger.atFine().log("path code owners result = %s", pathCodeOwnersResult); return this.pathCodeOwnersResult; @@ -343,11 +349,12 @@ * @param resolvedCodeOwnerConfigBuilder the builder for the resolved code owner config * @return list of unresolved imports, empty list if all imports were successfully resolved */ - private OptionalResultWithMessages<List<UnresolvedImport>> resolveImports( + private OptionalResultWithMessages<CodeOwnerConfigImports> resolveImports( CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, - Set<CodeOwnerConfigImport> codeOwnerConfigImports, + Set<CodeOwnerImport> codeOwnerConfigImports, CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) { - ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder(); StringBuilder messageBuilder = new StringBuilder(); try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) { logger.atFine().log("resolve imports of codeOwnerConfig %s", keyOfImportingCodeOwnerConfig); @@ -361,7 +368,7 @@ Map<BranchNameKey, ObjectId> revisionMap = new HashMap<>(); revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision()); - Queue<CodeOwnerConfigImport> codeOwnerConfigsToImport = new ArrayDeque<>(); + Queue<CodeOwnerImport> codeOwnerConfigsToImport = new ArrayDeque<>(); codeOwnerConfigsToImport.addAll(codeOwnerConfigImports); if (!codeOwnerConfigsToImport.isEmpty()) { messageBuilder.append( @@ -370,7 +377,7 @@ keyOfImportingCodeOwnerConfig.format(codeOwners))); } while (!codeOwnerConfigsToImport.isEmpty()) { - CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll(); + CodeOwnerImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll(); messageBuilder.append(codeOwnerConfigImport.format()); CodeOwnerConfigReference codeOwnerConfigReference = @@ -387,7 +394,7 @@ projectCache.get(keyOfImportedCodeOwnerConfig.project()); if (!projectState.isPresent()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -399,7 +406,7 @@ } if (!projectState.get().statePermitsRead()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -425,7 +432,7 @@ if (!mayBeImportedCodeOwnerConfig.isPresent()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -439,6 +446,13 @@ } CodeOwnerConfig importedCodeOwnerConfig = mayBeImportedCodeOwnerConfig.get(); + + resolvedImports.add( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigImport.importingCodeOwnerConfig(), + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + CodeOwnerConfigImportMode importMode = codeOwnerConfigReference.importMode(); logger.atFine().log("import mode = %s", importMode.name()); @@ -475,7 +489,7 @@ if (importMode.resolveImportsOfImport() && seenCodeOwnerConfigs.add(keyOfImportedCodeOwnerConfig)) { logger.atFine().log("resolve imports of imported code owner config"); - Set<CodeOwnerConfigImport> transitiveImports = new HashSet<>(); + Set<CodeOwnerImport> transitiveImports = new HashSet<>(); transitiveImports.addAll( getGlobalImports(codeOwnerConfigImport.importLevel() + 1, importedCodeOwnerConfig)); transitiveImports.addAll( @@ -495,7 +509,7 @@ transitiveImports.stream() .map( codeOwnerCfgImport -> - CodeOwnerConfigImport.create( + CodeOwnerImport.create( codeOwnerCfgImport.importLevel(), codeOwnerCfgImport.importingCodeOwnerConfig(), CodeOwnerConfigReference.copyWithNewImportMode( @@ -516,31 +530,31 @@ message = message.substring(0, message.length() - 1); } return OptionalResultWithMessages.create( - unresolvedImports.build(), + CodeOwnerConfigImports.create(resolvedImports.build(), unresolvedImports.build()), !message.isEmpty() ? ImmutableList.of(message) : ImmutableList.of()); } - private ImmutableSet<CodeOwnerConfigImport> getGlobalImports( + private ImmutableSet<CodeOwnerImport> getGlobalImports( int importLevel, CodeOwnerConfig codeOwnerConfig) { return codeOwnerConfig.imports().stream() .map( codeOwnerConfigReference -> - CodeOwnerConfigImport.create( + CodeOwnerImport.create( importLevel, codeOwnerConfig.key(), codeOwnerConfigReference)) .collect(toImmutableSet()); } - private ImmutableSet<CodeOwnerConfigImport> getPerFileImports( + private ImmutableSet<CodeOwnerImport> getPerFileImports( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, Set<CodeOwnerSet> codeOwnerSets) { - ImmutableSet.Builder<CodeOwnerConfigImport> codeOwnerConfigImports = ImmutableSet.builder(); + ImmutableSet.Builder<CodeOwnerImport> codeOwnerConfigImports = ImmutableSet.builder(); for (CodeOwnerSet codeOwnerSet : codeOwnerSets) { codeOwnerSet.imports().stream() .forEach( codeOwnerConfigReference -> codeOwnerConfigImports.add( - CodeOwnerConfigImport.create( + CodeOwnerImport.create( importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, @@ -623,7 +637,7 @@ } @AutoValue - abstract static class CodeOwnerConfigImport { + abstract static class CodeOwnerImport { /** * The import level. * @@ -683,7 +697,7 @@ return levels > 0 ? String.format("%" + (levels * 2) + "s", "") : ""; } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference) { @@ -691,7 +705,7 @@ importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, Optional.empty()); } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference, @@ -703,13 +717,28 @@ Optional.of(codeOwnerSet)); } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference, Optional<CodeOwnerSet> codeOwnerSet) { - return new AutoValue_PathCodeOwners_CodeOwnerConfigImport( + return new AutoValue_PathCodeOwners_CodeOwnerImport( importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, codeOwnerSet); } } + + @AutoValue + abstract static class CodeOwnerConfigImports { + /** Imported code owner configs the could be resolved. */ + abstract ImmutableList<CodeOwnerConfigImport> resolved(); + + /** Imported code owner configs the could not be resolved. */ + abstract ImmutableList<CodeOwnerConfigImport> unresolved(); + + static CodeOwnerConfigImports create( + ImmutableList<CodeOwnerConfigImport> resolved, + ImmutableList<CodeOwnerConfigImport> unresolved) { + return new AutoValue_PathCodeOwners_CodeOwnerConfigImports(resolved, unresolved); + } + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java index 93a36be..5b262bf 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -36,8 +36,11 @@ /** Gets the resolved code owner config. */ abstract CodeOwnerConfig codeOwnerConfig(); + /** Gets a list of resolved imports. */ + public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports(); + /** Gets a list of unresolved imports. */ - public abstract ImmutableList<UnresolvedImport> unresolvedImports(); + public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports(); /** Whether there are unresolved imports. */ public boolean hasUnresolvedImports() { @@ -104,14 +107,21 @@ return MoreObjects.toStringHelper(this) .add("path", path()) .add("codeOwnerConfig", codeOwnerConfig()) + .add("resolvedImports", resolvedImports()) .add("unresolvedImports", unresolvedImports()) .toString(); } /** Creates a {@link PathCodeOwnersResult} instance. */ public static PathCodeOwnersResult create( - Path path, CodeOwnerConfig codeOwnerConfig, List<UnresolvedImport> unresolvedImports) { + Path path, + CodeOwnerConfig codeOwnerConfig, + List<CodeOwnerConfigImport> resolvedImports, + List<CodeOwnerConfigImport> unresolvedImports) { return new AutoValue_PathCodeOwnersResult( - path, codeOwnerConfig, ImmutableList.copyOf(unresolvedImports)); + path, + codeOwnerConfig, + ImmutableList.copyOf(resolvedImports), + ImmutableList.copyOf(unresolvedImports)); } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java deleted file mode 100644 index bea1e08..0000000 --- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java +++ /dev/null
@@ -1,57 +0,0 @@ -// 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 com.google.auto.value.AutoValue; -import com.google.common.base.MoreObjects; - -/** Information about an unresolved import. */ -@AutoValue -public abstract class UnresolvedImport { - /** Key of the importing code owner config. */ - public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig(); - - /** Key of the imported code owner config. */ - public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig(); - - /** The code owner config reference that was attempted to be resolved. */ - public abstract CodeOwnerConfigReference codeOwnerConfigReference(); - - /** Message explaining why the code owner config reference couldn't be resolved. */ - public abstract String message(); - - @Override - public final String toString() { - return MoreObjects.toStringHelper(this) - .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig()) - .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig()) - .add("codeOwnerConfigReference", codeOwnerConfigReference()) - .add("message", message()) - .toString(); - } - - /** Creates a {@link UnresolvedImport} instance. */ - static UnresolvedImport create( - CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, - CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, - CodeOwnerConfigReference codeOwnerConfigReference, - String message) { - return new AutoValue_UnresolvedImport( - keyOfImportingCodeOwnerConfig, - keyOfImportedCodeOwnerConfig, - codeOwnerConfigReference, - message); - } -}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java index 52958d9..7946468 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -20,7 +20,7 @@ import com.google.inject.Inject; import java.nio.file.Path; -/** Class to format an {@link UnresolvedImport} as a user-readable string. */ +/** Class to format an {@link CodeOwnerConfigImport} as a user-readable string. */ public class UnresolvedImportFormatter { private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final ProjectCache projectCache; @@ -37,7 +37,7 @@ } /** Returns a user-readable string representation of the given unresolved import. */ - public String format(UnresolvedImport unresolvedImport) { + public String format(CodeOwnerConfigImport unresolvedImport) { return String.format( "The import of %s:%s:%s in %s:%s:%s cannot be resolved: %s", unresolvedImport.keyOfImportedCodeOwnerConfig().project(), @@ -46,10 +46,16 @@ unresolvedImport.keyOfImportingCodeOwnerConfig().project(), unresolvedImport.keyOfImportingCodeOwnerConfig().shortBranchName(), getFilePath(unresolvedImport.keyOfImportingCodeOwnerConfig()), - unresolvedImport.message()); + unresolvedImport + .errorMessage() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "unresolved import %s must have an error message", unresolvedImport)))); } - private Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { + public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { return getBackend(codeOwnerConfigKey).getFilePath(codeOwnerConfigKey); } @@ -60,6 +66,10 @@ * returned. */ private CodeOwnerBackend getBackend(CodeOwnerConfig.Key codeOwnerConfigKey) { + // For unresolved imports the project may not exist. Trying to get the project config for + // non-existing projects fails, hence check whether the project exists before trying to access + // the project config and fall back to the default code owner backend if the project doesn't + // exist. if (projectCache.get(codeOwnerConfigKey.project()).isPresent()) { return codeOwnersPluginConfiguration .getProjectConfig(codeOwnerConfigKey.project())
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java index a216eb1..cb565c5 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -18,6 +18,7 @@ import com.google.gerrit.entities.Project; import com.google.gerrit.server.cache.PerThreadCache; +import com.google.gerrit.server.cache.PerThreadProjectCache; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -69,8 +70,9 @@ */ public CodeOwnersPluginProjectConfigSnapshot getProjectConfig(Project.NameKey projectName) { requireNonNull(projectName, "projectName"); - return PerThreadCache.getOrCompute( - PerThreadCache.Key.create(CodeOwnersPluginProjectConfigSnapshot.class, projectName), + return PerThreadProjectCache.getOrCompute( + PerThreadCache.Key.create( + Project.NameKey.class, projectName, "CodeOwnersPluginConfiguration"), () -> codeOwnersPluginProjectConfigSnapshotFactory.create(projectName)); } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java index f02e54d..786d3e4 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -84,6 +84,7 @@ @Nullable private FallbackCodeOwners fallbackCodeOwners; @Nullable private Integer maxPathsInChangeMessages; @Nullable private Boolean enableAsyncMessageOnAddReviewer; + @Nullable private Boolean enableAsyncMessageOnCodeOwnerApproval; @Nullable private ImmutableSet<CodeOwnerReference> globalCodeOwners; @Nullable private ImmutableSet<Account.Id> exemptedAccounts; @Nullable private Optional<String> overrideInfoUrl; @@ -333,6 +334,18 @@ return enableAsyncMessageOnAddReviewer; } + /** + * Gets whether applying a code owner approval should post a change message asynchronously instead + * of synchronously extending the change message that is posted by Gerrit core. + */ + public boolean enableAsyncMessageOnCodeOwnerApproval() { + if (enableAsyncMessageOnCodeOwnerApproval == null) { + enableAsyncMessageOnCodeOwnerApproval = + generalConfig.enableAsyncMessageOnCodeOwnerApproval(projectName, pluginConfig); + } + return enableAsyncMessageOnCodeOwnerApproval; + } + /** Gets the global code owners. */ public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners() { if (globalCodeOwners == null) {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java index 810486f..ada9105 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -65,6 +65,8 @@ public static final String KEY_FILE_EXTENSION = "fileExtension"; public static final String KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER = "enableAsyncMessageOnAddReviewer"; + public static final String KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL = + "enableAsyncMessageOnCodeOwnerApproval"; public static final String KEY_ENABLE_CODE_OWNER_CONFIG_FILES_WITH_FILE_EXTENSIONS = "enableCodeOwnerConfigFilesWithFileExtensions"; public static final String KEY_READ_ONLY = "readOnly"; @@ -476,6 +478,25 @@ } /** + * Gets whether applying a code owner approval should post a change message asynchronously instead + * of synchronously extending the change message that is posted by Gerrit core. + * + * @param project the project for which the enable async message on code owner approval + * configuration should be read + * @param pluginConfig the plugin config from which the enable async message on code owner + * approval configuration should be read + * @return whether applying a code owner approval should post a change message asynchronously + * instead of synchronously extending the change message that is posted by Gerrit core + */ + boolean enableAsyncMessageOnCodeOwnerApproval(Project.NameKey project, Config pluginConfig) { + return getBooleanConfig( + project, + pluginConfig, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + /* defaultValue= */ true); + } + + /** * Gets the enable validation on branch creation configuration from the given plugin config for * the specified project with fallback to {@code gerrit.config} and default to {@code true}. *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java index 513bd12..941a31c 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
@@ -23,6 +23,7 @@ import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.update.RetryHelper; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -47,12 +48,14 @@ CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory, FindOwnersCodeOwnerConfigParser codeOwnerConfigParser, GitRepositoryManager repoManager, + ProjectCache projectCache, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, RetryHelper retryHelper) { super( codeOwnersPluginConfiguration, repoManager, + projectCache, serverIdent, metaDataUpdateInternalFactory, retryHelper,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java index ccb59df..a761bf6 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/proto/ProtoBackend.java
@@ -23,6 +23,7 @@ import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.update.RetryHelper; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -49,6 +50,7 @@ ProtoBackend( CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, GitRepositoryManager repoManager, + ProjectCache projectCache, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, RetryHelper retryHelper, @@ -57,6 +59,7 @@ super( codeOwnersPluginConfiguration, repoManager, + projectCache, serverIdent, metaDataUpdateInternalFactory, retryHelper,
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java index fa90564..4a6c37d 100644 --- a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java +++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -33,6 +33,7 @@ public class CodeOwnerMetrics { // latency metrics public final Timer1<String> addChangeMessageOnAddReviewer; + public final Timer0 addChangeMessageOnCodeOwnerApproval; public final Timer0 computeFileStatus; public final Timer1<Boolean> computeFileStatuses; public final Timer0 computeOwnedPaths; @@ -85,6 +86,11 @@ .description( "Whether the change message was posted synchronously or asynchronously.") .build()); + this.addChangeMessageOnCodeOwnerApproval = + createTimer( + "add_change_message_on_code_owner_approval", + "Latency for asynchronously adding a change message with the owned path when a code" + + " owner approval is applied"); this.computeFileStatus = createTimer("compute_file_status", "Latency for computing the file status of one file"); this.computeFileStatuses = @@ -105,8 +111,8 @@ this.extendChangeMessageOnPostReview = createTimer( "extend_change_message_on_post_review", - "Latency for extending the change message with the owned path when a code owner" - + " approval is applied"); + "Latency for synchronously extending the change message with the owned path when a" + + " code owner approval is applied"); this.getChangedFiles = createTimer("get_changed_files", "Latency for getting changed files from diff cache"); this.prepareFileStatusComputation =
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java index 677e67c..0fbb01d 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -38,6 +38,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; import com.google.gerrit.plugins.codeowners.backend.CodeOwner; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation; @@ -92,6 +93,7 @@ private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy; private final Provider<CodeOwnerResolver> codeOwnerResolver; private final CodeOwnerJson.Factory codeOwnerJsonFactory; + private final CodeOwnerConfigFileJson codeOwnerConfigFileJson; private final EnumSet<ListAccountsOption> options; private final Set<String> hexOptions; @@ -167,7 +169,8 @@ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, - CodeOwnerJson.Factory codeOwnerJsonFactory) { + CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson) { this.accountVisibility = accountVisibility; this.accounts = accounts; this.accountControlFactory = accountControlFactory; @@ -178,6 +181,7 @@ this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy; this.codeOwnerResolver = codeOwnerResolver; this.codeOwnerJsonFactory = codeOwnerJsonFactory; + this.codeOwnerConfigFileJson = codeOwnerConfigFileJson; this.options = EnumSet.noneOf(ListAccountsOption.class); this.hexOptions = new HashSet<>(); } @@ -213,6 +217,8 @@ ListMultimap<CodeOwner, CodeOwnerAnnotation> annotations = LinkedListMultimap.create(); AtomicBoolean ownedByAllUsers = new AtomicBoolean(false); ImmutableList.Builder<String> debugLogsBuilder = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigFileInfo> codeOwnerConfigFileInfosBuilder = + ImmutableList.builder(); codeOwnerConfigHierarchy.visit( rsrc.getBranch(), rsrc.getRevision(), @@ -221,6 +227,12 @@ CodeOwnerResolverResult pathCodeOwners = codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath()); + codeOwnerConfigFileInfosBuilder.add( + codeOwnerConfigFileJson.format( + codeOwnerConfig.key(), + pathCodeOwners.resolvedImports(), + pathCodeOwners.unresolvedImports())); + debugLogsBuilder.addAll(pathCodeOwners.messages()); codeOwners.addAll(pathCodeOwners.codeOwners()); annotations.putAll(pathCodeOwners.annotations()); @@ -308,7 +320,7 @@ ImmutableMap<CodeOwner, Double> scoredCodeOwners = codeOwnerScorings.getScorings(filteredCodeOwners); - ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(rsrc, scoredCodeOwners); + ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(scoredCodeOwners); if (highestScoreOnly) { Optional<Double> highestScore = @@ -325,7 +337,7 @@ codeOwnersInfo.codeOwners = codeOwnerJsonFactory.create(getFillOptions()).format(sortedAndLimitedCodeOwners); codeOwnersInfo.ownedByAllUsers = ownedByAllUsers.get() ? true : null; - + codeOwnersInfo.codeOwnerConfigs = codeOwnerConfigFileInfosBuilder.build(); ImmutableList<String> debugLogs = debugLogsBuilder.build(); codeOwnersInfo.debugLogs = debug ? debugLogs : null; logger.atFine().log("debug logs: %s", debugLogs); @@ -473,16 +485,15 @@ if (options.contains(ListAccountsOption.ALL_EMAILS)) { // Secondary emails are only visible to users that have the 'Modify Account' global // capability. - permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT); + permissionBackend.currentUser().check(GlobalPermission.VIEW_SECONDARY_EMAILS); fillOptions.add(FillOptions.EMAIL); fillOptions.add(FillOptions.SECONDARY_EMAILS); } return fillOptions; } - private ImmutableList<CodeOwner> sortAndLimit( - R rsrc, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { - return sortCodeOwners(rsrc, seed, scoredCodeOwners).limit(limit).collect(toImmutableList()); + private ImmutableList<CodeOwner> sortAndLimit(ImmutableMap<CodeOwner, Double> scoredCodeOwners) { + return sortCodeOwners(seed, scoredCodeOwners).limit(limit).collect(toImmutableList()); } /** @@ -492,13 +503,12 @@ * * <p>The order of code owners with the same score is random. * - * @param rsrc resource on which this REST endpoint is invoked * @param seed seed that should be used to randomize the order * @param scoredCodeOwners the code owners with their scores * @return the sorted code owners */ private Stream<CodeOwner> sortCodeOwners( - R rsrc, Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { + Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { return randomizeOrder(seed, scoredCodeOwners.keySet()) .sorted(Comparator.comparingDouble(scoredCodeOwners::get).reversed()); }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java new file mode 100644 index 0000000..fe17a3f --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
@@ -0,0 +1,119 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.restapi; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport; +import com.google.gerrit.plugins.codeowners.backend.UnresolvedImportFormatter; +import com.google.inject.Inject; +import java.util.List; + +/** Collection of routines to populate {@link CodeOwnerConfigFileInfo}. */ +public class CodeOwnerConfigFileJson { + private final UnresolvedImportFormatter unresolvedImportFormatter; + + @Inject + CodeOwnerConfigFileJson(UnresolvedImportFormatter unresolvedImportFormatter) { + this.unresolvedImportFormatter = unresolvedImportFormatter; + } + + /** + * Formats the provided code owner config file information as a {@link CodeOwnerConfigFileInfo}. + * + * @param codeOwnerConfigKey the key of the code owner config file as {@link + * CodeOwnerConfigFileInfo} + * @param resolvedImports code owner config files which have been successfully imported directly + * or indirectly + * @param unresolvedImports code owner config files which are imported directly or indirectly but + * couldn't be resolved + * @return the provided {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig.Key} + * as {@link CodeOwnerConfigFileInfo} + */ + public CodeOwnerConfigFileInfo format( + CodeOwnerConfig.Key codeOwnerConfigKey, + List<CodeOwnerConfigImport> resolvedImports, + List<CodeOwnerConfigImport> unresolvedImports) { + requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey"); + requireNonNull(resolvedImports, "resolvedImports"); + requireNonNull(unresolvedImports, "unresolvedImports"); + + CodeOwnerConfigFileInfo info = new CodeOwnerConfigFileInfo(); + + info.project = codeOwnerConfigKey.project().get(); + info.branch = codeOwnerConfigKey.branchNameKey().branch(); + info.path = unresolvedImportFormatter.getFilePath(codeOwnerConfigKey).toString(); + + ImmutableList<CodeOwnerConfigFileInfo> unresolvedImportInfos = + unresolvedImports.stream() + .filter( + unresolvedImport -> + unresolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .map( + unresolvedImport -> { + CodeOwnerConfigFileInfo unresolvedCodeOwnerConfigFileInfo = + format( + unresolvedImport.keyOfImportedCodeOwnerConfig(), + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of()); + unresolvedCodeOwnerConfigFileInfo.importMode = + unresolvedImport.codeOwnerConfigReference().importMode(); + unresolvedCodeOwnerConfigFileInfo.unresolvedErrorMessage = + unresolvedImport + .errorMessage() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "unresolved import %s must have an error message", + unresolvedImport))); + return unresolvedCodeOwnerConfigFileInfo; + }) + .collect(toImmutableList()); + info.unresolvedImports = !unresolvedImportInfos.isEmpty() ? unresolvedImportInfos : null; + + ImmutableList<CodeOwnerConfigFileInfo> importInfos = + resolvedImports.stream() + .filter( + resolvedImport -> + resolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .map( + resolvedImport -> { + CodeOwnerConfigFileInfo resolvedCodeOwnerConfigFileInfo = + format( + resolvedImport.keyOfImportedCodeOwnerConfig(), + removeImportEntriesFor(resolvedImports, codeOwnerConfigKey), + removeImportEntriesFor(unresolvedImports, codeOwnerConfigKey)); + resolvedCodeOwnerConfigFileInfo.importMode = + resolvedImport.codeOwnerConfigReference().importMode(); + return resolvedCodeOwnerConfigFileInfo; + }) + .collect(toImmutableList()); + info.imports = !importInfos.isEmpty() ? importInfos : null; + + return info; + } + + private ImmutableList<CodeOwnerConfigImport> removeImportEntriesFor( + List<CodeOwnerConfigImport> imports, CodeOwnerConfig.Key codeOwnerConfigKey) { + return imports.stream() + .filter(i -> !i.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .collect(toImmutableList()); + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java index e0a9145..85410e9 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -82,6 +82,7 @@ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson, GitRepositoryManager repoManager) { super( accountVisibility, @@ -93,7 +94,8 @@ codeOwnersPluginConfiguration, codeOwnerConfigHierarchy, codeOwnerResolver, - codeOwnerJsonFactory); + codeOwnerJsonFactory, + codeOwnerConfigFileJson); this.repoManager = repoManager; }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java index 3ed9a51..b7149c5 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -73,7 +73,8 @@ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, ServiceUserClassifier serviceUserClassifier, - CodeOwnerJson.Factory codeOwnerJsonFactory) { + CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson) { super( accountVisibility, accounts, @@ -84,7 +85,8 @@ codeOwnersPluginConfiguration, codeOwnerConfigHierarchy, codeOwnerResolver, - codeOwnerJsonFactory); + codeOwnerJsonFactory, + codeOwnerConfigFileJson); this.serviceUserClassifier = serviceUserClassifier; }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java new file mode 100644 index 0000000..4d09a10 --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
@@ -0,0 +1,188 @@ +// Copyright (C) 2023 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.ListSubject.elements; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +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.truth.ListSubject; + +/** {@link Subject} for doing assertions on {@link CodeOwnerConfigFileInfo}s. */ +public class CodeOwnerConfigFileInfoSubject extends Subject { + /** + * Starts fluent chain to do assertions on a {@link CodeOwnerConfigFileInfo}. + * + * @param codeOwnerConfigFileInfo the code owner config file info on which assertions should be + * done + * @return the created {@link CodeOwnerConfigFileInfoSubject} + */ + public static CodeOwnerConfigFileInfoSubject assertThat( + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) { + return assertAbout(codeOwnerConfigFileInfos()).that(codeOwnerConfigFileInfo); + } + + public static Factory<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + codeOwnerConfigFileInfos() { + return CodeOwnerConfigFileInfoSubject::new; + } + + private final CodeOwnerConfigFileInfo codeOwnerConfigFileInfo; + + private CodeOwnerConfigFileInfoSubject( + FailureMetadata metadata, CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) { + super(metadata, codeOwnerConfigFileInfo); + this.codeOwnerConfigFileInfo = codeOwnerConfigFileInfo; + } + + /** Returns a subject for the project of the code owner config file info. */ + public StringSubject hasProjectThat() { + return check("project()").that(codeOwnerConfigFileInfo().project); + } + + /** Returns a subject for the branch of the code owner config file info. */ + public StringSubject hasBranchThat() { + return check("branch()").that(codeOwnerConfigFileInfo().branch); + } + + /** Returns a subject for the path of the code owner config file info. */ + public StringSubject hasPathThat() { + return check("path()").that(codeOwnerConfigFileInfo().path); + } + + /** + * Returns a {@link ListSubject} for the (resolved) imports of the code owner config file info. + */ + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> hasImportsThat() { + return check("imports()") + .about(elements()) + .thatCustom(codeOwnerConfigFileInfo().imports, codeOwnerConfigFileInfos()); + } + + /** + * Returns a {@link ListSubject} for the unresolved imports of the code owner config file info. + */ + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + hasUnresolvedImportsThat() { + return check("unresolvedimports()") + .about(elements()) + .thatCustom(codeOwnerConfigFileInfo().unresolvedImports, codeOwnerConfigFileInfos()); + } + + /** Returns a subject for the import mode of the code owner config file info. */ + public Subject hasImportModeThat() { + return check("importMode()").that(codeOwnerConfigFileInfo().importMode); + } + + /** Returns a subject for the unresolved error message of the code owner config file info. */ + public Subject hasUnresolvedErrorMessageThat() { + return check("unresolvedErrorMessage()").that(codeOwnerConfigFileInfo().unresolvedErrorMessage); + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertKey( + CodeOwnerBackend codeOwnerBackend, CodeOwnerConfig.Key codeOwnerConfigKey) { + hasProjectThat().isEqualTo(codeOwnerConfigKey.project().get()); + hasBranchThat().isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + hasPathThat().isEqualTo(codeOwnerBackend.getFilePath(codeOwnerConfigKey).toString()); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoResolvedImports() { + hasImportsThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertResolvedImport( + CodeOwnerBackend codeOwnerBackend, + CodeOwnerConfig.Key codeOwnerConfigKey, + CodeOwnerConfigImportMode importMode) { + hasImportsThat().hasSize(1); + CodeOwnerConfigFileInfoSubject subjectForResolvedImport = hasImportsThat().element(0); + subjectForResolvedImport + .assertKey(codeOwnerBackend, codeOwnerConfigKey) + .assertImportMode(importMode) + .assertNoUnresolvedErrorMessage(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoUnresolvedImports() { + hasUnresolvedImportsThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertUnresolvedImport( + CodeOwnerBackend codeOwnerBackend, + CodeOwnerConfig.Key codeOwnerConfigKey, + CodeOwnerConfigImportMode importMode, + String unresolvedErrorMessage) { + hasUnresolvedImportsThat().hasSize(1); + CodeOwnerConfigFileInfoSubject subjectForUnresolvedImport = + hasUnresolvedImportsThat().element(0); + subjectForUnresolvedImport + .assertKey(codeOwnerBackend, codeOwnerConfigKey) + .assertImportMode(importMode) + .assertUnresolvedErrorMessage(unresolvedErrorMessage); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoImports() { + assertNoResolvedImports(); + assertNoUnresolvedImports(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertImportMode(CodeOwnerConfigImportMode importMode) { + hasImportModeThat().isEqualTo(importMode); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoImportMode() { + hasImportModeThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertUnresolvedErrorMessage( + String unresolvedErrorMessage) { + hasUnresolvedErrorMessageThat().isEqualTo(unresolvedErrorMessage); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoUnresolvedErrorMessage() { + hasUnresolvedErrorMessageThat().isNull(); + return this; + } + + private CodeOwnerConfigFileInfo codeOwnerConfigFileInfo() { + isNotNull(); + return codeOwnerConfigFileInfo; + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java index 11433e2..9e5fb44 100644 --- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java +++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.testing; import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject.codeOwnerConfigFileInfos; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.codeOwnerInfos; import static com.google.gerrit.truth.ListSubject.elements; @@ -22,6 +23,7 @@ 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.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; import com.google.gerrit.truth.ListSubject; @@ -65,6 +67,13 @@ } } + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + hasCodeOwnerConfigsThat() { + return check("codeOwnerConfigs()") + .about(elements()) + .thatCustom(codeOwnersInfo().codeOwnerConfigs, codeOwnerConfigFileInfos()); + } + public IterableSubject hasDebugLogsThat() { return check("debugLogs").that(codeOwnersInfo().debugLogs); }
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java index 0083497..c8e0c7c 100644 --- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java +++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -463,12 +463,9 @@ codeOwnersPluginConfiguration.getProjectConfig(branchNameKey.project()); logger.atFine().log("force = %s", force); if (!force && codeOwnersConfig.isDisabled(branchNameKey.branch())) { - return Optional.of( - ValidationResult.create( - pluginName, - "skipping validation of code owner config files", - new CommitValidationMessage( - "code-owners functionality is disabled", ValidationMessage.Type.HINT))); + logger.atFine().log( + "skipping validation of code owner config files due to code-owners functionality is disabled"); + return Optional.empty(); } try {
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java index 4417295..2443ef5 100644 --- a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java +++ b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
@@ -106,7 +106,8 @@ } catch (PermissionBackendException e) { throw newInternalServerError( String.format( - "Failed to check %s capability", SkipCodeOwnerConfigValidationCapability.ID), + "Failed to check %s~%s capability", + pluginName, SkipCodeOwnerConfigValidationCapability.ID), e); } }
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 74c9eb2..2568906 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -17,6 +17,7 @@ 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.CodeOwnerConfigFileInfoSubject.assertThat; 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.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat; @@ -25,6 +26,7 @@ import static java.util.stream.Collectors.toList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; @@ -44,13 +46,16 @@ import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwners; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; +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.CodeOwnerSet; +import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig; import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerCapability; import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch; import com.google.inject.Inject; @@ -77,11 +82,14 @@ @Inject private GroupOperations groupOperations; @Inject private ProjectOperations projectOperations; + private CodeOwnerBackend backend; + protected TestPathExpressions testPathExpressions; @Before public void setup() throws Exception { testPathExpressions = plugin.getSysInjector().getInstance(TestPathExpressions.class); + backend = plugin.getSysInjector().getInstance(BackendConfig.class).getDefaultBackend(); } /** Must return the {@link CodeOwners} API against which the tests should be run. */ @@ -98,7 +106,10 @@ @Test public void getCodeOwnersWhenNoCodeOwnerConfigsExist() throws Exception { - assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty(); + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); + assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -115,6 +126,7 @@ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -136,29 +148,32 @@ private void testGetCodeOwners(boolean useAbsolutePath) throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(admin.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(user.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey3 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user2.email()) + .create(); CodeOwnersInfo codeOwnersInfo = queryCodeOwners(useAbsolutePath ? "/foo/bar/baz.md" : "foo/bar/baz.md"); @@ -172,6 +187,27 @@ .comparingElementsUsing(hasAccountName()) .containsExactly(null, null, null); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(3); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo3 = codeOwnersInfo.codeOwnerConfigs.get(0); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(2); + assertThat(codeOwnerConfigFileInfo3) + .assertKey(backend, codeOwnerConfigKey3) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo2) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo1) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo).hasDebugLogsThat().isNull(); } @@ -184,24 +220,26 @@ // 1. code owner config that makes "user2" a code owner, inheriting code owners from parent code // owner configs is enabled by default - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user2.email()) + .create(); // 2. code owner config that makes "user" a code owner, code owners from parent code owner // configs are ignored - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .ignoreParentCodeOwners() - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .ignoreParentCodeOwners() + .addCodeOwnerEmail(user.email()) + .create(); // 3. code owner config that makes "admin" a code owner and assigns code ownership to all users, // but for this test this code owner config is ignored, since the 2. code owner config ignores @@ -227,36 +265,71 @@ .containsExactly(user2.id(), user.id()) .inOrder(); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(0); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1); + assertThat(codeOwnerConfigFileInfo1) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo2) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test public void getPerFileCodeOwners() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("txt")) - .addCodeOwnerEmail(admin.email()) - .build()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("md")) - .addCodeOwnerEmail(user.email()) - .build()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("txt")) + .addCodeOwnerEmail(admin.email()) + .build()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("md")) + .addCodeOwnerEmail(user.email()) + .build()) + .create(); - assertThat(queryCodeOwners("/foo/bar/config.txt")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/config.txt"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(admin.id()); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + + codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id()); - assertThat(queryCodeOwners("/foo/bar/main.config")).hasCodeOwnersThat().isEmpty(); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + + codeOwnersInfo = queryCodeOwners("/foo/bar/main.config"); + assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -314,8 +387,9 @@ } @Test - public void cannotGetCodeOwnersWithSecondaryEmailsWithoutModifyAccountCapability() - throws Exception { + public void + cannotGetCodeOwnersWithSecondaryEmailsWithoutViewSecondaryEmailAndWithoutModifyAccountCapability() + throws Exception { // create a code owner config codeOwnerConfigOperations .newCodeOwnerConfig() @@ -339,7 +413,7 @@ queryCodeOwners( getCodeOwnersApi().query().withOptions(ListAccountsOption.ALL_EMAILS), "/foo/bar/baz.md")); - assertThat(exception).hasMessageThat().isEqualTo("modify account not permitted"); + assertThat(exception).hasMessageThat().isEqualTo("view secondary emails not permitted"); } @Test @@ -548,22 +622,24 @@ TestAccount user2 = accountCreator.user2(); // create some code owner configs - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(admin.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user.email()) - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user.email()) + .addCodeOwnerEmail(user2.email()) + .create(); // get code owners with different limits CodeOwnersInfo codeOwnersInfo = @@ -576,18 +652,51 @@ .element(0) .hasAccountIdThat() .isAnyOf(user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md"); assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md"); assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(admin.id(), user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -632,10 +741,12 @@ public void getGlobalCodeOwners() throws Exception { TestAccount globalOwner = accountCreator.create("global_owner", "global.owner@example.com", "Global Owner", null); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(globalOwner.id()); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -767,18 +878,25 @@ @Test public void getDefaultCodeOwners() throws Exception { // Create default code owner config file in refs/meta/config. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(RefNames.REFS_CONFIG) - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(RefNames.REFS_CONFIG) + .folderPath("/") + .addCodeOwnerEmail(user.email()) + .create(); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -1460,6 +1578,22 @@ .element(1) .hasAccountIdThat() .isAnyOf(user.id(), user2.id()); + + codeOwnersInfo = + queryCodeOwners( + getCodeOwnersApi() + .query() + .withHighestScoreOnly(/* highestScoreOnly= */ true) + .withLimit(1), + "/foo/bar/baz.md"); + assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1); + // the first 2 code owners have the same scoring, so their order is random and we don't know + // which of them is returned + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .element(0) + .hasAccountIdThat() + .isAnyOf(user.id(), user2.id()); } @Test @@ -1643,4 +1777,196 @@ } assertThat(foundDifferentOrder).isTrue(); } + + @Test + public void getCodeOwnersWithUnresolvedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + Project.NameKey nonExistingProject = Project.nameKey("non-existing"); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS"); + CodeOwnerConfigReference nonResolvableCodeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(nonResolvableCodeOwnerConfigReference) + .addCodeOwnerEmail(admin.email()) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), admin.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoResolvedImports() + .assertUnresolvedImport( + backend, + keyOfImportedCodeOwnerConfig, + nonResolvableCodeOwnerConfigReference.importMode(), + String.format("project %s not found", nonExistingProject)); + } + + @Test + public void getCodeOwnersWithResolvedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(codeOwnerConfigReference) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), admin.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference.importMode()); + } + + @Test + public void getCodeOwnersWithNestedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + TestAccount user2 = accountCreator.user2(); + + Project.NameKey nonExistingProject = Project.nameKey("non-existing"); + CodeOwnerConfig.Key keyOfUnresolvableCodeOwnerConfig = + CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS"); + CodeOwnerConfigReference unresolvableCodeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfUnresolvableCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .addImport(unresolvableCodeOwnerConfigReference) + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .addImport(codeOwnerConfigReference2) + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig1); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(codeOwnerConfigReference1) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), user2.id(), admin.id()); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoImportMode() + .assertNoUnresolvedErrorMessage() + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig1, codeOwnerConfigReference1.importMode()); + + codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportedCodeOwnerConfig1) + .assertImportMode(codeOwnerConfigReference1.importMode()) + .assertNoUnresolvedErrorMessage() + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig2, codeOwnerConfigReference2.importMode()); + + codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportedCodeOwnerConfig2) + .assertImportMode(codeOwnerConfigReference2.importMode()) + .assertNoUnresolvedErrorMessage() + .assertNoResolvedImports() + .assertUnresolvedImport( + backend, + keyOfUnresolvableCodeOwnerConfig, + unresolvableCodeOwnerConfigReference.importMode(), + String.format("project %s not found", nonExistingProject)); + } + + @Test + @GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo") + public void returnedOwnersFileContainsFileExtension() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName(getCodeOwnerConfigFileName() + ".foo") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md"); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(admin.id()); + + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(1); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = codeOwnersInfo.codeOwnerConfigs.get(0); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java index 83eb13f..8c5ce92 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -22,6 +22,7 @@ import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerCheckInfoSubject.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import com.google.common.collect.ImmutableSet; import com.google.gerrit.acceptance.RestResponse; @@ -58,7 +59,7 @@ import com.google.gerrit.server.ServerInitiated; import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.externalids.ExternalIdFactory; -import com.google.gerrit.server.account.externalids.ExternalIdNotes; +import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.inject.Inject; import com.google.inject.Provider; @@ -100,7 +101,7 @@ @Test public void checkCodeOwnerForSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get( @@ -576,7 +577,8 @@ assertThat(checkCodeOwnerInfo) .hasDebugLogsThatContainAllOf( String.format( - "found the all users wildcard ('%s') as a code owner in %s which makes %s a code owner", + "found the all users wildcard ('%s') as a code owner in %s which makes %s a code" + + " owner", CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH), CodeOwnerResolver.ALL_USERS_WILDCARD));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java index 51b52da..c195401 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -449,10 +449,7 @@ .codeOwnerConfig(createCodeOwnerConfigKey("/")) .getJGitFilePath(), "INVALID"); - assertOkWithHints( - r, - "skipping validation of code owner config files", - "code-owners functionality is disabled"); + assertOkWithoutMessages(r); } @Test
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java index f3e7d1b..2d2c8c7 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; @@ -716,7 +717,7 @@ try (Repository repo = repoManager.openRepository(project)) { RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name())); ru.setForceUpdate(true); - assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); + assertThat(testRefAction(() -> ru.delete())).isEqualTo(RefUpdate.Result.FORCED); assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNull(); } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java index 70fb164..e3b51b4 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.entities.BranchNameKey; @@ -45,6 +46,9 @@ * using the {@link GerritConfig} annotation). */ public class CodeOwnersOnAddReviewerIT extends AbstractCodeOwnersIT { + private static String TEST_PATH = "foo/bar.baz"; + private static String TEST_PATH_ESCAPED = "`foo/bar.baz`"; + @Test @GerritConfig(name = "plugin.code-owners.disabled", value = "true") public void noChangeMessageAddedIfCodeOwnersFuctionalityIsDisabled() throws Exception { @@ -56,7 +60,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -74,7 +78,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -86,7 +90,7 @@ public void noChangeMessageAddedIfInvalidCodeOwnerConfigFilesExist() throws Exception { createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName()); - String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -104,8 +108,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -114,7 +117,7 @@ .isEqualTo( String.format( "%s, who was added as reviewer owns the following files:\n* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); } @Test @@ -127,15 +130,17 @@ .addCodeOwnerEmail(user.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; + String testPath1 = "foo/bar.baz"; + String testPath1Escaped = "`foo/bar.baz`"; + String testPath2 = "foo/baz.bar"; + String testPath2Escaped = "`foo/baz.bar`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", "bar/foo.baz", "file content", @@ -150,7 +155,9 @@ .isEqualTo( String.format( "%s, who was added as reviewer owns the following files:\n* %s\n* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path1, path2)); + AccountTemplateUtil.getAccountTemplate(user.id()), + testPath1Escaped, + testPath2Escaped)); } @Test @@ -163,8 +170,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -188,21 +194,25 @@ .addCodeOwnerEmail(user.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; - String path3 = "bar/foo.baz"; - String path4 = "bar/baz.foo"; + String testPath1 = "foo/bar.baz"; + String testPath1Escaped = "`foo/bar.baz`"; + String testPath2 = "foo/baz.bar"; + String testPath2Escaped = "`foo/baz.bar`"; + String testPath3 = "bar/foo.baz"; + String testPath3Escaped = "`bar/foo.baz`"; + String testPath4 = "bar/baz.foo"; + String testPath4Escaped = "`bar/baz.foo`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", - path3, + testPath3, "file content", - path4, + testPath4, "file content")) .getChangeId(); @@ -217,7 +227,11 @@ + "* %s\n" + "* %s\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path4, path3, path1, path2)); + AccountTemplateUtil.getAccountTemplate(user.id()), + testPath4Escaped, + testPath3Escaped, + testPath1Escaped, + testPath2Escaped)); } @Test @@ -231,24 +245,27 @@ .addCodeOwnerEmail(user.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; - String path3 = "bar/foo.baz"; - String path4 = "bar/baz.foo"; - String path5 = "baz/foo.bar"; + String testPath1 = "foo/bar.baz"; + String testPath2 = "foo/baz.bar"; + String testPath3 = "bar/foo.baz"; + String testPath3Escaped = "`bar/foo.baz`"; + String testPath4 = "bar/baz.foo"; + String testPath4Escaped = "`bar/baz.foo`"; + String testPath5 = "baz/foo.bar"; + String testPath5Escaped = "`baz/foo.bar`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", - path3, + testPath3, "file content", - path4, + testPath4, "file content", - path5, + testPath5, "file content")) .getChangeId(); @@ -263,7 +280,10 @@ + "* %s\n" + "* %s\n" + "(more files)\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path4, path3, path5)); + AccountTemplateUtil.getAccountTemplate(user.id()), + testPath4Escaped, + testPath3Escaped, + testPath5Escaped)); } @Test @@ -334,8 +354,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // Add reviewer via PostReview. gApi.changes().id(changeId).current().review(ReviewInput.create().reviewer(user.email())); @@ -345,7 +364,7 @@ .isEqualTo( String.format( "%s, who was added as reviewer owns the following files:\n* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); } @Test @@ -361,8 +380,7 @@ .addCodeOwnerEmail(user2.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // Add code owners 'user' and 'user2' as reviewers. gApi.changes() @@ -379,9 +397,9 @@ "%s, who was added as reviewer owns the following files:\n* %s\n\n" + "%s, who was added as reviewer owns the following files:\n* %s\n", AccountTemplateUtil.getAccountTemplate(user.id()), - path, + TEST_PATH_ESCAPED, AccountTemplateUtil.getAccountTemplate(user2.id()), - path)); + TEST_PATH_ESCAPED)); } @Test @@ -395,8 +413,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // 'admin' grants a code owner approval (Code-Review+1) and adds 'user' as reviewer. gApi.changes().id(changeId).current().review(ReviewInput.recommend().reviewer(user.email())); @@ -412,12 +429,42 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); assertThat(Iterables.getLast(messages).message) .isEqualTo( String.format( "%s, who was added as reviewer owns the following files:\n* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); + } + + @Test + public void markdownCharactersInPathsAreEscaped() throws Exception { + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(user.email()) + .create(); + + testMarkdownCharactersInPathsAreEscaped('`', user); + } + + private void testMarkdownCharactersInPathsAreEscaped( + char markdownCharacter, TestAccount codeOwner) throws Exception { + String testPath = markdownCharacter + "foo" + markdownCharacter + ".bar"; + String testPathEscaped = "`\\" + markdownCharacter + "foo\\" + markdownCharacter + ".bar`"; + + String changeId = createChange("Test Change", testPath, "file content").getChangeId(); + + gApi.changes().id(changeId).addReviewer(codeOwner.email()); + + Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages; + assertThat(Iterables.getLast(messages).message) + .isEqualTo( + String.format( + "%s, who was added as reviewer owns the following files:\n* %s\n", + AccountTemplateUtil.getAccountTemplate(codeOwner.id()), testPathEscaped)); } @Test @@ -431,8 +478,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).addReviewer(user.email()); @@ -440,7 +486,7 @@ changeId, String.format( "%s, who was added as reviewer owns the following files:\n* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); } private void assertAsyncChangeMessage(String changeId, String expectedChangeMessage) @@ -453,6 +499,7 @@ }); } + @CanIgnoreReturnValue private <T> T assertAsync(Callable<T> assertion) throws Exception { return RetryerBuilder.<T>newBuilder() .retryIfException(t -> true)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java index b2a6630..b0caf0a 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -631,6 +631,27 @@ } @Test + public void configureEnableAsyncMessageOnCodeOwnerApproval() throws Exception { + fetchRefsMetaConfig(); + + Config cfg = new Config(); + cfg.setBoolean( + CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, + /* subsection= */ null, + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + false); + setCodeOwnersConfig(cfg); + + PushResult r = pushRefsMetaConfig(); + assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK); + assertThat( + codeOwnersPluginConfiguration + .getProjectConfig(project) + .enableAsyncMessageOnCodeOwnerApproval()) + .isFalse(); + } + + @Test public void cannotSetInvalidMaxPathsInChangeMessages() throws Exception { fetchRefsMetaConfig();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java index 5ddd85f..843b656 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigInfoSubject.assertThatOptional; import static com.google.gerrit.testing.GerritJUnit.assertThrows; @@ -54,7 +55,7 @@ @GerritConfig(name = "plugin.code-owners.enableExperimentalRestEndpoints", value = "true") public void getCodeOwnerConfigFromSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java index a78d420..34d2271 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat; @@ -73,7 +74,7 @@ @Test public void getCodeOwnersFromSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java index d3fe19a..37739b1 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
@@ -16,11 +16,16 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +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.project.TestProjectUpdate; @@ -34,13 +39,26 @@ import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.testing.FakeEmailSender.Message; import com.google.inject.Inject; +import java.time.Duration; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.concurrent.Callable; import org.junit.Test; -/** Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerApproval}. */ +/** + * Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerApproval}. + * + * <p>For tests the change message that is posted when a code owner approval is applied, is added + * synchronously by default (see {@link AbstractCodeOwnersIT #defaultConfig()}). Tests that want to + * verify the asynchronous posting of this change message need to set {@code + * plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval=true} in {@code gerrit.config} + * explicitly (by using the {@link GerritConfig} annotation). + */ public class OnCodeOwnerApprovalIT extends AbstractCodeOwnersIT { + private static String TEST_PATH = "foo/bar.baz"; + private static String TEST_PATH_ESCAPED = "`foo/bar.baz`"; + @Inject private RequestScopeOperations requestScopeOperations; @Inject private ProjectOperations projectOperations; @@ -55,8 +73,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -68,8 +85,7 @@ public void changeMessageNotExtendedIfInvalidCodeOwnerConfigFilesExist() throws Exception { createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName()); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -87,8 +103,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -100,7 +115,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -113,8 +128,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -134,7 +148,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -163,8 +177,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -176,7 +189,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); // Apply the Code-Review+1 approval again and add an unrelated vote (Code-Review+1 is ignored). ReviewInput reviewInput = ReviewInput.recommend(); @@ -213,8 +226,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -226,13 +238,13 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); // Apply the Code-Review+1 approval again and add a comment (Code-Review +1 is ignored) ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput(); commentInput.line = 1; commentInput.message = "some comment"; - commentInput.path = path; + commentInput.path = TEST_PATH; ReviewInput reviewInput = ReviewInput.recommend(); reviewInput.comments = new HashMap<>(); reviewInput.comments.put(commentInput.path, Lists.newArrayList(commentInput)); @@ -254,8 +266,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -267,7 +278,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); // Apply the Code-Review+1 by another code owner requestScopeOperations.setApiUser(user.id()); @@ -281,7 +292,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); } @Test @@ -294,8 +305,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -310,7 +320,7 @@ + "By voting Code-Review+2 the following files are still code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -326,8 +336,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); approve(changeId); @@ -342,7 +351,7 @@ + "By voting Code-Review+1 the following files are still explicitly code-owner" + " approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -358,8 +367,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -374,7 +382,7 @@ + "By voting Code-Review+2 the following files are still explicitly code-owner" + " approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -387,8 +395,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); approve(changeId); @@ -403,7 +410,7 @@ + "By voting Code-Review+1 the following files are still code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -416,8 +423,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -432,7 +438,7 @@ + "By removing the Code-Review vote the following files are no longer" + " code-owner approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -446,8 +452,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -462,7 +467,7 @@ + "By voting Code-Review-1 the following files are no longer code-owner" + " approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -475,15 +480,17 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; + String testPath1 = "foo/bar.baz"; + String testPath1Escaped = "`foo/bar.baz`"; + String testPath2 = "foo/baz.bar"; + String testPath2Escaped = "`foo/baz.bar`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", "bar/foo.baz", "file content", @@ -502,7 +509,9 @@ + " %s:\n" + "* %s\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path1, path2)); + AccountTemplateUtil.getAccountTemplate(admin.id()), + testPath1Escaped, + testPath2Escaped)); } @Test @@ -517,6 +526,7 @@ // createChangeWithFileRename creates a change with 2 patch sets String oldPath = "foo/bar.baz"; + String oldPathEscaped = "`foo/bar.baz`"; String newPath = "bar/baz.bar"; String changeId = createChangeWithFileRename(oldPath, newPath); @@ -530,7 +540,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), oldPath)); + AccountTemplateUtil.getAccountTemplate(admin.id()), oldPathEscaped)); } @Test @@ -570,7 +580,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -590,8 +600,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -603,7 +612,7 @@ + "By voting Code-Review+1 the following files are now explicitly code-owner" + " approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -618,8 +627,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -637,7 +645,7 @@ + "\n" + "The listed files are still implicitly approved by %s.\n", AccountTemplateUtil.getAccountTemplate(admin.id()), - path, + TEST_PATH_ESCAPED, AccountTemplateUtil.getAccountTemplate(admin.id()))); } @@ -653,8 +661,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -672,7 +679,7 @@ + "\n" + "The listed files are still implicitly approved by %s.\n", AccountTemplateUtil.getAccountTemplate(admin.id()), - path, + TEST_PATH_ESCAPED, AccountTemplateUtil.getAccountTemplate(admin.id()))); } @@ -688,8 +695,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange(user, "Test Change", path, "file content").getChangeId(); + String changeId = createChange(user, "Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -701,7 +707,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -717,8 +723,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange(user, "Test Change", path, "file content").getChangeId(); + String changeId = createChange(user, "Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -733,7 +738,7 @@ + "By removing the Code-Review vote the following files are no longer" + " code-owner approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -749,8 +754,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange(user, "Test Change", path, "file content").getChangeId(); + String changeId = createChange(user, "Test Change", TEST_PATH, "file content").getChangeId(); recommend(changeId); @@ -765,7 +769,7 @@ + "By voting Code-Review-1 the following files are no longer code-owner" + " approved by %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -779,21 +783,25 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; - String path3 = "bar/foo.baz"; - String path4 = "bar/baz.foo"; + String testPath1 = "foo/bar.baz"; + String testPath1Escaped = "`foo/bar.baz`"; + String testPath2 = "foo/baz.bar"; + String testPath2Escaped = "`foo/baz.bar`"; + String testPath3 = "bar/foo.baz"; + String testPath3Escaped = "`bar/foo.baz`"; + String testPath4 = "bar/baz.foo"; + String testPath4Escaped = "`bar/baz.foo`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", - path3, + testPath3, "file content", - path4, + testPath4, "file content")) .getChangeId(); @@ -810,7 +818,11 @@ + "* %s\n" + "* %s\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path4, path3, path1, path2)); + AccountTemplateUtil.getAccountTemplate(admin.id()), + testPath4Escaped, + testPath3Escaped, + testPath1Escaped, + testPath2Escaped)); } @Test @@ -824,24 +836,27 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path1 = "foo/bar.baz"; - String path2 = "foo/baz.bar"; - String path3 = "bar/foo.baz"; - String path4 = "bar/baz.foo"; - String path5 = "baz/foo.bar"; + String testPath1 = "foo/bar.baz"; + String testPath2 = "foo/baz.bar"; + String testPath3 = "bar/foo.baz"; + String testPath3Escaped = "`bar/foo.baz`"; + String testPath4 = "bar/baz.foo"; + String testPath4Escaped = "`bar/baz.foo`"; + String testPath5 = "baz/foo.bar"; + String testPath5Escaped = "`baz/foo.bar`"; String changeId = createChange( "Test Change", ImmutableMap.of( - path1, + testPath1, "file content", - path2, + testPath2, "file content", - path3, + testPath3, "file content", - path4, + testPath4, "file content", - path5, + testPath5, "file content")) .getChangeId(); @@ -858,7 +873,10 @@ + "* %s\n" + "* %s\n" + "(more files)\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path4, path3, path5)); + AccountTemplateUtil.getAccountTemplate(admin.id()), + testPath4Escaped, + testPath3Escaped, + testPath5Escaped)); } @Test @@ -903,13 +921,12 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput(); commentInput.line = 1; commentInput.message = "some comment"; - commentInput.path = path; + commentInput.path = TEST_PATH; ReviewInput reviewInput = ReviewInput.recommend(); reviewInput.comments = new HashMap<>(); reviewInput.comments.put(commentInput.path, Lists.newArrayList(commentInput)); @@ -924,7 +941,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -937,7 +954,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // create a second patch set amendChange(changeId); @@ -1069,8 +1086,7 @@ input.ignoreSelfApproval = true; gApi.projects().name(allProjects.get()).label("Code-Review").update(input); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); requestScopeOperations.setApiUser(user.id()); recommend(changeId); @@ -1083,7 +1099,7 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(user.id()), path)); + AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED)); } @Test @@ -1096,8 +1112,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // Apply the Code-Review-1. gApi.changes().id(changeId).current().review(ReviewInput.dislike()); @@ -1120,8 +1135,7 @@ .addCodeOwnerEmail(user.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); // Do the voting as a different user to trigger an email notification (if the only recipient is // also the sender the email is omitted). @@ -1141,6 +1155,94 @@ + "By voting Code-Review+1 the following files are now code-owner approved by" + " %s <%s>:\n" + "* %s\n", - user.fullName(), user.email(), path)); + user.fullName(), user.email(), TEST_PATH_ESCAPED)); + } + + @Test + public void markdownCharactersInPathsAreEscaped() throws Exception { + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(user.email()) + .create(); + + + testMarkdownCharactersInPathsAreEscaped('`', user); + } + + private void testMarkdownCharactersInPathsAreEscaped( + char markdownCharacter, TestAccount codeOwner) throws Exception { + String testPath = markdownCharacter + "foo" + markdownCharacter + ".bar"; + String testPathEscaped = "`\\" + markdownCharacter + "foo\\" + markdownCharacter + ".bar`"; + + String changeId = createChange("Test Change", testPath, "file content").getChangeId(); + + requestScopeOperations.setApiUser(user.id()); + recommend(changeId); + + Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages; + assertThat(Iterables.getLast(messages).message) + .isEqualTo( + String.format( + "Patch Set 1: Code-Review+1\n\n" + + "By voting Code-Review+1 the following files are now code-owner approved by" + + " %s:\n" + + "* %s\n", + AccountTemplateUtil.getAccountTemplate(codeOwner.id()), testPathEscaped)); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "true") + public void changeMessageListsNewlyApprovedPaths_async() throws Exception { + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(admin.email()) + .create(); + + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); + + int numberOfChangeMessages = gApi.changes().id(changeId).get().messages.size(); + + recommend(changeId); + + // expect that 2 changes messages are posted, one for applying the approval and one to inform + // about the owned paths + int expectedNumberOfChangeMessages = numberOfChangeMessages + 2; + + assertAsyncChangeMessage( + changeId, + String.format( + "By voting Code-Review+1 the following files are now code-owner approved by" + + " %s:\n" + + "* %s\n", + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED), + expectedNumberOfChangeMessages); + } + + private void assertAsyncChangeMessage( + String changeId, String expectedChangeMessage, int expectedNumberOfChangeMessages) + throws Exception { + assertAsync( + () -> { + Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages; + assertThat(Iterables.getLast(messages).message).isEqualTo(expectedChangeMessage); + assertThat(messages).hasSize(expectedNumberOfChangeMessages); + return null; + }); + } + + @CanIgnoreReturnValue + private <T> T assertAsync(Callable<T> assertion) throws Exception { + return RetryerBuilder.<T>newBuilder() + .retryIfException(t -> true) + .withStopStrategy( + StopStrategies.stopAfterDelay(Duration.ofSeconds(3).toMillis(), MILLISECONDS)) + .build() + .call(() -> assertion.call()); } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java index eaf507e..9997432 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
@@ -42,6 +42,9 @@ /** Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerOverride}. */ public class OnCodeOwnerOverrrideIT extends AbstractCodeOwnersIT { + private static String TEST_PATH = "foo/bar.baz"; + private static String TEST_PATH_ESCAPED = "`foo/bar.baz`"; + @Inject private RequestScopeOperations requestScopeOperations; @Inject private ProjectOperations projectOperations; @@ -289,13 +292,12 @@ throws Exception { createOwnersOverrideLabel(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput(); commentInput.line = 1; commentInput.message = "some comment"; - commentInput.path = path; + commentInput.path = TEST_PATH; ReviewInput reviewInput = new ReviewInput(); reviewInput.labels = new HashMap<>(); reviewInput.labels.put("Owners-Override", (short) 1); @@ -401,8 +403,7 @@ createOwnersOverrideLabel(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); ReviewInput reviewInput = new ReviewInput(); reviewInput.labels = new HashMap<>(); @@ -431,7 +432,7 @@ "By voting Code-Review+1 the following files are now code-owner approved by" + " %s:\n" + "* %s\n", - AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED)); } @Test @@ -632,8 +633,7 @@ .addCodeOwnerEmail(admin.email()) .create(); - String path = "foo/bar.baz"; - String changeId = createChange("Test Change", path, "file content").getChangeId(); + String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId(); gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", -1));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java index 463d623..0eb2793 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java
@@ -15,14 +15,19 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigSubject.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.AtomicLongMap; +import com.google.gerrit.acceptance.ExtensionRegistry; +import com.google.gerrit.acceptance.ExtensionRegistry.Registration; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; @@ -31,6 +36,7 @@ import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.Permission; import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -42,7 +48,17 @@ import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFileUpdateScanner; import com.google.gerrit.plugins.codeowners.restapi.RenameEmail; import com.google.inject.Inject; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.Before; import org.junit.Test; @@ -58,6 +74,7 @@ @Inject private AccountOperations accountOperations; @Inject private ProjectOperations projectOperations; @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ExtensionRegistry extensionRegistry; private CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner; @@ -288,7 +305,7 @@ projectOperations .project(allProjects) .forUpdate() - .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS)) + .add(allowCapability(GlobalCapability.VIEW_SECONDARY_EMAILS).group(REGISTERED_USERS)) .update(); String secondaryEmail = "admin-foo@example.com"; @@ -298,8 +315,12 @@ RenameEmailInput input = new RenameEmailInput(); input.oldEmail = admin.email(); input.newEmail = secondaryEmail; - RenameEmailResultInfo result = renameEmail(project, "master", input); - assertThat(result.commit).isNotNull(); + RefUpdateCounter refUpdateCounter = new RefUpdateCounter(); + try (Registration registration = extensionRegistry.newRegistration().add(refUpdateCounter)) { + RenameEmailResultInfo result = renameEmail(project, "master", input); + assertThat(result.commit).isNotNull(); + } + refUpdateCounter.assertRefUpdateFor(RefUpdateCounter.projectRef(project, "refs/heads/master")); assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get()) .hasCodeOwnerSetsThat() @@ -342,8 +363,12 @@ RenameEmailInput input = new RenameEmailInput(); input.oldEmail = admin.email(); input.newEmail = secondaryEmail; - RenameEmailResultInfo result = renameEmail(project, "master", input); - assertThat(result.commit).isNotNull(); + RefUpdateCounter refUpdateCounter = new RefUpdateCounter(); + try (Registration registration = extensionRegistry.newRegistration().add(refUpdateCounter)) { + RenameEmailResultInfo result = renameEmail(project, "master", input); + assertThat(result.commit).isNotNull(); + } + refUpdateCounter.assertRefUpdateFor(RefUpdateCounter.projectRef(project, "refs/heads/master")); assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get()) .hasCodeOwnerSetsThat() @@ -668,7 +693,7 @@ } @Test - public void renameEmail_emailThatContainsEmailToBeReplacesAsSubstringStaysIntact() + public void renameEmail_emailThatContainsEmailToBeReplacedAsSubstringStaysIntact() throws Exception { skipTestIfRenameEmailNotSupportedByCodeOwnersBackend(); @@ -719,6 +744,122 @@ secondaryEmail, otherUser1.email(), otherUser2.email(), otherUser3.email()); } + @Test + public void renameEmailDoesNotTouchCodeOwnerConfigsThatDoNotContainTheEmail() throws Exception { + skipTestIfRenameEmailNotSupportedByCodeOwnersBackend(); + + TestAccount user2 = accountCreator.user2(); + + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .addCodeOwnerEmail(user.email()) + .create(); + + // Create a code owner config that doesn't contain the email to be replaced. + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(user2.email()) + .create(); + + // grant all users direct push permissions + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS)) + .update(); + + String secondaryEmail = "user-foo@example.com"; + accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update(); + + requestScopeOperations.setApiUser(user.id()); + RenameEmailInput input = new RenameEmailInput(); + input.oldEmail = user.email(); + input.newEmail = secondaryEmail; + RenameEmailResultInfo result = renameEmail(project, "master", input); + assertThat(result.commit).isNotNull(); + + assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get()) + .hasCodeOwnerSetsThat() + .onlyElement() + .hasCodeOwnersEmailsThat() + .containsExactly(secondaryEmail, admin.email()); + + // Check that the second code owner config is still intact. + assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get()) + .hasCodeOwnerSetsThat() + .onlyElement() + .hasCodeOwnersEmailsThat() + .containsExactly(user2.email()); + } + + @Test + public void renameEmailDoesNotTouchNonCodeOwnerConfigFiles() throws Exception { + skipTestIfRenameEmailNotSupportedByCodeOwnersBackend(); + + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .addCodeOwnerEmail(user.email()) + .create(); + + // Create non code owner config files. + String contentFileA = "some content"; + String contentFileB = + String.format( + "some content that contains the email %s that is being renamed", user.email()); + try (TestRepository<Repository> testRepo = + new TestRepository<>(repoManager.openRepository(project))) { + testRepo.update( + "master", + testRepo + .commit() + .message("Update project.config from test") + .parent(projectOperations.project(project).getHead("master")) + .add("A", contentFileA) + .add("B", contentFileB)); + } + + // grant all users direct push permissions + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS)) + .update(); + + String secondaryEmail = "user-foo@example.com"; + accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update(); + + requestScopeOperations.setApiUser(user.id()); + RenameEmailInput input = new RenameEmailInput(); + input.oldEmail = user.email(); + input.newEmail = secondaryEmail; + RenameEmailResultInfo result = renameEmail(project, "master", input); + assertThat(result.commit).isNotNull(); + + assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get()) + .hasCodeOwnerSetsThat() + .onlyElement() + .hasCodeOwnersEmailsThat() + .containsExactly(secondaryEmail, admin.email()); + + // Check that the non code owner config files are still intact. + assertThat(getFileContent(project, "master", "A")).hasValue(contentFileA); + assertThat(getFileContent(project, "master", "B")).hasValue(contentFileB); + } + private RenameEmailResultInfo renameEmail( Project.NameKey projectName, String branchName, RenameEmailInput input) throws RestApiException { @@ -732,4 +873,62 @@ // the proto backend doesn't support renaming emails assumeThatCodeOwnersBackendIsNotProtoBackend(); } + + private Optional<String> getFileContent(Project.NameKey project, String branch, String fileName) { + try (Repository repo = repoManager.openRepository(project); + RevWalk rw = new RevWalk(repo)) { + if (!branch.startsWith(Constants.R_REFS)) { + branch = Constants.R_HEADS + branch; + } + Ref ref = repo.exactRef(branch); + if (ref == null) { + return Optional.empty(); + } + RevTree tree = rw.parseTree(ref.getObjectId()); + TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), fileName, tree); + if (tw == null) { + return Optional.empty(); + } + ObjectLoader loader = rw.getObjectReader().open(tw.getObjectId(0)); + String fileContent = new String(loader.getCachedBytes(), UTF_8); + return Optional.of(fileContent); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static class RefUpdateCounter implements GitReferenceUpdatedListener { + private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create(); + + static String projectRef(Project.NameKey project, String ref) { + return projectRef(project.get(), ref); + } + + static String projectRef(String project, String ref) { + return project + ":" + ref; + } + + @Override + public void onGitReferenceUpdated(Event event) { + countsByProjectRefs.incrementAndGet(projectRef(event.getProjectName(), event.getRefName())); + } + + void clear() { + countsByProjectRefs.clear(); + } + + void assertRefUpdateFor(String... projectRefs) { + Map<String, Long> expectedRefUpdateCounts = new HashMap<>(); + for (String projectRef : projectRefs) { + expectedRefUpdateCounts.put(projectRef, 1L); + } + assertRefUpdateFor(expectedRefUpdateCounts); + } + + void assertRefUpdateFor(Map<String, Long> expectedProjectRefUpdateCounts) { + assertThat(countsByProjectRefs.asMap()) + .containsExactlyEntriesIn(expectedProjectRefUpdateCounts); + clear(); + } + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java index 17d481c..977af94 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java
@@ -19,13 +19,13 @@ import static com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject.assertThatCollection; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; import com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject; import com.google.inject.Inject; @@ -42,14 +42,14 @@ @TestPlugin( name = "code-owners", sysModule = "com.google.gerrit.plugins.codeowners.acceptance.TestBatchModule") -public class CodeOwnerSubmitRuleBatchIT extends LightweightPluginDaemonTest { +public class CodeOwnerSubmitRuleBatchIT extends AbstractCodeOwnersTest { @Inject private ProjectOperations projectOperations; @Inject private RequestScopeOperations requestScopeOperations; private CodeOwnerConfigOperations codeOwnerConfigOperations; @Before - public void testSetup() throws Exception { + public void setUpCodeOwnersPlugin() throws Exception { codeOwnerConfigOperations = plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java index 5c31eed..b6c3a43 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java
@@ -52,7 +52,7 @@ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ @ConfigSuite.Default public static Config defaultConfig() { - Config cfg = new Config(); + Config cfg = AbstractCodeOwnersTest.defaultConfig(); cfg.setEnum( "plugin", "code-owners",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java index a665ad4..43ab903 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
@@ -46,7 +46,7 @@ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ @ConfigSuite.Default public static Config defaultConfig() { - Config cfg = new Config(); + Config cfg = AbstractCodeOwnersTest.defaultConfig(); cfg.setString( "plugin", "code-owners", OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL, "Owners-Override+1"); return cfg;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java index fc73235..28f83f5 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java
@@ -55,7 +55,7 @@ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ @ConfigSuite.Default public static Config defaultConfig() { - Config cfg = new Config(); + Config cfg = AbstractCodeOwnersTest.defaultConfig(); cfg.setBoolean( "plugin", "code-owners", GeneralConfig.KEY_ENABLE_STICKY_APPROVALS, /* value= */ true); return cfg;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java new file mode 100644 index 0000000..b6efbc6 --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import org.junit.Test; + +/** Tests for {@link CodeOwnerConfigImport}. */ +public class CodeOwnerConfigImportTest extends AbstractAutoValueTest { + @Test + public void toStringIncludesAllData_resolvedImport() throws Exception { + CodeOwnerConfigImport resolvedImport = + CodeOwnerConfigImport.createResolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")); + assertThatToStringIncludesAllData(resolvedImport, CodeOwnerConfigImport.class); + } + + @Test + public void toStringIncludesAllData_unresolvedImport() throws Exception { + CodeOwnerConfigImport unresolvedImport = + CodeOwnerConfigImport.createUnresolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), + "test message"); + assertThatToStringIncludesAllData(unresolvedImport, CodeOwnerConfigImport.class); + } +}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java index e3e9e6a..433922f 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java
@@ -60,7 +60,7 @@ } @Test - public void getProject() throws Exception { + public void testGetProject() throws Exception { Project.NameKey project = Project.nameKey("project"); CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/foo/bar/");
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java index 18ea017..deab817 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
@@ -29,7 +29,17 @@ /* annotations= */ ImmutableMultimap.of(), /* ownedByAllUsers= */ false, /* hasUnresolvedCodeOwners= */ false, - /* hasUnresolvedImports= */ false, + ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))), + ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), + "test message")), ImmutableList.of("test message")); assertThatToStringIncludesAllData(codeOwnerResolverResult, CodeOwnerResolverResult.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java index b32c3b5..6171aca 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -30,7 +30,7 @@ import com.google.gerrit.server.ServerInitiated; import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.externalids.ExternalIdFactory; -import com.google.gerrit.server.account.externalids.ExternalIdNotes; +import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.inject.Inject; import com.google.inject.Key; @@ -257,7 +257,8 @@ .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", + "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 @@ -273,7 +274,8 @@ .hasMessagesThat() .contains( String.format( - "resolved code owner email %s: account %s is referenced by secondary email and user %s can see secondary emails", + "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. @@ -287,7 +289,8 @@ .hasMessagesThat() .contains( String.format( - "email %s is visible to the calling user %s: email is a secondary email that is owned by this user", + "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. @@ -302,7 +305,8 @@ .hasMessagesThat() .contains( String.format( - "email %s is visible to user %s: email is a secondary email that is owned by this user", + "email %s is visible to user %s: email is a secondary email that is owned by this" + + " user", secondaryEmail, user.username())); } @@ -324,7 +328,8 @@ .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", + "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 @@ -340,7 +345,8 @@ .hasMessagesThat() .contains( String.format( - "cannot resolve code owner email %s: account %s is referenced by secondary email but user %s cannot see secondary emails", + "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())); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java index d270fdc..d9b3a71 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
@@ -27,19 +27,27 @@ @Test public void toStringIncludesAllData() throws Exception { CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/"); - CodeOwnerConfigReference codeOwnerConfigReference = + CodeOwnerConfigReference resolvableCodeOwnerConfigReference = CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"); + CodeOwnerConfigReference unresolvableCodeOwnerConfigReference = + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS"); PathCodeOwnersResult pathCodeOwnersResult = PathCodeOwnersResult.create( Paths.get("/foo/bar/baz.md"), CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION) - .addImport(codeOwnerConfigReference) + .addImport(resolvableCodeOwnerConfigReference) + .addImport(unresolvableCodeOwnerConfigReference) .build(), ImmutableList.of( - UnresolvedImport.create( + CodeOwnerConfigImport.createResolvedImport( codeOwnerConfigKey, CodeOwnerConfig.Key.create(project, "master", "/bar/"), - codeOwnerConfigReference, + resolvableCodeOwnerConfigReference)), + ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + codeOwnerConfigKey, + CodeOwnerConfig.Key.create(project, "master", "/baz/"), + unresolvableCodeOwnerConfigReference, "test message"))); assertThatToStringIncludesAllData(pathCodeOwnersResult, PathCodeOwnersResult.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java index 9563ade..04df442 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -239,7 +239,8 @@ pathCodeOwnersFactory.createWithoutCache( emptyCodeOwnerConfig, Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty(); - assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().hasUnresolvedImports()).isFalse(); + assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().resolvedImports()).isEmpty(); + assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().unresolvedImports()).isEmpty(); } @Test @@ -471,6 +472,11 @@ @Test public void nonResolveableImportIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with non-resolveable import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -478,9 +484,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.ALL, "/non-existing/OWNERS")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -494,24 +498,40 @@ // Expectation: we get the global code owner from the importing code owner config, the // non-resolveable import is silently ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test public void importOfNonCodeOwnerConfigFileIsIgnored() throws Exception { // create a file that looks like a code owner config file, but which has a name that is not // allowed as code owner config file - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("FOO") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("FOO") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create config with import of non code owner config file CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -520,7 +540,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -538,7 +558,19 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test @@ -548,14 +580,17 @@ // parameter fileExtension) or file extensions are enabled for code owner config files (config // paramater enableCodeOwnerConfigFilesWithFileExtensions). Both is not the case here, hence any // import of this file in another code owner config file should get ignored. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("OWNERS.foo") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("OWNERS.foo") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create the importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -564,8 +599,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -584,7 +618,19 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test @@ -595,14 +641,17 @@ // Create a code owner config file with a file extension. This file is considered as a code // owner config file since file extensions for code owner config files are enabled (paramater // enableCodeOwnerConfigFilesWithFileExtensions). - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("OWNERS.FOO") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("OWNERS.FOO") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create the importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -611,8 +660,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -630,7 +678,13 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -644,6 +698,19 @@ } private void testImportGlobalCodeOwners(CodeOwnerConfigImportMode importMode) throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -652,18 +719,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -674,17 +732,40 @@ // Expectation: we get the global code owners from the importing and the imported code owner // config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importPerFileCodeOwners_importModeAll() throws Exception { + // create imported config with matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -695,44 +776,53 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the matching per-file code owners from the importing and the imported // code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void nonMatchingPerFileCodeOwnersAreNotImported_importModeAll() throws Exception { + // create imported config with non-matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.txt") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -743,27 +833,13 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with non-matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.txt") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -771,17 +847,41 @@ // Expectation: we only get the matching per-file code owners from the importing code owner // config, the per-file code owners from the imported code owner config are not relevant since // they do not match - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void perFileCodeOwnersAreNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -792,28 +892,13 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -821,48 +906,57 @@ // Expectation: we only get the matching per-file code owners from the importing code owner // config, the matching per-file code owners from the imported code owner config are not // relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importIgnoreGlobalAndParentCodeOwnersFlagFromMatchingPerFileCodeOwnerSet_importModeAll() throws Exception { + // create imported config with matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -872,50 +966,58 @@ // the matching per-file code owner set in the imported code owner config has the // ignoreGlobalAndParentCodeOwners flag set to true which causes global code owners to be // ignored, in addition this flag causes parent code owners to be ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isTrue(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreGlobalAndParentCodeOwnersFlagIsNotImportedFromNonMatchingPerFileCodeOwnerSet_importModeAll() throws Exception { + // create imported config with non-matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.txt") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with non-matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.txt") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -924,50 +1026,58 @@ // per-file code owners from the imported code owner config and its // ignoreGlobalAndParentCodeOwners flag are not relevant since the per-file code owner set does // not match - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreGlobalAndParentCodeOwnersFlagIsNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -976,89 +1086,114 @@ // matching per-file code owners from the imported code owner config and its // ignoreGlobalAndParentCodeOwners flag are not relevant with import mode // GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importIgnoreParentCodeOwnersFlag_importModeAll() throws Exception { + // create imported config with the ignoreParentCodeOnwers flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with the ignoreParentCodeOnwers flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: ignoreParentCodeOwners is true because the ignoreParentCodeOwners flag in the // imported code owner config is set to true - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isTrue(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreParentCodeOwnersFlagNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with the ignoreParentCodeOnwers flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with the ignoreParentCodeOnwers flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: ignoreParentCodeOwners is false because the ignoreParentCodeOwners flag in the // imported code owner config is not relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1077,51 +1212,69 @@ throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig2); + + // create imported config with global code owner and import + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig1); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and import - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/baz/OWNERS")) - .create(); - - // create config with global code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerEmail(user2.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config, the imported code // owner config and the code owner config that is imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1130,47 +1283,54 @@ throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with per file code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + + // create imported config with global code owner and import with import mode ALL + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport( + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2)) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with global code owner and import with import mode // GLOBAL_CODE_OWNER_SETS_ONLY - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and import with import mode ALL - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create config with per file code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -1178,11 +1338,23 @@ // Expectation: we get the global owners from the importing code owner config and the imported // code owner config but not the per file code owner from the code owner config that is imported // by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfImportedCodeOwnerConfig2))); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1197,6 +1369,21 @@ private void testImportCodeOwnerConfigWithNameExtension(String nameOfImportedCodeOwnerConfig) throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .fileName(nameOfImportedCodeOwnerConfig) + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1205,22 +1392,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, - "/bar/" + nameOfImportedCodeOwnerConfig)) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .fileName(nameOfImportedCodeOwnerConfig) - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -1231,47 +1405,74 @@ // Expectation: we get the global code owners from the importing and the imported code owner // config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void cyclicImports() throws Exception { + CodeOwnerConfigReference codeOwnerConfigReference2 = + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS"); + + // create imported config with global code owner and that imports the importing config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") + .fileName("OWNERS") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and that imports the importing config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS")) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig, + importingCodeOwnerConfigKey, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1327,72 +1528,91 @@ @Test public void importWithRelativePath() throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/foo/bar/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "../baz/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/baz/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importFromNonExistingProjectIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(Project.nameKey("non-existing"), "master", "/bar/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import from non-existing project - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(Project.nameKey("non-existing")) - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format("project %s not found", keyOfImportedCodeOwnerConfig.project()))); } @Test @@ -1402,32 +1622,33 @@ ConfigInput configInput = new ConfigInput(); configInput.state = ProjectState.HIDDEN; gApi.projects().name(hiddenProject.get()).config(configInput); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(hiddenProject) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(hiddenProject) + .branch("master") + .folderPath("/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create importing config with global code owner and import from the hidden project - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/OWNERS") - .setProject(hiddenProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); @@ -1435,85 +1656,111 @@ // Expectation: we get the global owners from the importing code owner config, the global code // owners from the imported code owner config are ignored since the project that contains the // code owner config is hidden - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "state of project %s doesn't permit read", + keyOfImportedCodeOwnerConfig.project()))); } @Test public void importFromNonExistingBranchIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "non-existing", "/bar/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import from non-existing branch - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(project) - .setBranch("non-existing") - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + "code owner config does not exist (revision = current)")); } @Test public void importFromOtherProject() throws Exception { Project.NameKey otherProject = projectOperations.newProject().create(); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1527,43 +1774,52 @@ // Create other branches in other project. createBranch(BranchNameKey.create(otherProject, branchName)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch(branchName) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") + .setProject(otherProject) + .build(); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch(branchName) .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch(branchName) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead(branchName), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1572,44 +1828,50 @@ String otherBranch = "foo"; createBranch(BranchNameKey.create(project, otherBranch)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(otherBranch) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(project) - .setBranch(otherBranch) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(otherBranch) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1620,48 +1882,60 @@ String otherBranch = "foo"; createBranch(BranchNameKey.create(otherProject, otherBranch)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch(otherBranch) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .setBranch(otherBranch) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch(otherBranch) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void nonResolveablePerFileImportIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with non-resolveable per file import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1673,10 +1947,7 @@ CodeOwnerSet.builder() .addCodeOwnerEmail(admin.email()) .addPathExpression("foo.md") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, - "/non-existing/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); @@ -1690,16 +1961,50 @@ // Expectation: we get the per file code owner from the importing code owner config, the // non-resolveable per file import is silently ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test public void perFileImport() throws Exception { TestAccount user2 = accountCreator.user2(); + // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file + // code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .addCodeOwnerEmail(user.email()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with per code owner and per file import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1711,28 +2016,10 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); - // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file - // code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .addCodeOwnerEmail(user.email()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -1744,23 +2031,58 @@ // Expectation: we get the per file code owners from the importing and the global code owner // from the imported code owner config, but not the per file code owner from the imported code // owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); // Expectation: the ignoreParentCodeOwners flag from the imported code owner config is ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importsOfPerFileImportedCodeOwnerConfigAreResolved() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config with global code owner and global import + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with per file code owner and per file import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -1770,56 +2092,75 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .build()) .create(); - // create imported config with global code owner and global import - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/baz/OWNERS")) - .create(); - - // create config with global code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerEmail(user2.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config, the imported code // owner config and the code owner config that is imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void onlyGlobalCodeOwnersAreImportedForTransitivePerFileImports() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with per file code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + + // create imported config with per global owner and global import with mode ALL + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport( + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2)) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with per file code owner and per file import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -1829,39 +2170,14 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .build()) .create(); - // create imported config with per global owner and global import with mode ALL - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create config with per file code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -1869,104 +2185,156 @@ // Expectation: we get the global owners from the importing code owner config and the imported // code owner config, but not the per file code owner from the code owner config that is // imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfImportedCodeOwnerConfig2))); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void onlyMatchingTransitivePerFileImportsAreImported() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config for *.md files + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/md/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create config with global code owner that is imported by the imported config for *.txt files + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig3 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/txt/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference3 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig3); + + // create imported config with 2 per file imports, one for *.md files and one for *.txt + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addImport(codeOwnerConfigReference2) + .build()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.txt") + .addImport(codeOwnerConfigReference3) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with global import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with 2 per file imports, one for *.md files and one for *.txt - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/md/OWNERS")) - .build()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.txt") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/txt/OWNERS")) - .build()) - .create(); - - // create config with global code owner that is imported by the imported config for *.md files - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/md/") - .addCodeOwnerEmail(user.email()) - .create(); - - // create config with global code owner that is imported by the imported config for *.txt files - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/txt/") - .addCodeOwnerEmail(user2.email()) - .create(); - // Expectation for foo.xyz file: code owners is empty since foo.xyz neither matches *.md nor // *.txt Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.xyz")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()).isEmpty(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); // Expectation for foo.md file: code owners contains only user since foo.md only matches *.md pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); // Expectation for foo.txt file: code owners contains only user2 since foo.txt only matches // *.txt pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.txt")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user2.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig3, + codeOwnerConfigReference3)); } @Test @@ -2090,6 +2458,20 @@ @Test public void perFileRuleThatIgnoresGlobalCodeOwnersCanImportGlobalCodeOwnersFromOtherFile() throws Exception { + // create imported config with a global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config that: // * has a global code owner // * has a per-file import for md files @@ -2105,21 +2487,10 @@ CodeOwnerSet.builder() .addPathExpression(testPathExpressions.matchFileType("md")) .setIgnoreGlobalAndParentCodeOwners() - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); - // create imported config with a global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2131,11 +2502,17 @@ // Expectation: we get the global code owner from the imported code owner config (since it is // imported by a matching per-file rule), the global code owner from the importing code owner // config is ignored (since the matching per-file rule ignores parent and global code owners) - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2146,6 +2523,24 @@ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null); + // create imported config that has a matching per-file rule + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("md")) + .addCodeOwnerEmail(user3.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config that has a global import with mode ALL and a per-file rule for md // files that ignores global and parent code owners CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -2155,8 +2550,7 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet( CodeOwnerSet.builder() .addPathExpression(testPathExpressions.matchFileType("md")) @@ -2165,20 +2559,6 @@ .build()) .create(); - // create imported config that has a matching per-file rule - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user2.email()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("md")) - .addCodeOwnerEmail(user3.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2191,11 +2571,17 @@ // owner config and the code owner from the matching per-file rule in the imported code owner // config, the global code owners are ignored since there is a matching per-file rule that // ignores parent and global code owners - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email(), user3.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2204,6 +2590,35 @@ Project.NameKey otherProject = projectOperations.newProject().create(); + // create transitively imported config in other project + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config in other project + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -2212,32 +2627,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config in other project - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create transitively imported config in other project - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2248,11 +2640,21 @@ // Expectation: we get the global owners from the importing code owner config and from the // directly and transitively imported code owner configs in the other project - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2261,6 +2663,35 @@ Project.NameKey otherProject = projectOperations.newProject().create(); + // create transitively imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/baz/") + .fileName("OWNERS") + .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -2269,32 +2700,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "baz/OWNERS")) - .create(); - - // create transitively imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/baz/") - .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2305,11 +2713,21 @@ // Expectation: we get the global owners from the importing code owner config and from the // directly and transitively imported code owner configs + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } private CodeOwnerConfig.Builder createCodeOwnerBuilder() {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java deleted file mode 100644 index ce0494f..0000000 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java +++ /dev/null
@@ -1,31 +0,0 @@ -// Copyright (C) 2021 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.plugins.codeowners.backend; - -import org.junit.Test; - -/** Tests for {@link UnresolvedImport}. */ -public class UnresolvedImportTest extends AbstractAutoValueTest { - @Test - public void toStringIncludesAllData() throws Exception { - UnresolvedImport unresolvedImport = - UnresolvedImport.create( - CodeOwnerConfig.Key.create(project, "master", "/"), - CodeOwnerConfig.Key.create(project, "master", "/bar/"), - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), - "test message"); - assertThatToStringIncludesAllData(unresolvedImport, UnresolvedImport.class); - } -}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java index 970aa74..17663b2 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -18,6 +18,7 @@ import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER; +import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_CODE_OWNER_CONFIG_FILES_WITH_FILE_EXTENSIONS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_STICKY_APPROVALS; @@ -52,12 +53,19 @@ import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy; import com.google.gerrit.server.git.validators.CommitValidationMessage; import com.google.gerrit.server.git.validators.ValidationMessage; +import com.google.gerrit.testing.ConfigSuite; import org.eclipse.jgit.lib.Config; import org.junit.Before; import org.junit.Test; /** Tests for {@link com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig}. */ public class GeneralConfigTest extends AbstractCodeOwnersTest { + @ConfigSuite.Default + public static Config defaultConfig() { + // Override the settings from AbstractCodeOwnersTest.defaultConfig(). + return new Config(); + } + private GeneralConfig generalConfig; @Before @@ -2104,4 +2112,76 @@ throws Exception { assertThat(generalConfig.enableAsyncMessageOnAddReviewer(project, new Config())).isTrue(); } + + @Test + public void cannotGetEnableAsyncMessageOnCodeOwnerApprovalForNullProject() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + generalConfig.enableAsyncMessageOnCodeOwnerApproval( + /* project= */ null, new Config())); + assertThat(npe).hasMessageThat().isEqualTo("project"); + } + + @Test + public void cannotGetEnableAsyncMessageOnCodeOwnerApprovalForNullPluginConfig() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + generalConfig.enableAsyncMessageOnCodeOwnerApproval( + project, /* pluginConfig= */ null)); + assertThat(npe).hasMessageThat().isEqualTo("pluginConfig"); + } + + @Test + public void noEnableAsyncMessageOnCodeOwnerApproval() throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void + enableAsyncMessageOnCodeOwnerApprovalConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel() + throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())) + .isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void + enableAsyncMessageOnCodeOwnerApprovalConfigurationInPluginConfigOverridesEnableAsyncMessageOnCodeOwnerApprovalConfigurationInGerritConfig() + throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, + /* subsection= */ null, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + "true"); + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, cfg)).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void invalidEnableAsyncMessageOnCodeOwnerApprovalConfigurationInPluginConfigIsIgnored() + throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, + /* subsection= */ null, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + "INVALID"); + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, cfg)).isFalse(); + } + + @Test + @GerritConfig( + name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", + value = "INVALID") + public void invalidEnableAsyncMessageOnCodeOwnerApprovalConfigurationInGerritConfigIsIgnored() + throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())).isTrue(); + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java new file mode 100644 index 0000000..ed0b301 --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
@@ -0,0 +1,388 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.restapi; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.gerrit.entities.Project; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link CodeOwnerConfigFileJson}. */ +public class CodeOwnerConfigFileJsonIT extends AbstractCodeOwnersIT { + private CodeOwnerConfigFileJson CodeOwnerConfigFileJson; + + @Before + public void setUpCodeOwnersPlugin() throws Exception { + CodeOwnerConfigFileJson = plugin.getSysInjector().getInstance(CodeOwnerConfigFileJson.class); + } + + @Test + public void cannotFormatWithNullCodeOwnerConfigKey() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + /* codeOwnerConfigKey= */ null, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of())); + assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey"); + } + + @Test + public void cannotFormatWithNullResolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/"); + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ null, + /* unresolvedImports= */ ImmutableList.of())); + assertThat(npe).hasMessageThat().isEqualTo("resolvedImports"); + } + + @Test + public void cannotFormatWithNullUnresolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/"); + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ null)); + assertThat(npe).hasMessageThat().isEqualTo("unresolvedImports"); + } + + @Test + public void formatWithoutImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/baz/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of()); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.imports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + } + + @Test + public void formatWithUnresolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + codeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + "error message"))); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.imports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.unresolvedImports).hasSize(1); + CodeOwnerConfigFileInfo unresolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.unresolvedImports); + assertThat(unresolvedImportInfo.project) + .isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(unresolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(unresolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(unresolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(unresolvedImportInfo.imports).isNull(); + assertThat(unresolvedImportInfo.unresolvedImports).isNull(); + assertThat(unresolvedImportInfo.unresolvedErrorMessage).isEqualTo("error message"); + } + + @Test + public void formatWithImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference)), + /* unresolvedImports= */ ImmutableList.of()); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(resolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(resolvedImportInfo.imports).isNull(); + assertThat(resolvedImportInfo.unresolvedImports).isNull(); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + } + + @Test + public void formatWithNestedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/baz") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig1 = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz1/"); + CodeOwnerConfigReference nestedCodeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfNestedImportedCodeOwnerConfig1); + + CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig2 = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz2/"); + CodeOwnerConfigReference nestedCodeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfNestedImportedCodeOwnerConfig2); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig, + keyOfNestedImportedCodeOwnerConfig1, + nestedCodeOwnerConfigReference1)), + /* unresolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + keyOfImportedCodeOwnerConfig, + keyOfNestedImportedCodeOwnerConfig2, + nestedCodeOwnerConfigReference2, + "error message"))); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(resolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo nestedResolvedImportInfo = + Iterables.getOnlyElement(resolvedImportInfo.imports); + assertThat(nestedResolvedImportInfo.project) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.project().get()); + assertThat(nestedResolvedImportInfo.branch) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.branchNameKey().branch()); + assertThat(nestedResolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations + .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig1) + .getFilePath()); + assertThat(nestedResolvedImportInfo.importMode) + .isEqualTo(nestedCodeOwnerConfigReference1.importMode()); + assertThat(nestedResolvedImportInfo.imports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedImports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.unresolvedImports).hasSize(1); + CodeOwnerConfigFileInfo nestedUnresolvedImportInfo1 = + Iterables.getOnlyElement(resolvedImportInfo.unresolvedImports); + assertThat(nestedUnresolvedImportInfo1.project) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.project().get()); + assertThat(nestedUnresolvedImportInfo1.branch) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.branchNameKey().branch()); + assertThat(nestedUnresolvedImportInfo1.path) + .isEqualTo( + codeOwnerConfigOperations + .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig2) + .getFilePath()); + assertThat(nestedUnresolvedImportInfo1.importMode) + .isEqualTo(nestedCodeOwnerConfigReference2.importMode()); + assertThat(nestedUnresolvedImportInfo1.imports).isNull(); + assertThat(nestedUnresolvedImportInfo1.unresolvedImports).isNull(); + assertThat(nestedUnresolvedImportInfo1.unresolvedErrorMessage).isEqualTo("error message"); + } + + @Test + public void formatWithCyclicImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey1); + + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/baz") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey2); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey1, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey1, codeOwnerConfigKey2, codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey2, codeOwnerConfigKey1, codeOwnerConfigReference2)), + /* unresolvedImports= */ ImmutableList.of()); + + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey1.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(codeOwnerConfigKey2.project().get()); + assertThat(resolvedImportInfo.branch).isEqualTo(codeOwnerConfigKey2.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference1.importMode()); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo nestedResolvedImportInfo = + Iterables.getOnlyElement(resolvedImportInfo.imports); + assertThat(nestedResolvedImportInfo.project).isEqualTo(codeOwnerConfigKey1.project().get()); + assertThat(nestedResolvedImportInfo.branch) + .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch()); + assertThat(nestedResolvedImportInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath()); + assertThat(nestedResolvedImportInfo.importMode) + .isEqualTo(codeOwnerConfigReference1.importMode()); + assertThat(nestedResolvedImportInfo.imports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedImports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull(); + } +}
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md index 80c5b59..63feaf7 100644 --- a/resources/Documentation/config.md +++ b/resources/Documentation/config.md
@@ -695,6 +695,22 @@ in `@PLUGIN@.config`.\ By default `true`. +<a id="pluginCodeOwnersEnableAsyncMessageOnCodeOwnerApproval">plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval</a> +: When a code owner approval is applied on a change the @PLUGIN@ plugin + posts a change message that lists the paths that are owned by the code + owner to inform that these paths are code owner approved now. This + setting controls whether this change message should be posted + asynchronously instead of synchronously extending the change message + that is posted for the approval by Gerrit core.\ + Posting these change messages asynchronously improves the latency for + post review, since it does not need to wait until the owned paths are + computed for the code owner (computing the owned path for a user is + rather expensive).\ + Can be overridden per project by setting + [codeOwners.enableAsyncMessageOnCodeOwnerApproval](#codeOwnersEnableAsyncMessageOnCodeOwnerApproval) + in `@PLUGIN@.config`.\ + By default `true`. + <a id="pluginCodeOwnersMaxCodeOwnerConfigCacheSize">plugin.@PLUGIN@.maxCodeOwnerConfigCacheSize</a> : When computing code owner file statuses for a change (e.g. to compute the results for the code owners submit rule) parsed code owner config @@ -1277,6 +1293,25 @@ [plugin.@PLUGIN@.enableAsyncMessageOnAddReviewer](#pluginCodeOwnersEnableAsyncMessageOnAddReviewer) in `gerrit.config` is used. +<a id="codeOwnersEnableAsyncMessageOnCodeOwnerApproval">codeOwners.enableAsyncMessageOnCodeOwnerApproval</a> +: When a code owner approval is applied on a change the @PLUGIN@ plugin + posts a change message that lists the paths that are owned by the code + owner to inform that these paths are code owner approved now. This + setting controls whether this change message should be posted + asynchronously instead of synchronously extending the change message + that is posted for the approval by Gerrit core.\ + Posting these change messages asynchronously improves the latency for + post review, since it does not need to wait until the owned paths are + computed for the code owner (computing the owned path for a user is + rather expensive).\ + Overrides the global setting + [plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval](#pluginCodeOwnersEnableAsyncMessageOnOwnerApproval) + in `gerrit.config` and the `codeOwners.enableAsyncMessageOnCodeOwnerApproval` + setting from parent projects.\ + If not set, the global setting + [plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval](#pluginCodeOwnersEnableAsyncMessageOnCodeOwnerApproval) + in `gerrit.config` is used. + --- Back to [@PLUGIN@ documentation index](index.html)
diff --git a/resources/Documentation/metrics.md b/resources/Documentation/metrics.md index b5f7765..5a54abb 100644 --- a/resources/Documentation/metrics.md +++ b/resources/Documentation/metrics.md
@@ -12,6 +12,9 @@ added as a reviewer. ** `post_type': Whether the change message was posted synchronously or asynchronously. +* `add_change_message_on_code_owner_approval`: + Latency for asynchronously adding a change message with the owned path when + a code owner approval is applied. * `compute_file_status`: Latency for computing the file status for one file. * `compute_file_statuses`: @@ -25,8 +28,8 @@ * `compute_patch_set_approvals`: Latency for computing the approvals of the current patch set. * `extend_change_message_on_post_review`: - Latency for extending the change message with the owned path when a code owner - approval is applied. + Latency for synchronously extending the change message with the owned path + when a code owner approval is applied. * `get_changed_files`: Latency for getting changed files from diff cache. * `prepare_file_status_computation`:
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md index 0185851..c97ba2c 100644 --- a/resources/Documentation/rest-api.md +++ b/resources/Documentation/rest-api.md
@@ -475,6 +475,71 @@ (visible) users is returned, as many as are needed to fill up the requested limit. +#### Request + +``` + GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0 +``` + +#### Response + +``` + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "code_owners": [ + { + "account": { + "_account_id": 1000096 + } + }, + { + "account": { + "_account_id": 1001439 + } + }, + { + "account": { + "_account_id": 1007265 + } + }, + { + "account": { + "_account_id": 1009877 + } + }, + { + "account": { + "_account_id": 1002930 + } + } + ], + "code_owner_configs": [ + { + "project": "foo/bar", + "branch": "master", + "path": "/docs/OWNERS" + }, + { + "project": "foo/bar", + "branch": "master", + "path": "/OWNERS", + "imports": [ + { + "project": "foo", + "branch": "master", + "path": "/OWNERS", + "import_mode": "ALL", + } + ] + } + ] + } +``` + #### <a id="scoringFactors">Scoring Factors The following factors are taken into account for computing the scores of the @@ -550,36 +615,6 @@ The same applies for [default code owners](config-guide.html#codeOwners). -#### Request - -``` - GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0 -``` - -#### Response - -``` - HTTP/1.1 200 OK - Content-Disposition: attachment - Content-Type: application/json; charset=UTF-8 - - )]}' - { - "code_owners": [ - { - "account": { - "_account_id": 1000096 - } - }, - { - "account": { - "_account_id": 1001439 - }, - } - ] - } -``` - #### <a id="batch-list-code-owners"> Batch Request There is no REST endpoint that allows to retrieve code owners for multiple @@ -918,6 +953,22 @@ --- +### <a id="code-owner-config-file-info"> CodeOwnerConfigFileInfo +The `CodeOwnerConfigFileInfo` entity contains information about a code owner +config file and its imports. + +| Field Name | | Description | +| ---------- | -------- | ----------- | +| `project` || The name of the project from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded. +| `branch` || The name of the branch from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded. +| `path` || The absolute path of the code owner config file. +| `imports` | optional | Imported code owner config files as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. +| `unresolved_imports` | optional | Imported code owner config files that couldn't be resolved as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. +| `unresolved_error_message` | optional | Message explaining why this code owner config couldn't be resolved. Only set if the `CodeOwnerConfigFileInfo` represents an import code owner config file that couldn't be resolved. +| `import_mode` | optional | The import mode (`ALL` or `GLOBAL_CODE_OWNER_SETS_ONLY`). Only set if the `CodeOwnerConfigFileInfo` represents an imported code owner config file. + +--- + ### <a id="code-owner-config-info"> CodeOwnerConfigInfo The `CodeOwnerConfigInfo` entity contains information about a code owner config for a path. @@ -1042,6 +1093,7 @@ | ------------- | -------- | ----------- | | `code_owners` | | List of code owners as [CodeOwnerInfo](#code-owner-info) entities. The code owners are sorted by a score that is computed from mutliple [scoring factors](#scoringFactors). | `owned_by_all_users` | optional | Whether the path is owned by all users. Not set if `false`. +| `code_owner_configs` || The code owner config files that have been inspected to gather the code owners as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. | `debug_logs` | optional | Debug logs that may help to understand why a user is or isn't suggested as a code owner. Only set if requested via `--debug`. This information is purely for debugging and the output may be changed at any time. This means bot callers must not parse the debug logs. ### <a id="file-code-owner-status-info"> FileCodeOwnerStatusInfo
diff --git a/web/code-owners-api.ts b/web/code-owners-api.ts index 8f5ace1..edad393 100644 --- a/web/code-owners-api.ts +++ b/web/code-owners-api.ts
@@ -21,7 +21,6 @@ AccountInfo, BranchName, ChangeInfo, - NumericChangeId, RepoName, } from '@gerritcodereview/typescript-api/rest-api'; @@ -150,6 +149,10 @@ owned_changed_files?: Array<OwnedChangedFileInfo>; } +function changeBaseURL(change: ChangeInfo): string { + return `/changes/${encodeURIComponent(change.project)}~${change._number}`; +} + /** * Responsible for communicating with the rest-api * @@ -186,9 +189,9 @@ * @doc * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#change-endpoints */ - listOwnerStatus(changeId: NumericChangeId): Promise<CodeOwnerStatusInfo> { + listOwnerStatus(change: ChangeInfo): Promise<CodeOwnerStatusInfo> { return this.get( - `/changes/${changeId}/code_owners.status?limit=100000` + `${changeBaseURL(change)}/code_owners.status?limit=100000` ) as Promise<CodeOwnerStatusInfo>; } @@ -196,12 +199,14 @@ * Returns a promise fetching which files are owned by a given user as well * as which reviewers own which files. */ - listOwnedPaths(changeId: NumericChangeId, account: AccountInfo) { + listOwnedPaths(change: ChangeInfo, account: AccountInfo) { if (!account.email && !account._account_id) return Promise.resolve(undefined); const user = account.email ?? account._account_id; return this.get( - `/changes/${changeId}/revisions/current/owned_paths?user=${user}&limit=10000&check_reviewers` + `${changeBaseURL( + change + )}/revisions/current/owned_paths?user=${user}&limit=10000&check_reviewers` ) as Promise<OwnedPathsInfo>; } @@ -212,12 +217,12 @@ * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch */ listOwnersForPath( - changeId: NumericChangeId, + change: ChangeInfo, path: string, limit: number ): Promise<CodeOwnersInfo> { return this.get( - `/changes/${changeId}/revisions/current/code_owners` + + `${changeBaseURL(change)}/revisions/current/code_owners` + `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS` ) as Promise<CodeOwnersInfo>; } @@ -247,7 +252,7 @@ const config = (await this.get( `/projects/${encodeURIComponent(project)}/` + `branches/${encodeURIComponent(branch)}/` + - `code_owners.branch_config` + 'code_owners.branch_config' )) as CodeOwnerBranchConfigInfo; return config; } catch (err) { @@ -313,7 +318,7 @@ listOwnerStatus(): Promise<CodeOwnerStatusInfo> { return this.fetchOnce('listOwnerStatus', () => - this.codeOwnerApi.listOwnerStatus(this.change._number) + this.codeOwnerApi.listOwnerStatus(this.change) ) as Promise<CodeOwnerStatusInfo>; } @@ -321,7 +326,7 @@ const account = await this.getAccount(); if (!account) return undefined; return this.fetchOnce('listOwnedPaths', () => - this.codeOwnerApi.listOwnedPaths(this.change._number, account) + this.codeOwnerApi.listOwnedPaths(this.change, account) ) as Promise<OwnedPathsInfo | undefined>; }
diff --git a/web/code-owners-fetcher.ts b/web/code-owners-fetcher.ts index 2057641..33fa3fa 100644 --- a/web/code-owners-fetcher.ts +++ b/web/code-owners-fetcher.ts
@@ -14,11 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { - ChangeInfo, - NumericChangeId, -} from '@gerritcodereview/typescript-api/rest-api.js'; +import {ChangeInfo} from '@gerritcodereview/typescript-api/rest-api.js'; import {RestPluginApi} from '@gerritcodereview/typescript-api/rest.js'; import {CodeOwnersApi, FetchedOwner, OwnerStatus} from './code-owners-api.js'; import {FileStatus} from './code-owners-model.js'; @@ -59,7 +55,7 @@ */ constructor( private readonly codeOwnerApi: CodeOwnersApi, - private readonly changeId: NumericChangeId, + private readonly change: ChangeInfo, private readonly filesToFetch: Array<string>, private readonly ownersLimit: number, maxConcurrentRequest: number @@ -76,7 +72,7 @@ try { this.fetchedOwners.set(filePath, { owners: await this.codeOwnerApi.listOwnersForPath( - this.changeId, + this.change, filePath, this.ownersLimit ), @@ -178,7 +174,7 @@ this.totalFetchCount = filesToFetch.length; this.ownersFetcher = new OwnersFetcher( this.codeOwnerApi, - this.change._number, + this.change, filesToFetch, this.options.ownersLimit, this.options.maxConcurrentRequests
diff --git a/web/code-owners-model.ts b/web/code-owners-model.ts index 38d0f78..61fba03 100644 --- a/web/code-owners-model.ts +++ b/web/code-owners-model.ts
@@ -87,6 +87,7 @@ codeOwnerStatusMap: Map<string, FileStatus>; rawStatuses: Array<FileCodeOwnerStatusInfo>; newerPatchsetUploaded: boolean; + accounts?: {[account_id: number]: AccountInfo}; } export interface FileStatus { @@ -94,6 +95,7 @@ status: OwnerStatus; newPath?: string | null; oldPath?: string | null; + reasons?: Array<string>; } export const BestSuggestionsLimit = 5;
diff --git a/web/code-owners-service.ts b/web/code-owners-service.ts index 17b16ba..5f44337 100644 --- a/web/code-owners-service.ts +++ b/web/code-owners-service.ts
@@ -205,6 +205,7 @@ newerPatchsetUploaded: this.isOnNewerPatchset( ownerStatus.patch_set_number ), + accounts: ownerStatus.accounts, }; } @@ -288,6 +289,7 @@ changeType: cur.change_type, status: oldPathStatus.status, newPath: newPathStatus ? newPathStatus.path : null, + reasons: oldPathStatus.reasons, }); } if (newPathStatus) { @@ -295,6 +297,7 @@ changeType: cur.change_type, status: newPathStatus.status, oldPath: oldPathStatus ? oldPathStatus.path : null, + reasons: newPathStatus.reasons, }); } return prev; @@ -333,12 +336,18 @@ private isOnNewerPatchset(patchsetId: number) { if (this.change.current_revision === undefined) return false; const latestRevision = this.change.revisions![this.change.current_revision]; + if (latestRevision._number === 'edit') { + return false; + } return patchsetId > latestRevision._number; } private isOnOlderPatchset(patchsetId: number) { if (this.change.current_revision === undefined) return false; const latestRevision = this.change.revisions![this.change.current_revision]; + if (latestRevision._number === 'edit') { + return false; + } return patchsetId < latestRevision._number; }
diff --git a/web/code-owners-service_test.ts b/web/code-owners-service_test.ts index e9abbb1..f547a8e 100644 --- a/web/code-owners-service_test.ts +++ b/web/code-owners-service_test.ts
@@ -119,7 +119,7 @@ getApiStub .withArgs( sinon.match.any, - `/changes/${fakeChange._number}/code_owners.status?limit=100000`, + `/changes/${fakeChange.project}~${fakeChange._number}/code_owners.status?limit=100000`, sinon.match.any, sinon.match.any ) @@ -148,7 +148,7 @@ assert.isTrue(getApiStub.called); assert.equal( getApiStub.lastCall.args[1], - `/changes/${fakeChange._number}/code_owners.status?limit=100000` + `/changes/${fakeChange.project}~${fakeChange._number}/code_owners.status?limit=100000` ); }); @@ -212,7 +212,7 @@ getApiStub .withArgs( sinon.match.any, - `/changes/${fakeChange._number}/code_owners.status?limit=100000`, + `/changes/${fakeChange.project}~${fakeChange._number}/code_owners.status?limit=100000`, sinon.match.any, sinon.match.any )
diff --git a/web/owner-status-column.ts b/web/owner-status-column.ts index b8006e5..9b0cb86 100644 --- a/web/owner-status-column.ts +++ b/web/owner-status-column.ts
@@ -32,6 +32,8 @@ basePatchNum: BasePatchSetNum; } +const ACCOUNT_TEMPLATE_REGEX = '<GERRIT_ACCOUNT_(\\d+)>'; + const MAGIC_FILES = ['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL']; const STATUS_CODE = { NO_STATUS: 'no-status', @@ -199,6 +201,9 @@ @property({type: String, reflect: true, attribute: 'owner-status'}) ownerStatus?: string; + @property({type: Array}) + ownerReasons?: Array<string>; + static override get styles() { return [ css` @@ -251,11 +256,28 @@ this.computeStatus(); } + private renderReason(reason: string): string { + let reasonWithAccounts = reason.replace( + new RegExp(ACCOUNT_TEMPLATE_REGEX, 'g'), + (_accountIdTemplate, accountId) => { + const parsedAccountId = Number(accountId); + const accountInText = (this.status?.accounts || {})[parsedAccountId]; + if (!accountInText) { + return `Gerrit Account ${parsedAccountId}`; + } + return accountInText.display_name ?? accountInText.name ?? ''; + } + ); + return ( + reasonWithAccounts.charAt(0).toUpperCase() + reasonWithAccounts.slice(1) + ); + } + override render() { if ( this.computeHidden() || this.status === undefined || - !this.path || + !this.path || MAGIC_FILES.includes(this.path) ) return nothing; @@ -263,7 +285,10 @@ } private renderStatus() { - const info = STATUS_TOOLTIP[this.ownerStatus!]; + let info = STATUS_TOOLTIP[this.ownerStatus!]; + if (this.ownerReasons) { + info = this.ownerReasons.map(r => this.renderReason(r)).join('\n'); + } const summary = STATUS_SUMMARY[this.ownerStatus!]; const icon = STATUS_ICON[this.ownerStatus!]; return html` @@ -303,6 +328,7 @@ return html` <gr-avatar-stack .accounts=${allOwners.slice(0, 3)} .forceFetch=${true} + .enableHover=${true} > <gr-tooltip-content slot="fallback" @@ -358,23 +384,32 @@ const statuses = (paths ?? []) .filter(path => !MAGIC_FILES.includes(path)) - .map(path => this.extractStatus(codeOwnerStatusMap.get(path)!, false)); + .map(path => ({ + status: this.extractStatus(codeOwnerStatusMap.get(path), false), + reasons: codeOwnerStatusMap.get(path)?.reasons, + })); // oldPath may contain null, so filter that as well. const oldStatuses = (oldPaths ?? []) .filter(path => !MAGIC_FILES.includes(path) && !!path) - .map(path => this.extractStatus(codeOwnerStatusMap.get(path)!, true)); + .map(path => ({ + status: this.extractStatus(codeOwnerStatusMap.get(path), true), + reasons: codeOwnerStatusMap.get(path)?.reasons, + })); const allStatuses = statuses.concat(oldStatuses); if (allStatuses.length === 0) { return; } - this.ownerStatus = allStatuses.reduce((a, b) => - STATUS_PRIORITY_ORDER.indexOf(a) < STATUS_PRIORITY_ORDER.indexOf(b) + const computedStatus = allStatuses.reduce((a, b) => + STATUS_PRIORITY_ORDER.indexOf(a.status) < + STATUS_PRIORITY_ORDER.indexOf(b.status) ? a : b ); + this.ownerStatus = computedStatus.status; + this.ownerReasons = computedStatus.reasons; } - private extractStatus(statusItem: FileStatus, oldPath: boolean) { + private extractStatus(statusItem: FileStatus | undefined, oldPath: boolean) { if (statusItem === undefined) { return STATUS_CODE.NO_STATUS; } else if (statusItem.status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
diff --git a/web/suggest-owners-trigger.ts b/web/suggest-owners-trigger.ts index f0cf9c9..fb2ca98 100644 --- a/web/suggest-owners-trigger.ts +++ b/web/suggest-owners-trigger.ts
@@ -56,13 +56,6 @@ </gr-button> <span class="extras"> <a - @click=${this.reportBugClick} - href="https://bugs.chromium.org/p/gerrit/issues/entry?template=code-owners-plugin" - target="_blank" - > - <gr-icon icon="bug_report" filled title="report a problem"></gr-icon> - </a> - <a @click=${this.reportDocClick} href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md" target="_blank" @@ -96,8 +89,4 @@ private reportDocClick() { this.reporting?.reportInteraction('code-owners-doc-click'); } - - private reportBugClick() { - this.reporting?.reportInteraction('code-owners-bug-click'); - } }
diff --git a/web/suggest-owners.ts b/web/suggest-owners.ts index fbda31c..4fe2a7d 100644 --- a/web/suggest-owners.ts +++ b/web/suggest-owners.ts
@@ -362,7 +362,7 @@ if (!isLoading) return nothing; return html` <p class="loading"> - <span class="loadingSpin"></span> + <span class="loadingSpin"><gr-icon icon="sync"></gr-icon></span> ${this.selectedSuggestionsLoadProgress} </p> `;