Follow whole chain of parents for OWNERS in submit requirements

Extract functionality that traverses the whole chain of parents as a
static method in PathOwners and use it also to get a chain of parents
in the OwnersSubmitRequirement.

Bug: Issue 16893
Change-Id: I6db35e4b6c8800bf98c4db01394e8faf1775b8ed
diff --git a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
index 17e4347..b904715 100644
--- a/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
+++ b/owners-common/src/main/java/com/googlesource/gerrit/owners/common/PathOwners.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.DiffSummary;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.project.ProjectState;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
@@ -204,6 +205,12 @@
     return label;
   }
 
+  public static List<Project.NameKey> getParents(ProjectState projectState) {
+    return projectState.parents().stream()
+        .map(ProjectState::getNameKey)
+        .collect(Collectors.toList());
+  }
+
   /**
    * Fetched the owners for the associated patch list.
    *
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
index baff42d..e456879 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
@@ -31,7 +31,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -62,11 +61,7 @@
 
             metrics.countConfigLoads.increment();
             try (Timer0.Context ctx = metrics.loadConfig.start()) {
-              List<Project.NameKey> parentProjectsNameKeys =
-                  projectState.parents().stream()
-                      .map(ProjectState::getNameKey)
-                      .collect(Collectors.toList());
-
+              List<Project.NameKey> parentProjectsNameKeys = PathOwners.getParents(projectState);
               String branch = StoredValues.getChange(engine).getDest().branch();
               return new PathOwners(
                   accounts,
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
index 97d8098..0a2c5dd 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersSubmitRequirement.java
@@ -51,8 +51,6 @@
 import com.googlesource.gerrit.owners.common.PathOwnersEntriesCache;
 import com.googlesource.gerrit.owners.common.PluginSettings;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -200,11 +198,7 @@
     try (Timer0.Context ctx = metrics.loadConfig.start()) {
       String branch = cd.change().getDest().branch();
 
-      List<Project.NameKey> parents =
-          Optional.<Project.NameKey>ofNullable(projectState.getProject().getParent())
-              .map(Arrays::asList)
-              .orElse(Collections.emptyList());
-
+      List<Project.NameKey> parents = PathOwners.getParents(projectState);
       Project.NameKey nameKey = projectState.getNameKey();
       try (Repository repo = repoManager.openRepository(nameKey)) {
         PathOwners pathOwners =
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 e93e34b..47b104c 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
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -45,6 +44,7 @@
 import com.googlesource.gerrit.owners.entities.FilesOwnersResponse;
 import com.googlesource.gerrit.owners.entities.GroupOwner;
 import com.googlesource.gerrit.owners.entities.Owner;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -102,10 +102,7 @@
 
     Project.NameKey project = change.getProject();
     List<Project.NameKey> projectParents =
-        projectCache.get(project).stream()
-            .flatMap(s -> s.parents().stream())
-            .map(ProjectState::getNameKey)
-            .collect(Collectors.toList());
+        projectCache.get(project).map(PathOwners::getParents).orElse(Collections.emptyList());
 
     try (Repository repository = repositoryManager.openRepository(project)) {
       Set<String> changePaths = new HashSet<>(changeDataFactory.create(change).currentFilePaths());
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
index 002dc4b..5f6be28 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
@@ -23,6 +23,7 @@
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.util.stream.Collectors.joining;
 
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
@@ -34,9 +35,11 @@
 import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
 import com.google.gerrit.extensions.common.SubmitRecordInfo;
@@ -46,6 +49,8 @@
 import com.googlesource.gerrit.owners.common.LabelDefinition;
 import java.util.Collection;
 import java.util.stream.Stream;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
 @TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
@@ -359,6 +364,40 @@
     assertThat(ownersVoteSufficient.requirements).containsExactly(READY);
   }
 
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireApprovalFromGrandParentProjectOwner() throws Exception {
+    Project.NameKey parentProjectName =
+        createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY);
+    Project.NameKey childProjectName =
+        createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY);
+    TestRepository<InMemoryRepository> childRepo = cloneProject(childProjectName);
+
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRefsMetaConfig(true, admin2, allProjects);
+
+    PushOneCommit.Result r =
+        createCommitAndPush(childRepo, "refs/for/master", "Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+  }
+
   private void verifyHasSubmitRecord(
       Collection<SubmitRecordInfo> records, String label, SubmitRecordInfo.Label.Status status) {
     assertThat(
@@ -430,6 +469,17 @@
     pushOwnersToMaster(String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()));
   }
 
+  private void addOwnerFileToRefsMetaConfig(
+      boolean inherit, TestAccount u, Project.NameKey projectName) throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // owners:
+    // - u.email()
+    pushOwnersToRefsMetaConfig(
+        String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()), projectName);
+  }
+
   protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
       throws Exception {
     // Add OWNERS file to root:
@@ -455,4 +505,15 @@
         .to(RefNames.fullName("master"))
         .assertOkStatus();
   }
+
+  private void pushOwnersToRefsMetaConfig(String owners, Project.NameKey projectName)
+      throws Exception {
+    TestRepository<InMemoryRepository> project = cloneProject(projectName);
+    GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
+    project.reset(RefNames.REFS_CONFIG);
+    pushFactory
+        .create(admin.newIdent(), project, "Add OWNER file", "OWNERS", owners)
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+  }
 }