Make response of get code owners REST endpoints extensible

At the moment the get code owners REST endpoints return a list of
CodeOwnerInfo's. This doesn't allow us to add further fields to the
response in the future (e.g. we consider adding an
"owned_by_all_users" boolean field). To make the response extensible
we change it from returning List<CodeOwnerInfo> to returning
CodeOwnersInfo that has a "code_owners" field that contains the code
owners as List<CodeOwnerInfo>.

This is a backwards incompatible change and breaks existing callers of
this API. To avoid this the frontend was adapted to be able to
understand both response formats and other existing callers that we know
are okay to be broken temporarily until they adapt to the new response
format.

Change-Id: Ib0c4c8edf3952b48bccdc99baf77380a3b3f0c49
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
index e88bdb8..674d4a5 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
@@ -24,7 +24,6 @@
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.EnumSet;
-import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 
@@ -56,7 +55,7 @@
      *     exist
      * @return the code owners for the given path
      */
-    public abstract List<CodeOwnerInfo> get(Path path) throws RestApiException;
+    public abstract CodeOwnersInfo get(Path path) throws RestApiException;
 
     /**
      * Lists the code owners for the given path.
@@ -65,7 +64,7 @@
      *     exist
      * @return the code owners for the given path
      */
-    public List<CodeOwnerInfo> get(String path) throws RestApiException {
+    public CodeOwnersInfo get(String path) throws RestApiException {
       return get(Paths.get(path));
     }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
new file mode 100644
index 0000000..5ce7ac2
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * Representation of a code owners list in the REST API.
+ *
+ * <p>This class determines the JSON format for the response of the {@code
+ * com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch} and {@code
+ * com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInChange} REST endpoints.
+ */
+public class CodeOwnersInfo {
+  /** List of code owners. */
+  public List<CodeOwnerInfo> codeOwners;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
index fab0648..da27858 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInBranchImpl.java
@@ -18,8 +18,8 @@
 
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInBranchCollection;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch;
 import com.google.gerrit.server.project.BranchResource;
@@ -27,7 +27,6 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.nio.file.Path;
-import java.util.List;
 
 /** Implementation of the {@link CodeOwners} API for a branch. */
 public class CodeOwnersInBranchImpl implements CodeOwners {
@@ -53,7 +52,7 @@
   public QueryRequest query() {
     return new QueryRequest() {
       @Override
-      public List<CodeOwnerInfo> get(Path path) throws RestApiException {
+      public CodeOwnersInfo get(Path path) throws RestApiException {
         try {
           GetCodeOwnersForPathInBranch getCodeOwners = getCodeOwnersProvider.get();
           getOptions().forEach(getCodeOwners::addOption);
diff --git a/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
index f91c08f..16bbd78 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/CodeOwnersInChangeImpl.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInChangeCollection;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInChange;
 import com.google.gerrit.server.change.RevisionResource;
@@ -28,7 +28,6 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.nio.file.Path;
-import java.util.List;
 
 /** Implementation of the {@link CodeOwners} API for a revision in a change. */
 public class CodeOwnersInChangeImpl implements CodeOwners {
@@ -54,7 +53,7 @@
   public QueryRequest query() {
     return new QueryRequest() {
       @Override
-      public List<CodeOwnerInfo> get(Path path) throws RestApiException {
+      public CodeOwnersInfo get(Path path) throws RestApiException {
         try {
           if (getRevision().isPresent()) {
             throw new BadRequestException("specifying revision is not supported");
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index 7a010e7..a0ab229 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -32,7 +32,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
@@ -137,7 +137,7 @@
     this.hexOptions = new HashSet<>();
   }
 
-  protected Response<List<CodeOwnerInfo>> applyImpl(R rsrc)
+  protected Response<CodeOwnersInfo> applyImpl(R rsrc)
       throws AuthException, BadRequestException, PermissionBackendException {
     parseHexOptions();
     validateLimit();
@@ -212,10 +212,12 @@
       }
     }
 
-    return Response.ok(
+    CodeOwnersInfo codeOwnersInfo = new CodeOwnersInfo();
+    codeOwnersInfo.codeOwners =
         codeOwnerJsonFactory
             .create(getFillOptions())
-            .format(sortAndLimit(distanceScoring.build(), ImmutableSet.copyOf(codeOwners))));
+            .format(sortAndLimit(distanceScoring.build(), ImmutableSet.copyOf(codeOwners)));
+    return Response.ok(codeOwnersInfo);
   }
 
   private CodeOwnerResolverResult getGlobalCodeOwners(Project.NameKey projectName) {
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
index b1d3957..e4099c6 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
@@ -35,7 +35,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
-import java.util.List;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -93,7 +92,7 @@
   }
 
   @Override
-  public Response<List<CodeOwnerInfo>> apply(CodeOwnersInBranchCollection.PathResource rsrc)
+  public Response<CodeOwnersInfo> apply(CodeOwnersInBranchCollection.PathResource rsrc)
       throws RestApiException, PermissionBackendException, IOException {
     if (revision != null) {
       validateRevision(rsrc.getBranch(), revision);
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
index 784d42b..5e188b8 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.util.List;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
@@ -73,7 +72,7 @@
   }
 
   @Override
-  public Response<List<CodeOwnerInfo>> apply(CodeOwnersInChangeCollection.PathResource rsrc)
+  public Response<CodeOwnersInfo> apply(CodeOwnersInChangeCollection.PathResource rsrc)
       throws RestApiException, PermissionBackendException {
     return super.applyImpl(rsrc);
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerInfoSubject.java
index be0e392..29df00b 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerInfoSubject.java
@@ -59,7 +59,7 @@
     return ListSubject.assertThat(codeOwnerInfos, codeOwnerInfos());
   }
 
-  private static Factory<CodeOwnerInfoSubject, CodeOwnerInfo> codeOwnerInfos() {
+  public static Factory<CodeOwnerInfoSubject, CodeOwnerInfo> codeOwnerInfos() {
     return CodeOwnerInfoSubject::new;
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
new file mode 100644
index 0000000..5e30c97
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.codeOwnerInfos;
+import static com.google.gerrit.truth.ListSubject.elements;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
+import com.google.gerrit.truth.ListSubject;
+
+/** {@link Subject} for doing assertions on {@link CodeOwnersInfo}s. */
+public class CodeOwnersInfoSubject extends Subject {
+  /**
+   * Starts fluent chain to do assertions on a {@link CodeOwnersInfo}.
+   *
+   * @param codeOwnersInfo the code owners info on which assertions should be done
+   * @return the created {@link CodeOwnersInfoSubject}
+   */
+  public static CodeOwnersInfoSubject assertThat(CodeOwnersInfo codeOwnersInfo) {
+    return assertAbout(codeOwnersInfos()).that(codeOwnersInfo);
+  }
+
+  private static Factory<CodeOwnersInfoSubject, CodeOwnersInfo> codeOwnersInfos() {
+    return CodeOwnersInfoSubject::new;
+  }
+
+  private final CodeOwnersInfo codeOwnersInfo;
+
+  private CodeOwnersInfoSubject(FailureMetadata metadata, CodeOwnersInfo codeOwnersInfo) {
+    super(metadata, codeOwnersInfo);
+    this.codeOwnersInfo = codeOwnersInfo;
+  }
+
+  public ListSubject<CodeOwnerInfoSubject, CodeOwnerInfo> hasCodeOwnersThat() {
+    return check("codeOwners()")
+        .about(elements())
+        .thatCustom(codeOwnersInfo().codeOwners, codeOwnerInfos());
+  }
+
+  private CodeOwnersInfo codeOwnersInfo() {
+    isNotNull();
+    return codeOwnersInfo;
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
index b5d874f..9138ef0 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -17,9 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
-import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.assertThatList;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountName;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
@@ -43,8 +43,8 @@
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch;
 import com.google.inject.Inject;
@@ -81,18 +81,18 @@
   /** Must return the {@link CodeOwners} API against which the tests should be run. */
   protected abstract CodeOwners getCodeOwnersApi() throws RestApiException;
 
-  protected List<CodeOwnerInfo> queryCodeOwners(String path) throws RestApiException {
+  protected CodeOwnersInfo queryCodeOwners(String path) throws RestApiException {
     return queryCodeOwners(getCodeOwnersApi().query(), path);
   }
 
-  protected List<CodeOwnerInfo> queryCodeOwners(CodeOwners.QueryRequest queryRequest, String path)
+  protected CodeOwnersInfo queryCodeOwners(CodeOwners.QueryRequest queryRequest, String path)
       throws RestApiException {
     return queryRequest.get(path);
   }
 
   @Test
   public void getCodeOwnersWhenNoCodeOwnerConfigsExist() throws Exception {
-    assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
+    assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -105,7 +105,7 @@
         .addCodeOwnerEmail(admin.email())
         .addCodeOwnerEmail(user.email())
         .create();
-    assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
+    assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -145,13 +145,15 @@
         .addCodeOwnerEmail(user2.email())
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(useAbsolutePath ? "/foo/bar/baz.md" : "foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id(), user.id(), admin.id())
         .inOrder();
-    assertThat(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountName())
         .containsExactly(null, null, null);
   }
@@ -200,8 +202,9 @@
     // The 3. code owner config is ignored since the 2. code owner config has set
     // 'ignoreParentCodeOwners=true'. Hence the expected code owners are only the users that are
     // code owner according to the 1. and 2. code owner config: user2 + user
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id(), user.id())
         .inOrder();
@@ -227,12 +230,14 @@
         .create();
 
     assertThat(queryCodeOwners("/foo/bar/config.txt"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id());
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
-    assertThat(queryCodeOwners("/foo/bar/main.config")).isEmpty();
+    assertThat(queryCodeOwners("/foo/bar/main.config")).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -245,11 +250,15 @@
         .addCodeOwnerEmail(admin.email())
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(
             getCodeOwnersApi().query().withOptions(ListAccountsOption.DETAILS), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
-    assertThat(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountName())
         .containsExactly(admin.fullName());
   }
@@ -270,12 +279,16 @@
     accountOperations.account(admin.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
 
     // Make the request with the admin user that has the 'Modify Account' global capability.
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(
             getCodeOwnersApi().query().withOptions(ListAccountsOption.ALL_EMAILS),
             "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .onlyElement()
         .hasSecondaryEmailsThat()
         .containsExactly(secondaryEmail);
@@ -325,17 +338,22 @@
     accountOperations.account(admin.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
 
     // Make the request with the admin user that has the 'Modify Account' global capability.
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(
             getCodeOwnersApi()
                 .query()
                 .withOptions(ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS),
             "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
-    assertThat(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountName())
         .containsExactly(admin.fullName());
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .onlyElement()
         .hasSecondaryEmailsThat()
         .containsExactly(secondaryEmail);
@@ -397,6 +415,7 @@
 
     // Make the request as admin who can see all accounts.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), user3.id());
 
@@ -408,6 +427,7 @@
     // We expect only user2 and user3 as code owner (user and admin should be filtered
     // out because user2 cannot see their accounts).
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id(), user3.id());
   }
@@ -426,6 +446,7 @@
 
     // Check that both code owners are suggested.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id());
 
@@ -439,6 +460,7 @@
 
     // Expect that 'user' is filtered out now.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id());
   }
@@ -460,12 +482,14 @@
 
     // admin has the "Modify Account" global capability and hence can see secondary emails
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
 
     // user can see the own secondary email
     requestScopeOperations.setApiUser(user.id());
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
 
@@ -473,7 +497,7 @@
     // email
     TestAccount user2 = accountCreator.user2();
     requestScopeOperations.setApiUser(user2.id());
-    assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
+    assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -494,8 +518,10 @@
     codeOwnerConfigCreation.create();
 
     // Assert that the result is limited by the default limit.
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar.md");
-    assertThat(codeOwnerInfos).hasSize(GetCodeOwnersForPathInBranch.DEFAULT_LIMIT);
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .hasSize(GetCodeOwnersForPathInBranch.DEFAULT_LIMIT);
   }
 
   @Test
@@ -521,20 +547,26 @@
         .create();
 
     // get code owners with different limits
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(1);
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1);
     // the first 2 code owners have the same scoring, so their order is random and we don't know
     // which of them we get when the limit is 1
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
 
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
   }
@@ -569,8 +601,11 @@
         .addCodeOwnerEmail(admin.email())
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
   }
 
   @Test
@@ -579,6 +614,7 @@
     TestAccount globalOwner =
         accountCreator.create("global_owner", "global.owner@example.com", "Global Owner", null);
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(globalOwner.id());
   }
@@ -588,20 +624,23 @@
   public void getAllUsersAsGlobalCodeOwners() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id(), admin.id());
 
     // Query code owners with a limit.
     requestScopeOperations.setApiUser(user.id());
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(2);
-    assertThatList(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(2);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(0)
         .hasAccountIdThat()
         .isAnyOf(user.id(), user2.id(), admin.id());
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(1)
         .hasAccountIdThat()
         .isAnyOf(user.id(), user2.id(), admin.id());
@@ -640,37 +679,57 @@
         .create();
 
     // get code owners with different limits
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(1);
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1);
     // the first 2 code owners have the same scoring, so their order is random and we don't know
     // which of them we get when the limit is 1
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
 
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(4);
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(4);
     // the order of the first 2 code owners is random
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(1)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(2)
+        .hasAccountIdThat()
+        .isEqualTo(admin.id());
     // the order of the global code owners is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(3)
         .hasAccountIdThat()
         .isAnyOf(globalOwner1.id(), globalOwner2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), globalOwner1.id(), globalOwner2.id());
   }
@@ -687,6 +746,7 @@
         .create();
 
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
   }
@@ -738,64 +798,100 @@
         .create();
 
     // get code owners with different limits
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(1);
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1);
     // the first 2 code owners have the same scoring, so their order is random and we don't know
     // which of them we get when the limit is 1
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
 
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(4);
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(4).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(4);
     // the order of the first 2 code owners is random
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(1)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(2)
+        .hasAccountIdThat()
+        .isEqualTo(admin.id());
     // the order of the default code owners is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(3)
         .hasAccountIdThat()
         .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(5).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(
             admin.id(), user.id(), user2.id(), defaultCodeOwner1.id(), defaultCodeOwner2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(6).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(6);
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(6).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(6);
     // the order of the first 2 code owners is random
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(1).hasAccountIdThat().isAnyOf(user.id(), user2.id());
-    assertThatList(codeOwnerInfos).element(2).hasAccountIdThat().isEqualTo(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(1)
+        .hasAccountIdThat()
+        .isAnyOf(user.id(), user2.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(2)
+        .hasAccountIdThat()
+        .isEqualTo(admin.id());
     // the order of the default code owners is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(3)
         .hasAccountIdThat()
         .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(4)
         .hasAccountIdThat()
         .isAnyOf(defaultCodeOwner1.id(), defaultCodeOwner2.id());
     // the order of the global code owners is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(5)
         .hasAccountIdThat()
         .isAnyOf(globalOwner1.id(), globalOwner2.id());
 
-    codeOwnerInfos = getCodeOwnersApi().query().withLimit(7).get("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = getCodeOwnersApi().query().withLimit(7).get("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(
             admin.id(),
@@ -821,20 +917,23 @@
         .addCodeOwnerEmail("*")
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id(), admin.id());
 
     // Query code owners with a limit.
     requestScopeOperations.setApiUser(user.id());
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(2);
-    assertThatList(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(2);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(0)
         .hasAccountIdThat()
         .isAnyOf(user.id(), user2.id(), admin.id());
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .element(1)
         .hasAccountIdThat()
         .isAnyOf(user.id(), user2.id(), admin.id());
@@ -859,28 +958,37 @@
 
     // user can only see itself
     requestScopeOperations.setApiUser(user.id());
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(user.id());
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id());
 
     // user2 can see user3 and itself
     requestScopeOperations.setApiUser(user2.id());
-    codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id(), user3.id());
 
     // admin can see all users
     requestScopeOperations.setApiUser(admin.id());
-    codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), user3.id());
 
     // Query code owners with a limit, user2 can see user3 and itself
     requestScopeOperations.setApiUser(user2.id());
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(1);
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user2.id(), user3.id());
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user2.id(), user3.id());
   }
 
   @Test
@@ -910,30 +1018,37 @@
     // user can only see itself, user2 (because user is owner of a group that contains user2) and
     // user3 (because user3 is member of a group that is visible to all users)
     requestScopeOperations.setApiUser(user.id());
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id(), user2.id(), user3.id());
 
     // user2 can see user3 and itself
     requestScopeOperations.setApiUser(user2.id());
-    codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id(), user3.id());
 
     // admin can see all users
     requestScopeOperations.setApiUser(admin.id());
-    codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), user3.id());
 
     // Query code owners with a limit, user2 can see user3 and itself
     requestScopeOperations.setApiUser(user2.id());
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(1);
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isAnyOf(user2.id(), user3.id());
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(1);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isAnyOf(user2.id(), user3.id());
   }
 
   @Test
@@ -952,8 +1067,8 @@
 
     // Use user, since admin is allowed to view all accounts.
     requestScopeOperations.setApiUser(user.id());
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).isEmpty();
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -976,8 +1091,9 @@
         .addCodeOwnerEmail("*")
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
   }
@@ -998,13 +1114,17 @@
 
     // Query code owners with limits, "*" is resolved to random users, but user2 should always be
     // included since this user is set explicitly as code owner
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).hasSize(2);
-    assertThatList(codeOwnerInfos).comparingElementsUsing(hasAccountId()).contains(user2.id());
+    assertThat(codeOwnersInfo).hasCodeOwnersThat().hasSize(2);
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .contains(user2.id());
 
-    codeOwnerInfos = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
-    assertThatList(codeOwnerInfos)
+    codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(1), "/foo/bar/baz.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id());
   }
@@ -1026,18 +1146,22 @@
 
     long seed = (new Random()).nextLong();
 
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md");
     // all code owners have the same score, hence their order is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
 
     // Check that the order for further requests that use the same seed is the same.
     List<Account.Id> expectedAccountIds =
-        codeOwnerInfos.stream().map(info -> Account.id(info.account._accountId)).collect(toList());
+        codeOwnersInfo.codeOwners.stream()
+            .map(info -> Account.id(info.account._accountId))
+            .collect(toList());
     for (int i = 0; i < 10; i++) {
-      assertThatList(queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md"))
+      assertThat(queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md"))
+          .hasCodeOwnersThat()
           .comparingElementsUsing(hasAccountId())
           .containsExactlyElementsIn(expectedAccountIds)
           .inOrder();
@@ -1060,18 +1184,22 @@
 
     long seed = (new Random()).nextLong();
 
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md");
     // all code owners have the same score, hence their order is random
-    assertThatList(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id());
 
     // Check that the order for further requests that use the same seed is the same.
     List<Account.Id> expectedAccountIds =
-        codeOwnerInfos.stream().map(info -> Account.id(info.account._accountId)).collect(toList());
+        codeOwnersInfo.codeOwners.stream()
+            .map(info -> Account.id(info.account._accountId))
+            .collect(toList());
     for (int i = 0; i < 10; i++) {
-      assertThatList(queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md"))
+      assertThat(queryCodeOwners(getCodeOwnersApi().query().withSeed(seed), "/foo/bar/baz.md"))
+          .hasCodeOwnersThat()
           .comparingElementsUsing(hasAccountId())
           .containsExactlyElementsIn(expectedAccountIds)
           .inOrder();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
index 2e14b98..c260776 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.assertThatList;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.fail;
@@ -30,8 +30,8 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSetModification;
 import com.google.inject.Inject;
@@ -82,24 +82,31 @@
         .addCodeOwnerEmail(user.email())
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), user3.id(), user4.id());
 
     // The first code owner in the result should be user as user has the best distance score.
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isEqualTo(user.id());
 
     // The order of the other code owners is random since they have the same score.
     // Check that the order of the code owners with the same score is different for further requests
     // at least once.
     List<Account.Id> accountIdsInRetrievedOrder1 =
-        codeOwnerInfos.stream().map(info -> Account.id(info.account._accountId)).collect(toList());
+        codeOwnersInfo.codeOwners.stream()
+            .map(info -> Account.id(info.account._accountId))
+            .collect(toList());
     boolean foundOtherOrder = false;
     for (int i = 0; i < 10; i++) {
-      codeOwnerInfos = queryCodeOwners("/foo/bar.md");
+      codeOwnersInfo = queryCodeOwners("/foo/bar.md");
       List<Account.Id> accountIdsInRetrievedOrder2 =
-          codeOwnerInfos.stream()
+          codeOwnersInfo.codeOwners.stream()
               .map(info -> Account.id(info.account._accountId))
               .collect(toList());
       if (!accountIdsInRetrievedOrder1.equals(accountIdsInRetrievedOrder2)) {
@@ -137,16 +144,20 @@
     assertThat(revision1).isNotEqualTo(revision2);
 
     // For the first revision we expect that only 'admin' is returned as code owner.
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         queryCodeOwners(
             getCodeOwnersApi().query().forRevision(revision1.name()), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
 
     // For the second revision we expect that 'admin' and 'user' are returned as code owners.
-    codeOwnerInfos =
+    codeOwnersInfo =
         queryCodeOwners(
             getCodeOwnersApi().query().forRevision(revision2.name()), "/foo/bar/baz.md");
-    assertThat(codeOwnerInfos)
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id());
   }
@@ -212,6 +223,7 @@
 
     // Expect that 'serviceUser' is included.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), serviceUser.id());
   }
@@ -227,6 +239,7 @@
         .addMember(serviceUser.id())
         .update();
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(serviceUser.id());
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
index b74df75..c3e7ee1 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.assertThatList;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
@@ -34,8 +34,8 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwners;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.inject.Inject;
 import java.util.List;
@@ -92,7 +92,7 @@
   }
 
   @Override
-  protected List<CodeOwnerInfo> queryCodeOwners(CodeOwners.QueryRequest queryRequest, String path)
+  protected CodeOwnersInfo queryCodeOwners(CodeOwners.QueryRequest queryRequest, String path)
       throws RestApiException {
     assertWithMessage("test path %s was not registered", path)
         .that(gApi.changes().id(changeId).current().files())
@@ -125,22 +125,29 @@
         .addCodeOwnerEmail(user.email())
         .create();
 
-    List<CodeOwnerInfo> codeOwnerInfos = queryCodeOwners("/foo/bar.md");
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar.md");
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), user.id(), user2.id(), user3.id(), user4.id());
 
     // The first code owner in the result should be user as user has the best distance score.
-    assertThatList(codeOwnerInfos).element(0).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .element(0)
+        .hasAccountIdThat()
+        .isEqualTo(user.id());
 
     // The order of the other code owners is random since they have the same score.
     // Check that the order of the code owners with the same score is the same for further requests.
     List<Account.Id> accountIdsInRetrievedOrder1 =
-        codeOwnerInfos.stream().map(info -> Account.id(info.account._accountId)).collect(toList());
+        codeOwnersInfo.codeOwners.stream()
+            .map(info -> Account.id(info.account._accountId))
+            .collect(toList());
     for (int i = 0; i < 10; i++) {
-      codeOwnerInfos = queryCodeOwners("/foo/bar.md");
+      codeOwnersInfo = queryCodeOwners("/foo/bar.md");
       List<Account.Id> accountIdsInRetrievedOrder2 =
-          codeOwnerInfos.stream()
+          codeOwnersInfo.codeOwners.stream()
               .map(info -> Account.id(info.account._accountId))
               .collect(toList());
       if (!accountIdsInRetrievedOrder1.equals(accountIdsInRetrievedOrder2)) {
@@ -165,9 +172,12 @@
     String path = "/foo/bar/baz.txt";
     String changeId = createChangeWithFileDeletion(path);
 
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         codeOwnersApiFactory.change(changeId, "current").query().get(path);
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(user.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id());
   }
 
   @Test
@@ -194,15 +204,17 @@
     String newPath = "/foo/new/bar.txt";
     String changeId = createChangeWithFileRename(oldPath, newPath);
 
-    List<CodeOwnerInfo> codeOwnerInfosNewPath =
+    CodeOwnersInfo codeOwnersInfoNewPath =
         codeOwnersApiFactory.change(changeId, "current").query().get(newPath);
-    assertThat(codeOwnerInfosNewPath)
+    assertThat(codeOwnersInfoNewPath)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
 
-    List<CodeOwnerInfo> codeOwnerInfosOldPath =
+    CodeOwnersInfo codeOwnersInfoOldPath =
         codeOwnersApiFactory.change(changeId, "current").query().get(oldPath);
-    assertThat(codeOwnerInfosOldPath)
+    assertThat(codeOwnersInfoOldPath)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user2.id());
   }
@@ -240,9 +252,12 @@
     // Check that 'user' is anyway suggested as code owner for the file in the private change since
     // by adding 'user' as reviewer the change would get visible to 'user'.
     requestScopeOperations.setApiUser(changeOwner.id());
-    List<CodeOwnerInfo> codeOwnerInfos =
+    CodeOwnersInfo codeOwnersInfo =
         codeOwnersApiFactory.change(changeId, "current").query().get(TEST_PATHS.get(0));
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(user.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(user.id());
   }
 
   @Test
@@ -262,6 +277,7 @@
 
     // Check that both code owners are suggested.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id(), serviceUser.id());
 
@@ -274,6 +290,7 @@
 
     // Expect that 'serviceUser' is filtered out now.
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(admin.id());
   }
@@ -288,7 +305,7 @@
         .forUpdate()
         .addMember(serviceUser.id())
         .update();
-    assertThat(queryCodeOwners("/foo/bar/baz.md")).isEmpty();
+    assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty();
   }
 
   @Test
@@ -303,6 +320,7 @@
         .create();
 
     assertThat(queryCodeOwners("/foo/bar/baz.md"))
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountId())
         .containsExactly(user.id());
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/AbstractGetCodeOwnersForPathRestIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/AbstractGetCodeOwnersForPathRestIT.java
index dcd0cbb..cadf021 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/AbstractGetCodeOwnersForPathRestIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/AbstractGetCodeOwnersForPathRestIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountName;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableSet;
@@ -27,11 +28,10 @@
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
-import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import java.util.Arrays;
-import java.util.List;
 import org.junit.Test;
 
 /**
@@ -93,13 +93,17 @@
                 "o=" + ListAccountsOption.DETAILS.name(),
                 "o=" + ListAccountsOption.ALL_EMAILS.name()));
     r.assertOK();
-    List<CodeOwnerInfo> codeOwnerInfos =
-        newGson().fromJson(r.getReader(), new TypeToken<List<CodeOwnerInfo>>() {}.getType());
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo =
+        newGson().fromJson(r.getReader(), new TypeToken<CodeOwnersInfo>() {}.getType());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountName())
         .containsExactly(admin.fullName());
-    assertThat(Iterables.getOnlyElement(codeOwnerInfos).account.secondaryEmails)
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwners).account.secondaryEmails)
         .containsExactly(secondaryEmail);
   }
 
@@ -127,13 +131,17 @@
                         ImmutableSet.of(
                             ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS))));
     r.assertOK();
-    List<CodeOwnerInfo> codeOwnerInfos =
-        newGson().fromJson(r.getReader(), new TypeToken<List<CodeOwnerInfo>>() {}.getType());
-    assertThat(codeOwnerInfos).comparingElementsUsing(hasAccountId()).containsExactly(admin.id());
-    assertThat(codeOwnerInfos)
+    CodeOwnersInfo codeOwnersInfo =
+        newGson().fromJson(r.getReader(), new TypeToken<CodeOwnersInfo>() {}.getType());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
+        .comparingElementsUsing(hasAccountId())
+        .containsExactly(admin.id());
+    assertThat(codeOwnersInfo)
+        .hasCodeOwnersThat()
         .comparingElementsUsing(hasAccountName())
         .containsExactly(admin.fullName());
-    assertThat(Iterables.getOnlyElement(codeOwnerInfos).account.secondaryEmails)
+    assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwners).account.secondaryEmails)
         .containsExactly(secondaryEmail);
   }
 
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index f7b536a..6656020 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -421,7 +421,8 @@
 | `seed`       | optional | Seed, as a long value, that should be used to shuffle code owners that have the same score. Can be used to make the sort order stable across several requests, e.g. to get the same set of random code owners for different file paths that have the same code owners. Important: the sort order is only stable if the requests use the same seed **and** the same limit. In addition, the sort order is not guaranteed to be stable if new accounts are created in between the requests, or if the account visibility is changed.
 | `revision`   | optional | Revision from which the code owner configs should be read as commit SHA1. Can be used to read historic code owners from this branch, but imports from other branches or repositories as well as default and global code owners from `refs/meta/config` are still read from the current revisions. If not specified the code owner configs are read from the HEAD revision of the branch. Not supported for getting code owners for a path in a change.
 
-As a response a list of [CodeOwnerInfo](#code-owner-info) entities is returned.
+As a response a [CodeOwnersInfo](#code-owners-info) entity is returned that
+contains a list of code owners as [CodeOwnerInfo](#code-owner-info) entities.
 The returned code owners are sorted by an internal score that expresses how good
 the code owners are considered as reviewers/approvers for the path. Code owners
 with higher scores are returned first. If code owners have the same score the
@@ -429,6 +430,7 @@
 assigned to '*') a random set of (visible) users is returned, as many as are
 needed to fill up the requested limit.
 
+#### <a id="scores">
 The following factors are taken into account for computing the scores of the
 listed code owners:
 
@@ -453,18 +455,20 @@
   Content-Type: application/json; charset=UTF-8
 
   )]}'
-  [
-    {
-      "account": {
-        "_account_id": 1000096
-      }
-    },
-    {
-      "account": {
-        "_account_id": 1001439
+  {
+    "code_owners": [
+      {
+        "account": {
+          "_account_id": 1000096
+        }
       },
-    }
-  ]
+      {
+        "account": {
+          "_account_id": 1001439
+        },
+      }
+    ]
+  }
 ```
 
 #### <a id="batch-list-code-owners"> Batch Request
@@ -781,6 +785,13 @@
 | `disabled` | optional | Whether the code owners functionality is disabled for the project. If `true` the code owners API is disabled and submitting changes doesn't require code owner approvals. Not set if `false`.
 | `disabled_branches` | optional | Branches for which the code owners functionality is disabled. Configurations for non-existing and non-visible branches are omitted. Not set if the `disabled` field is `true` or if no branch specific status configuration is returned.
 
+### <a id="code-owners-info"> CodeOwnersInfo
+The `CodeOwnersInfo` entity contains information about a list of code owners.
+
+| Field Name    | Description |
+| ------------- | ----------- |
+| `code_owners` | List of code owners as [CodeOwnerInfo](#code-owner-info) entities. The code owners are sorted by [score](#scores).
+
 ### <a id="file-code-owner-status-info"> FileCodeOwnerStatusInfo
 The `FileCodeOwnerStatusInfo` entity describes the code owner statuses for a
 file in a change.