summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/CommentsUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/CommentsUtil.java')
-rw-r--r--java/com/google/gerrit/server/CommentsUtil.java518
1 files changed, 518 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
new file mode 100644
index 0000000000..d72d73afb6
--- /dev/null
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -0,0 +1,518 @@
+// 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 com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
+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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+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.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.update.BatchUpdateReviewDb;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+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.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * Utility functions to manipulate Comments.
+ *
+ * <p>These methods either query for and update Comments in the NoteDb or ReviewDb, depending on the
+ * state of the NotesMigration.
+ */
+@Singleton
+public class CommentsUtil {
+ public static final Ordering<Comment> COMMENT_ORDER =
+ new Ordering<Comment>() {
+ @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<CommentInfo> COMMENT_INFO_ORDER =
+ new Ordering<CommentInfo>() {
+ @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 new 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<Comparable<?>> NULLS_FIRST = Ordering.natural().nullsFirst();
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+ private final NotesMigration migration;
+ private final String serverId;
+
+ @Inject
+ CommentsUtil(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ NotesMigration migration,
+ @GerritServerId String serverId) {
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ this.migration = migration;
+ this.serverId = serverId;
+ }
+
+ public Comment newComment(
+ ChangeContext ctx,
+ String path,
+ PatchSet.Id psId,
+ short side,
+ String message,
+ @Nullable Boolean unresolved,
+ @Nullable String parentUuid)
+ throws OrmException, 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.patchSetId);
+ Optional<Comment> parent = getPublished(ctx.getDb(), 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<Comment> getPublished(ReviewDb db, ChangeNotes notes, Comment.Key key)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return getReviewDb(db, notes, key);
+ }
+ return publishedByChange(db, notes).stream().filter(c -> key.equals(c.key)).findFirst();
+ }
+
+ public Optional<Comment> getDraft(
+ ReviewDb db, ChangeNotes notes, IdentifiedUser user, Comment.Key key) throws OrmException {
+ if (!migration.readChanges()) {
+ Optional<Comment> c = getReviewDb(db, notes, key);
+ if (c.isPresent() && !c.get().author.getId().equals(user.getAccountId())) {
+ throw new OrmException(
+ String.format(
+ "Expected draft %s to belong to account %s, but it belongs to %s",
+ key, user.getAccountId(), c.get().author.getId()));
+ }
+ return c;
+ }
+ return draftByChangeAuthor(db, notes, user.getAccountId()).stream()
+ .filter(c -> key.equals(c.key))
+ .findFirst();
+ }
+
+ private Optional<Comment> getReviewDb(ReviewDb db, ChangeNotes notes, Comment.Key key)
+ throws OrmException {
+ return Optional.ofNullable(
+ db.patchComments().get(PatchLineComment.Key.from(notes.getChangeId(), key)))
+ .map(plc -> plc.asComment(serverId));
+ }
+
+ public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), PUBLISHED));
+ }
+
+ notes.load();
+ return sort(Lists.newArrayList(notes.getComments().values()));
+ }
+
+ public List<RobotComment> robotCommentsByChange(ChangeNotes notes) throws OrmException {
+ if (!migration.readChanges()) {
+ return ImmutableList.of();
+ }
+
+ notes.load();
+ return sort(Lists.newArrayList(notes.getRobotComments().values()));
+ }
+
+ public List<Comment> draftByChange(ReviewDb db, ChangeNotes notes) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
+ }
+
+ List<Comment> comments = new ArrayList<>();
+ for (Ref ref : getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ comments.addAll(draftByChangeAuthor(db, notes, account));
+ }
+ }
+ return sort(comments);
+ }
+
+ private List<Comment> byCommentStatus(
+ ResultSet<PatchLineComment> comments, PatchLineComment.Status status) {
+ return toComments(
+ serverId, Lists.newArrayList(Iterables.filter(comments, c -> c.getStatus() == status)));
+ }
+
+ public List<Comment> byPatchSet(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(toComments(serverId, db.patchComments().byPatchSet(psId).toList()));
+ }
+ List<Comment> comments = new ArrayList<>();
+ comments.addAll(publishedByPatchSet(db, notes, psId));
+
+ for (Ref ref : getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
+ }
+ }
+ return sort(comments);
+ }
+
+ public List<Comment> publishedByChangeFile(
+ ReviewDb db, ChangeNotes notes, Change.Id changeId, String file) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(
+ toComments(serverId, db.patchComments().publishedByChangeFile(changeId, file).toList()));
+ }
+ return commentsOnFile(notes.load().getComments().values(), file);
+ }
+
+ public List<Comment> publishedByPatchSet(ReviewDb db, ChangeNotes notes, PatchSet.Id psId)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return removeCommentsOnAncestorOfCommitMessage(
+ sort(toComments(serverId, db.patchComments().publishedByPatchSet(psId).toList())));
+ }
+ return removeCommentsOnAncestorOfCommitMessage(
+ commentsOnPatchSet(notes.load().getComments().values(), psId));
+ }
+
+ public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return ImmutableList.of();
+ }
+ 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<Comment> removeCommentsOnAncestorOfCommitMessage(List<Comment> list) {
+ return list.stream()
+ .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename))
+ .collect(toList());
+ }
+
+ public List<Comment> draftByPatchSetAuthor(
+ ReviewDb db, PatchSet.Id psId, Account.Id author, ChangeNotes notes) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(
+ toComments(serverId, db.patchComments().draftByPatchSetAuthor(psId, author).toList()));
+ }
+ return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
+ }
+
+ public List<Comment> draftByChangeFileAuthor(
+ ReviewDb db, ChangeNotes notes, String file, Account.Id author) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(
+ toComments(
+ serverId,
+ db.patchComments()
+ .draftByChangeFileAuthor(notes.getChangeId(), file, author)
+ .toList()));
+ }
+ return commentsOnFile(notes.load().getDraftComments(author).values(), file);
+ }
+
+ public List<Comment> draftByChangeAuthor(ReviewDb db, ChangeNotes notes, Account.Id author)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return Streams.stream(db.patchComments().draftByAuthor(author))
+ .filter(c -> c.getPatchSetId().getParentKey().equals(notes.getChangeId()))
+ .map(plc -> plc.asComment(serverId))
+ .sorted(COMMENT_ORDER)
+ .collect(toList());
+ }
+ List<Comment> comments = new ArrayList<>();
+ comments.addAll(notes.getDraftComments(author).values());
+ return sort(comments);
+ }
+
+ public void putComments(
+ ReviewDb db, ChangeUpdate update, PatchLineComment.Status status, Iterable<Comment> comments)
+ throws OrmException {
+ for (Comment c : comments) {
+ update.putComment(status, c);
+ }
+ db.patchComments().upsert(toPatchLineComments(update.getId(), status, comments));
+ }
+
+ public void putRobotComments(ChangeUpdate update, Iterable<RobotComment> comments) {
+ for (RobotComment c : comments) {
+ update.putRobotComment(c);
+ }
+ }
+
+ public void deleteComments(ReviewDb db, ChangeUpdate update, Iterable<Comment> comments)
+ throws OrmException {
+ for (Comment c : comments) {
+ update.deleteComment(c);
+ }
+ db.patchComments()
+ .delete(toPatchLineComments(update.getId(), PatchLineComment.Status.DRAFT, comments));
+ }
+
+ public void deleteCommentByRewritingHistory(
+ ReviewDb db, ChangeUpdate update, Comment.Key commentKey, PatchSet.Id psId, String newMessage)
+ throws OrmException {
+ if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
+ PatchLineComment.Key key =
+ new PatchLineComment.Key(new Patch.Key(psId, commentKey.filename), commentKey.uuid);
+
+ if (db instanceof BatchUpdateReviewDb) {
+ db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+ }
+ db = ReviewDbUtil.unwrapDb(db);
+
+ PatchLineComment patchLineComment = db.patchComments().get(key);
+
+ if (!patchLineComment.getStatus().equals(PUBLISHED)) {
+ throw new OrmException(String.format("comment %s is not published", key));
+ }
+
+ patchLineComment.setMessage(newMessage);
+ db.patchComments().upsert(Collections.singleton(patchLineComment));
+ }
+
+ update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
+ }
+
+ public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ for (Ref ref : getDraftRefs(repo, changeId)) {
+ bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
+ }
+ bru.setRefLogMessage("Delete drafts from NoteDb", false);
+ bru.execute(rw, NullProgressMonitor.INSTANCE);
+ for (ReceiveCommand cmd : bru.getCommands()) {
+ if (cmd.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException(
+ String.format(
+ "Failed to delete draft comment ref %s at %s: %s (%s)",
+ cmd.getRefName(), cmd.getOldId(), cmd.getResult(), cmd.getMessage()));
+ }
+ }
+ }
+ }
+
+ private static List<Comment> commentsOnFile(Collection<Comment> allComments, String file) {
+ List<Comment> 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 <T extends Comment> List<T> commentsOnPatchSet(
+ Collection<T> allComments, PatchSet.Id psId) {
+ List<T> result = new ArrayList<>(allComments.size());
+ for (T c : allComments) {
+ if (c.key.patchSetId == psId.get()) {
+ result.add(c);
+ }
+ }
+ return sort(result);
+ }
+
+ public static void setCommentRevId(Comment c, PatchListCache cache, Change change, PatchSet ps)
+ throws PatchListNotAvailableException {
+ checkArgument(
+ c.key.patchSetId == ps.getId().get(),
+ "cannot set RevId for patch set %s on comment %s",
+ ps.getId(),
+ c);
+ if (c.revId == null) {
+ if (Side.fromShort(c.side) == Side.PARENT) {
+ if (c.side < 0) {
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, -c.side));
+ } else {
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, null));
+ }
+ } else {
+ c.revId = ps.getRevision().get();
+ }
+ }
+ }
+
+ /**
+ * Get NoteDb draft refs for a change.
+ *
+ * <p>Works if NoteDb is not enabled, but the results are not meaningful.
+ *
+ * <p>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<Ref> getDraftRefs(Change.Id changeId) throws OrmException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getDraftRefs(repo, changeId);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId) throws IOException {
+ return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId));
+ }
+
+ private static <T extends Comment> List<T> sort(List<T> comments) {
+ comments.sort(COMMENT_ORDER);
+ return comments;
+ }
+
+ public static Iterable<PatchLineComment> toPatchLineComments(
+ Change.Id changeId, PatchLineComment.Status status, Iterable<Comment> comments) {
+ return FluentIterable.from(comments).transform(c -> PatchLineComment.from(changeId, status, c));
+ }
+
+ public static List<Comment> toComments(
+ final String serverId, Iterable<PatchLineComment> comments) {
+ return COMMENT_ORDER.sortedCopy(
+ FluentIterable.from(comments).transform(plc -> plc.asComment(serverId)));
+ }
+}