Merge "Add a has:enabled_code-owners and has:approval_code-owners operands"
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedChangedFileInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedChangedFileInfo.java
new file mode 100644
index 0000000..971a823
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedChangedFileInfo.java
@@ -0,0 +1,35 @@
+// 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;
+
+/**
+ * JSON representation of a file that was changed in a change for which the user owns the new path,
+ * the old path or both paths.
+ */
+public class OwnedChangedFileInfo {
+  /**
+   * Owner information for the new path.
+   *
+   * <p>Not set for deletions.
+   */
+  public OwnedPathInfo newPath;
+
+  /**
+   * Owner information for the old path.
+   *
+   * <p>Only set for deletions and renames.
+   */
+  public OwnedPathInfo oldPath;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java
new file mode 100644
index 0000000..bda0e27
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java
@@ -0,0 +1,28 @@
+// 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;
+
+/** JSON representation of a file path the may be owned by the user. */
+public class OwnedPathInfo {
+  /** The path of the file that may be owned by the user. */
+  public String path;
+
+  /**
+   * Whether the user owns this path.
+   *
+   * <p>Not set if {@code false}.
+   */
+  public Boolean owned;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
index ba41418..98e9e90 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
@@ -24,11 +24,25 @@
  */
 public class OwnedPathsInfo {
   /**
-   * List of the owned paths.
+   * List of files that were changed in a change for which the user owns the new path, the old path
+   * or both paths.
+   *
+   * <p>The entries are sorted alphabetically by new path, and by old path if new path is not
+   * present.
+   *
+   * <p>Contains at most as many entries as the limit that was specified on the request.
+   */
+  public List<OwnedChangedFileInfo> ownedChangedFiles;
+
+  /**
+   * The list of the owned new and old paths that are contained in {@link #ownedChangedFiles}.
    *
    * <p>The paths are returned as absolute paths.
    *
    * <p>The paths are sorted alphabetically.
+   *
+   * <p>May contain more entries than the limit that was specified on the request (if the users owns
+   * new and old path of renamed files).
    */
   public List<String> ownedPaths;
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 6988acb..f015cb0 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -129,7 +129,7 @@
    * @return the paths of the files in the given patch set that are owned by the specified account
    * @throws ResourceConflictException if the destination branch of the change no longer exists
    */
-  public ImmutableList<Path> getOwnedPaths(
+  public ImmutableList<OwnedChangedFile> getOwnedPaths(
       ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId, int start, int limit)
       throws ResourceConflictException {
     try (Timer0.Context ctx = codeOwnerMetrics.computeOwnedPaths.start()) {
@@ -142,25 +142,45 @@
           patchSet.id().get(),
           start,
           limit);
-      Stream<Path> ownedPaths =
+
+      Stream<FileCodeOwnerStatus> fileStatuses =
           getFileStatusesForAccount(changeNotes, patchSet, accountId)
-              .flatMap(
-                  fileCodeOwnerStatus ->
-                      Stream.of(
-                              fileCodeOwnerStatus.newPathStatus(),
-                              fileCodeOwnerStatus.oldPathStatus())
-                          .filter(Optional::isPresent)
-                          .map(Optional::get))
               .filter(
-                  pathCodeOwnerStatus -> pathCodeOwnerStatus.status() == CodeOwnerStatus.APPROVED)
-              .map(PathCodeOwnerStatus::path);
+                  fileStatus ->
+                      (fileStatus.newPathStatus().isPresent()
+                              && fileStatus.newPathStatus().get().status()
+                                  == CodeOwnerStatus.APPROVED)
+                          || (fileStatus.oldPathStatus().isPresent()
+                              && fileStatus.oldPathStatus().get().status()
+                                  == CodeOwnerStatus.APPROVED));
       if (start > 0) {
-        ownedPaths = ownedPaths.skip(start);
+        fileStatuses = fileStatuses.skip(start);
       }
       if (limit > 0) {
-        ownedPaths = ownedPaths.limit(limit);
+        fileStatuses = fileStatuses.limit(limit);
       }
-      return ownedPaths.collect(toImmutableList());
+
+      return fileStatuses
+          .map(
+              fileStatus ->
+                  OwnedChangedFile.create(
+                      fileStatus
+                          .newPathStatus()
+                          .map(
+                              newPathStatus ->
+                                  OwnedPath.create(
+                                      newPathStatus.path(),
+                                      newPathStatus.status() == CodeOwnerStatus.APPROVED))
+                          .orElse(null),
+                      fileStatus
+                          .oldPathStatus()
+                          .map(
+                              oldPathStatus ->
+                                  OwnedPath.create(
+                                      oldPathStatus.path(),
+                                      oldPathStatus.status() == CodeOwnerStatus.APPROVED))
+                          .orElse(null)))
+          .collect(toImmutableList());
     } catch (IOException | DiffNotAvailableException e) {
       throw new CodeOwnersInternalServerErrorException(
           String.format(
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
index 3cc8401..a694ce5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -211,12 +211,13 @@
       try {
         // limit + 1, so that we can show an indicator if there are more than <limit> files.
         ownedPaths =
-            codeOwnerApprovalCheck.getOwnedPaths(
-                changeNotes,
-                changeNotes.getCurrentPatchSet(),
-                reviewerAccountId,
-                /* start= */ 0,
-                limit + 1);
+            OwnedChangedFile.getOwnedPaths(
+                codeOwnerApprovalCheck.getOwnedPaths(
+                    changeNotes,
+                    changeNotes.getCurrentPatchSet(),
+                    reviewerAccountId,
+                    /* start= */ 0,
+                    limit + 1));
       } catch (RestApiException e) {
         logger.atFine().withCause(e).log(
             "Couldn't compute owned paths of change %s for account %s",
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
index 2383de2..5b6f6dd 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -124,12 +124,13 @@
     try {
       // limit + 1, so that we can show an indicator if there are more than <limit> files.
       ownedPaths =
-          codeOwnerApprovalCheck.getOwnedPaths(
-              changeNotes,
-              changeNotes.getCurrentPatchSet(),
-              user.getAccountId(),
-              /* start= */ 0,
-              limit + 1);
+          OwnedChangedFile.getOwnedPaths(
+              codeOwnerApprovalCheck.getOwnedPaths(
+                  changeNotes,
+                  changeNotes.getCurrentPatchSet(),
+                  user.getAccountId(),
+                  /* start= */ 0,
+                  limit + 1));
     } catch (RestApiException e) {
       logger.atFine().withCause(e).log(
           "Couldn't compute owned paths of change %s for account %s",
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OwnedChangedFile.java b/java/com/google/gerrit/plugins/codeowners/backend/OwnedChangedFile.java
new file mode 100644
index 0000000..edb3894
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OwnedChangedFile.java
@@ -0,0 +1,79 @@
+// 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.backend;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Representation of a file that was changed in the revision for which the user owns the new path,
+ * the old path or both paths.
+ */
+@AutoValue
+public abstract class OwnedChangedFile {
+  /**
+   * Owner information for the new path.
+   *
+   * <p>{@link Optional#empty()} for deletions.
+   */
+  public abstract Optional<OwnedPath> newPath();
+
+  /**
+   * Owner information for the old path.
+   *
+   * <p>Present only for deletions and renames.
+   */
+  public abstract Optional<OwnedPath> oldPath();
+
+  public static OwnedChangedFile create(@Nullable OwnedPath newPath, @Nullable OwnedPath oldPath) {
+    return new AutoValue_OwnedChangedFile(
+        Optional.ofNullable(newPath), Optional.ofNullable(oldPath));
+  }
+
+  /**
+   * Returns the owned paths that are contained in the given {@link OwnedChangedFile}s as new or old
+   * path, as a sorted list.
+   *
+   * <p>New or old paths that are not owned by the user are filtered out.
+   */
+  public static ImmutableList<Path> getOwnedPaths(
+      ImmutableList<OwnedChangedFile> ownedChangedFiles) {
+    return asPathStream(ownedChangedFiles.stream()).collect(toImmutableList());
+  }
+
+  /**
+   * Returns the owned paths that are contained in the given {@link OwnedChangedFile}s as new or old
+   * path, as a sorted stream.
+   *
+   * <p>New or old paths that are not owned by the user are filtered out.
+   */
+  public static Stream<Path> asPathStream(Stream<OwnedChangedFile> ownedChangedFiles) {
+    return ownedChangedFiles
+        .flatMap(
+            ownedChangedFile -> Stream.of(ownedChangedFile.newPath(), ownedChangedFile.oldPath()))
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .filter(OwnedPath::owned)
+        .map(ownedPath -> ownedPath.path())
+        .sorted(Comparator.comparing(path -> path.toString()));
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java b/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java
new file mode 100644
index 0000000..c6283a3
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java
@@ -0,0 +1,32 @@
+// 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.backend;
+
+import com.google.auto.value.AutoValue;
+import java.nio.file.Path;
+
+/** Representation of a file path the may be owned by the user. */
+@AutoValue
+public abstract class OwnedPath {
+  /** The path of the file that may be owned by the user. */
+  public abstract Path path();
+
+  /** Whether the user owns this path. */
+  public abstract boolean owned();
+
+  public static OwnedPath create(Path path, boolean owned) {
+    return new AutoValue_OwnedPath(path, owned);
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java
index 1f2298b..c6a447b 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java
@@ -24,8 +24,12 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.plugins.codeowners.api.OwnedChangedFileInfo;
+import com.google.gerrit.plugins.codeowners.api.OwnedPathInfo;
 import com.google.gerrit.plugins.codeowners.api.OwnedPathsInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerApprovalCheck;
+import com.google.gerrit.plugins.codeowners.backend.OwnedChangedFile;
+import com.google.gerrit.plugins.codeowners.backend.OwnedPath;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.change.RevisionResource;
@@ -89,7 +93,7 @@
 
     Account.Id accountId = resolveAccount();
 
-    ImmutableList<Path> ownedPaths =
+    ImmutableList<OwnedChangedFile> ownedChangedFiles =
         codeOwnerApprovalCheck.getOwnedPaths(
             revisionResource.getNotes(),
             revisionResource.getPatchSet(),
@@ -98,9 +102,16 @@
             limit + 1);
 
     OwnedPathsInfo ownedPathsInfo = new OwnedPathsInfo();
-    ownedPathsInfo.more = ownedPaths.size() > limit ? true : null;
+    ownedPathsInfo.more = ownedChangedFiles.size() > limit ? true : null;
+    ownedPathsInfo.ownedChangedFiles =
+        ownedChangedFiles.stream()
+            .limit(limit)
+            .map(GetOwnedPaths::toOwnedChangedFileInfo)
+            .collect(toImmutableList());
     ownedPathsInfo.ownedPaths =
-        ownedPaths.stream().limit(limit).map(Path::toString).collect(toImmutableList());
+        OwnedChangedFile.asPathStream(ownedChangedFiles.stream().limit(limit))
+            .map(Path::toString)
+            .collect(toImmutableList());
     return Response.ok(ownedPathsInfo);
   }
 
@@ -122,4 +133,22 @@
       throw new BadRequestException("limit must be positive");
     }
   }
+
+  private static OwnedChangedFileInfo toOwnedChangedFileInfo(OwnedChangedFile ownedChangedFile) {
+    OwnedChangedFileInfo info = new OwnedChangedFileInfo();
+    if (ownedChangedFile.newPath().isPresent()) {
+      info.newPath = toOwnedPathInfo(ownedChangedFile.newPath().get());
+    }
+    if (ownedChangedFile.oldPath().isPresent()) {
+      info.oldPath = toOwnedPathInfo(ownedChangedFile.oldPath().get());
+    }
+    return info;
+  }
+
+  private static OwnedPathInfo toOwnedPathInfo(OwnedPath ownedPath) {
+    OwnedPathInfo info = new OwnedPathInfo();
+    info.path = ownedPath.path().toString();
+    info.owned = ownedPath.owned() ? true : null;
+    return info;
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java
new file mode 100644
index 0000000..7897058
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java
@@ -0,0 +1,88 @@
+// 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 com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.api.OwnedChangedFileInfo;
+
+/** {@link Subject} for doing assertions on {@link OwnedChangedFileInfo}s. */
+public class OwnedChangedFileInfoSubject extends Subject {
+  /**
+   * Starts fluent chain to do assertions on a {@link OwnedChangedFileInfo}.
+   *
+   * @param ownedChangedFileInfo the owned changed file info on which assertions should be done
+   * @return the created {@link OwnedChangedFileInfoSubject}
+   */
+  public static OwnedChangedFileInfoSubject assertThat(OwnedChangedFileInfo ownedChangedFileInfo) {
+    return assertAbout(ownedChangedFileInfos()).that(ownedChangedFileInfo);
+  }
+
+  private static Factory<OwnedChangedFileInfoSubject, OwnedChangedFileInfo>
+      ownedChangedFileInfos() {
+    return OwnedChangedFileInfoSubject::new;
+  }
+
+  private final OwnedChangedFileInfo ownedChangedFileInfo;
+
+  private OwnedChangedFileInfoSubject(
+      FailureMetadata metadata, OwnedChangedFileInfo ownedChangedFileInfo) {
+    super(metadata, ownedChangedFileInfo);
+    this.ownedChangedFileInfo = ownedChangedFileInfo;
+  }
+
+  public void hasEmptyNewPath() {
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNull();
+  }
+
+  public void hasOwnedNewPath(String expectedOwnedNewPath) {
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNotNull();
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath.path).isEqualTo(expectedOwnedNewPath);
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath.owned).isTrue();
+  }
+
+  public void hasNonOwnedNewPath(String expectedNonOwnedNewPath) {
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNotNull();
+    check("ownedNewPath")
+        .that(ownedChangedFileInfo().newPath.path)
+        .isEqualTo(expectedNonOwnedNewPath);
+    check("ownedNewPath").that(ownedChangedFileInfo().newPath.owned).isNull();
+  }
+
+  public void hasEmptyOldPath() {
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath).isNull();
+  }
+
+  public void hasOwnedOldPath(String expectedOwnedOldPath) {
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath).isNotNull();
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath.path).isEqualTo(expectedOwnedOldPath);
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath.owned).isTrue();
+  }
+
+  public void hasNonOwnedOldPath(String expectedNonOwnedOldPath) {
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath).isNotNull();
+    check("ownedOldPath")
+        .that(ownedChangedFileInfo().oldPath.path)
+        .isEqualTo(expectedNonOwnedOldPath);
+    check("ownedOldPath").that(ownedChangedFileInfo().oldPath.owned).isNull();
+  }
+
+  private OwnedChangedFileInfo ownedChangedFileInfo() {
+    isNotNull();
+    return ownedChangedFileInfo;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java
index d6a1072..08b762a 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java
@@ -49,6 +49,10 @@
     return check("ownedPaths()").that(ownedPathsInfo().ownedPaths);
   }
 
+  public IterableSubject hasOwnedChangedFilesThat() {
+    return check("ownedChangedFiles()").that(ownedPathsInfo().ownedChangedFiles);
+  }
+
   public BooleanSubject hasMoreThat() {
     return check("more()").that(ownedPathsInfo().more);
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java
index 681513b..5b19b9a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java
@@ -15,13 +15,18 @@
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.OwnedChangedFileInfoSubject.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.OwnedPathsInfoSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
@@ -29,6 +34,9 @@
 import com.google.gerrit.plugins.codeowners.restapi.GetOwnedPaths;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import org.junit.Test;
 
 /**
@@ -130,11 +138,155 @@
             .getOwnedPaths()
             .forUser(user.email())
             .get();
+
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasEmptyOldPath();
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasEmptyOldPath();
+
     assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder();
     assertThat(ownedPathsInfo).hasMoreThat().isNull();
   }
 
   @Test
+  public void getOwnedPathsForDeletedFiles() throws Exception {
+    setAsCodeOwners("/foo/", user);
+
+    String path1 = "/foo/bar/baz.md";
+    String path2 = "/foo/baz/bar.md";
+    String path3 = "/bar/foo.md";
+
+    createChange(
+            "Change Adding Files",
+            ImmutableMap.of(
+                JgitPath.of(path1).get(),
+                "file content 1",
+                JgitPath.of(path2).get(),
+                "file content 2",
+                JgitPath.of(path3).get(),
+                "file content 3"))
+        .getChangeId();
+
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change Deleting Files",
+            ImmutableMap.of(
+                JgitPath.of(path1).get(),
+                "file content 1",
+                JgitPath.of(path2).get(),
+                "file content 2",
+                JgitPath.of(path3).get(),
+                "file content 3"));
+    Result r = push.rm("refs/for/master");
+    r.assertOkStatus();
+    String changeId = r.getChangeId();
+
+    OwnedPathsInfo ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId)
+            .current()
+            .getOwnedPaths()
+            .forUser(user.email())
+            .get();
+
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasEmptyNewPath();
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(path1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasEmptyNewPath();
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedOldPath(path2);
+
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder();
+    assertThat(ownedPathsInfo).hasMoreThat().isNull();
+  }
+
+  @Test
+  public void getOwnedPathsForRenamedFiles() throws Exception {
+    setAsCodeOwners("/foo/", user);
+
+    // Rename 1: user owns old and new path
+    String newPath1 = "/foo/test1.md";
+    String oldPath1 = "/foo/bar/test1.md";
+
+    // Rename 2: user owns only new path
+    String newPath2 = "/foo/test2.md";
+    String oldPath2 = "/other/test2.md";
+
+    // Rename 3: user owns only old path
+    String newPath3 = "/other/test3.md";
+    String oldPath3 = "/foo/test3.md";
+
+    // Rename 4: user owns neither old nor new path
+    String newPath4 = "/other/test4.md";
+    String oldPath4 = "/other/foo/test4.md";
+
+    String changeId1 =
+        createChange(
+                "Change Adding Files",
+                ImmutableMap.of(
+                    JgitPath.of(oldPath1).get(),
+                    "file content 1",
+                    JgitPath.of(oldPath2).get(),
+                    "file content 2",
+                    JgitPath.of(oldPath3).get(),
+                    "file content 3",
+                    JgitPath.of(oldPath4).get(),
+                    "file content 4"))
+            .getChangeId();
+
+    // The PushOneCommit test API doesn't support renaming files in a change. Use the change edit
+    // Java API instead.
+    ChangeInput changeInput = new ChangeInput();
+    changeInput.project = project.get();
+    changeInput.branch = "master";
+    changeInput.subject = "Change Renaming Files";
+    changeInput.baseChange = changeId1;
+    String changeId2 = gApi.changes().create(changeInput).get().changeId;
+    gApi.changes().id(changeId2).edit().create();
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath1).get(), JgitPath.of(newPath1).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath2).get(), JgitPath.of(newPath2).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath3).get(), JgitPath.of(newPath3).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath4).get(), JgitPath.of(newPath4).get());
+    gApi.changes().id(changeId2).edit().publish(new PublishChangeEditInput());
+
+    OwnedPathsInfo ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId2)
+            .current()
+            .getOwnedPaths()
+            .forUser(user.email())
+            .get();
+
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(3);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(newPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedOldPath(oldPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasNonOwnedNewPath(newPath3);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedOldPath(oldPath3);
+
+    List<String> ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2, oldPath3);
+    Collections.sort(ownedPaths);
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder();
+
+    assertThat(ownedPathsInfo).hasMoreThat().isNull();
+  }
+
+  @Test
   public void getOwnedPathForOwnUser() throws Exception {
     setAsRootCodeOwners(admin);
 
@@ -371,6 +523,203 @@
   }
 
   @Test
+  public void getOwnedPathsForRenamedFilesWithLimit() throws Exception {
+    setAsCodeOwners("/foo/", user);
+
+    // Rename 1: user owns old and new path
+    String newPath1 = "/foo/test1.md";
+    String oldPath1 = "/foo/bar/test1.md";
+
+    // Rename 2: user owns only new path
+    String newPath2 = "/foo/test2.md";
+    String oldPath2 = "/other/test2.md";
+
+    // Rename 3: user owns only old path
+    String newPath3 = "/other/test3.md";
+    String oldPath3 = "/foo/test3.md";
+
+    // Rename 4: user owns neither old nor new path
+    String newPath4 = "/other/test4.md";
+    String oldPath4 = "/other/foo/test4.md";
+
+    String changeId1 =
+        createChange(
+                "Change Adding Files",
+                ImmutableMap.of(
+                    JgitPath.of(oldPath1).get(),
+                    "file content 1",
+                    JgitPath.of(oldPath2).get(),
+                    "file content 2",
+                    JgitPath.of(oldPath3).get(),
+                    "file content 3",
+                    JgitPath.of(oldPath4).get(),
+                    "file content 4"))
+            .getChangeId();
+
+    // The PushOneCommit test API doesn't support renaming files in a change. Use the change edit
+    // Java API instead.
+    ChangeInput changeInput = new ChangeInput();
+    changeInput.project = project.get();
+    changeInput.branch = "master";
+    changeInput.subject = "Change Renaming Files";
+    changeInput.baseChange = changeId1;
+    String changeId2 = gApi.changes().create(changeInput).get().changeId;
+    gApi.changes().id(changeId2).edit().create();
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath1).get(), JgitPath.of(newPath1).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath2).get(), JgitPath.of(newPath2).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath3).get(), JgitPath.of(newPath3).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath4).get(), JgitPath.of(newPath4).get());
+    gApi.changes().id(changeId2).edit().publish(new PublishChangeEditInput());
+
+    OwnedPathsInfo ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId2)
+            .current()
+            .getOwnedPaths()
+            .withLimit(1)
+            .forUser(user.email())
+            .get();
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1);
+    List<String> ownedPaths = Arrays.asList(newPath1, oldPath1);
+    Collections.sort(ownedPaths);
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths);
+    assertThat(ownedPathsInfo).hasMoreThat().isTrue();
+
+    ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId2)
+            .current()
+            .getOwnedPaths()
+            .withLimit(2)
+            .forUser(user.email())
+            .get();
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(newPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedOldPath(oldPath2);
+    ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2);
+    Collections.sort(ownedPaths);
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder();
+    assertThat(ownedPathsInfo).hasMoreThat().isTrue();
+
+    ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId2)
+            .current()
+            .getOwnedPaths()
+            .withLimit(3)
+            .forUser(user.email())
+            .get();
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(3);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(newPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedOldPath(oldPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasNonOwnedNewPath(newPath3);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedOldPath(oldPath3);
+    ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2, oldPath3);
+    Collections.sort(ownedPaths);
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder();
+    assertThat(ownedPathsInfo).hasMoreThat().isNull();
+  }
+
+  @Test
+  public void getOwnedPathsForRenamedFilesWithStartAndLimit() throws Exception {
+    setAsCodeOwners("/foo/", user);
+
+    // Rename 1: user owns old and new path
+    String newPath1 = "/foo/test1.md";
+    String oldPath1 = "/foo/bar/test1.md";
+
+    // Rename 2: user owns only new path
+    String newPath2 = "/foo/test2.md";
+    String oldPath2 = "/other/test2.md";
+
+    // Rename 3: user owns only old path
+    String newPath3 = "/other/test3.md";
+    String oldPath3 = "/foo/test3.md";
+
+    // Rename 4: user owns neither old nor new path
+    String newPath4 = "/other/test4.md";
+    String oldPath4 = "/other/foo/test4.md";
+
+    String changeId1 =
+        createChange(
+                "Change Adding Files",
+                ImmutableMap.of(
+                    JgitPath.of(oldPath1).get(),
+                    "file content 1",
+                    JgitPath.of(oldPath2).get(),
+                    "file content 2",
+                    JgitPath.of(oldPath3).get(),
+                    "file content 3",
+                    JgitPath.of(oldPath4).get(),
+                    "file content 4"))
+            .getChangeId();
+
+    // The PushOneCommit test API doesn't support renaming files in a change. Use the change edit
+    // Java API instead.
+    ChangeInput changeInput = new ChangeInput();
+    changeInput.project = project.get();
+    changeInput.branch = "master";
+    changeInput.subject = "Change Renaming Files";
+    changeInput.baseChange = changeId1;
+    String changeId2 = gApi.changes().create(changeInput).get().changeId;
+    gApi.changes().id(changeId2).edit().create();
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath1).get(), JgitPath.of(newPath1).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath2).get(), JgitPath.of(newPath2).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath3).get(), JgitPath.of(newPath3).get());
+    gApi.changes()
+        .id(changeId2)
+        .edit()
+        .renameFile(JgitPath.of(oldPath4).get(), JgitPath.of(newPath4).get());
+    gApi.changes().id(changeId2).edit().publish(new PublishChangeEditInput());
+
+    OwnedPathsInfo ownedPathsInfo =
+        changeCodeOwnersApiFactory
+            .change(changeId2)
+            .current()
+            .getOwnedPaths()
+            .withStart(1)
+            .withLimit(2)
+            .forUser(user.email())
+            .get();
+    assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasNonOwnedOldPath(oldPath2);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedNewPath(newPath3);
+    assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedOldPath(oldPath3);
+    List<String> ownedPaths = Arrays.asList(newPath2, oldPath3);
+    Collections.sort(ownedPaths);
+    assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths);
+    assertThat(ownedPathsInfo).hasMoreThat().isNull();
+  }
+
+  @Test
   public void getOwnedPathsLimitedByDefault() throws Exception {
     setAsRootCodeOwners(user);
 
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 6e3d739..d42cf50 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -729,7 +729,7 @@
 | Field Name   |           | Description |
 | ------------ | --------- | ----------- |
 | `start`\|`S` | optional  | Number of owned paths to skip. Allows to page over the owned files. By default 0.
-| `limit`\|`n` | optional  | Limit defining how many owned files should be returned at most. By default 50.
+| `limit`\|`n` | optional  | Limit defining how many [OwnedChangedFileInfo](#owned-changed-file-info) entities should be returned at most. By default 50.
 | `user`       | mandatory | user for which the owned paths should be returned
 
 #### Request
@@ -749,9 +749,45 @@
 
   )]}'
   {
+    "owned_changed_files": [
+      {
+        "new_path": {
+          "path": "/foo/bar/baz.md",
+          "owned": true
+        }
+      },
+      {
+        "old_path": {
+          "path": "/foo/baz/bar.md",
+          "owned": true
+        }
+      },
+      {
+        "new_path": {
+          "path": "/foo/new-name.md",
+          "owned": true
+        },
+        "old_path": {
+          "path": "/foo/old-name.md",
+          "owned": true
+        }
+      },
+      {
+        "new_path": {
+          "path": "/xyz/new-name.md"
+        },
+        "old_path": {
+          "path": "/abc/old-name.md",
+          "owned": true
+        }
+      }
+    ],
     "owned_paths": [
+      "/abc/old-name.md",
       "/foo/bar/baz.md",
       "/foo/baz/bar.md",
+      "/foo/new-name.md",
+      "/foo/old-name.md"
     ]
   }
 ```
@@ -1035,13 +1071,33 @@
 | `invalid_code_owner_config_info_url` | optional | Optional URL for a page that provides project/host-specific information about how to deal with invalid code owner config files.
 |`fallback_code_owners` || Policy that controls who should own paths that have no code owners defined. Possible values are: `NONE`: Paths for which no code owners are defined are owned by no one. `PROJECT_OWNERS`: Paths for which no code owners are defined are owned by the project owners. `ALL_USERS`: Paths for which no code owners are defined are owned by all users.
 
+### <a id="owned-changed-file-info"> OwnedChangedFileInfo
+The `OwnedChangedFileInfo` entity contains information about a file that was
+changed in a change for which the user owns the new path, the old path or both
+paths.
+
+| Field Name |          | Description |
+| ---------- | -------- | ----------- |
+| `new_path` | optional | Owner information for the new path as a [OwnedPathInfo](#owned-path-info) entity. Not set for deletions.
+| `old_path` | optional | Owner information for the old path as a [OwnedPathInfo](#owned-path-info) entity. Only set for deletions and renames.
+
+### <a id="owned-path-info"> OwnedPathInfo
+The `OwnedPathInfo` entity contains information about a file path the may be
+owned by the user.
+
+| Field Name |          | Description |
+| ---------- | -------- | ----------- |
+| `path`     |          | The absolute file path.
+| `owned`    | optional | `true` is the path is owned by the user. Otherwise unset.
+
 ### <a id="owned-paths-info"> OwnedPathsInfo
 The `OwnedPathsInfo` entity contains paths that are owned by a user.
 
 
 | Field Name    |          | Description |
 | ------------- | -------- | ----------- |
-| `owned_paths` |          |The owned paths as absolute paths, sorted alphabetically.
+| `owned_changed_files`   || List of files that were changed in the revision for which the user owns the new path, the old path or both paths. The entries are sorted alphabetically by new path, and by old path if new path is not present. Contains at most as many entries as the limit that was specified on the request.
+| `owned_paths` |          | The list of the owned new and old paths that are contained in the `owned_changed_files` field. The paths are returned as absolute paths and are sorted alphabetically. May contain more entries than the limit that was specified on the request (if the users owns new and old path of renamed files).
 | `more`        | optional | Whether the request would deliver more results if not limited. Not set if `false`.
 
 ### <a id="path-code-owner-status-info"> PathCodeOwnerStatusInfo