Merge changes Iad263b16,If74c346e,I5682bf9b

* changes:
  CommentsUtil: Don't use the diff cache to look up the parent commit
  Cover some more behaviors of ported comments with tests
  Ignore comments on non-existing patchsets when porting
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index 465c419..b8c841c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.testsuite.change;
 
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
-
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Comment.Status;
 import com.google.gerrit.entities.HumanComment;
@@ -32,8 +30,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -58,7 +54,6 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final CommentsUtil commentsUtil;
-  private final PatchListCache patchListCache;
 
   private final ChangeNotes changeNotes;
   private final PatchSet.Id patchsetId;
@@ -73,14 +68,12 @@
       GenericFactory userFactory,
       BatchUpdate.Factory batchUpdateFactory,
       CommentsUtil commentsUtil,
-      PatchListCache patchListCache,
       @Assisted ChangeNotes changeNotes,
       @Assisted PatchSet.Id patchsetId) {
     this.repositoryManager = repositoryManager;
     this.userFactory = userFactory;
     this.batchUpdateFactory = batchUpdateFactory;
     this.commentsUtil = commentsUtil;
-    this.patchListCache = patchListCache;
     this.changeNotes = changeNotes;
     this.patchsetId = patchsetId;
   }
@@ -154,7 +147,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext context) throws Exception {
+    public boolean updateChange(ChangeContext context) {
       HumanComment comment = toNewComment(context, commentCreation);
       ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
       changeUpdate.putComment(commentCreation.status(), comment);
@@ -165,8 +158,7 @@
       return true;
     }
 
-    private HumanComment toNewComment(ChangeContext context, TestCommentCreation commentCreation)
-        throws PatchListNotAvailableException {
+    private HumanComment toNewComment(ChangeContext context, TestCommentCreation commentCreation) {
       String message = commentCreation.message().orElse("The text of a test comment.");
 
       String filePath = commentCreation.file().orElse(Patch.PATCHSET_LEVEL);
@@ -197,11 +189,8 @@
           .map(PerPatchsetOperationsImpl::toCommentRange)
           .ifPresent(range -> newComment.setLineNbrAndRange(null, range));
 
-      setCommentCommitId(
-          newComment,
-          patchListCache,
-          context.getChange(),
-          changeNotes.getPatchSets().get(patchsetId));
+      commentsUtil.setCommentCommitId(
+          newComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId));
       return newComment;
     }
   }
@@ -236,7 +225,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext context) throws Exception {
+    public boolean updateChange(ChangeContext context) {
       RobotComment robotComment = toNewRobotComment(context, robotCommentCreation);
       ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
       changeUpdate.putRobotComment(robotComment);
@@ -248,8 +237,7 @@
     }
 
     private RobotComment toNewRobotComment(
-        ChangeContext context, TestRobotCommentCreation robotCommentCreation)
-        throws PatchListNotAvailableException {
+        ChangeContext context, TestRobotCommentCreation robotCommentCreation) {
       String message = robotCommentCreation.message().orElse("The text of a test robot comment.");
 
       String filePath = robotCommentCreation.file().orElse(Patch.PATCHSET_LEVEL);
@@ -281,11 +269,8 @@
         newRobotComment.properties = robotCommentCreation.properties();
       }
 
-      setCommentCommitId(
-          newRobotComment,
-          patchListCache,
-          context.getChange(),
-          changeNotes.getPatchSets().get(patchsetId));
+      commentsUtil.setCommentCommitId(
+          newRobotComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId));
       return newRobotComment;
     }
   }
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 4d5778a..b752791 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
@@ -55,6 +56,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
 
 /** Utility functions to manipulate Comments. */
 @Singleton
@@ -109,13 +111,18 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final String serverId;
+  private final PatchListCache patchListCache;
 
   @Inject
   CommentsUtil(
-      GitRepositoryManager repoManager, AllUsersName allUsers, @GerritServerId String serverId) {
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers,
+      @GerritServerId String serverId,
+      PatchListCache patchListCache) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.serverId = serverId;
+    this.patchListCache = patchListCache;
   }
 
   public HumanComment newHumanComment(
@@ -372,41 +379,63 @@
     return sort(result);
   }
 
-  public static void setCommentCommitId(Comment c, PatchListCache cache, Change change, PatchSet ps)
-      throws PatchListNotAvailableException {
+  public void setCommentCommitId(Comment c, Change change, PatchSet ps) {
     checkArgument(
         c.key.patchSetId == ps.id().get(),
         "cannot set commit ID for patch set %s on comment %s",
         ps.id(),
         c);
     if (c.getCommitId() == null) {
-      c.setCommitId(determineCommitId(cache, change, ps, c.side));
+      // This code is very much down into our stack and shouldn't be used for validation. Hence,
+      // don't throw an exception here if we can't find a commit for the indicated side but
+      // simply use the all-null ObjectId.
+      c.setCommitId(determineCommitId(change, ps, c.side).orElseGet(ObjectId::zeroId));
     }
   }
 
   /**
    * Determines the SHA-1 of the commit referenced by the (change, patchset, side) triple.
    *
-   * @param patchListCache the cache to use for SHA-1 lookups
    * @param change the change to which the commit belongs
    * @param patchset the patchset to which the commit belongs
    * @param side the side indicating which commit of the patchset to take. 1 is the patchset commit,
    *     0 the parent commit (or auto-merge for changes representing merge commits); -x the xth
    *     parent commit of a merge commit
-   * @return the commit SHA-1
-   * @throws PatchListNotAvailableException if the SHA-1 is unavailable for an unknown reason
+   * @return the commit SHA-1 or an empty {@link Optional} if the side isn't available for the given
+   *     change/patchset
+   * @throws StorageException if the SHA-1 is unavailable for an unknown reason
    */
-  public static ObjectId determineCommitId(
-      PatchListCache patchListCache, Change change, PatchSet patchset, short side)
-      throws PatchListNotAvailableException {
+  public Optional<ObjectId> determineCommitId(Change change, PatchSet patchset, short side) {
     if (Side.fromShort(side) == Side.PARENT) {
       if (side < 0) {
-        return patchListCache.getOldId(change, patchset, -side);
-      } else {
-        return patchListCache.getOldId(change, patchset, null);
+        int parentNumber = Math.abs(side);
+        return resolveParentCommit(change.getProject(), patchset, parentNumber);
       }
-    } else {
-      return patchset.commitId();
+      return Optional.of(resolveAutoMergeCommit(change, patchset));
+    }
+    return Optional.of(patchset.commitId());
+  }
+
+  private Optional<ObjectId> resolveParentCommit(
+      Project.NameKey project, PatchSet patchset, int parentNumber) {
+    try (Repository repository = repoManager.openRepository(project)) {
+      RevCommit commit = repository.parseCommit(patchset.commitId());
+      if (commit.getParentCount() < parentNumber) {
+        return Optional.empty();
+      }
+      return Optional.of(commit.getParent(parentNumber - 1));
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
+  }
+
+  private ObjectId resolveAutoMergeCommit(Change change, PatchSet patchset) {
+    try {
+      // TODO(ghareeb): Adjust after the auto-merge code was moved out of the diff caches. Also
+      // unignore the test in PortedCommentsIT.
+      return patchListCache.getOldId(change, patchset, null);
+    } catch (PatchListNotAvailableException e) {
+      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index 658af15..4d19dd0 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -22,15 +22,12 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.validators.CommentForValidation;
 import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.inject.Inject;
@@ -44,16 +41,13 @@
 public class PublishCommentUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final PatchListCache patchListCache;
   private final PatchSetUtil psUtil;
   private final CommentsUtil commentsUtil;
 
   @Inject
-  PublishCommentUtil(
-      CommentsUtil commentsUtil, PatchListCache patchListCache, PatchSetUtil psUtil) {
+  PublishCommentUtil(CommentsUtil commentsUtil, PatchSetUtil psUtil) {
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.patchListCache = patchListCache;
   }
 
   public void publish(
@@ -101,11 +95,7 @@
       // Draft may have been created by a different real user; copy the current real user. (Only
       // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
       ctx.getUser().updateRealAccountId(draftComment::setRealAuthor);
-      try {
-        CommentsUtil.setCommentCommitId(draftComment, patchListCache, notes.getChange(), ps);
-      } catch (PatchListNotAvailableException e) {
-        throw new StorageException(e);
-      }
+      commentsUtil.setCommentCommitId(draftComment, notes.getChange(), ps);
       commentsToPublish.add(draftComment);
     }
     commentsUtil.putHumanComments(changeUpdate, HumanComment.Status.PUBLISHED, commentsToPublish);
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index aea5308..df38118 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -59,8 +59,6 @@
 import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
 import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -106,7 +104,6 @@
   private final ChangeMessagesUtil changeMessagesUtil;
   private final CommentsUtil commentsUtil;
   private final OneOffRequestContext oneOffRequestContext;
-  private final PatchListCache patchListCache;
   private final PatchSetUtil psUtil;
   private final Provider<InternalChangeQuery> queryProvider;
   private final DynamicMap<MailFilter> mailFilters;
@@ -126,7 +123,6 @@
       ChangeMessagesUtil changeMessagesUtil,
       CommentsUtil commentsUtil,
       OneOffRequestContext oneOffRequestContext,
-      PatchListCache patchListCache,
       PatchSetUtil psUtil,
       Provider<InternalChangeQuery> queryProvider,
       DynamicMap<MailFilter> mailFilters,
@@ -143,7 +139,6 @@
     this.changeMessagesUtil = changeMessagesUtil;
     this.commentsUtil = commentsUtil;
     this.oneOffRequestContext = oneOffRequestContext;
-    this.patchListCache = patchListCache;
     this.psUtil = psUtil;
     this.queryProvider = queryProvider;
     this.mailFilters = mailFilters;
@@ -330,8 +325,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws UnprocessableEntityException, PatchListNotAvailableException {
+    public boolean updateChange(ChangeContext ctx) throws UnprocessableEntityException {
       patchSet = psUtil.get(ctx.getNotes(), psId);
       notes = ctx.getNotes();
       if (patchSet == null) {
@@ -419,8 +413,7 @@
     }
 
     private HumanComment persistentCommentFromMailComment(
-        ChangeContext ctx, MailComment mailComment, PatchSet patchSetForComment)
-        throws PatchListNotAvailableException {
+        ChangeContext ctx, MailComment mailComment, PatchSet patchSetForComment) {
       String fileName;
       // The patch set that this comment is based on is different if this
       // comment was sent in reply to a comment on a previous patch set.
@@ -452,7 +445,7 @@
         comment.range = mailComment.getInReplyTo().range;
         comment.unresolved = mailComment.getInReplyTo().unresolved;
       }
-      CommentsUtil.setCommentCommitId(comment, patchListCache, ctx.getChange(), patchSetForComment);
+      commentsUtil.setCommentCommitId(comment, ctx.getChange(), patchSetForComment);
       return comment;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index f29c6e6..70d1eaf 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.restapi.account;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
@@ -40,8 +39,6 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -80,7 +77,6 @@
   private final Provider<CommentJson> commentJsonProvider;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
-  private final PatchListCache patchListCache;
 
   @Inject
   DeleteDraftComments(
@@ -92,8 +88,7 @@
       ChangeJson.Factory changeJsonFactory,
       Provider<CommentJson> commentJsonProvider,
       CommentsUtil commentsUtil,
-      PatchSetUtil psUtil,
-      PatchListCache patchListCache) {
+      PatchSetUtil psUtil) {
     this.userProvider = userProvider;
     this.batchUpdateFactory = batchUpdateFactory;
     this.queryBuilderProvider = queryBuilderProvider;
@@ -103,7 +98,6 @@
     this.commentJsonProvider = commentJsonProvider;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.patchListCache = patchListCache;
   }
 
   @Override
@@ -176,14 +170,13 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws PatchListNotAvailableException, PermissionBackendException {
+    public boolean updateChange(ChangeContext ctx) throws PermissionBackendException {
       ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder();
       boolean dirty = false;
       for (HumanComment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), accountId)) {
         dirty = true;
         PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), c.key.patchSetId);
-        setCommentCommitId(c, patchListCache, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
+        commentsUtil.setCommentCommitId(c, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
         commentsUtil.deleteHumanComments(ctx.getUpdate(psId), Collections.singleton(c));
         comments.add(humanCommentFormatter.format(c));
       }
diff --git a/java/com/google/gerrit/server/restapi/change/CommentPorter.java b/java/com/google/gerrit/server/restapi/change/CommentPorter.java
index 05241e2..e2abf29 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentPorter.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentPorter.java
@@ -67,10 +67,12 @@
   private final GitPositionTransformer positionTransformer =
       new GitPositionTransformer(BestPositionOnConflict.INSTANCE);
   private final PatchListCache patchListCache;
+  private final CommentsUtil commentsUtil;
 
   @Inject
-  public CommentPorter(PatchListCache patchListCache) {
+  public CommentPorter(PatchListCache patchListCache, CommentsUtil commentsUtil) {
     this.patchListCache = patchListCache;
+    this.commentsUtil = commentsUtil;
   }
 
   /**
@@ -131,13 +133,21 @@
       ImmutableList<HumanComment> patchsetComments = commentsPerPatchset.get(originalPatchsetId);
       PatchSet originalPatchset =
           notes.getPatchSets().get(PatchSet.id(notes.getChangeId(), originalPatchsetId));
-      portedComments.addAll(
-          portSamePatchset(
-              notes.getProjectName(),
-              notes.getChange(),
-              originalPatchset,
-              targetPatchset,
-              patchsetComments));
+      if (originalPatchset != null) {
+        portedComments.addAll(
+            portSamePatchset(
+                notes.getProjectName(),
+                notes.getChange(),
+                originalPatchset,
+                targetPatchset,
+                patchsetComments));
+      } else {
+        logger.atWarning().log(
+            String.format(
+                "Some comments which should be ported refer to the non-existent patchset %s of"
+                    + " change %d. Omitting %d affected comments.",
+                originalPatchsetId, notes.getChangeId().get(), patchsetComments.size()));
+      }
     }
     return portedComments.build();
   }
@@ -200,13 +210,22 @@
       PatchSet targetPatchset,
       short side)
       throws PatchListNotAvailableException {
-    ObjectId originalCommit =
-        CommentsUtil.determineCommitId(patchListCache, change, originalPatchset, side);
-    ObjectId targetCommit =
-        CommentsUtil.determineCommitId(patchListCache, change, targetPatchset, side);
+    ObjectId originalCommit = determineCommitId(change, originalPatchset, side);
+    ObjectId targetCommit = determineCommitId(change, targetPatchset, side);
     return loadCommitMappings(project, originalCommit, targetCommit);
   }
 
+  private ObjectId determineCommitId(Change change, PatchSet patchset, short side) {
+    return commentsUtil
+        .determineCommitId(change, patchset, side)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Commit indicated by change %d, patchset %d, side %d doesn't exist.",
+                        change.getId().get(), patchset.id().get(), side)));
+  }
+
   private ImmutableSet<Mapping> loadCommitMappings(
       Project.NameKey project, ObjectId originalCommit, ObjectId targetCommit)
       throws PatchListNotAvailableException {
@@ -277,6 +296,11 @@
       // No line -> use 0 = file comment or any other comment type without an explicit line.
       portedComment.lineNbr = newPosition.lineRange().map(range -> range.start() + 1).orElse(0);
     }
+    if (Patch.PATCHSET_LEVEL.equals(portedComment.key.filename)) {
+      // Correct the side of the comment to Side.REVISION (= 1) if the comment was changed to
+      // patchset level.
+      portedComment.side = 1;
+    }
     return portedComment;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 399da8f..8476767 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.HumanComment;
@@ -32,8 +31,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -51,20 +48,17 @@
   private final Provider<CommentJson> commentJson;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
-  private final PatchListCache patchListCache;
 
   @Inject
   CreateDraftComment(
       BatchUpdate.Factory updateFactory,
       Provider<CommentJson> commentJson,
       CommentsUtil commentsUtil,
-      PatchSetUtil psUtil,
-      PatchListCache patchListCache) {
+      PatchSetUtil psUtil) {
     this.updateFactory = updateFactory;
     this.commentJson = commentJson;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.patchListCache = patchListCache;
   }
 
   @Override
@@ -111,8 +105,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, UnprocessableEntityException,
-            PatchListNotAvailableException {
+        throws ResourceNotFoundException, UnprocessableEntityException {
       PatchSet ps = psUtil.get(ctx.getNotes(), psId);
       if (ps == null) {
         throw new ResourceNotFoundException("patch set not found: " + psId);
@@ -133,7 +126,7 @@
       comment.setLineNbrAndRange(in.line, in.range);
       comment.tag = in.tag;
 
-      setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
+      commentsUtil.setCommentCommitId(comment, ctx.getChange(), ps);
 
       commentsUtil.putHumanComments(
           ctx.getUpdate(psId), HumanComment.Status.DRAFT, Collections.singleton(comment));
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index 71fd4d2..51a0b8e 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
-
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
@@ -28,8 +26,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -45,18 +41,13 @@
   private final BatchUpdate.Factory updateFactory;
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
-  private final PatchListCache patchListCache;
 
   @Inject
   DeleteDraftComment(
-      BatchUpdate.Factory updateFactory,
-      CommentsUtil commentsUtil,
-      PatchSetUtil psUtil,
-      PatchListCache patchListCache) {
+      BatchUpdate.Factory updateFactory, CommentsUtil commentsUtil, PatchSetUtil psUtil) {
     this.updateFactory = updateFactory;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
-    this.patchListCache = patchListCache;
   }
 
   @Override
@@ -79,8 +70,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, PatchListNotAvailableException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException {
       Optional<HumanComment> maybeComment =
           commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
@@ -92,7 +82,7 @@
         throw new ResourceNotFoundException("patch set not found: " + psId);
       }
       HumanComment c = maybeComment.get();
-      setCommentCommitId(c, patchListCache, ctx.getChange(), ps);
+      commentsUtil.setCommentCommitId(c, ctx.getChange(), ps);
       commentsUtil.deleteHumanComments(ctx.getUpdate(psId), Collections.singleton(c));
       return true;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 7a4a891..3367ca6 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
@@ -893,7 +892,7 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws ResourceConflictException, UnprocessableEntityException, IOException,
-            PatchListNotAvailableException, CommentsRejectedException {
+            CommentsRejectedException {
       user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
       ps = psUtil.get(ctx.getNotes(), psId);
@@ -951,7 +950,7 @@
     }
 
     private boolean insertComments(ChangeContext ctx, List<RobotComment> newRobotComments)
-        throws PatchListNotAvailableException, CommentsRejectedException {
+        throws CommentsRejectedException {
       Map<String, List<CommentInput>> inputComments = in.comments;
       if (inputComments == null) {
         inputComments = Collections.emptyMap();
@@ -1003,7 +1002,7 @@
             comment.message = inputComment.message;
           }
 
-          setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
+          commentsUtil.setCommentCommitId(comment, ctx.getChange(), ps);
           comment.setLineNbrAndRange(inputComment.line, inputComment.range);
           comment.tag = in.tag;
 
@@ -1082,8 +1081,7 @@
       return !newRobotComments.isEmpty();
     }
 
-    private List<RobotComment> getNewRobotComments(ChangeContext ctx)
-        throws PatchListNotAvailableException {
+    private List<RobotComment> getNewRobotComments(ChangeContext ctx) {
       List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
 
       Set<CommentSetEntry> existingIds =
@@ -1103,8 +1101,7 @@
     }
 
     private RobotComment createRobotCommentFromInput(
-        ChangeContext ctx, String path, RobotCommentInput robotCommentInput)
-        throws PatchListNotAvailableException {
+        ChangeContext ctx, String path, RobotCommentInput robotCommentInput) {
       RobotComment robotComment =
           commentsUtil.newRobotComment(
               ctx,
@@ -1119,7 +1116,7 @@
       robotComment.properties = robotCommentInput.properties;
       robotComment.setLineNbrAndRange(robotCommentInput.line, robotCommentInput.range);
       robotComment.tag = in.tag;
-      setCommentCommitId(robotComment, patchListCache, ctx.getChange(), ps);
+      commentsUtil.setCommentCommitId(robotComment, ctx.getChange(), ps);
       robotComment.fixSuggestions = createFixSuggestionsFromInput(robotCommentInput.fixSuggestions);
       return robotComment;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 39de43d..84a3d89 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
@@ -32,8 +31,6 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -54,7 +51,6 @@
   private final CommentsUtil commentsUtil;
   private final PatchSetUtil psUtil;
   private final Provider<CommentJson> commentJson;
-  private final PatchListCache patchListCache;
 
   @Inject
   PutDraftComment(
@@ -62,14 +58,12 @@
       DeleteDraftComment delete,
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
-      Provider<CommentJson> commentJson,
-      PatchListCache patchListCache) {
+      Provider<CommentJson> commentJson) {
     this.updateFactory = updateFactory;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
     this.commentJson = commentJson;
-    this.patchListCache = patchListCache;
   }
 
   @Override
@@ -114,8 +108,7 @@
     }
 
     @Override
-    public boolean updateChange(ChangeContext ctx)
-        throws ResourceNotFoundException, PatchListNotAvailableException {
+    public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException {
       Optional<HumanComment> maybeComment =
           commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
       if (!maybeComment.isPresent()) {
@@ -143,7 +136,7 @@
         commentsUtil.deleteHumanComments(update, Collections.singleton(origComment));
         comment.key.filename = in.path;
       }
-      setCommentCommitId(comment, patchListCache, ctx.getChange(), ps);
+      commentsUtil.setCommentCommitId(comment, ctx.getChange(), ps);
       commentsUtil.putHumanComments(
           update,
           HumanComment.Status.DRAFT,
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
index 783fa49..d76d114 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
@@ -34,6 +34,8 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.truth.NullAwareCorrespondence;
@@ -43,6 +45,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class PortedCommentsIT extends AbstractDaemonTest {
@@ -339,6 +342,51 @@
   }
 
   @Test
+  public void commentOnInvalidParentIsPorted() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+    // Add comments.
+    String commentUuid = newComment(patchset1Id).onSecondParentCommit().create();
+
+    List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+    assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(commentUuid);
+  }
+
+  @Test
+  public void commentsOnInvalidPositionArePorted() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+    // Add comments.
+    String commentUuid1 = newComment(patchset1Id).onFileLevelOf("not-existing file").create();
+    String commentUuid2 = newComment(patchset1Id).onLine(3).ofFile("myFile").create();
+
+    List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+    assertThat(portedComments)
+        .comparingElementsUsing(hasUuid())
+        .containsExactly(commentUuid1, commentUuid2);
+  }
+
+  @Test
+  public void commentsOnInvalidPositionKeepTheirInvalidPosition() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+    // Add comments.
+    newComment(patchset1Id).onFileLevelOf("not-existing file").create();
+
+    Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+    assertThatMap(portedComments).keys().containsExactly("not-existing file");
+  }
+
+  @Test
   public void portedCommentHasOriginalUuid() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId = changeOps.newChange().create();
@@ -998,6 +1046,42 @@
   }
 
   @Test
+  public void overlappingRangeCommentsArePortedToNewPosition() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id =
+        changeOps
+            .change(changeId)
+            .newPatchset()
+            .file("myFile")
+            .content("Line 1\nLine 1.1\nLine 2\n")
+            .create();
+    // Add comment.
+    String commentUuid1 =
+        newComment(patchset1Id)
+            .fromLine(2)
+            .charOffset(0)
+            .toLine(2)
+            .charOffset(3)
+            .ofFile("myFile")
+            .create();
+    String commentUuid2 =
+        newComment(patchset1Id)
+            .fromLine(2)
+            .charOffset(1)
+            .toLine(2)
+            .charOffset(4)
+            .ofFile("myFile")
+            .create();
+
+    CommentInfo portedComment1 = getPortedComment(patchset2Id, commentUuid1);
+    assertThat(portedComment1).range().startLine().isEqualTo(3);
+    CommentInfo portedComment2 = getPortedComment(patchset2Id, commentUuid2);
+    assertThat(portedComment2).range().startLine().isEqualTo(3);
+  }
+
+  @Test
   public void portedLineCommentCanHandleAddedLines() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
@@ -1241,6 +1325,28 @@
   }
 
   @Test
+  public void overlappingLineCommentsArePortedToNewPosition() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id =
+        changeOps
+            .change(changeId)
+            .newPatchset()
+            .file("myFile")
+            .content("Line 1\nLine 1.1\nLine 2\n")
+            .create();
+    // Add comment.
+    String commentUuid1 = newComment(patchset1Id).onLine(2).ofFile("myFile").create();
+    String commentUuid2 = newComment(patchset1Id).onLine(2).ofFile("myFile").create();
+
+    CommentInfo portedComment1 = getPortedComment(patchset2Id, commentUuid1);
+    assertThat(portedComment1).line().isEqualTo(3);
+    CommentInfo portedComment2 = getPortedComment(patchset2Id, commentUuid2);
+    assertThat(portedComment2).line().isEqualTo(3);
+  }
+
+  @Test
   public void portedFileCommentIsObliviousToAdjustedFileContent() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
@@ -1553,6 +1659,169 @@
     assertThat(portedComment).line().isGreaterThan(2);
   }
 
+  @Test
+  public void commentOnFirstParentIsPortedToSingleParentWhenPatchsetChangedToNonMergeCommit()
+      throws Exception {
+    // Set up change and patchsets.
+    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id childChangeId =
+        changeOps
+            .newChange()
+            .mergeOf()
+            .change(parent1ChangeId)
+            .and()
+            .change(parent2ChangeId)
+            .create();
+    PatchSet.Id childPatchset1Id =
+        changeOps.change(childChangeId).currentPatchset().get().patchsetId();
+    PatchSet.Id parent1PatchsetId2 =
+        changeOps
+            .change(parent1ChangeId)
+            .newPatchset()
+            .file("file1")
+            .content("Line 0\nLine 1\n")
+            .create();
+    PatchSet.Id childPatchset2Id =
+        changeOps
+            .change(childChangeId)
+            .newPatchset()
+            .parent()
+            .patchset(parent1PatchsetId2)
+            .create();
+    // Add comment.
+    String commentUuid =
+        newComment(childPatchset1Id).onParentCommit().onLine(1).ofFile("file1").create();
+
+    CommentInfo portedComment = getPortedComment(childPatchset2Id, commentUuid);
+
+    assertThat(portedComment).line().isEqualTo(2);
+    assertThat(portedComment).side().isEqualTo(Side.PARENT);
+    assertThat(portedComment).parent().isEqualTo(1);
+  }
+
+  @Test
+  public void commentOnSecondParentBecomesPatchsetLevelCommentWhenPatchsetChangedToNonMergeCommit()
+      throws Exception {
+    // Set up change and patchsets.
+    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id childChangeId =
+        changeOps
+            .newChange()
+            .mergeOf()
+            .change(parent1ChangeId)
+            .and()
+            .change(parent2ChangeId)
+            .create();
+    PatchSet.Id childPatchset1Id =
+        changeOps.change(childChangeId).currentPatchset().get().patchsetId();
+    PatchSet.Id childPatchset2Id =
+        changeOps.change(childChangeId).newPatchset().parent().change(parent1ChangeId).create();
+    // Add comment.
+    String commentUuid =
+        newComment(childPatchset1Id).onSecondParentCommit().onLine(1).ofFile("file2").create();
+
+    Map<String, List<CommentInfo>> portedComments = getPortedComments(childPatchset2Id);
+    assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+    CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+    assertThat(portedComment).line().isNull();
+    assertThat(portedComment).side().isNull();
+    assertThat(portedComment).parent().isNull();
+  }
+
+  @Test
+  // TODO(ghareeb): Adjust implementation in CommentsUtil to use the new auto-merge code instead of
+  // PatchListCache#getOldId which returns the wrong result if a change isn't a merge commit.
+  @Ignore
+  public void
+      commentOnAutoMergeCommitBecomesPatchsetLevelCommentWhenPatchsetChangedToNonMergeCommit()
+          throws Exception {
+    // Set up change and patchsets.
+    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id parent2ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id childChangeId =
+        changeOps
+            .newChange()
+            .mergeOf()
+            .change(parent1ChangeId)
+            .and()
+            .change(parent2ChangeId)
+            .create();
+    PatchSet.Id childPatchset1Id =
+        changeOps.change(childChangeId).currentPatchset().get().patchsetId();
+    PatchSet.Id childPatchset2Id =
+        changeOps.change(childChangeId).newPatchset().parent().change(parent1ChangeId).create();
+    // Add comment.
+    String commentUuid =
+        newComment(childPatchset1Id).onAutoMergeCommit().onLine(1).ofFile("file1").create();
+
+    Map<String, List<CommentInfo>> portedComments = getPortedComments(childPatchset2Id);
+    assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+    CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+    assertThat(portedComment).line().isNull();
+    assertThat(portedComment).side().isNull();
+    assertThat(portedComment).parent().isNull();
+  }
+
+  @Test
+  public void whitespaceOnlyModificationsAreAlsoConsideredWhenPorting() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").create();
+    PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchset2Id =
+        changeOps.change(changeId).newPatchset().file("myFile").content("\nLine 1\n").create();
+    // Add comment.
+    String commentUuid = newComment(patchset1Id).onLine(1).ofFile("myFile").create();
+
+    CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+    assertThat(portedComment).line().isEqualTo(2);
+  }
+
+  @Test
+  public void deletedCommentContentIsNotCachedInPortedComments() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().create();
+    PatchSet.Id patchsetId1 = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchsetId2 = changeOps.change(changeId).newPatchset().create();
+    // Add comment.
+    String commentUuid = newComment(patchsetId1).message("Confidential content").create();
+
+    getPortedComment(patchsetId2, commentUuid);
+    gApi.changes()
+        .id(changeId.get())
+        .revision(patchsetId1.get())
+        .comment(commentUuid)
+        .delete(new DeleteCommentInput());
+    CommentInfo portedComment = getPortedComment(patchsetId2, commentUuid);
+
+    assertThat(portedComment).message().doesNotContain("Confidential content");
+  }
+
+  @Test
+  public void setOfPortedCommentsCanChangeOnRepeatedCalls() throws Exception {
+    // Set up change and patchsets.
+    Change.Id changeId = changeOps.newChange().create();
+    PatchSet.Id patchsetId1 = changeOps.change(changeId).currentPatchset().get().patchsetId();
+    PatchSet.Id patchsetId2 = changeOps.change(changeId).newPatchset().create();
+    // Add comment.
+    String commentUuid1 = newComment(patchsetId1).unresolved().create();
+
+    ImmutableList<CommentInfo> pastPortedComments = flatten(getPortedComments(patchsetId2));
+    // Set the existing comment thread to resolved, so it won't be ported anymore.
+    newComment(patchsetId1).parentUuid(commentUuid1).resolved().create();
+    // Create a new comment which should show up as ported comment.
+    String commentUuid2 = newComment(patchsetId1).create();
+    ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchsetId2));
+
+    // Ensure that results are not cached between calls. This should not be necessary as the diffs
+    // are already cached. If we need to also cache the ported comments in the future, we'll need to
+    // identify ALL situations when the set of ported comments changes.
+    assertThat(portedComments).isNotEqualTo(pastPortedComments);
+    assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(commentUuid2);
+  }
+
   private TestCommentCreation.Builder newComment(PatchSet.Id patchsetId) {
     // Create unresolved comments by default as only those are ported. Tests get override the
     // unresolved state by explicitly setting it.
diff --git a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
index 9c30fc9..8891cd2 100644
--- a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.Comparator.comparing;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyShort;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -31,6 +32,7 @@
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.ComparisonType;
 import com.google.gerrit.server.patch.PatchList;
@@ -41,6 +43,7 @@
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import java.sql.Timestamp;
 import java.util.Arrays;
+import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Rule;
 import org.junit.Test;
@@ -56,6 +59,7 @@
   @Rule public final MockitoRule mockito = MockitoJUnit.rule();
 
   @Mock private PatchListCache patchListCache;
+  @Mock private CommentsUtil commentsUtil;
 
   private int uuidCounter = 0;
 
@@ -68,9 +72,10 @@
     PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
     ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
 
-    CommentPorter commentPorter = new CommentPorter(patchListCache);
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
     HumanComment comment = createComment(patchset1.id(), "myFile");
-    when(patchListCache.getOldId(any(), any(), any())).thenReturn(dummyObjectId);
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenReturn(Optional.of(dummyObjectId));
     when(patchListCache.get(any(PatchListKey.class), any(Project.NameKey.class)))
         .thenThrow(PatchListNotAvailableException.class);
     ImmutableList<HumanComment> portedComments =
@@ -88,9 +93,10 @@
     PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
     ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
 
-    CommentPorter commentPorter = new CommentPorter(patchListCache);
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
     HumanComment comment = createComment(patchset1.id(), "myFile");
-    when(patchListCache.getOldId(any(), any(), any())).thenReturn(dummyObjectId);
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenReturn(Optional.of(dummyObjectId));
     when(patchListCache.get(any(PatchListKey.class), any(Project.NameKey.class)))
         .thenThrow(IllegalStateException.class);
     ImmutableList<HumanComment> portedComments =
@@ -100,7 +106,7 @@
   }
 
   @Test
-  public void commentsAreNotDroppedWhenRetrievingCommitSha1sHasUnexpectedError() throws Exception {
+  public void commentsAreNotDroppedWhenRetrievingCommitSha1sHasUnexpectedError() {
     Project.NameKey project = Project.nameKey("myProject");
     Change.Id changeId = Change.id(1);
     Change change = createChange(project, changeId);
@@ -108,9 +114,10 @@
     PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
     ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
 
-    CommentPorter commentPorter = new CommentPorter(patchListCache);
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
     HumanComment comment = createComment(patchset1.id(), "myFile");
-    when(patchListCache.getOldId(any(), any(), any())).thenThrow(IllegalStateException.class);
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenThrow(IllegalStateException.class);
     ImmutableList<HumanComment> portedComments =
         commentPorter.portComments(changeNotes, patchset2, ImmutableList.of(comment));
 
@@ -126,9 +133,10 @@
     PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
     ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
 
-    CommentPorter commentPorter = new CommentPorter(patchListCache);
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
     HumanComment comment = createComment(patchset1.id(), "myFile");
-    when(patchListCache.getOldId(any(), any(), any())).thenReturn(dummyObjectId);
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenReturn(Optional.of(dummyObjectId));
     when(patchListCache.get(any(PatchListKey.class), any(Project.NameKey.class)))
         .thenThrow(IllegalStateException.class);
     ImmutableList<HumanComment> portedComments =
@@ -149,18 +157,13 @@
     PatchSet patchset3 = createPatchset(PatchSet.id(changeId, 3));
     ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2, patchset3);
 
-    CommentPorter commentPorter = new CommentPorter(patchListCache);
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
     // Place the comments on different patchsets to have two different diff requests.
     HumanComment comment1 = createComment(patchset1.id(), "myFile");
     HumanComment comment2 = createComment(patchset2.id(), "myFile");
-    when(patchListCache.getOldId(any(), any(), any())).thenReturn(dummyObjectId);
-    PatchList emptyDiff =
-        new PatchList(
-            dummyObjectId,
-            dummyObjectId,
-            false,
-            ComparisonType.againstOtherPatchSet(),
-            new PatchListEntry[0]);
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenReturn(Optional.of(dummyObjectId));
+    PatchList emptyDiff = getEmptyDiff();
     // Throw an exception on the first diff request but return an actual value on the second.
     when(patchListCache.get(any(PatchListKey.class), any(Project.NameKey.class)))
         .thenThrow(IllegalStateException.class)
@@ -173,6 +176,29 @@
     assertThat(portedComments).comparingElementsUsing(hasFilePath()).contains("myFile");
   }
 
+  @Test
+  public void commentsWithInvalidPatchsetsAreIgnored() throws Exception {
+    Project.NameKey project = Project.nameKey("myProject");
+    Change.Id changeId = Change.id(1);
+    Change change = createChange(project, changeId);
+    PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
+    PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
+    // Leave out patchset 1 (e.g. reserved for draft patchsets in the past).
+    ChangeNotes changeNotes = mockChangeNotes(project, change, patchset2);
+
+    CommentPorter commentPorter = new CommentPorter(patchListCache, commentsUtil);
+    HumanComment comment = createComment(patchset1.id(), "myFile");
+    when(commentsUtil.determineCommitId(any(), any(), anyShort()))
+        .thenReturn(Optional.of(dummyObjectId));
+    PatchList emptyDiff = getEmptyDiff();
+    when(patchListCache.get(any(PatchListKey.class), any(Project.NameKey.class)))
+        .thenReturn(emptyDiff);
+    ImmutableList<HumanComment> portedComments =
+        commentPorter.portComments(changeNotes, patchset2, ImmutableList.of(comment));
+
+    assertThat(portedComments).isEmpty();
+  }
+
   private Change createChange(Project.NameKey project, Change.Id changeId) {
     return new Change(
         Change.key("changeKey"),
@@ -224,4 +250,13 @@
   private Correspondence<HumanComment, String> hasFilePath() {
     return NullAwareCorrespondence.transforming(comment -> comment.key.filename, "hasFilePath");
   }
+
+  private PatchList getEmptyDiff() {
+    return new PatchList(
+        dummyObjectId,
+        dummyObjectId,
+        false,
+        ComparisonType.againstOtherPatchSet(),
+        new PatchListEntry[0]);
+  }
 }