Inherit OWNERS from all parent projects

As a follow-up of I50c83466, consider the full
chain of parent projects when considering the inheritance
of project-level OWNERS stored in refs/meta/config.

Change-Id: I39491bce0dda726c49f1e2ccbc6eb299b7eb6152
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
index 35828f4..bae340a 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
@@ -46,14 +46,17 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -211,10 +214,15 @@
     try {
       ChangeApi cApi = changes.id(cId.get());
       ChangeInfo change = cApi.get();
-      Optional<Project.NameKey> maybeParentProjectNameKey =
+      List<NameKey> parentProjectsNameKeys =
           projectCache
-              .get(Project.NameKey.parse(change.project))
-              .map(p -> p.getProject().getParent());
+              .get(NameKey.parse(change.project))
+              .map(
+                  p ->
+                      p.parents().stream()
+                          .map(ProjectState::getNameKey)
+                          .collect(Collectors.toList()))
+              .orElse(Collections.emptyList());
 
       PatchList patchList = getPatchList(repository, event, change);
       if (patchList != null) {
@@ -223,7 +231,7 @@
                 accounts,
                 repositoryManager,
                 repository,
-                maybeParentProjectNameKey,
+                parentProjectsNameKeys,
                 cfg.isBranchDisabled(change.branch) ? Optional.empty() : Optional.of(change.branch),
                 patchList,
                 cfg.expandGroups());
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 d37a086..7e3a311 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
@@ -20,6 +20,7 @@
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.googlesource.gerrit.owners.common.JgitWrapper.getBlobAsBytes;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.SetMultimap;
@@ -37,6 +38,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -56,7 +58,7 @@
 
   private final Repository repository;
 
-  private final Optional<Project.NameKey> maybeParentRepo;
+  private final List<Project.NameKey> parentProjectsNames;
 
   private final PatchList patchList;
 
@@ -78,13 +80,13 @@
       Accounts accounts,
       GitRepositoryManager repositoryManager,
       Repository repository,
-      Optional<Project.NameKey> maybeParentRepo,
+      List<Project.NameKey> parentProjectsNames,
       Optional<String> branchWhenEnabled,
       PatchList patchList,
       boolean expandGroups) {
     this.repositoryManager = repositoryManager;
     this.repository = repository;
-    this.maybeParentRepo = maybeParentRepo;
+    this.parentProjectsNames = parentProjectsNames;
     this.patchList = patchList;
     this.parser = new ConfigurationParser(accounts);
     this.accounts = accounts;
@@ -141,10 +143,8 @@
     OwnersMap ownersMap = new OwnersMap();
     try {
       // Using a `map` would have needed a try/catch inside the lamba, resulting in more code
-      Optional<PathOwnersEntry> maybeParentPathOwnersEntry =
-          maybeParentRepo.isPresent()
-              ? Optional.of(getPathOwnersEntry(maybeParentRepo.get(), RefNames.REFS_CONFIG))
-              : Optional.empty();
+      List<PathOwnersEntry> parentsPathOwnersEntries =
+          getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG);
       PathOwnersEntry projectEntry = getPathOwnersEntry(repository, RefNames.REFS_CONFIG);
       PathOwnersEntry rootEntry = getPathOwnersEntry(repository, branch);
 
@@ -154,7 +154,7 @@
       for (String path : modifiedPaths) {
         currentEntry =
             resolvePathEntry(
-                path, branch, projectEntry, maybeParentPathOwnersEntry, rootEntry, entries);
+                path, branch, projectEntry, parentsPathOwnersEntries, rootEntry, entries);
 
         // add owners and reviewers to file for matcher predicates
         ownersMap.addFileOwners(path, currentEntry.getOwners());
@@ -189,11 +189,15 @@
     }
   }
 
-  private PathOwnersEntry getPathOwnersEntry(Project.NameKey projectName, String branch)
-      throws IOException {
-    try (Repository repo = repositoryManager.openRepository(projectName)) {
-      return getPathOwnersEntry(repo, branch);
+  private List<PathOwnersEntry> getPathOwnersEntries(
+      List<Project.NameKey> projectNames, String branch) throws IOException {
+    ImmutableList.Builder<PathOwnersEntry> pathOwnersEntries = ImmutableList.builder();
+    for (Project.NameKey projectName : projectNames) {
+      try (Repository repo = repositoryManager.openRepository(projectName)) {
+        pathOwnersEntries = pathOwnersEntries.add(getPathOwnersEntry(repo, branch));
+      }
     }
+    return pathOwnersEntries.build();
   }
 
   private PathOwnersEntry getPathOwnersEntry(Repository repo, String branch) throws IOException {
@@ -232,7 +236,7 @@
       String path,
       String branch,
       PathOwnersEntry projectEntry,
-      Optional<PathOwnersEntry> maybeParentPathOwnersEntry,
+      List<PathOwnersEntry> parentsPathOwnersEntries,
       PathOwnersEntry rootEntry,
       Map<String, PathOwnersEntry> entries)
       throws IOException {
@@ -244,8 +248,8 @@
     calculateCurrentEntry(rootEntry, projectEntry, currentEntry);
 
     // Inherit from Parent Project if OWNER in Project enables inheritance
-    if (maybeParentPathOwnersEntry.isPresent()) {
-      calculateCurrentEntry(projectEntry, maybeParentPathOwnersEntry.get(), currentEntry);
+    for (PathOwnersEntry parentPathOwnersEntry : parentsPathOwnersEntries) {
+      calculateCurrentEntry(projectEntry, parentPathOwnersEntry, currentEntry);
     }
 
     // Iterate through the parent paths, not including the file name
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/Config.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/Config.java
index 0426488..f298164 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/Config.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/Config.java
@@ -36,7 +36,8 @@
 public abstract class Config {
   protected GitRepositoryManager repositoryManager;
   protected Repository repository;
-  protected Repository parentRepository;
+  protected Repository parentRepository1;
+  protected Repository parentRepository2;
   protected PatchList patchList;
   protected ConfigurationParser parser;
   protected TestAccounts accounts = new TestAccounts();
@@ -47,7 +48,8 @@
 
     repositoryManager = PowerMock.createMock(GitRepositoryManager.class);
     repository = PowerMock.createMock(Repository.class);
-    parentRepository = PowerMock.createMock(Repository.class);
+    parentRepository1 = PowerMock.createMock(Repository.class);
+    parentRepository2 = PowerMock.createMock(Repository.class);
     parser = new ConfigurationParser(accounts);
   }
 
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
index 07a83d1..e8b4167 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/PathOwnersTest.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.owners.common;
 
 import static com.googlesource.gerrit.owners.common.MatcherConfig.suffixMatcher;
+import static java.util.Collections.EMPTY_LIST;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
@@ -30,6 +31,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,8 +48,10 @@
   private static final boolean EXPAND_GROUPS = true;
   private static final boolean DO_NOT_EXPAND_GROUPS = false;
   public static final String CLASSIC_FILE_TXT = "classic/file.txt";
-  public static final Project.NameKey parentRepositoryNameKey =
-      Project.NameKey.parse("parentRepository");
+  public static final Project.NameKey parentRepository1NameKey =
+      Project.NameKey.parse("parentRepository1");
+  public static final Project.NameKey parentRepository2NameKey =
+      Project.NameKey.parse("parentRepository2");
 
   @Override
   @Before
@@ -64,7 +68,7 @@
             accounts,
             repositoryManager,
             repository,
-            Optional.empty(),
+            Collections.EMPTY_LIST,
             branch,
             patchList,
             EXPAND_GROUPS);
@@ -84,7 +88,7 @@
             accounts,
             repositoryManager,
             repository,
-            Optional.empty(),
+            EMPTY_LIST,
             branch,
             patchList,
             DO_NOT_EXPAND_GROUPS);
@@ -104,7 +108,7 @@
             accounts,
             repositoryManager,
             repository,
-            Optional.empty(),
+            EMPTY_LIST,
             Optional.empty(),
             patchList,
             EXPAND_GROUPS);
@@ -122,13 +126,7 @@
 
     PathOwners owners2 =
         new PathOwners(
-            accounts,
-            repositoryManager,
-            repository,
-            Optional.empty(),
-            branch,
-            patchList,
-            EXPAND_GROUPS);
+            accounts, repositoryManager, repository, EMPTY_LIST, branch, patchList, EXPAND_GROUPS);
     Set<Account.Id> ownersSet2 = owners2.get().get(CLASSIC_OWNERS);
 
     // in this case we are inheriting the acct3 from /OWNERS
@@ -152,13 +150,7 @@
 
     PathOwners owners =
         new PathOwners(
-            accounts,
-            repositoryManager,
-            repository,
-            Optional.empty(),
-            branch,
-            patchList,
-            EXPAND_GROUPS);
+            accounts, repositoryManager, repository, EMPTY_LIST, branch, patchList, EXPAND_GROUPS);
 
     Map<String, Set<Account.Id>> fileOwners = owners.getFileOwners();
     assertEquals(1, fileOwners.size());
@@ -176,13 +168,13 @@
     expectConfig(
         "OWNERS",
         RefNames.REFS_CONFIG,
-        parentRepository,
+        parentRepository1,
         createConfig(true, owners(), suffixMatcher(".sql", USER_A_EMAIL_COM, USER_B_EMAIL_COM)));
 
     String fileName = "file.sql";
     creatingPatchList(Collections.singletonList(fileName));
 
-    mockParentRepository();
+    mockParentRepository(parentRepository1NameKey, parentRepository1);
     replayAll();
 
     PathOwners owners =
@@ -190,7 +182,7 @@
             accounts,
             repositoryManager,
             repository,
-            Optional.of(parentRepositoryNameKey),
+            Arrays.asList(parentRepository1NameKey),
             branch,
             patchList,
             EXPAND_GROUPS);
@@ -204,11 +196,55 @@
     assertTrue(ownersSet.contains(USER_B_ID));
   }
 
-  private void mockParentRepository() throws IOException {
-    expect(repositoryManager.openRepository(eq(parentRepositoryNameKey)))
-        .andReturn(parentRepository)
-        .anyTimes();
-    parentRepository.close();
+  @Test
+  public void testProjectInheritFromMultipleParentProjects() throws Exception {
+    expectConfig("OWNERS", "master", createConfig(true, owners()));
+    expectConfig("OWNERS", RefNames.REFS_CONFIG, repository, createConfig(true, owners()));
+    expectConfig(
+        "OWNERS",
+        RefNames.REFS_CONFIG,
+        parentRepository1,
+        createConfig(true, owners(), suffixMatcher(".sql", USER_A_EMAIL_COM)));
+    expectConfig(
+        "OWNERS",
+        RefNames.REFS_CONFIG,
+        parentRepository2,
+        createConfig(true, owners(), suffixMatcher(".java", USER_B_EMAIL_COM)));
+
+    String sqlFileName = "file.sql";
+    String javaFileName = "file.java";
+    creatingPatchList(Arrays.asList(sqlFileName, javaFileName));
+
+    mockParentRepository(parentRepository1NameKey, parentRepository1);
+    mockParentRepository(parentRepository2NameKey, parentRepository2);
+    replayAll();
+
+    PathOwners owners =
+        new PathOwners(
+            accounts,
+            repositoryManager,
+            repository,
+            Arrays.asList(parentRepository1NameKey, parentRepository2NameKey),
+            branch,
+            patchList,
+            EXPAND_GROUPS);
+
+    Map<String, Set<Account.Id>> fileOwners = owners.getFileOwners();
+    assertEquals(fileOwners.size(), 2);
+
+    Set<Account.Id> ownersSet1 = fileOwners.get(sqlFileName);
+    assertEquals(1, ownersSet1.size());
+    assertTrue(ownersSet1.contains(USER_A_ID));
+
+    Set<Account.Id> ownersSet2 = fileOwners.get(javaFileName);
+    assertEquals(1, ownersSet2.size());
+    assertTrue(ownersSet2.contains(USER_B_ID));
+  }
+
+  private void mockParentRepository(Project.NameKey repositoryName, Repository repository)
+      throws IOException {
+    expect(repositoryManager.openRepository(eq(repositoryName))).andReturn(repository).anyTimes();
+    repository.close();
     expectLastCall();
   }
 
@@ -223,13 +259,7 @@
 
     PathOwners owners =
         new PathOwners(
-            accounts,
-            repositoryManager,
-            repository,
-            Optional.empty(),
-            branch,
-            patchList,
-            EXPAND_GROUPS);
+            accounts, repositoryManager, repository, EMPTY_LIST, branch, patchList, EXPAND_GROUPS);
     Set<Account.Id> ownersSet = owners.get().get("dir/subdir/OWNERS");
 
     assertEquals(3, ownersSet.size());
diff --git a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
index aeffb90..2bcf2a7 100644
--- a/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
+++ b/owners-common/src/test/java/com/googlesource/gerrit/owners/common/RegexTest.java
@@ -21,6 +21,7 @@
 import static com.googlesource.gerrit.owners.common.MatcherConfig.regexMatcher;
 import static com.googlesource.gerrit.owners.common.MatcherConfig.suffixMatcher;
 import static com.googlesource.gerrit.owners.common.StreamUtils.iteratorStream;
+import static java.util.Collections.EMPTY_LIST;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.powermock.api.easymock.PowerMock.replayAll;
@@ -153,13 +154,7 @@
     // function under test
     PathOwners owners =
         new PathOwners(
-            accounts,
-            repositoryManager,
-            repository,
-            Optional.empty(),
-            branch,
-            patchList,
-            EXPAND_GROUPS);
+            accounts, repositoryManager, repository, EMPTY_LIST, branch, patchList, EXPAND_GROUPS);
 
     // assertions on classic owners
     Set<Account.Id> ownersSet = owners.get().get("project/OWNERS");
@@ -257,13 +252,7 @@
 
     PathOwners owners =
         new PathOwners(
-            accounts,
-            repositoryManager,
-            repository,
-            Optional.empty(),
-            branch,
-            patchList,
-            EXPAND_GROUPS);
+            accounts, repositoryManager, repository, EMPTY_LIST, branch, patchList, EXPAND_GROUPS);
 
     Set<String> ownedFiles = owners.getFileOwners().keySet();
     assertThat(ownedFiles).containsExactly("project/file.sql");
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 72f2cf4..7181ab6 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java
@@ -26,6 +26,9 @@
 import com.googlesource.gerrit.owners.common.Accounts;
 import com.googlesource.gerrit.owners.common.PathOwners;
 import com.googlesource.gerrit.owners.common.PluginSettings;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -51,8 +54,10 @@
             ProjectState projectState = StoredValues.PROJECT_STATE.get(engine);
             GitRepositoryManager gitRepositoryManager = StoredValues.REPO_MANAGER.get(engine);
 
-            Optional<Project.NameKey> maybeParentProjectNameKey =
-                Optional.ofNullable(projectState.getProject().getParent());
+            List<Project.NameKey> maybeParentProjectNameKey =
+                Optional.ofNullable(projectState.getProject().getParent())
+                    .map(Arrays::asList)
+                    .orElse(Collections.emptyList());
 
             String branch = StoredValues.getChange(engine).getDest().branch();
             return new 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 b05e2b2..a6c4fae 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
@@ -43,11 +43,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.EnumSet;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -90,8 +86,11 @@
     Change change = revision.getChange();
     int id = revision.getChangeResource().getChange().getChangeId();
 
-    Optional<Project.NameKey> maybeParentProjectNameKey =
-        projectCache.get(change.getProject()).map(p -> p.getProject().getParent());
+    List<Project.NameKey> maybeParentProjectNameKey =
+        projectCache
+            .get(change.getProject())
+            .map(p -> Arrays.asList(p.getProject().getParent()))
+            .orElse(Collections.emptyList());
 
     try (Repository repository = repositoryManager.openRepository(change.getProject())) {
       PatchList patchList = patchListCache.get(change, ps);
diff --git a/owners/src/main/resources/Documentation/config.md b/owners/src/main/resources/Documentation/config.md
index 9b74897..680b550 100644
--- a/owners/src/main/resources/Documentation/config.md
+++ b/owners/src/main/resources/Documentation/config.md
@@ -112,7 +112,7 @@
 OWNERS is used as global default.
 
 If the global project OWNERS has the 'inherited: true', it will check for a global project OWNERS
-in the parent project.
+in all parent projects up to All-Projects.
 
 ## Example 1 - OWNERS file without matchers and default Gerrit submit rules