Merge changes I80b916f6,Ib1dc87a6,I42092800,I5c8486ef,I4c8338bc, ... * changes: Add test helper methods to set users as code owner Use method to create arbitrary code owner config to avoid bootstrapping mode Remove duplicate methods to create non-parsable code owner config Allow to ignore self approvals for overrides Document that overrides are sticky depending on the label configuration Allow to ignore self approvals for required approval
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java index f944314..0ed3041 100644 --- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java +++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -36,6 +36,7 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.plugins.codeowners.JgitPath; 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.config.CodeOwnersPluginConfiguration; import com.google.gerrit.plugins.codeowners.config.StatusConfig; import com.google.inject.Inject; @@ -230,6 +231,69 @@ } /** + * Creates a non-parseable code owner config file at the given path. + * + * @param path path of the code owner config file + */ + protected void createNonParseableCodeOwnerConfig(String path) throws Exception { + disableCodeOwnersForProject(project); + String changeId = + createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID") + .getChangeId(); + approve(changeId); + gApi.changes().id(changeId).current().submit(); + enableCodeOwnersForProject(project); + } + + /** + * Creates a default code owner config with the given test accounts as code owners. + * + * @param testAccounts the accounts of the users that should be code owners + */ + protected void setAsDefaultCodeOwners(TestAccount... testAccounts) { + setAsCodeOwners(RefNames.REFS_CONFIG, "/", testAccounts); + } + + /** + * Creates a root code owner config with the given test accounts as code owners. + * + * @param testAccounts the accounts of the users that should be code owners + */ + protected void setAsRootCodeOwners(TestAccount... testAccounts) { + setAsCodeOwners("/", testAccounts); + } + + /** + * Creates a code owner config at the given path with the given test accounts as code owners. + * + * @param path the path of the code owner config file + * @param testAccounts the accounts of the users that should be code owners + */ + protected void setAsCodeOwners(String path, TestAccount... testAccounts) { + setAsCodeOwners("master", path, testAccounts); + } + + /** + * Creates a code owner config at the given path with the given test accounts as code owners. + * + * @param branchName the name of the branch in which the code owner config should be created + * @param path the path of the code owner config file + * @param testAccounts the accounts of the users that should be code owners + */ + private void setAsCodeOwners(String branchName, String path, TestAccount... testAccounts) { + Builder newCodeOwnerConfigBuilder = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(branchName) + .folderPath(path); + for (TestAccount testAccount : testAccounts) { + newCodeOwnerConfigBuilder.addCodeOwnerEmail(testAccount.email()); + } + newCodeOwnerConfigBuilder.create(); + } + + /** * Creates a new change for the given test account. * * @param testAccount the account that should own the new change
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java index 7b83265..2751067 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -172,20 +172,6 @@ .projectName(changeNotes.getProjectName().get()) .changeId(changeNotes.getChangeId().get()) .build())) { - RequiredApproval requiredApproval = - codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName()); - logger.atFine().log("requiredApproval = %s", requiredApproval); - - ImmutableSet<RequiredApproval> overrideApprovals = - codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName()); - boolean hasOverride = hasOverride(overrideApprovals, changeNotes); - logger.atFine().log( - "hasOverride = %s (overrideApprovals = %s)", hasOverride, overrideApprovals); - - BranchNameKey branch = changeNotes.getChange().getDest(); - ObjectId revision = getDestBranchRevision(changeNotes.getChange()); - logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name()); - boolean enableImplicitApprovalFromUploader = codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName()); Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); @@ -193,6 +179,20 @@ "patchSetUploader = %d, implicit approval from uploader is %s", patchSetUploader.get(), enableImplicitApprovalFromUploader ? "enabled" : "disabled"); + RequiredApproval requiredApproval = + codeOwnersPluginConfiguration.getRequiredApproval(changeNotes.getProjectName()); + logger.atFine().log("requiredApproval = %s", requiredApproval); + + ImmutableSet<RequiredApproval> overrideApprovals = + codeOwnersPluginConfiguration.getOverrideApproval(changeNotes.getProjectName()); + boolean hasOverride = hasOverride(overrideApprovals, changeNotes, patchSetUploader); + logger.atFine().log( + "hasOverride = %s (overrideApprovals = %s)", hasOverride, overrideApprovals); + + BranchNameKey branch = changeNotes.getChange().getDest(); + ObjectId revision = getDestBranchRevision(changeNotes.getChange()); + logger.atFine().log("dest branch %s has revision %s", branch.branch(), revision.name()); + CodeOwnerResolverResult globalCodeOwners = codeOwnerResolver .get() @@ -207,9 +207,10 @@ !codeOwnerConfigScannerFactory.create().containsAnyCodeOwnerConfigFile(branch); logger.atFine().log("isBootstrapping = %s", isBootstrapping); - ImmutableSet<Account.Id> reviewerAccountIds = getReviewerAccountIds(changeNotes); + ImmutableSet<Account.Id> reviewerAccountIds = + getReviewerAccountIds(requiredApproval, changeNotes, patchSetUploader); ImmutableSet<Account.Id> approverAccountIds = - getApproverAccountIds(requiredApproval, changeNotes); + getApproverAccountIds(requiredApproval, changeNotes, patchSetUploader); logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds); return changedFiles @@ -798,8 +799,19 @@ * * @param changeNotes the change notes */ - private ImmutableSet<Account.Id> getReviewerAccountIds(ChangeNotes changeNotes) { - return changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER); + private ImmutableSet<Account.Id> getReviewerAccountIds( + RequiredApproval requiredApproval, ChangeNotes changeNotes, Account.Id patchSetUploader) { + ImmutableSet<Account.Id> reviewerAccountIds = + changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER); + if (requiredApproval.labelType().isIgnoreSelfApproval() + && reviewerAccountIds.contains(patchSetUploader)) { + logger.atFine().log( + "Removing patch set uploader %s from reviewers since the label of the required" + + " approval (%s) is configured to ignore self approvals", + patchSetUploader, requiredApproval.labelType()); + return filterOutAccount(reviewerAccountIds, patchSetUploader); + } + return reviewerAccountIds; } /** @@ -811,21 +823,40 @@ * @param changeNotes the change notes */ private ImmutableSet<Account.Id> getApproverAccountIds( - RequiredApproval requiredApproval, ChangeNotes changeNotes) { - return StreamSupport.stream( - approvalsUtil - .byPatchSet( - changeNotes, - changeNotes.getCurrentPatchSet().id(), - /** revWalk */ - null, - /** repoConfig */ - null) - .spliterator(), - /** parallel */ - false) - .filter(requiredApproval::isApprovedBy) - .map(PatchSetApproval::accountId) + RequiredApproval requiredApproval, ChangeNotes changeNotes, Account.Id patchSetUploader) { + ImmutableSet<Account.Id> approverAccountIds = + StreamSupport.stream( + approvalsUtil + .byPatchSet( + changeNotes, + changeNotes.getCurrentPatchSet().id(), + /** revWalk */ + null, + /** repoConfig */ + null) + .spliterator(), + /** parallel */ + false) + .filter(requiredApproval::isApprovedBy) + .map(PatchSetApproval::accountId) + .collect(toImmutableSet()); + + if (requiredApproval.labelType().isIgnoreSelfApproval() + && approverAccountIds.contains(patchSetUploader)) { + logger.atFine().log( + "Removing patch set uploader %s from approvers since the label of the required" + + " approval (%s) is configured to ignore self approvals", + patchSetUploader, requiredApproval.labelType()); + return filterOutAccount(approverAccountIds, patchSetUploader); + } + + return approverAccountIds; + } + + private ImmutableSet<Account.Id> filterOutAccount( + ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) { + return accountIds.stream() + .filter(accountId -> !accountId.equals(accountIdToFilterOut)) .collect(toImmutableSet()); } @@ -834,11 +865,34 @@ * * @param overrideApprovals approvals that count as override for the code owners submit check. * @param changeNotes the change notes + * @param patchSetUploader account ID of the patch set uploader * @return whether the given change has an override approval */ private boolean hasOverride( - ImmutableSet<RequiredApproval> overrideApprovals, ChangeNotes changeNotes) { + ImmutableSet<RequiredApproval> overrideApprovals, + ChangeNotes changeNotes, + Account.Id patchSetUploader) { + ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals = + overrideApprovals.stream() + .filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval()) + .collect(toImmutableSet()); return changeNotes.getApprovals().get(changeNotes.getCurrentPatchSet().id()).stream() + .filter( + approval -> { + // If the approval is from the patch set uploader and if it matches any of the labels + // for which self approvals are ignored, filter it out. + if (approval.accountId().equals(patchSetUploader) + && overrideApprovalsThatIgnoreSelfApprovals.stream() + .anyMatch( + requiredApproval -> + requiredApproval + .labelType() + .getLabelId() + .equals(approval.key().labelId()))) { + return false; + } + return true; + }) .anyMatch( patchSetApproval -> overrideApprovals.stream()
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java index 57bc488..60c14d2 100644 --- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java +++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
@@ -23,6 +23,7 @@ import com.google.common.collect.Iterables; import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.LabelType; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; @@ -158,6 +159,14 @@ */ public boolean areImplicitApprovalsEnabled(Project.NameKey project) { requireNonNull(project, "project"); + LabelType requiredLabel = getRequiredApproval(project).labelType(); + if (requiredLabel.isIgnoreSelfApproval()) { + logger.atFine().log( + "ignoring implicit approval configuration on project %s since the label of the required" + + " approval (%s) is configured to ignore self approvals", + project, requiredLabel); + return false; + } return generalConfig.getEnableImplicitApprovals(getPluginConfig(project)); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java index e6870c2..9a31ca1 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
@@ -35,7 +35,6 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.plugins.codeowners.JgitPath; import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; @@ -608,16 +607,6 @@ throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName()); } - private void createNonParseableCodeOwnerConfig(String path) throws Exception { - disableCodeOwnersForProject(project); - String changeId = - createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID") - .getChangeId(); - approve(changeId); - gApi.changes().id(changeId).current().submit(); - enableCodeOwnersForProject(project); - } - private String getParsingErrorMessage( ImmutableMap<Class<? extends CodeOwnerBackend>, String> messagesByBackend) { CodeOwnerBackend codeOwnerBackend = backendConfig.getDefaultBackend();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java index 31d1b87..c17e4e2 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
@@ -206,7 +206,7 @@ @Test public void getCodeOwnerConfigFilesIfInvalidCodeOwnerConfigFilesExist() throws Exception { - createInvalidCodeOwnerConfig(getCodeOwnerConfigFileName()); + createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName()); CodeOwnerConfig.Key codeOwnerConfigKey = codeOwnerConfigOperations @@ -230,7 +230,7 @@ @Test public void includeInvalidCodeOwnerConfigFiles() throws Exception { String nameOfInvalidCodeOwnerConfigFile = getCodeOwnerConfigFileName(); - createInvalidCodeOwnerConfig(nameOfInvalidCodeOwnerConfigFile); + createNonParseableCodeOwnerConfig(nameOfInvalidCodeOwnerConfigFile); CodeOwnerConfig.Key codeOwnerConfigKey = codeOwnerConfigOperations @@ -444,14 +444,4 @@ } throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName()); } - - private void createInvalidCodeOwnerConfig(String path) throws Exception { - disableCodeOwnersForProject(project); - String changeId = - createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID") - .getChangeId(); - approve(changeId); - gApi.changes().id(changeId).current().submit(); - enableCodeOwnersForProject(project); - } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java index f9e79ed..d5cafb9 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -31,7 +31,6 @@ import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.Change; -import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; import com.google.gerrit.extensions.common.LabelDefinitionInput; @@ -230,13 +229,7 @@ public void getStatusForFileAddition_pending() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -268,13 +261,7 @@ public void getStatusForFileModification_pending() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); createChange("Test Change", JgitPath.of(path).get(), "file content").getChangeId(); @@ -308,13 +295,7 @@ public void getStatusForFileDeletion_pending() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = createChangeWithFileDeletion(path); @@ -345,13 +326,7 @@ public void getStatusForFileRename_pendingOldPath() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/bar/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -388,13 +363,7 @@ public void getStatusForFileRename_pendingNewPath() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/baz/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/baz/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -429,13 +398,7 @@ @Test public void getStatusForFileAddition_approved() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -462,13 +425,7 @@ @Test public void getStatusForFileModification_approved() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); createChange("Test Change", JgitPath.of(path).get(), "file content").getChangeId(); @@ -497,13 +454,7 @@ @Test public void getStatusForFileDeletion_approved() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = createChangeWithFileDeletion(path); @@ -529,13 +480,7 @@ @Test public void getStatusForFileRename_approvedOldPath() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/bar/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -568,13 +513,7 @@ @Test public void getStatusForFileRename_approvedNewPath() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/baz/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/baz/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -620,13 +559,7 @@ private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileAddition( boolean implicitApprovalsEnabled) throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -667,13 +600,7 @@ private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileModification( boolean implicitApprovalsEnabled) throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); createChange("Test Change", JgitPath.of(path).get(), "file content").getChangeId(); @@ -715,13 +642,7 @@ private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileDeletion( boolean implicitApprovalsEnabled) throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = createChangeWithFileDeletion(path); @@ -762,13 +683,7 @@ private void testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnOldPath( boolean implicitApprovalsEnabled) throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/bar/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -815,13 +730,7 @@ private void testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnNewPath( boolean implicitApprovalsEnabled) throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/baz/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsCodeOwners("/foo/baz/", user); Path oldPath = Paths.get("/foo/bar/abc.txt"); Path newPath = Paths.get("/foo/baz/abc.txt"); @@ -854,13 +763,7 @@ @Test @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") public void getStatusForFileAddition_noImplicitlyApprovalByChangeOwner() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(admin.email()) - .create(); + setAsRootCodeOwners(admin); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -888,13 +791,7 @@ throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -1066,13 +963,7 @@ if (!bootstrappingMode) { // Create a code owner config file so that we are not in the bootstrapping mode. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(admin.email()) - .create(); + createArbitraryCodeOwnerConfigFile(); } // Create a change as a user that is not a code owner. @@ -1150,13 +1041,8 @@ accountCreator.create("bot", "bot@example.com", "Bot", /* displayName= */ null); if (!bootstrappingMode) { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(admin.email()) - .create(); + // Create a code owner config file so that we are not in the bootstrapping mode. + createArbitraryCodeOwnerConfigFile(); } Path path = Paths.get("/foo/bar.baz"); @@ -1198,13 +1084,7 @@ if (!bootstrappingMode) { // Create a code owner config file so that we are not in the bootstrapping mode. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(admin.email()) - .create(); + createArbitraryCodeOwnerConfigFile(); } // Create a change as a user that is not a code owner. @@ -1276,13 +1156,7 @@ throws Exception { if (!bootstrappingMode) { // Create a code owner config file so that we are not in the bootstrapping mode. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + createArbitraryCodeOwnerConfigFile(); } // Create a change. @@ -1352,13 +1226,8 @@ private void testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner( boolean implicitApprovalsEnabled, boolean bootstrappingMode) throws Exception { if (!bootstrappingMode) { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + // Create a code owner config file so that we are not in the bootstrapping mode. + createArbitraryCodeOwnerConfigFile(); } // Create a change as a user that is a code owner only through the global code ownership. @@ -1395,17 +1264,9 @@ private void testAnyReviewerWhenEveryoneIsGlobalCodeOwner(boolean bootstrappingMode) throws Exception { - TestAccount user2 = accountCreator.user2(); - if (!bootstrappingMode) { // Create a code owner config file so that we are not in the bootstrapping mode. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user2.email()) - .create(); + createArbitraryCodeOwnerConfigFile(); } // Create a change as a user that is a code owner only through the global code ownership. @@ -1446,28 +1307,9 @@ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user2.email()) - .create(); - - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user3.email()) - .create(); + setAsCodeOwners("/", user); + setAsCodeOwners("/foo/", user2); + setAsCodeOwners("/foo/bar/", user3); Path path = Paths.get("/foo/bar/baz.txt"); String changeId = @@ -1641,21 +1483,8 @@ public void isSubmittable() throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); - - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user2.email()) - .create(); + setAsCodeOwners("/foo/", user); + setAsCodeOwners("/bar/", user2); String changeId = pushFactory @@ -2096,14 +1925,7 @@ public void approvedByStickyApprovalOnOldPatchSet() throws Exception { TestAccount user2 = accountCreator.user2(); - // Create a code owner config file with 'user' as code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); // Create a change as a user that is not a code owner. Path path = Paths.get("/foo/bar.baz"); @@ -2174,14 +1996,7 @@ public void codeReviewPlus2CountsAsApprovalIfCodeReviewPlus1IsRequired() throws Exception { TestAccount user2 = accountCreator.user2(); - // Create a code owner config file with 'user' as code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsRootCodeOwners(user); // Create a change as 'user2' that is not a code owner. Path path = Paths.get("/foo/bar.baz"); @@ -2243,14 +2058,7 @@ TestAccount user2 = accountCreator.user2(); - // Create a code owner config file with 'admin' as code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(admin.email()) - .create(); + setAsRootCodeOwners(admin); // Create a change as 'user' that is not a code owner. Path path = Paths.get("/foo/bar.baz"); @@ -2290,14 +2098,7 @@ public void noBootstrappingIfDefaultCodeOwnerConfigExists() throws Exception { TestAccount user2 = accountCreator.user2(); - // Create default code owner config file in refs/meta/config. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(RefNames.REFS_CONFIG) - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsDefaultCodeOwners(user); // Create a change as a user that is neither a code owner nor a project owner. Path path = Paths.get("/foo/bar.baz"); @@ -2357,14 +2158,7 @@ public void approvedByDefaultCodeOwner() throws Exception { TestAccount user2 = accountCreator.user2(); - // Create default code owner config file in refs/meta/config. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(RefNames.REFS_CONFIG) - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsDefaultCodeOwners(user); // Create a change as a user that is not a code owner. Path path = Paths.get("/foo/bar.baz"); @@ -2418,14 +2212,7 @@ private void testImplicitlyApprovedByDefaultCodeOwner(boolean implicitApprovalsEnabled) 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(); + setAsDefaultCodeOwners(user); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -2451,14 +2238,7 @@ public void defaultCodeOwnerAsReviewer() throws Exception { TestAccount user2 = accountCreator.user2(); - // Create default code owner config file in refs/meta/config. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(RefNames.REFS_CONFIG) - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + setAsDefaultCodeOwners(user); // Create a change as a user that is not a code owner. Path path = Paths.get("/foo/bar.baz");
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java index e087873..d2d2c5b 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java
@@ -69,14 +69,7 @@ TestAccount codeOwner = accountCreator.create( "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); - - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(codeOwner.email()) - .create(); + setAsRootCodeOwners(codeOwner); Path path = Paths.get("/foo/bar.baz"); String changeId = @@ -130,14 +123,7 @@ TestAccount codeOwner = accountCreator.create( "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); - - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(codeOwner.email()) - .create(); + setAsRootCodeOwners(codeOwner); Path path = Paths.get("/foo/bar.baz"); String changeId =
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java new file mode 100644 index 0000000..9ba465a --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
@@ -0,0 +1,411 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import static com.google.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject.assertThatStream; + +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.entities.Change; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.plugins.codeowners.JgitPath; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerStatus; +import com.google.gerrit.plugins.codeowners.config.OverrideApprovalConfig; +import com.google.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.testing.ConfigSuite; +import com.google.inject.Inject; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import org.eclipse.jgit.lib.Config; +import org.junit.Before; +import org.junit.Test; + +public class CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest extends AbstractCodeOwnersTest { + @Inject private ChangeNotes.Factory changeNotesFactory; + @Inject private RequestScopeOperations requestScopeOperations; + + private CodeOwnerApprovalCheck codeOwnerApprovalCheck; + + /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + cfg.setString( + "plugin", "code-owners", OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL, "Owners-Override+1"); + return cfg; + } + + @Before + public void setUpCodeOwnersPlugin() throws Exception { + codeOwnerApprovalCheck = plugin.getSysInjector().getInstance(CodeOwnerApprovalCheck.class); + } + + @Before + public void defineOwnersOverrideLabel() throws Exception { + createOwnersOverrideLabel(); + } + + @Before + public void disableSelfApprovals() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.ignoreSelfApproval = true; + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + gApi.projects().name(project.get()).label("Owners-Override").update(input); + } + + @Test + public void notApprovedByUploaderWhoIsChangeOwner() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(codeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add a self Code-Review+1 (= code owner approval). + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // Verify that the file is not approved (since self approvals are ignored). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + @Test + public void approvedByChangeOwnerThatIsNotUploader() throws Exception { + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + setAsRootCodeOwners(changeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Upload another patch set by another user. + amendChange(admin, changeId); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add a Code-Review+1 (= code owner approval) from the change owner. + requestScopeOperations.setApiUser(changeOwner.id()); + recommend(changeId); + + // Verify that the file is approved now (since the change owner is not the uploader of the + // current patch set). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.APPROVED); + } + + @Test + public void notApprovedByUploader() throws Exception { + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Upload another patch set by a code owner. + amendChange(codeOwner, changeId); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add the code owner as reviewer. + gApi.changes().id(changeId).addReviewer(user.email()); + + // Verify that the file is not pending (the code owner is the uploader of the current patch set + // and self approvals are ignored). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add a Code-Review+1 (= code owner approval) by the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // Verify that the file is not approved (since the code owner is the uploader of the current + // patch set and self approvals are ignored). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void notImplicitlyApprovedByUploaderWhoIsChangeOwner() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(codeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void notImplicitlyApprovedByUploader() throws Exception { + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Upload another patch set by a code owner. + amendChange(codeOwner, changeId); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + @Test + public void notOverriddenByUploaderWhoIsChangeOwner() throws Exception { + // create arbitrary code owner config to avoid entering the bootstrapping code path in + // CodeOwnerApprovalCheck + createArbitraryCodeOwnerConfigFile(); + + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add an override approval. + requestScopeOperations.setApiUser(changeOwner.id()); + gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1)); + + // Verify that the file is not approved (since self approvals on the override label are + // ignored). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + @Test + public void overridenByChangeOwnerThatIsNotUploader() throws Exception { + // create arbitrary code owner config to avoid entering the bootstrapping code path in + // CodeOwnerApprovalCheck + createArbitraryCodeOwnerConfigFile(); + + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Upload another patch set by another user. + amendChange(admin, changeId); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add an override approval from the change owner. + requestScopeOperations.setApiUser(changeOwner.id()); + gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1)); + + // Verify that the file is approved now (since the change owner is not the uploader of the + // current patch set and hence the override counts). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.APPROVED); + } + + @Test + public void notOverridenByUploader() throws Exception { + // create arbitrary code owner config to avoid entering the bootstrapping code path in + // CodeOwnerApprovalCheck + createArbitraryCodeOwnerConfigFile(); + + TestAccount changeOwner = + accountCreator.create( + "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange(changeOwner, "Change Adding A File", JgitPath.of(path).get(), "file content") + .getChangeId(); + + // Upload another patch set by another user. + amendChange(admin, changeId); + + // Verify that the file is not approved. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + + // Add an override approval. + gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", 1)); + + // Verify that the file is not approved (since the override from the uploader is ignored). + fileCodeOwnerStatuses = codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeId)); + fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + } + + private ChangeNotes getChangeNotes(String changeId) throws Exception { + return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number)); + } +}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java index c26f000..df2e602 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
@@ -25,7 +25,6 @@ import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.RefNames; -import com.google.gerrit.plugins.codeowners.JgitPath; import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; import java.nio.file.Paths; @@ -153,7 +152,7 @@ @Test public void visitorNotInvokedForInvalidCodeOwnerConfigFiles() throws Exception { - createInvalidCodeOwnerConfig("/OWNERS"); + createNonParseableCodeOwnerConfig("/OWNERS"); visit(); verifyZeroInteractions(visitor); @@ -167,7 +166,7 @@ @Test public void visitorInvokedForValidCodeOwnerConfigFilesEvenIfInvalidCodeOwnerConfigFileExist() throws Exception { - createInvalidCodeOwnerConfig("/OWNERS"); + createNonParseableCodeOwnerConfig("/OWNERS"); // Create a valid code owner config file. CodeOwnerConfig.Key codeOwnerConfigKey = @@ -554,7 +553,7 @@ @Test public void containsACodeOwnerConfigFile_invalidCodeOwnerConfigFileExists() throws Exception { - createInvalidCodeOwnerConfig("/OWNERS"); + createNonParseableCodeOwnerConfig("/OWNERS"); codeOwnerConfigOperations .newCodeOwnerConfig() @@ -574,7 +573,7 @@ @Test public void containsOnlyInvalidCodeOwnerConfigFiles() throws Exception { - createInvalidCodeOwnerConfig("/OWNERS"); + createNonParseableCodeOwnerConfig("/OWNERS"); assertThat( codeOwnerConfigScannerFactory @@ -625,14 +624,4 @@ .includeDefaultCodeOwnerConfig(includeDefaultCodeOwnerConfig) .visit(BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback); } - - private void createInvalidCodeOwnerConfig(String path) throws Exception { - disableCodeOwnersForProject(project); - String changeId = - createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID") - .getChangeId(); - approve(changeId); - gApi.changes().id(changeId).current().submit(); - enableCodeOwnersForProject(project); - } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java index 6acb268..641bf10 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
@@ -29,6 +29,7 @@ import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project.NameKey; import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.LabelDefinitionInput; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl; import com.google.gerrit.extensions.registration.RegistrationHandle; @@ -910,6 +911,17 @@ .isEqualTo(FallbackCodeOwners.NONE); } + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void implicitApprovalsAreDisabledIfRequiredLabelIgnoresSelfApprovals() throws Exception { + assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isTrue(); + + LabelDefinitionInput input = new LabelDefinitionInput(); + input.ignoreSelfApproval = true; + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + assertThat(codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(project)).isFalse(); + } + private void configureDisabled(Project.NameKey project, String disabled) throws Exception { setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled); }
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md index 4519ae2..32d3ae9 100644 --- a/resources/Documentation/config.md +++ b/resources/Documentation/config.md
@@ -81,6 +81,11 @@ <a id="pluginCodeOwnersEnableImplicitApprovals">plugin.@PLUGIN@.enableImplictApprovals</a> : Whether an implicit code owner approval from the last uploader is assumed.\ + This setting has no effect if self approvals from the last uploader are + ignored because the [required label](#pluginCodeOwnersRequiredApproval) + is configured to [ignore self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader.\ If enabled, code owners need to be aware of their implicit approval when they upload new patch sets for other users (e.g. if a contributor pushes a change to a wrong branch and a code owner helps them to get it rebased @@ -150,6 +155,10 @@ rules](../../../Documentation/config-labels.html#label_copyAnyScore) enabled so that votes are sticky across patch sets, also the code owner approvals will be sticky.\ + If the definition of the configured label [ignores self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader, any vote from the uploader is ignored for the code + owners check.\ Can be overridden per project by setting [codeOwners.requiredApproval](#codeOwnersRequiredApproval) in `@PLUGIN@.config`.\ @@ -167,6 +176,14 @@ The configured labels must exist for all projects for which this setting applies (all projects that have code owners enabled and for which this setting is not overridden).\ + If the definition of the configured labels has [copy + rules](../../../Documentation/config-labels.html#label_copyAnyScore) + enabled so that votes are sticky across patch sets, also the code owner + overrides will be sticky.\ + If the definition of a configured label [ignores self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader, any override vote from the uploader on that label is + ignored for the code owners check.\ Can be overridden per project by setting [codeOwners.overrideApproval](#codeOwnersOverrideApproval) in `@PLUGIN@.config`.\ @@ -343,6 +360,11 @@ <a id="codeOwnersEnableImplicitApprovals">codeOwners.enableImplicitApprovals</a> : Whether an implicit code owner approval from the last uploader is assumed.\ + This setting has no effect if self approvals from the last uploader are + ignored because the [required label](#codeOwnersRequiredApproval) + is configured to [ignore self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader.\ If enabled, code owners need to be aware of their implicit approval when they upload new patch sets for other users (e.g. if a contributor pushes a change to a wrong branch and a code owner helps them to get it rebased @@ -412,6 +434,10 @@ rules](../../../Documentation/config-labels.html#label_copyAnyScore) enabled so that votes are sticky across patch sets, also the code owner approvals will be sticky.\ + If the definition of the configured label [ignores self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader, any vote from the uploader is ignored for the code + owners check.\ Overrides the global setting [plugin.@PLUGIN@.requiredApproval](#pluginCodeOwnersRequiredApproval) in `gerrit.config`.\ @@ -431,6 +457,14 @@ The configured labels must exist for all projects for which this setting applies (all projects that have code owners enabled and for which this setting is not overridden).\ + If the definition of the configured labels has [copy + rules](../../../Documentation/config-labels.html#label_copyAnyScore) + enabled so that votes are sticky across patch sets, also the code owner + overrides will be sticky.\ + If the definition of a configured label [ignores self + approvals](../../../Documentation/config-labels.html#label_ignoreSelfApproval) + from the uploader, any override vote from the uploader on that label is + ignored for the code owners check.\ Overrides the global setting [plugin.@PLUGIN@.overrideApproval](#pluginCodeOwnersOverrideApproval) in `gerrit.config`.\