Extend 'GetFilesOwners' with Owners submit requirements
When 'owners.enableSubmitRequirements = true' calculate file owners with
label and score defined in the `OWNERS` file. In case when no custom
label rules are defined there fallback to the Gerrit's CodeReview label
defnition.
Notes:
* the existing `GetFilesOwnersIT` content was moved to the common
abstract class so that current functionality could be also confirmed
when submit requirement were enabled (in
`GetFilesOwnersSubmitRequirementsIT`)
* `GetFilesOwnersSubmitRequirementsIT` contains extra cases to cover
label defined in the `OWNERS` file
Bug: Issue 40015590
Change-Id: Id03e2485f3980ac91bc2041ebfc89d05b431a612
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
index 6bdb9a2..5f5b1fb 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
@@ -20,7 +20,6 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
-import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.client.ListChangesOption;
@@ -69,9 +68,7 @@
private final PathOwnersEntriesCache cache;
static final String MISSING_CODE_REVIEW_LABEL =
- String.format(
- "Cannot calculate file onwers state when %s label is not configured",
- LabelId.CODE_REVIEW);
+ "Cannot calculate file owners state when review label is not configured";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
@@ -97,18 +94,6 @@
throws AuthException, BadRequestException, ResourceConflictException, Exception {
Change change = revision.getChange();
ChangeData changeData = revision.getChangeResource().getChangeData();
- Short codeReviewMaxValue =
- changeData
- .getLabelTypes()
- .byLabel(LabelId.CODE_REVIEW)
- .map(LabelType::getMaxPositive)
- .orElseThrow(
- () -> {
- logger.atInfo().log(
- "Project %s has no Code-Review label configured hence getting file owners is not possible.",
- revision.getProject());
- return new ResourceNotFoundException(MISSING_CODE_REVIEW_LABEL);
- });
Project.NameKey project = change.getProject();
List<Project.NameKey> projectParents =
@@ -149,34 +134,73 @@
Map<Integer, Map<String, Integer>> ownersLabels = getLabels(change.getChangeId());
+ LabelAndScore label = getLabelDefinition(owners, changeData);
+
Map<String, Set<GroupOwner>> filesWithPendingOwners =
Maps.filterEntries(
fileToOwners,
(fileOwnerEntry) ->
!isApprovedByOwner(
- fileExpandedOwners.get(fileOwnerEntry.getKey()),
- ownersLabels,
- codeReviewMaxValue));
+ fileExpandedOwners.get(fileOwnerEntry.getKey()), ownersLabels, label));
return Response.ok(new FilesOwnersResponse(ownersLabels, filesWithPendingOwners));
}
}
+ private LabelAndScore getLabelDefinition(PathOwners owners, ChangeData changeData)
+ throws ResourceNotFoundException {
+
+ try {
+ return Optional.of(pluginSettings.enableSubmitRequirement())
+ .filter(Boolean::booleanValue)
+ .flatMap(enabled -> getLabelFromOwners(owners, changeData))
+ .orElseGet(
+ () ->
+ new LabelAndScore(
+ LabelId.CODE_REVIEW, getMaxScoreForLabel(changeData, LabelId.CODE_REVIEW)));
+ } catch (LabelNotFoundException e) {
+ logger.atInfo().withCause(e).log("Invalid configuration");
+ throw new ResourceNotFoundException(MISSING_CODE_REVIEW_LABEL, e);
+ }
+ }
+
+ private Optional<LabelAndScore> getLabelFromOwners(PathOwners owners, ChangeData changeData)
+ throws LabelNotFoundException {
+ return owners
+ .getLabel()
+ .map(
+ label ->
+ new LabelAndScore(
+ label.getName(),
+ label
+ .getScore()
+ .orElseGet(() -> getMaxScoreForLabel(changeData, label.getName()))));
+ }
+
+ private short getMaxScoreForLabel(ChangeData changeData, String labelId)
+ throws LabelNotFoundException {
+ return changeData
+ .getLabelTypes()
+ .byLabel(labelId)
+ .map(label -> label.getMaxPositive())
+ .orElseThrow(() -> new LabelNotFoundException(changeData.change().getProject(), labelId));
+ }
+
private boolean isApprovedByOwner(
Set<GroupOwner> fileOwners,
Map<Integer, Map<String, Integer>> ownersLabels,
- short codeReviewMaxValue) {
+ LabelAndScore label) {
return fileOwners.stream()
.filter(owner -> owner instanceof Owner)
.map(owner -> ((Owner) owner).getId())
- .flatMap(ownerId -> codeReviewLabelValue(ownersLabels, ownerId))
- .anyMatch(value -> value == codeReviewMaxValue);
+ .flatMap(ownerId -> codeReviewLabelValue(ownersLabels, ownerId, label.getLabelId()))
+ .anyMatch(value -> value >= label.getScore());
}
private Stream<Integer> codeReviewLabelValue(
- Map<Integer, Map<String, Integer>> ownersLabels, int ownerId) {
+ Map<Integer, Map<String, Integer>> ownersLabels, int ownerId, String labelId) {
return Stream.ofNullable(ownersLabels.get(ownerId))
- .flatMap(m -> Stream.ofNullable(m.get(LabelId.CODE_REVIEW)));
+ .flatMap(m -> Stream.ofNullable(m.get(labelId)));
}
/**
@@ -227,4 +251,30 @@
.get(accountId)
.map(as -> new Owner(as.account().fullName(), as.account().id().get()));
}
+
+ static class LabelNotFoundException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ LabelNotFoundException(Project.NameKey project, String labelId) {
+ super(String.format("Project %s has no %s label defined", project, labelId));
+ }
+ }
+
+ private static class LabelAndScore {
+ private final String labelId;
+ private final short score;
+
+ private LabelAndScore(String labelId, short score) {
+ this.labelId = labelId;
+ this.score = score;
+ }
+
+ private String getLabelId() {
+ return labelId;
+ }
+
+ private short getScore() {
+ return score;
+ }
+ }
}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
index 1c551ec..76c4962 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
@@ -15,348 +15,9 @@
package com.googlesource.gerrit.owners.restapi;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.UseLocalDisk;
-import com.google.gerrit.acceptance.config.GlobalPluginConfig;
-import com.google.gerrit.entities.LabelId;
-import com.google.gerrit.entities.LabelType;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.Project.NameKey;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.server.project.testing.TestLabels;
-import com.googlesource.gerrit.owners.entities.FilesOwnersResponse;
-import com.googlesource.gerrit.owners.entities.GroupOwner;
-import com.googlesource.gerrit.owners.entities.Owner;
-import java.util.Arrays;
-import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.compress.utils.Sets;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.FetchResult;
-import org.eclipse.jgit.util.FS;
-import org.junit.Test;
@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
@UseLocalDisk
-public class GetFilesOwnersIT extends LightweightPluginDaemonTest {
-
- private static final String REFS_META_CONFIG = RefNames.REFS_META + "config";
- private GetFilesOwners ownersApi;
- private Owner rootOwner;
- private Owner projectOwner;
- private NameKey parentProjectName;
- private NameKey childProjectName;
- private TestRepository<InMemoryRepository> childRepo;
- private TestRepository<InMemoryRepository> parentRepo;
- private TestRepository<InMemoryRepository> allProjectsRepo;
-
- @Override
- public void setUpTestPlugin() throws Exception {
- super.setUpTestPlugin();
-
- rootOwner = new Owner(admin.fullName(), admin.id().get());
- projectOwner = new Owner(user.fullName(), user.id().get());
- ownersApi = plugin.getSysInjector().getInstance(GetFilesOwners.class);
-
- parentProjectName =
- createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY);
- parentRepo = cloneProjectWithMetaRefs(parentProjectName);
-
- childProjectName =
- createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY);
- childRepo = cloneProject(childProjectName);
-
- allProjectsRepo = cloneProjectWithMetaRefs(allProjects);
- }
-
- @Test
- public void shouldReturnExactFileOwners() throws Exception {
- addOwnerFileToRoot(true);
- assertChangeHasOwners(createChange().getChangeId());
- }
-
- @Test
- public void shouldReturnExactFileOwnersWhenOwnersIsSetToAllProjects() throws Exception {
- addOwnerFileWithMatchers(allProjectsRepo, REFS_META_CONFIG, true);
- assertChangeHasOwners(createChange(childRepo).getChangeId());
- }
-
- @Test
- public void shouldReturnExactFileOwnersWhenOwnersIsSetToParentProject() throws Exception {
- addOwnerFileWithMatchers(parentRepo, REFS_META_CONFIG, true);
- assertChangeHasOwners(createChange(childRepo).getChangeId());
- }
-
- @Test
- public void shouldReturnOwnersLabelsWhenNotApprovedByOwners() throws Exception {
- addOwnerFileToRoot(true);
- String changeId = createChange().getChangeId();
-
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files)
- .containsExactly("a.txt", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get())));
-
- assertThat(resp.value().ownersLabels).isEmpty();
- }
-
- @Test
- public void shouldReturnEmptyResponseWhenApprovedByOwners() throws Exception {
- addOwnerFileToRoot(true);
- String changeId = createChange().getChangeId();
- approve(changeId);
-
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files).isEmpty();
- }
-
- @Test
- @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
- public void shouldReturnResponseWithUnexpandedFileOwners() throws Exception {
- addOwnerFileToRoot(true);
- String changeId = createChange().getChangeId();
-
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files)
- .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username())));
- }
-
- @Test
- @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
- public void shouldReturnEmptyResponseWhenApprovedByOwnersWithUnexpandedFileOwners()
- throws Exception {
- addOwnerFileToRoot(true);
- String changeId = createChange().getChangeId();
- approve(changeId);
-
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files).isEmpty();
- }
-
- @Test
- @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
- public void shouldReturnResponseWithUnexpandedFileMatchersOwners() throws Exception {
- addOwnerFileWithMatchersToRoot(true);
- String changeId = createChange().getChangeId();
-
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files)
- .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username())));
- }
-
- @Test
- @UseLocalDisk
- public void shouldReturnInheritedOwnersFromProjectsOwners() throws Exception {
- assertInheritFromProject(project);
- }
-
- @Test
- @UseLocalDisk
- public void shouldReturnInheritedOwnersFromParentProjectsOwners() throws Exception {
- assertInheritFromProject(allProjects);
- }
-
- @Test
- @UseLocalDisk
- public void shouldReflectChangesInParentProject() throws Exception {
- addOwnerFileToProjectConfig(allProjects, true, admin);
-
- String changeId = createChange().getChangeId();
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
- assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner));
-
- addOwnerFileToProjectConfig(allProjects, true, user);
- resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
- assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(projectOwner));
- }
-
- @Test
- @UseLocalDisk
- public void shouldNotReturnInheritedOwnersFromProjectsOwners() throws Exception {
- assertNotInheritFromProject(project);
- }
-
- @Test
- @UseLocalDisk
- public void shouldNotReturnInheritedOwnersFromParentProjectsOwners() throws Exception {
- addOwnerFileToProjectConfig(project, false);
- assertNotInheritFromProject(allProjects);
- }
-
- @Test
- @UseLocalDisk
- public void shouldThrowExceptionWhenCodeReviewLabelIsNotConfigured() throws Exception {
- addOwnerFileToProjectConfig(project, false);
- replaceCodeReviewWithLabel(
- TestLabels.label(
- "Foo", TestLabels.value(1, "Foo is fine"), TestLabels.value(-1, "Foo is not fine")));
- String changeId = createChange().getChangeId();
-
- ResourceNotFoundException thrown =
- assertThrows(
- ResourceNotFoundException.class,
- () -> ownersApi.apply(parseCurrentRevisionResource(changeId)));
- assertThat(thrown).hasMessageThat().isEqualTo(GetFilesOwners.MISSING_CODE_REVIEW_LABEL);
- }
-
- private void replaceCodeReviewWithLabel(LabelType label) throws Exception {
- try (ProjectConfigUpdate u = updateProject(allProjects)) {
- u.getConfig().getLabelSections().remove(LabelId.CODE_REVIEW);
- u.getConfig().upsertLabelType(label);
- u.save();
- }
- }
-
- private static <T> Response<T> assertResponseOk(Response<T> response) {
- assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_OK);
- return response;
- }
-
- private void assertNotInheritFromProject(Project.NameKey projectNameKey) throws Exception {
- addOwnerFileToRoot(false);
- addOwnerFileToProjectConfig(projectNameKey, true);
-
- String changeId = createChange().getChangeId();
- assertChangeHasOwners(changeId);
- }
-
- private void assertChangeHasOwners(String changeId)
- throws AuthException, BadRequestException, ResourceConflictException, Exception {
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner));
- }
-
- private void assertInheritFromProject(Project.NameKey projectNameKey) throws Exception {
- addOwnerFileToRoot(true);
- addOwnerFileToProjectConfig(projectNameKey, true);
-
- String changeId = createChange().getChangeId();
- Response<FilesOwnersResponse> resp =
- assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
-
- assertThat(resp.value().files)
- .containsExactly("a.txt", Sets.newHashSet(rootOwner, projectOwner));
- }
-
- private void addOwnerFileToProjectConfig(Project.NameKey projectNameKey, boolean inherit)
- throws Exception {
- addOwnerFileToProjectConfig(projectNameKey, inherit, user);
- }
-
- private void addOwnerFileToProjectConfig(
- Project.NameKey projectNameKey, boolean inherit, TestAccount account) throws Exception {
- TestRepository<InMemoryRepository> project = cloneProject(projectNameKey);
- GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
- project.reset(RefNames.REFS_CONFIG);
- pushFactory
- .create(
- admin.newIdent(),
- project,
- "Add OWNER file",
- "OWNERS",
- String.format(
- "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n",
- inherit, account.email()))
- .to(RefNames.REFS_CONFIG);
- }
-
- private void addOwnerFileToRoot(boolean inherit) throws Exception {
- // Add OWNERS file to root:
- //
- // inherited: true
- // owners:
- // - admin
- merge(
- createChange(
- testRepo,
- "master",
- "Add OWNER file",
- "OWNERS",
- String.format("inherited: %s\nowners:\n- %s\n", inherit, admin.email()),
- ""));
- }
-
- private void addOwnerFileWithMatchersToRoot(boolean inherit) throws Exception {
- addOwnerFileWithMatchers(testRepo, "master", inherit);
- }
-
- private void addOwnerFileWithMatchers(TestRepository<?> repo, String targetRef, boolean inherit)
- throws Exception {
- // Add OWNERS file to root:
- //
- // inherited: true
- // matchers:
- // - suffix: .txt
- // owners:
- // - admin@mail.com
- Result changeCreated =
- createChange(
- repo,
- targetRef,
- "Add OWNER file",
- "OWNERS",
- String.format(
- "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n",
- inherit, admin.email()),
- "");
- changeCreated.assertOkStatus();
- merge(changeCreated);
- }
-
- public TestRepository<InMemoryRepository> cloneProjectWithMetaRefs(Project.NameKey project)
- throws Exception {
- String uri = registerRepoConnection(project, admin);
- String initialRef = "refs/remotes/origin/config";
- DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
-
- InMemoryRepository.Builder b = new InMemoryRepository.Builder().setRepositoryDescription(desc);
- if (uri.startsWith("ssh://")) {
- // SshTransport depends on a real FS to read ~/.ssh/config, but InMemoryRepository by default
- // uses a null FS.
- // Avoid leaking user state into our tests.
- b.setFS(FS.detect().setUserHome(null));
- }
- InMemoryRepository dest = b.build();
- Config cfg = dest.getConfig();
- cfg.setString("remote", "origin", "url", uri);
- cfg.setStringList(
- "remote",
- "origin",
- "fetch",
- Arrays.asList(
- "+refs/heads/*:refs/remotes/origin/*", "+refs/meta/config:refs/remotes/origin/config"));
- TestRepository<InMemoryRepository> testRepo = GitUtil.newTestRepository(dest);
- FetchResult result = testRepo.git().fetch().setRemote("origin").call();
- if (result.getTrackingRefUpdate(initialRef) != null) {
- testRepo.reset(initialRef);
- }
- return testRepo;
- }
-}
+public class GetFilesOwnersIT extends GetFilesOwnersITAbstract {}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java
new file mode 100644
index 0000000..936f3d3
--- /dev/null
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersITAbstract.java
@@ -0,0 +1,361 @@
+// 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.googlesource.gerrit.owners.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.server.project.testing.TestLabels;
+import com.googlesource.gerrit.owners.entities.FilesOwnersResponse;
+import com.googlesource.gerrit.owners.entities.GroupOwner;
+import com.googlesource.gerrit.owners.entities.Owner;
+import com.googlesource.gerrit.owners.restapi.GetFilesOwners.LabelNotFoundException;
+import java.util.Arrays;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.compress.utils.Sets;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+public abstract class GetFilesOwnersITAbstract extends LightweightPluginDaemonTest {
+
+ private static final String REFS_META_CONFIG = RefNames.REFS_META + "config";
+ protected GetFilesOwners ownersApi;
+ private Owner rootOwner;
+ private Owner projectOwner;
+ private NameKey parentProjectName;
+ private NameKey childProjectName;
+ private TestRepository<InMemoryRepository> childRepo;
+ private TestRepository<InMemoryRepository> parentRepo;
+ private TestRepository<InMemoryRepository> allProjectsRepo;
+
+ @Override
+ public void setUpTestPlugin() throws Exception {
+ super.setUpTestPlugin();
+
+ rootOwner = new Owner(admin.fullName(), admin.id().get());
+ projectOwner = new Owner(user.fullName(), user.id().get());
+ ownersApi = plugin.getSysInjector().getInstance(GetFilesOwners.class);
+
+ parentProjectName =
+ createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY);
+ parentRepo = cloneProjectWithMetaRefs(parentProjectName);
+
+ childProjectName =
+ createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY);
+ childRepo = cloneProject(childProjectName);
+
+ allProjectsRepo = cloneProjectWithMetaRefs(allProjects);
+ }
+
+ @Test
+ public void shouldReturnExactFileOwners() throws Exception {
+ addOwnerFileToRoot(true);
+ assertChangeHasOwners(createChange().getChangeId());
+ }
+
+ @Test
+ public void shouldReturnExactFileOwnersWhenOwnersIsSetToAllProjects() throws Exception {
+ addOwnerFileWithMatchers(allProjectsRepo, REFS_META_CONFIG, true);
+ assertChangeHasOwners(createChange(childRepo).getChangeId());
+ }
+
+ @Test
+ public void shouldReturnExactFileOwnersWhenOwnersIsSetToParentProject() throws Exception {
+ addOwnerFileWithMatchers(parentRepo, REFS_META_CONFIG, true);
+ assertChangeHasOwners(createChange(childRepo).getChangeId());
+ }
+
+ @Test
+ public void shouldReturnOwnersLabelsWhenNotApprovedByOwners() throws Exception {
+ addOwnerFileToRoot(true);
+ String changeId = createChange().getChangeId();
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files)
+ .containsExactly("a.txt", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get())));
+
+ assertThat(resp.value().ownersLabels).isEmpty();
+ }
+
+ @Test
+ public void shouldReturnEmptyResponseWhenApprovedByOwners() throws Exception {
+ addOwnerFileToRoot(true);
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files).isEmpty();
+ }
+
+ @Test
+ @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
+ public void shouldReturnResponseWithUnexpandedFileOwners() throws Exception {
+ addOwnerFileToRoot(true);
+ String changeId = createChange().getChangeId();
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files)
+ .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username())));
+ }
+
+ @Test
+ @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
+ public void shouldReturnEmptyResponseWhenApprovedByOwnersWithUnexpandedFileOwners()
+ throws Exception {
+ addOwnerFileToRoot(true);
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files).isEmpty();
+ }
+
+ @Test
+ @GlobalPluginConfig(pluginName = "owners", name = "owners.expandGroups", value = "false")
+ public void shouldReturnResponseWithUnexpandedFileMatchersOwners() throws Exception {
+ addOwnerFileWithMatchersToRoot(true);
+ String changeId = createChange().getChangeId();
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files)
+ .containsExactly("a.txt", Sets.newHashSet(new GroupOwner(admin.username())));
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldReturnInheritedOwnersFromProjectsOwners() throws Exception {
+ assertInheritFromProject(project);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldReturnInheritedOwnersFromParentProjectsOwners() throws Exception {
+ assertInheritFromProject(allProjects);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldReflectChangesInParentProject() throws Exception {
+ addOwnerFileToProjectConfig(allProjects, true, admin);
+
+ String changeId = createChange().getChangeId();
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner));
+
+ addOwnerFileToProjectConfig(allProjects, true, user);
+ resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(projectOwner));
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldNotReturnInheritedOwnersFromProjectsOwners() throws Exception {
+ assertNotInheritFromProject(project);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldNotReturnInheritedOwnersFromParentProjectsOwners() throws Exception {
+ addOwnerFileToProjectConfig(project, false);
+ assertNotInheritFromProject(allProjects);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void shouldThrowExceptionWhenCodeReviewLabelIsNotConfigured() throws Exception {
+ addOwnerFileToProjectConfig(project, false);
+ replaceCodeReviewWithLabel(
+ TestLabels.label(
+ "Foo", TestLabels.value(1, "Foo is fine"), TestLabels.value(-1, "Foo is not fine")));
+ String changeId = createChange().getChangeId();
+
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(thrown).hasMessageThat().isEqualTo(GetFilesOwners.MISSING_CODE_REVIEW_LABEL);
+ assertThat(thrown).hasCauseThat().isInstanceOf(LabelNotFoundException.class);
+ }
+
+ protected void replaceCodeReviewWithLabel(LabelType label) throws Exception {
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ u.getConfig().getLabelSections().remove(LabelId.CODE_REVIEW);
+ u.getConfig().upsertLabelType(label);
+ u.save();
+ }
+ }
+
+ protected static <T> Response<T> assertResponseOk(Response<T> response) {
+ assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_OK);
+ return response;
+ }
+
+ private void assertNotInheritFromProject(Project.NameKey projectNameKey) throws Exception {
+ addOwnerFileToRoot(false);
+ addOwnerFileToProjectConfig(projectNameKey, true);
+
+ String changeId = createChange().getChangeId();
+ assertChangeHasOwners(changeId);
+ }
+
+ private void assertChangeHasOwners(String changeId)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files).containsExactly("a.txt", Sets.newHashSet(rootOwner));
+ }
+
+ private void assertInheritFromProject(Project.NameKey projectNameKey) throws Exception {
+ addOwnerFileToRoot(true);
+ addOwnerFileToProjectConfig(projectNameKey, true);
+
+ String changeId = createChange().getChangeId();
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+
+ assertThat(resp.value().files)
+ .containsExactly("a.txt", Sets.newHashSet(rootOwner, projectOwner));
+ }
+
+ private void addOwnerFileToProjectConfig(Project.NameKey projectNameKey, boolean inherit)
+ throws Exception {
+ addOwnerFileToProjectConfig(projectNameKey, inherit, user);
+ }
+
+ private void addOwnerFileToProjectConfig(
+ Project.NameKey projectNameKey, boolean inherit, TestAccount account) throws Exception {
+ TestRepository<InMemoryRepository> project = cloneProject(projectNameKey);
+ GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
+ project.reset(RefNames.REFS_CONFIG);
+ pushFactory
+ .create(
+ admin.newIdent(),
+ project,
+ "Add OWNER file",
+ "OWNERS",
+ String.format(
+ "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n",
+ inherit, account.email()))
+ .to(RefNames.REFS_CONFIG);
+ }
+
+ private void addOwnerFileToRoot(boolean inherit) throws Exception {
+ // Add OWNERS file to root:
+ //
+ // inherited: true
+ // owners:
+ // - admin
+ merge(
+ createChange(
+ testRepo,
+ "master",
+ "Add OWNER file",
+ "OWNERS",
+ String.format("inherited: %s\nowners:\n- %s\n", inherit, admin.email()),
+ ""));
+ }
+
+ private void addOwnerFileWithMatchersToRoot(boolean inherit) throws Exception {
+ addOwnerFileWithMatchers(testRepo, "master", inherit);
+ }
+
+ private void addOwnerFileWithMatchers(TestRepository<?> repo, String targetRef, boolean inherit)
+ throws Exception {
+ // Add OWNERS file to root:
+ //
+ // inherited: true
+ // matchers:
+ // - suffix: .txt
+ // owners:
+ // - admin@mail.com
+ Result changeCreated =
+ createChange(
+ repo,
+ targetRef,
+ "Add OWNER file",
+ "OWNERS",
+ String.format(
+ "inherited: %s\nmatchers:\n" + "- suffix: .txt\n owners:\n - %s\n",
+ inherit, admin.email()),
+ "");
+ changeCreated.assertOkStatus();
+ merge(changeCreated);
+ }
+
+ public TestRepository<InMemoryRepository> cloneProjectWithMetaRefs(Project.NameKey project)
+ throws Exception {
+ String uri = registerRepoConnection(project, admin);
+ String initialRef = "refs/remotes/origin/config";
+ DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
+
+ InMemoryRepository.Builder b = new InMemoryRepository.Builder().setRepositoryDescription(desc);
+ if (uri.startsWith("ssh://")) {
+ // SshTransport depends on a real FS to read ~/.ssh/config, but InMemoryRepository by default
+ // uses a null FS.
+ // Avoid leaking user state into our tests.
+ b.setFS(FS.detect().setUserHome(null));
+ }
+ InMemoryRepository dest = b.build();
+ Config cfg = dest.getConfig();
+ cfg.setString("remote", "origin", "url", uri);
+ cfg.setStringList(
+ "remote",
+ "origin",
+ "fetch",
+ Arrays.asList(
+ "+refs/heads/*:refs/remotes/origin/*", "+refs/meta/config:refs/remotes/origin/config"));
+ TestRepository<InMemoryRepository> testRepo = GitUtil.newTestRepository(dest);
+ FetchResult result = testRepo.git().fetch().setRemote("origin").call();
+ if (result.getTrackingRefUpdate(initialRef) != null) {
+ testRepo.reset(initialRef);
+ }
+ return testRepo;
+ }
+}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java
new file mode 100644
index 0000000..8172611
--- /dev/null
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersSubmitRequirementsIT.java
@@ -0,0 +1,143 @@
+// 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.googlesource.gerrit.owners.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.server.project.testing.TestLabels;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
+import com.googlesource.gerrit.owners.entities.FilesOwnersResponse;
+import com.googlesource.gerrit.owners.entities.Owner;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.Map;
+import org.apache.commons.compress.utils.Sets;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
+@UseLocalDisk
+public class GetFilesOwnersSubmitRequirementsIT extends GetFilesOwnersITAbstract {
+ @Inject private ProjectOperations projectOperations;
+
+ @Override
+ public void setUpTestPlugin() throws Exception {
+ Config pluginCfg = pluginConfig.getGlobalPluginConfig("owners");
+ // enable submit requirements and store them to the file as there is no `ConfigSuite` mechanism
+ // for plugin config and there is no other way (but adding it to each test case) to enable it
+ // globally
+ pluginCfg.setBoolean("owners", null, "enableSubmitRequirement", true);
+ Files.writeString(
+ server.getSitePath().resolve("etc").resolve("owners.config"),
+ pluginCfg.toText(),
+ StandardOpenOption.CREATE,
+ StandardOpenOption.APPEND);
+ super.setUpTestPlugin();
+ }
+
+ @Test
+ public void shouldRequireConfiguredCodeReviewScore() throws Exception {
+ // configure submit requirement to require CR+1 only
+ addOwnerFileToRoot(LabelDefinition.parse("Code-Review,1").get(), admin);
+
+ String changeId = createChange("Add a file", "foo", "bar").getChangeId();
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files)
+ .containsExactly("foo", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get())));
+ assertThat(resp.value().ownersLabels).isEmpty();
+
+ // give CR+1 as requested
+ recommend(changeId);
+
+ resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files).isEmpty();
+ assertThat(resp.value().ownersLabels)
+ .containsExactly(admin.id().get(), Map.of(LabelId.CODE_REVIEW, 1));
+ }
+
+ @Test
+ public void shouldRequireConfiguredLabelAndScore() throws Exception {
+ // configure submit requirement to require LabelFoo+1
+ String label = "LabelFoo";
+ addOwnerFileToRoot(LabelDefinition.parse(String.format("%s,1", label)).get(), admin);
+ replaceCodeReviewWithLabel(label);
+
+ String changeId = createChange("Add a file", "foo", "bar").getChangeId();
+
+ Response<FilesOwnersResponse> resp =
+ assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files)
+ .containsExactly("foo", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get())));
+ assertThat(resp.value().ownersLabels).isEmpty();
+
+ // give LabelFoo+1 as requested
+ gApi.changes().id(changeId).current().review(new ReviewInput().label(label, 1));
+
+ resp = assertResponseOk(ownersApi.apply(parseCurrentRevisionResource(changeId)));
+ assertThat(resp.value().files).isEmpty();
+ assertThat(resp.value().ownersLabels).containsEntry(admin.id().get(), Map.of(label, 1));
+ }
+
+ private void addOwnerFileToRoot(LabelDefinition label, TestAccount u) throws Exception {
+ // Add OWNERS file to root:
+ //
+ // inherited: true
+ // label: label,score # score is optional
+ // owners:
+ // - u.email()
+ String owners =
+ String.format(
+ "inherited: true\nlabel: %s\nowners:\n- %s\n",
+ String.format(
+ "%s%s",
+ label.getName(),
+ label.getScore().map(value -> String.format(",%d", value)).orElse("")),
+ u.email());
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners)
+ .to(RefNames.fullName("master"))
+ .assertOkStatus();
+ }
+
+ private void replaceCodeReviewWithLabel(String labelId) throws Exception {
+ LabelType label =
+ TestLabels.label(labelId, TestLabels.value(1, "OK"), TestLabels.value(-1, "Not OK"));
+
+ replaceCodeReviewWithLabel(label);
+
+ // grant label to RegisteredUsers so that it is vote-able
+ String heads = RefNames.REFS_HEADS + "*";
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allowLabel(label.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1))
+ .update();
+ }
+}