
This class represented comments in ReviewDb. Since ReviewDb is gone, this class is no longer needed. Only PatchLineComment.Status is still needed. Move this enum into the Comment class. Signed-off-by: Edwin Kempin <ekempin@google.com> Change-Id: Ibd14562de9622df9dd816350ea2e5d0f77dc0172
354 lines
12 KiB
Java
354 lines
12 KiB
Java
// 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> 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 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 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<Comment> 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<Comment> getPublished(ChangeNotes notes, Comment.Key key) {
|
|
return publishedByChange(notes).stream().filter(c -> key.equals(c.key)).findFirst();
|
|
}
|
|
|
|
public Optional<Comment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
|
|
return draftByChangeAuthor(notes, user.getAccountId()).stream()
|
|
.filter(c -> key.equals(c.key))
|
|
.findFirst();
|
|
}
|
|
|
|
public List<Comment> publishedByChange(ChangeNotes notes) {
|
|
notes.load();
|
|
return sort(Lists.newArrayList(notes.getComments().values()));
|
|
}
|
|
|
|
public List<RobotComment> robotCommentsByChange(ChangeNotes notes) {
|
|
notes.load();
|
|
return sort(Lists.newArrayList(notes.getRobotComments().values()));
|
|
}
|
|
|
|
public List<Comment> draftByChange(ChangeNotes notes) {
|
|
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(notes, account));
|
|
}
|
|
}
|
|
return sort(comments);
|
|
}
|
|
|
|
public List<Comment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
|
|
List<Comment> 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<Comment> publishedByChangeFile(ChangeNotes notes, String file) {
|
|
return commentsOnFile(notes.load().getComments().values(), file);
|
|
}
|
|
|
|
public List<Comment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
|
|
return removeCommentsOnAncestorOfCommitMessage(
|
|
commentsOnPatchSet(notes.load().getComments().values(), psId));
|
|
}
|
|
|
|
public List<RobotComment> 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<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(
|
|
PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
|
|
return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
|
|
}
|
|
|
|
public List<Comment> draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author) {
|
|
return commentsOnFile(notes.load().getDraftComments(author).values(), file);
|
|
}
|
|
|
|
public List<Comment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
|
|
List<Comment> comments = new ArrayList<>();
|
|
comments.addAll(notes.getDraftComments(author).values());
|
|
return sort(comments);
|
|
}
|
|
|
|
public void putComments(ChangeUpdate update, Comment.Status status, Iterable<Comment> comments) {
|
|
for (Comment c : comments) {
|
|
update.putComment(status, c);
|
|
}
|
|
}
|
|
|
|
public void putRobotComments(ChangeUpdate update, Iterable<RobotComment> comments) {
|
|
for (RobotComment c : comments) {
|
|
update.putRobotComment(c);
|
|
}
|
|
}
|
|
|
|
public void deleteComments(ChangeUpdate update, Iterable<Comment> 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<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 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.
|
|
*
|
|
* <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) {
|
|
try (Repository repo = repoManager.openRepository(allUsers)) {
|
|
return getDraftRefs(repo, changeId);
|
|
} catch (IOException e) {
|
|
throw new StorageException(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;
|
|
}
|
|
}
|