// Copyright (C) 2014 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.server; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static java.util.stream.Collectors.toList; import com.google.common.collect.ComparisonChain; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Comment; import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.RefNames; import com.google.gerrit.entities.RobotComment; import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.GerritServerId; 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.ChangeContext; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; /** Utility functions to manipulate Comments. */ @Singleton public class CommentsUtil { public static final Ordering COMMENT_ORDER = new Ordering() { @Override public int compare(Comment c1, Comment c2) { return ComparisonChain.start() .compare(c1.key.filename, c2.key.filename) .compare(c1.key.patchSetId, c2.key.patchSetId) .compare(c1.side, c2.side) .compare(c1.lineNbr, c2.lineNbr) .compare(c1.writtenOn, c2.writtenOn) .result(); } }; public static final Ordering COMMENT_INFO_ORDER = new Ordering() { @Override public int compare(CommentInfo a, CommentInfo b) { return ComparisonChain.start() .compare(a.path, b.path, NULLS_FIRST) .compare(a.patchSet, b.patchSet, NULLS_FIRST) .compare(side(a), side(b)) .compare(a.line, b.line, NULLS_FIRST) .compare(a.inReplyTo, b.inReplyTo, NULLS_FIRST) .compare(a.message, b.message) .compare(a.id, b.id) .result(); } private int side(CommentInfo c) { return firstNonNull(c.side, Side.REVISION).ordinal(); } }; public static PatchSet.Id getCommentPsId(Change.Id changeId, Comment comment) { return PatchSet.id(changeId, comment.key.patchSetId); } public static String extractMessageId(@Nullable String tag) { if (tag == null || !tag.startsWith("mailMessageId=")) { return null; } return tag.substring("mailMessageId=".length()); } private static final Ordering> NULLS_FIRST = Ordering.natural().nullsFirst(); private final GitRepositoryManager repoManager; private final AllUsersName allUsers; private final String serverId; @Inject CommentsUtil( GitRepositoryManager repoManager, AllUsersName allUsers, @GerritServerId String serverId) { this.repoManager = repoManager; this.allUsers = allUsers; this.serverId = serverId; } public Comment newComment( ChangeContext ctx, String path, PatchSet.Id psId, short side, String message, @Nullable Boolean unresolved, @Nullable String parentUuid) throws UnprocessableEntityException { if (unresolved == null) { if (parentUuid == null) { // Default to false if comment is not descended from another. unresolved = false; } else { // Inherit unresolved value from inReplyTo comment if not specified. Comment.Key key = new Comment.Key(parentUuid, path, psId.get()); Optional parent = getPublished(ctx.getNotes(), key); if (!parent.isPresent()) { throw new UnprocessableEntityException("Invalid parentUuid supplied for comment"); } unresolved = parent.get().unresolved; } } Comment c = new Comment( new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()), ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId, unresolved); c.parentUuid = parentUuid; ctx.getUser().updateRealAccountId(c::setRealAuthor); return c; } public RobotComment newRobotComment( ChangeContext ctx, String path, PatchSet.Id psId, short side, String message, String robotId, String robotRunId) { RobotComment c = new RobotComment( new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()), ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId, robotId, robotRunId); ctx.getUser().updateRealAccountId(c::setRealAuthor); return c; } public Optional getPublished(ChangeNotes notes, Comment.Key key) { return publishedByChange(notes).stream().filter(c -> key.equals(c.key)).findFirst(); } public Optional getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) { return draftByChangeAuthor(notes, user.getAccountId()).stream() .filter(c -> key.equals(c.key)) .findFirst(); } public List publishedByChange(ChangeNotes notes) { notes.load(); return sort(Lists.newArrayList(notes.getComments().values())); } public List robotCommentsByChange(ChangeNotes notes) { notes.load(); return sort(Lists.newArrayList(notes.getRobotComments().values())); } public List draftByChange(ChangeNotes notes) { List comments = new ArrayList<>(); for (Ref ref : getDraftRefs(notes.getChangeId())) { Account.Id account = Account.Id.fromRefSuffix(ref.getName()); if (account != null) { comments.addAll(draftByChangeAuthor(notes, account)); } } return sort(comments); } public List byPatchSet(ChangeNotes notes, PatchSet.Id psId) { List comments = new ArrayList<>(); comments.addAll(publishedByPatchSet(notes, psId)); for (Ref ref : getDraftRefs(notes.getChangeId())) { Account.Id account = Account.Id.fromRefSuffix(ref.getName()); if (account != null) { comments.addAll(draftByPatchSetAuthor(psId, account, notes)); } } return sort(comments); } public List publishedByChangeFile(ChangeNotes notes, String file) { return commentsOnFile(notes.load().getComments().values(), file); } public List publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) { return removeCommentsOnAncestorOfCommitMessage( commentsOnPatchSet(notes.load().getComments().values(), psId)); } public List robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) { return commentsOnPatchSet(notes.load().getRobotComments().values(), psId); } /** * For the commit message the A side in a diff view is always empty when a comparison against an * ancestor is done, so there can't be any comments on this ancestor. However earlier we showed * the auto-merge commit message on side A when for a merge commit a comparison against the * auto-merge was done. From that time there may still be comments on the auto-merge commit * message and those we want to filter out. */ private List removeCommentsOnAncestorOfCommitMessage(List list) { return list.stream() .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename)) .collect(toList()); } public List draftByPatchSetAuthor( PatchSet.Id psId, Account.Id author, ChangeNotes notes) { return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId); } public List draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author) { return commentsOnFile(notes.load().getDraftComments(author).values(), file); } public List draftByChangeAuthor(ChangeNotes notes, Account.Id author) { List comments = new ArrayList<>(); comments.addAll(notes.getDraftComments(author).values()); return sort(comments); } public void putComments(ChangeUpdate update, Comment.Status status, Iterable comments) { for (Comment c : comments) { update.putComment(status, c); } } public void putRobotComments(ChangeUpdate update, Iterable comments) { for (RobotComment c : comments) { update.putRobotComment(c); } } public void deleteComments(ChangeUpdate update, Iterable comments) { for (Comment c : comments) { update.deleteComment(c); } } public void deleteCommentByRewritingHistory( ChangeUpdate update, Comment.Key commentKey, String newMessage) { update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage); } private static List commentsOnFile(Collection allComments, String file) { List result = new ArrayList<>(allComments.size()); for (Comment c : allComments) { String currentFilename = c.key.filename; if (currentFilename.equals(file)) { result.add(c); } } return sort(result); } private static List commentsOnPatchSet( Collection allComments, PatchSet.Id psId) { List result = new ArrayList<>(allComments.size()); for (T c : allComments) { if (c.key.patchSetId == psId.get()) { result.add(c); } } return sort(result); } public static void setCommentCommitId(Comment c, PatchListCache cache, Change change, PatchSet ps) throws PatchListNotAvailableException { 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) { if (Side.fromShort(c.side) == Side.PARENT) { if (c.side < 0) { c.setCommitId(cache.getOldId(change, ps, -c.side)); } else { c.setCommitId(cache.getOldId(change, ps, null)); } } else { c.setCommitId(ps.commitId()); } } } /** * Get NoteDb draft refs for a change. * *

Works if NoteDb is not enabled, but the results are not meaningful. * *

This is just a simple ref scan, so the results may potentially include refs for zombie draft * comments. A zombie draft is one which has been published but the write to delete the draft ref * from All-Users failed. * * @param changeId change ID. * @return raw refs from All-Users repo. */ public Collection getDraftRefs(Change.Id changeId) { try (Repository repo = repoManager.openRepository(allUsers)) { return getDraftRefs(repo, changeId); } catch (IOException e) { throw new StorageException(e); } } private Collection getDraftRefs(Repository repo, Change.Id changeId) throws IOException { return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId)); } private static List sort(List comments) { comments.sort(COMMENT_ORDER); return comments; } }