LCOV - code coverage report
Current view: top level - server - CommentsUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 159 186 85.5 %
Date: 2022-11-19 15:00:39 Functions: 46 51 90.2 %

          Line data    Source code
       1             : // Copyright (C) 2014 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.base.Preconditions.checkArgument;
      19             : import static java.util.Comparator.comparing;
      20             : import static java.util.stream.Collectors.toCollection;
      21             : import static java.util.stream.Collectors.toList;
      22             : 
      23             : import com.google.common.collect.ComparisonChain;
      24             : import com.google.common.collect.Lists;
      25             : import com.google.common.collect.Ordering;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.entities.Account;
      28             : import com.google.gerrit.entities.Change;
      29             : import com.google.gerrit.entities.ChangeMessage;
      30             : import com.google.gerrit.entities.Comment;
      31             : import com.google.gerrit.entities.HumanComment;
      32             : import com.google.gerrit.entities.Patch;
      33             : import com.google.gerrit.entities.PatchSet;
      34             : import com.google.gerrit.entities.Project;
      35             : import com.google.gerrit.entities.RefNames;
      36             : import com.google.gerrit.entities.RobotComment;
      37             : import com.google.gerrit.exceptions.StorageException;
      38             : import com.google.gerrit.extensions.client.Side;
      39             : import com.google.gerrit.extensions.common.CommentInfo;
      40             : import com.google.gerrit.server.config.AllUsersName;
      41             : import com.google.gerrit.server.config.GerritServerId;
      42             : import com.google.gerrit.server.git.GitRepositoryManager;
      43             : import com.google.gerrit.server.notedb.ChangeNotes;
      44             : import com.google.gerrit.server.notedb.ChangeUpdate;
      45             : import com.google.gerrit.server.patch.DiffNotAvailableException;
      46             : import com.google.gerrit.server.patch.DiffOperations;
      47             : import com.google.gerrit.server.patch.DiffOptions;
      48             : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
      49             : import com.google.gerrit.server.update.ChangeContext;
      50             : import com.google.inject.Inject;
      51             : import com.google.inject.Singleton;
      52             : import java.io.IOException;
      53             : import java.time.Instant;
      54             : import java.util.ArrayList;
      55             : import java.util.Collection;
      56             : import java.util.HashSet;
      57             : import java.util.List;
      58             : import java.util.Map;
      59             : import java.util.Optional;
      60             : import java.util.Set;
      61             : import org.eclipse.jgit.lib.ObjectId;
      62             : import org.eclipse.jgit.lib.Ref;
      63             : import org.eclipse.jgit.lib.Repository;
      64             : import org.eclipse.jgit.revwalk.RevCommit;
      65             : 
      66             : /** Utility functions to manipulate Comments. */
      67             : @Singleton
      68             : public class CommentsUtil {
      69         152 :   public static final Ordering<Comment> COMMENT_ORDER =
      70         152 :       new Ordering<>() {
      71             :         @Override
      72             :         public int compare(Comment c1, Comment c2) {
      73          20 :           return ComparisonChain.start()
      74          20 :               .compare(c1.key.filename, c2.key.filename)
      75          20 :               .compare(c1.key.patchSetId, c2.key.patchSetId)
      76          20 :               .compare(c1.side, c2.side)
      77          20 :               .compare(c1.lineNbr, c2.lineNbr)
      78          20 :               .compare(c1.writtenOn, c2.writtenOn)
      79          20 :               .result();
      80             :         }
      81             :       };
      82             : 
      83         152 :   public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
      84         152 :       new Ordering<>() {
      85             :         @Override
      86             :         public int compare(CommentInfo a, CommentInfo b) {
      87          11 :           return ComparisonChain.start()
      88          11 :               .compare(a.path, b.path, NULLS_FIRST)
      89          11 :               .compare(a.patchSet, b.patchSet, NULLS_FIRST)
      90          11 :               .compare(side(a), side(b))
      91          11 :               .compare(a.line, b.line, NULLS_FIRST)
      92          11 :               .compare(a.inReplyTo, b.inReplyTo, NULLS_FIRST)
      93          11 :               .compare(a.message, b.message)
      94          11 :               .compare(a.id, b.id)
      95          11 :               .result();
      96             :         }
      97             : 
      98             :         private int side(CommentInfo c) {
      99          11 :           return firstNonNull(c.side, Side.REVISION).ordinal();
     100             :         }
     101             :       };
     102             : 
     103             :   public static PatchSet.Id getCommentPsId(Change.Id changeId, Comment comment) {
     104           0 :     return PatchSet.id(changeId, comment.key.patchSetId);
     105             :   }
     106             : 
     107             :   @Nullable
     108             :   public static String extractMessageId(@Nullable String tag) {
     109           2 :     if (tag == null || !tag.startsWith("mailMessageId=")) {
     110           2 :       return null;
     111             :     }
     112           1 :     return tag.substring("mailMessageId=".length());
     113             :   }
     114             : 
     115         152 :   private static final Ordering<Comparable<?>> NULLS_FIRST = Ordering.natural().nullsFirst();
     116             : 
     117             :   private final DiffOperations diffOperations;
     118             :   private final GitRepositoryManager repoManager;
     119             :   private final AllUsersName allUsers;
     120             :   private final String serverId;
     121             : 
     122             :   @Inject
     123             :   CommentsUtil(
     124             :       DiffOperations diffOperations,
     125             :       GitRepositoryManager repoManager,
     126             :       AllUsersName allUsers,
     127         152 :       @GerritServerId String serverId) {
     128         152 :     this.diffOperations = diffOperations;
     129         152 :     this.repoManager = repoManager;
     130         152 :     this.allUsers = allUsers;
     131         152 :     this.serverId = serverId;
     132         152 :   }
     133             : 
     134             :   public HumanComment newHumanComment(
     135             :       ChangeNotes changeNotes,
     136             :       CurrentUser currentUser,
     137             :       Instant when,
     138             :       String path,
     139             :       PatchSet.Id psId,
     140             :       short side,
     141             :       String message,
     142             :       @Nullable Boolean unresolved,
     143             :       @Nullable String parentUuid) {
     144          28 :     if (unresolved == null) {
     145          23 :       if (parentUuid == null) {
     146             :         // Default to false if comment is not descended from another.
     147          23 :         unresolved = false;
     148             :       } else {
     149             :         // Inherit unresolved value from inReplyTo comment if not specified.
     150           4 :         Comment.Key key = new Comment.Key(parentUuid, path, psId.get());
     151           4 :         Optional<HumanComment> parent = getPublishedHumanComment(changeNotes, key);
     152             : 
     153             :         // If the comment was not found, it is descended from a robot comment, or the UUID is
     154             :         // invalid. Either way, we use the default.
     155           4 :         unresolved = parent.map(p -> p.unresolved).orElse(false);
     156             :       }
     157             :     }
     158          28 :     HumanComment c =
     159             :         new HumanComment(
     160          28 :             new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
     161          28 :             currentUser.getAccountId(),
     162             :             when,
     163             :             side,
     164             :             message,
     165             :             serverId,
     166          28 :             unresolved);
     167          28 :     c.parentUuid = parentUuid;
     168          28 :     currentUser.updateRealAccountId(c::setRealAuthor);
     169          28 :     return c;
     170             :   }
     171             : 
     172             :   public RobotComment newRobotComment(
     173             :       ChangeContext ctx,
     174             :       String path,
     175             :       PatchSet.Id psId,
     176             :       short side,
     177             :       String message,
     178             :       String robotId,
     179             :       String robotRunId) {
     180           9 :     RobotComment c =
     181             :         new RobotComment(
     182           9 :             new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
     183           9 :             ctx.getUser().getAccountId(),
     184           9 :             ctx.getWhen(),
     185             :             side,
     186             :             message,
     187             :             serverId,
     188             :             robotId,
     189             :             robotRunId);
     190           9 :     ctx.getUser().updateRealAccountId(c::setRealAuthor);
     191           9 :     return c;
     192             :   }
     193             : 
     194             :   public Optional<HumanComment> getPublishedHumanComment(ChangeNotes notes, Comment.Key key) {
     195           6 :     return publishedHumanCommentsByChange(notes).stream()
     196           6 :         .filter(c -> key.equals(c.key))
     197           6 :         .findFirst();
     198             :   }
     199             : 
     200             :   public Optional<HumanComment> getPublishedHumanComment(ChangeNotes notes, String uuid) {
     201           4 :     return publishedHumanCommentsByChange(notes).stream()
     202           4 :         .filter(c -> c.key.uuid.equals(uuid))
     203           4 :         .findFirst();
     204             :   }
     205             : 
     206             :   public Optional<HumanComment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
     207           9 :     return draftByChangeAuthor(notes, user.getAccountId()).stream()
     208           9 :         .filter(c -> key.equals(c.key))
     209           9 :         .findFirst();
     210             :   }
     211             : 
     212             :   public List<HumanComment> publishedHumanCommentsByChange(ChangeNotes notes) {
     213         103 :     notes.load();
     214         103 :     return sort(Lists.newArrayList(notes.getHumanComments().values()));
     215             :   }
     216             : 
     217             :   public List<RobotComment> robotCommentsByChange(ChangeNotes notes) {
     218         103 :     notes.load();
     219         103 :     return sort(Lists.newArrayList(notes.getRobotComments().values()));
     220             :   }
     221             : 
     222             :   public Optional<RobotComment> getRobotComment(ChangeNotes notes, String uuid) {
     223           3 :     return robotCommentsByChange(notes).stream().filter(c -> c.key.uuid.equals(uuid)).findFirst();
     224             :   }
     225             : 
     226             :   public List<HumanComment> draftByChange(ChangeNotes notes) {
     227           2 :     List<HumanComment> comments = new ArrayList<>();
     228           2 :     for (Ref ref : getDraftRefs(notes.getChangeId())) {
     229           2 :       Account.Id account = Account.Id.fromRefSuffix(ref.getName());
     230           2 :       if (account != null) {
     231           2 :         comments.addAll(draftByChangeAuthor(notes, account));
     232             :       }
     233           2 :     }
     234           2 :     return sort(comments);
     235             :   }
     236             : 
     237             :   public List<HumanComment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     238           0 :     List<HumanComment> comments = new ArrayList<>();
     239           0 :     comments.addAll(publishedByPatchSet(notes, psId));
     240             : 
     241           0 :     for (Ref ref : getDraftRefs(notes.getChangeId())) {
     242           0 :       Account.Id account = Account.Id.fromRefSuffix(ref.getName());
     243           0 :       if (account != null) {
     244           0 :         comments.addAll(draftByPatchSetAuthor(psId, account, notes));
     245             :       }
     246           0 :     }
     247           0 :     return sort(comments);
     248             :   }
     249             : 
     250             :   public List<HumanComment> publishedByChangeFile(ChangeNotes notes, String file) {
     251           0 :     return commentsOnFile(notes.load().getHumanComments().values(), file);
     252             :   }
     253             : 
     254             :   public List<HumanComment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     255          13 :     return removeCommentsOnAncestorOfCommitMessage(
     256          13 :         commentsOnPatchSet(notes.load().getHumanComments().values(), psId));
     257             :   }
     258             : 
     259             :   public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     260           4 :     return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
     261             :   }
     262             : 
     263             :   /**
     264             :    * This method populates the "changeMessageId" field of the comments parameter based on timestamp
     265             :    * matching. The comments objects will be modified.
     266             :    *
     267             :    * <p>Each comment will be matched to the nearest next change message in timestamp
     268             :    *
     269             :    * @param comments the list of comments
     270             :    * @param changeMessages list of change messages
     271             :    */
     272             :   public static void linkCommentsToChangeMessages(
     273             :       List<? extends CommentInfo> comments,
     274             :       List<ChangeMessage> changeMessages,
     275             :       boolean skipAutoGeneratedMessages) {
     276          14 :     ArrayList<ChangeMessage> sortedChangeMessages =
     277          14 :         changeMessages.stream()
     278          14 :             .sorted(comparing(ChangeMessage::getWrittenOn))
     279          14 :             .collect(toCollection(ArrayList::new));
     280             : 
     281          14 :     ArrayList<CommentInfo> sortedCommentInfos =
     282          14 :         comments.stream().sorted(comparing(c -> c.updated)).collect(toCollection(ArrayList::new));
     283             : 
     284          14 :     int cmItr = 0;
     285          14 :     for (CommentInfo comment : sortedCommentInfos) {
     286             :       // Keep advancing the change message pointer until we associate the comment to the next change
     287             :       // message in timestamp
     288          14 :       while (cmItr < sortedChangeMessages.size()) {
     289          14 :         ChangeMessage cm = sortedChangeMessages.get(cmItr);
     290          14 :         if (isAfter(comment, cm) || (skipAutoGeneratedMessages && isAutoGenerated(cm))) {
     291          13 :           cmItr += 1;
     292             :         } else {
     293             :           break;
     294             :         }
     295          13 :       }
     296          14 :       if (cmItr < changeMessages.size()) {
     297          14 :         comment.changeMessageId = sortedChangeMessages.get(cmItr).getKey().uuid();
     298             :       }
     299          14 :     }
     300          14 :   }
     301             : 
     302             :   private static boolean isAutoGenerated(ChangeMessage cm) {
     303             :     // Ignore Gerrit auto-generated messages, allowing to link against human change messages that
     304             :     // have an auto-generated tag
     305          12 :     return ChangeMessagesUtil.isAutogeneratedByGerrit(cm.getTag());
     306             :   }
     307             : 
     308             :   private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
     309          14 :     return c.getUpdated().isAfter(cm.getWrittenOn());
     310             :   }
     311             : 
     312             :   /**
     313             :    * For the commit message the A side in a diff view is always empty when a comparison against an
     314             :    * ancestor is done, so there can't be any comments on this ancestor. However earlier we showed
     315             :    * the auto-merge commit message on side A when for a merge commit a comparison against the
     316             :    * auto-merge was done. From that time there may still be comments on the auto-merge commit
     317             :    * message and those we want to filter out.
     318             :    */
     319             :   private List<HumanComment> removeCommentsOnAncestorOfCommitMessage(List<HumanComment> list) {
     320          13 :     return list.stream()
     321          13 :         .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename))
     322          13 :         .collect(toList());
     323             :   }
     324             : 
     325             :   public List<HumanComment> draftByPatchSetAuthor(
     326             :       PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
     327          26 :     return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
     328             :   }
     329             : 
     330             :   public List<HumanComment> draftByChangeFileAuthor(
     331             :       ChangeNotes notes, String file, Account.Id author) {
     332           0 :     return commentsOnFile(notes.load().getDraftComments(author).values(), file);
     333             :   }
     334             : 
     335             :   public List<HumanComment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
     336          16 :     List<HumanComment> comments = new ArrayList<>();
     337          16 :     comments.addAll(notes.getDraftComments(author).values());
     338          16 :     return sort(comments);
     339             :   }
     340             : 
     341             :   public void putHumanComments(
     342             :       ChangeUpdate update, Comment.Status status, Iterable<HumanComment> comments) {
     343          66 :     for (HumanComment c : comments) {
     344          25 :       update.putComment(status, c);
     345          25 :     }
     346          66 :   }
     347             : 
     348             :   public void putRobotComments(ChangeUpdate update, Iterable<RobotComment> comments) {
     349           7 :     for (RobotComment c : comments) {
     350           7 :       update.putRobotComment(c);
     351           7 :     }
     352           7 :   }
     353             : 
     354             :   public void deleteHumanComments(ChangeUpdate update, Iterable<HumanComment> comments) {
     355           9 :     for (HumanComment c : comments) {
     356           9 :       update.deleteComment(c);
     357           9 :     }
     358           9 :   }
     359             : 
     360             :   public void deleteCommentByRewritingHistory(
     361             :       ChangeUpdate update, Comment.Key commentKey, String newMessage) {
     362           3 :     update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
     363           3 :   }
     364             : 
     365             :   private static List<HumanComment> commentsOnFile(
     366             :       Collection<HumanComment> allComments, String file) {
     367           0 :     List<HumanComment> result = new ArrayList<>(allComments.size());
     368           0 :     for (HumanComment c : allComments) {
     369           0 :       String currentFilename = c.key.filename;
     370           0 :       if (currentFilename.equals(file)) {
     371           0 :         result.add(c);
     372             :       }
     373           0 :     }
     374           0 :     return sort(result);
     375             :   }
     376             : 
     377             :   private static <T extends Comment> List<T> commentsOnPatchSet(
     378             :       Collection<T> allComments, PatchSet.Id psId) {
     379          30 :     List<T> result = new ArrayList<>(allComments.size());
     380          30 :     for (T c : allComments) {
     381          23 :       if (c.key.patchSetId == psId.get()) {
     382          23 :         result.add(c);
     383             :       }
     384          23 :     }
     385          30 :     return sort(result);
     386             :   }
     387             : 
     388             :   public void setCommentCommitId(Comment c, Change change, PatchSet ps) {
     389          28 :     checkArgument(
     390          28 :         c.key.patchSetId == ps.id().get(),
     391             :         "cannot set commit ID for patch set %s on comment %s",
     392          28 :         ps.id(),
     393             :         c);
     394          28 :     if (c.getCommitId() == null) {
     395             :       // This code is very much down into our stack and shouldn't be used for validation. Hence,
     396             :       // don't throw an exception here if we can't find a commit for the indicated side but
     397             :       // simply use the all-null ObjectId.
     398          28 :       c.setCommitId(determineCommitId(change, ps, c.side).orElseGet(ObjectId::zeroId));
     399             :     }
     400          28 :   }
     401             : 
     402             :   /**
     403             :    * Determines the SHA-1 of the commit referenced by the (change, patchset, side) triple.
     404             :    *
     405             :    * @param change the change to which the commit belongs
     406             :    * @param patchset the patchset to which the commit belongs
     407             :    * @param side the side indicating which commit of the patchset to take. 1 is the patchset commit,
     408             :    *     0 the parent commit (or auto-merge for changes representing merge commits); -x the xth
     409             :    *     parent commit of a merge commit
     410             :    * @return the commit SHA-1 or an empty {@link Optional} if the side isn't available for the given
     411             :    *     change/patchset
     412             :    * @throws StorageException if the SHA-1 is unavailable for an unknown reason
     413             :    */
     414             :   public Optional<ObjectId> determineCommitId(Change change, PatchSet patchset, short side) {
     415          28 :     if (Side.fromShort(side) == Side.PARENT) {
     416           3 :       if (side < 0) {
     417           3 :         int parentNumber = Math.abs(side);
     418           3 :         return resolveParentCommit(change.getProject(), patchset, parentNumber);
     419             :       }
     420           3 :       return Optional.ofNullable(resolveAutoMergeCommit(change, patchset));
     421             :     }
     422          28 :     return Optional.of(patchset.commitId());
     423             :   }
     424             : 
     425             :   private Optional<ObjectId> resolveParentCommit(
     426             :       Project.NameKey project, PatchSet patchset, int parentNumber) {
     427           3 :     try (Repository repository = repoManager.openRepository(project)) {
     428           3 :       RevCommit commit = repository.parseCommit(patchset.commitId());
     429           3 :       if (commit.getParentCount() < parentNumber) {
     430           3 :         return Optional.empty();
     431             :       }
     432           3 :       return Optional.of(commit.getParent(parentNumber - 1));
     433           3 :     } catch (IOException e) {
     434           0 :       throw new StorageException(e);
     435             :     }
     436             :   }
     437             : 
     438             :   @Nullable
     439             :   private ObjectId resolveAutoMergeCommit(Change change, PatchSet patchset) {
     440             :     try {
     441             :       // TODO(ghareeb): Adjust after the auto-merge code was moved out of the diff caches. Also
     442             :       // unignore the test in PortedCommentsIT.
     443           3 :       Map<String, FileDiffOutput> modifiedFiles =
     444           3 :           diffOperations.listModifiedFilesAgainstParent(
     445           3 :               change.getProject(), patchset.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
     446           3 :       return modifiedFiles.isEmpty()
     447           0 :           ? null
     448           3 :           : modifiedFiles.values().iterator().next().oldCommitId();
     449           0 :     } catch (DiffNotAvailableException e) {
     450           0 :       throw new StorageException(e);
     451             :     }
     452             :   }
     453             : 
     454             :   /**
     455             :    * Get NoteDb draft refs for a change.
     456             :    *
     457             :    * <p>This is just a simple ref scan, so the results may potentially include refs for zombie draft
     458             :    * comments. A zombie draft is one which has been published but the write to delete the draft ref
     459             :    * from All-Users failed.
     460             :    *
     461             :    * @param changeId change ID.
     462             :    * @return raw refs from All-Users repo.
     463             :    */
     464             :   public Collection<Ref> getDraftRefs(Change.Id changeId) {
     465           5 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     466           5 :       return getDraftRefs(repo, changeId);
     467           0 :     } catch (IOException e) {
     468           0 :       throw new StorageException(e);
     469             :     }
     470             :   }
     471             : 
     472             :   /** returns all changes that contain draft comments of {@code accountId}. */
     473             :   public Collection<Change.Id> getChangesWithDrafts(Account.Id accountId) {
     474           7 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     475           7 :       return getChangesWithDrafts(repo, accountId);
     476           0 :     } catch (IOException e) {
     477           0 :       throw new StorageException(e);
     478             :     }
     479             :   }
     480             : 
     481             :   private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId) throws IOException {
     482           5 :     return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId));
     483             :   }
     484             : 
     485             :   private Collection<Change.Id> getChangesWithDrafts(Repository repo, Account.Id accountId)
     486             :       throws IOException {
     487           7 :     Set<Change.Id> changes = new HashSet<>();
     488           7 :     for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
     489           7 :       Integer accountIdFromRef = RefNames.parseRefSuffix(ref.getName());
     490           7 :       if (accountIdFromRef != null && accountIdFromRef == accountId.get()) {
     491           7 :         Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
     492           7 :         if (changeId == null) {
     493           0 :           continue;
     494             :         }
     495           7 :         changes.add(changeId);
     496             :       }
     497           7 :     }
     498           7 :     return changes;
     499             :   }
     500             : 
     501             :   private static <T extends Comment> List<T> sort(List<T> comments) {
     502         103 :     comments.sort(COMMENT_ORDER);
     503         103 :     return comments;
     504             :   }
     505             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750