diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java | 660 |
1 files changed, 0 insertions, 660 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java deleted file mode 100644 index b9fc5da917..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java +++ /dev/null @@ -1,660 +0,0 @@ -// Copyright (C) 2016 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.mail.send; - -import static java.util.stream.Collectors.toList; - -import com.google.common.base.Strings; -import com.google.common.collect.Ordering; -import com.google.gerrit.common.data.FilenameComparator; -import com.google.gerrit.common.errors.EmailException; -import com.google.gerrit.common.errors.NoSuchEntityException; -import com.google.gerrit.extensions.api.changes.NotifyHandling; -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.Project; -import com.google.gerrit.reviewdb.client.RobotComment; -import com.google.gerrit.server.CommentsUtil; -import com.google.gerrit.server.account.WatchConfig.NotifyType; -import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.mail.MailUtil; -import com.google.gerrit.server.mail.receive.Protocol; -import com.google.gerrit.server.patch.PatchFile; -import com.google.gerrit.server.patch.PatchList; -import com.google.gerrit.server.patch.PatchListNotAvailableException; -import com.google.gerrit.server.patch.PatchListObjectTooLargeException; -import com.google.gerrit.server.util.LabelVote; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; -import java.io.IOException; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Repository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Send comments, after the author of them hit used Publish Comments in the UI. */ -public class CommentSender extends ReplyToChangeSender { - private static final Logger log = LoggerFactory.getLogger(CommentSender.class); - - public interface Factory { - CommentSender create(Project.NameKey project, Change.Id id); - } - - private class FileCommentGroup { - public String filename; - public int patchSetId; - public PatchFile fileData; - public List<Comment> comments = new ArrayList<>(); - - /** @return a web link to the given patch set and file. */ - public String getLink() { - String url = getGerritUrl(); - if (url == null) { - return null; - } - - return new StringBuilder() - .append(url) - .append("#/c/") - .append(change.getId()) - .append('/') - .append(patchSetId) - .append('/') - .append(KeyUtil.encode(filename)) - .toString(); - } - - /** - * @return A title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]". - */ - public String getTitle() { - if (Patch.COMMIT_MSG.equals(filename)) { - return "Commit Message"; - } else if (Patch.MERGE_LIST.equals(filename)) { - return "Merge List"; - } else { - return "File " + filename; - } - } - } - - private List<Comment> inlineComments = Collections.emptyList(); - private String patchSetComment; - private List<LabelVote> labels = Collections.emptyList(); - private final CommentsUtil commentsUtil; - private final boolean incomingEmailEnabled; - private final String replyToAddress; - - @Inject - public CommentSender( - EmailArguments ea, - CommentsUtil commentsUtil, - @GerritServerConfig Config cfg, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "comment", newChangeData(ea, project, id)); - this.commentsUtil = commentsUtil; - this.incomingEmailEnabled = - cfg.getEnum("receiveemail", null, "protocol", Protocol.NONE).ordinal() - > Protocol.NONE.ordinal(); - this.replyToAddress = cfg.getString("sendemail", null, "replyToAddress"); - } - - public void setComments(List<Comment> comments) throws OrmException { - inlineComments = comments; - - Set<String> paths = new HashSet<>(); - for (Comment c : comments) { - if (!Patch.isMagic(c.key.filename)) { - paths.add(c.key.filename); - } - } - changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths)); - } - - public void setPatchSetComment(String comment) { - this.patchSetComment = comment; - } - - public void setLabels(List<LabelVote> labels) { - this.labels = labels; - } - - @Override - protected void init() throws EmailException { - super.init(); - - if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) { - ccAllApprovals(); - } - if (notify.compareTo(NotifyHandling.ALL) >= 0) { - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS, !change.isWorkInProgress() && !change.isPrivate()); - } - removeUsersThatIgnoredTheChange(); - - // Add header that enables identifying comments on parsed email. - // Grouping is currently done by timestamp. - setHeader("X-Gerrit-Comment-Date", timestamp); - - if (incomingEmailEnabled) { - if (replyToAddress == null) { - // Remove Reply-To and use outbound SMTP (default) instead. - removeHeader("Reply-To"); - } else { - setHeader("Reply-To", replyToAddress); - } - } - } - - @Override - public void formatChange() throws EmailException { - appendText(textTemplate("Comment")); - if (useHtml()) { - appendHtml(soyHtmlTemplate("CommentHtml")); - } - } - - @Override - public void formatFooter() throws EmailException { - appendText(textTemplate("CommentFooter")); - if (useHtml()) { - appendHtml(soyHtmlTemplate("CommentFooterHtml")); - } - } - - /** No longer used outside Velocity. Remove this method when VTL support is removed. */ - @Deprecated - public boolean hasInlineComments() { - return !inlineComments.isEmpty(); - } - - /** No longer used outside Velocity. Remove this method when VTL support is removed. */ - @Deprecated - public String getInlineComments() { - return getInlineComments(1); - } - - /** No longer used outside Velocity. Remove this method when VTL support is removed. */ - @Deprecated - public String getInlineComments(int lines) { - try (Repository repo = getRepository()) { - StringBuilder cmts = new StringBuilder(); - for (FileCommentGroup group : getGroupedInlineComments(repo)) { - String link = group.getLink(); - if (link != null) { - cmts.append(link).append('\n'); - } - cmts.append(group.getTitle()).append(":\n\n"); - for (Comment c : group.comments) { - appendComment(cmts, lines, group.fileData, c); - } - cmts.append("\n\n"); - } - return cmts.toString(); - } - } - - /** - * @return a list of FileCommentGroup objects representing the inline comments grouped by the - * file. - */ - private List<CommentSender.FileCommentGroup> getGroupedInlineComments(Repository repo) { - List<CommentSender.FileCommentGroup> groups = new ArrayList<>(); - // Get the patch list: - PatchList patchList = null; - if (repo != null) { - try { - patchList = getPatchList(); - } catch (PatchListObjectTooLargeException e) { - log.warn("Failed to get patch list: " + e.getMessage()); - } catch (PatchListNotAvailableException e) { - log.error("Failed to get patch list", e); - } - } - - // Loop over the comments and collect them into groups based on the file - // location of the comment. - FileCommentGroup currentGroup = null; - for (Comment c : inlineComments) { - // If it's a new group: - if (currentGroup == null - || !c.key.filename.equals(currentGroup.filename) - || c.key.patchSetId != currentGroup.patchSetId) { - currentGroup = new FileCommentGroup(); - currentGroup.filename = c.key.filename; - currentGroup.patchSetId = c.key.patchSetId; - groups.add(currentGroup); - if (patchList != null) { - try { - currentGroup.fileData = new PatchFile(repo, patchList, c.key.filename); - } catch (IOException e) { - log.warn( - "Cannot load {} from {} in {}", - c.key.filename, - patchList.getNewId().name(), - projectState.getName(), - e); - currentGroup.fileData = null; - } - } - } - - if (currentGroup.fileData != null) { - currentGroup.comments.add(c); - } - } - - Collections.sort(groups, Comparator.comparing(g -> g.filename, FilenameComparator.INSTANCE)); - return groups; - } - - /** No longer used except for Velocity. Remove this method when VTL support is removed. */ - @Deprecated - private void appendComment( - StringBuilder out, int contextLines, PatchFile currentFileData, Comment comment) { - if (comment instanceof RobotComment) { - RobotComment robotComment = (RobotComment) comment; - out.append("Robot Comment from ") - .append(robotComment.robotId) - .append(" (run ID ") - .append(robotComment.robotRunId) - .append("):\n"); - } - if (comment.range != null) { - appendRangedComment(out, currentFileData, comment); - } else { - appendLineComment(out, contextLines, currentFileData, comment); - } - } - - /** No longer used except for Velocity. Remove this method when VTL support is removed. */ - @Deprecated - private void appendRangedComment(StringBuilder out, PatchFile fileData, Comment comment) { - String prefix = getCommentLinePrefix(comment); - String emptyPrefix = Strings.padStart(": ", prefix.length(), ' '); - boolean firstLine = true; - for (String line : getLinesByRange(comment.range, fileData, comment.side)) { - out.append(firstLine ? prefix : emptyPrefix).append(line).append('\n'); - firstLine = false; - } - appendQuotedParent(out, comment); - out.append(comment.message.trim()).append('\n'); - } - - private String getCommentLinePrefix(Comment comment) { - int lineNbr = comment.range == null ? comment.lineNbr : comment.range.startLine; - StringBuilder sb = new StringBuilder(); - sb.append("PS").append(comment.key.patchSetId); - if (lineNbr != 0) { - sb.append(", Line ").append(lineNbr); - } - sb.append(": "); - return sb.toString(); - } - - /** - * @return the lines of file content in fileData that are encompassed by range on the given side. - */ - private List<String> getLinesByRange(Comment.Range range, PatchFile fileData, short side) { - List<String> lines = new ArrayList<>(); - - for (int n = range.startLine; n <= range.endLine; n++) { - String s = getLine(fileData, side, n); - if (n == range.startLine && n == range.endLine && range.startChar < range.endChar) { - s = s.substring(Math.min(range.startChar, s.length()), Math.min(range.endChar, s.length())); - } else if (n == range.startLine) { - s = s.substring(Math.min(range.startChar, s.length())); - } else if (n == range.endLine) { - s = s.substring(0, Math.min(range.endChar, s.length())); - } - lines.add(s); - } - return lines; - } - - /** No longer used except for Velocity. Remove this method when VTL support is removed. */ - @Deprecated - private void appendLineComment( - StringBuilder out, int contextLines, PatchFile currentFileData, Comment comment) { - short side = comment.side; - int lineNbr = comment.lineNbr; - - // Initialize maxLines to the known line number. - int maxLines = lineNbr; - - try { - maxLines = currentFileData.getLineCount(side); - } catch (IOException err) { - // The file could not be read, leave the max as is. - log.warn("Failed to read file {} on side {}", comment.key.filename, side, err); - } catch (NoSuchEntityException err) { - // The file could not be read, leave the max as is. - log.warn("Side {} of file {} didn't exist", side, comment.key.filename, err); - } - - int startLine = Math.max(1, lineNbr - contextLines + 1); - int stopLine = Math.min(maxLines, lineNbr + contextLines); - - for (int line = startLine; line <= lineNbr; ++line) { - appendFileLine(out, currentFileData, side, line); - } - appendQuotedParent(out, comment); - out.append(comment.message.trim()).append('\n'); - - for (int line = lineNbr + 1; line < stopLine; ++line) { - appendFileLine(out, currentFileData, side, line); - } - } - - /** No longer used except for Velocity. Remove this method when VTL support is removed. */ - @Deprecated - private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) { - String lineStr = getLine(fileData, side, line); - cmts.append("Line ").append(line).append(": ").append(lineStr).append("\n"); - } - - /** No longer used except for Velocity. Remove this method when VTL support is removed. */ - @Deprecated - private void appendQuotedParent(StringBuilder out, Comment child) { - Optional<Comment> parent = getParent(child); - if (parent.isPresent()) { - out.append("> ").append(getShortenedCommentMessage(parent.get())).append('\n'); - } - } - - /** - * Get the parent comment of a given comment. - * - * @param child the comment with a potential parent comment. - * @return an optional comment that will be present if the given comment has a parent, and is - * empty if it does not. - */ - private Optional<Comment> getParent(Comment child) { - if (child.parentUuid == null) { - return Optional.empty(); - } - - Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, child.key.patchSetId); - try { - return commentsUtil.getPublished(args.db.get(), changeData.notes(), key); - } catch (OrmException e) { - log.warn("Could not find the parent of this comment: {}", child.toString()); - return Optional.empty(); - } - } - - /** - * Retrieve the file lines referred to by a comment. - * - * @param comment The comment that refers to some file contents. The comment may be a line comment - * or a ranged comment. - * @param fileData The file on which the comment appears. - * @return file contents referred to by the comment. If the comment is a line comment, the result - * will be a list of one string. Otherwise it will be a list of one or more strings. - */ - private List<String> getLinesOfComment(Comment comment, PatchFile fileData) { - List<String> lines = new ArrayList<>(); - if (comment.lineNbr == 0) { - // file level comment has no line - return lines; - } - if (comment.range == null) { - lines.add(getLine(fileData, comment.side, comment.lineNbr)); - } else { - lines.addAll(getLinesByRange(comment.range, fileData, comment.side)); - } - return lines; - } - - /** - * @return a shortened version of the given comment's message. Will be shortened to 100 characters - * or the first line, or following the last period within the first 100 characters, whichever - * is shorter. If the message is shortened, an ellipsis is appended. - */ - protected static String getShortenedCommentMessage(String message) { - int threshold = 100; - String fullMessage = message.trim(); - String msg = fullMessage; - - if (msg.length() > threshold) { - msg = msg.substring(0, threshold); - } - - int lf = msg.indexOf('\n'); - int period = msg.lastIndexOf('.'); - - if (lf > 0) { - // Truncate if a line feed appears within the threshold. - msg = msg.substring(0, lf); - - } else if (period > 0) { - // Otherwise truncate if there is a period within the threshold. - msg = msg.substring(0, period + 1); - } - - // Append an ellipsis if the message has been truncated. - if (!msg.equals(fullMessage)) { - msg += " […]"; - } - - return msg; - } - - protected static String getShortenedCommentMessage(Comment comment) { - return getShortenedCommentMessage(comment.message); - } - - /** - * @return grouped inline comment data mapped to data structures that are suitable for passing - * into Soy. - */ - private List<Map<String, Object>> getCommentGroupsTemplateData(Repository repo) { - List<Map<String, Object>> commentGroups = new ArrayList<>(); - - for (CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) { - Map<String, Object> groupData = new HashMap<>(); - groupData.put("link", group.getLink()); - groupData.put("title", group.getTitle()); - groupData.put("patchSetId", group.patchSetId); - - List<Map<String, Object>> commentsList = new ArrayList<>(); - for (Comment comment : group.comments) { - Map<String, Object> commentData = new HashMap<>(); - commentData.put("lines", getLinesOfComment(comment, group.fileData)); - commentData.put("message", comment.message.trim()); - List<CommentFormatter.Block> blocks = CommentFormatter.parse(comment.message); - commentData.put("messageBlocks", commentBlocksToSoyData(blocks)); - - // Set the prefix. - String prefix = getCommentLinePrefix(comment); - commentData.put("linePrefix", prefix); - commentData.put("linePrefixEmpty", Strings.padStart(": ", prefix.length(), ' ')); - - // Set line numbers. - int startLine; - if (comment.range == null) { - startLine = comment.lineNbr; - } else { - startLine = comment.range.startLine; - commentData.put("endLine", comment.range.endLine); - } - commentData.put("startLine", startLine); - - // Set the comment link. - if (comment.lineNbr == 0) { - commentData.put("link", group.getLink()); - } else if (comment.side == 0) { - commentData.put("link", group.getLink() + "@a" + startLine); - } else { - commentData.put("link", group.getLink() + '@' + startLine); - } - - // Set robot comment data. - if (comment instanceof RobotComment) { - RobotComment robotComment = (RobotComment) comment; - commentData.put("isRobotComment", true); - commentData.put("robotId", robotComment.robotId); - commentData.put("robotRunId", robotComment.robotRunId); - commentData.put("robotUrl", robotComment.url); - } else { - commentData.put("isRobotComment", false); - } - - // If the comment has a quote, don't bother loading the parent message. - if (!hasQuote(blocks)) { - // Set parent comment info. - Optional<Comment> parent = getParent(comment); - if (parent.isPresent()) { - commentData.put("parentMessage", getShortenedCommentMessage(parent.get())); - } - } - - commentsList.add(commentData); - } - groupData.put("comments", commentsList); - - commentGroups.add(groupData); - } - return commentGroups; - } - - private List<Map<String, Object>> commentBlocksToSoyData(List<CommentFormatter.Block> blocks) { - return blocks.stream() - .map( - b -> { - Map<String, Object> map = new HashMap<>(); - switch (b.type) { - case PARAGRAPH: - map.put("type", "paragraph"); - map.put("text", b.text); - break; - case PRE_FORMATTED: - map.put("type", "pre"); - map.put("text", b.text); - break; - case QUOTE: - map.put("type", "quote"); - map.put("quotedBlocks", commentBlocksToSoyData(b.quotedBlocks)); - break; - case LIST: - map.put("type", "list"); - map.put("items", b.items); - break; - } - return map; - }) - .collect(toList()); - } - - private boolean hasQuote(List<CommentFormatter.Block> blocks) { - for (CommentFormatter.Block block : blocks) { - if (block.type == CommentFormatter.BlockType.QUOTE) { - return true; - } - } - return false; - } - - private Repository getRepository() { - try { - return args.server.openRepository(projectState.getNameKey()); - } catch (IOException e) { - return null; - } - } - - @Override - protected void setupSoyContext() { - super.setupSoyContext(); - boolean hasComments = false; - try (Repository repo = getRepository()) { - List<Map<String, Object>> files = getCommentGroupsTemplateData(repo); - soyContext.put("commentFiles", files); - hasComments = !files.isEmpty(); - } - - soyContext.put( - "patchSetCommentBlocks", commentBlocksToSoyData(CommentFormatter.parse(patchSetComment))); - soyContext.put("labels", getLabelVoteSoyData(labels)); - soyContext.put("commentCount", inlineComments.size()); - soyContext.put("commentTimestamp", getCommentTimestamp()); - soyContext.put( - "coverLetterBlocks", commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter()))); - - footers.add("Gerrit-Comment-Date: " + getCommentTimestamp()); - footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No")); - footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes")); - } - - private String getLine(PatchFile fileInfo, short side, int lineNbr) { - try { - return fileInfo.getLine(side, lineNbr); - } catch (IOException err) { - // Default to the empty string if the file cannot be safely read. - log.warn("Failed to read file on side {}", side, err); - return ""; - } catch (IndexOutOfBoundsException err) { - // Default to the empty string if the given line number does not appear - // in the file. - log.debug("Failed to get line number of file on side {}", side, err); - return ""; - } catch (NoSuchEntityException err) { - // Default to the empty string if the side cannot be found. - log.warn("Side {} of file didn't exist", side, err); - return ""; - } - } - - private List<Map<String, Object>> getLabelVoteSoyData(List<LabelVote> votes) { - List<Map<String, Object>> result = new ArrayList<>(); - for (LabelVote vote : votes) { - Map<String, Object> data = new HashMap<>(); - data.put("label", vote.label()); - - // Soy needs the short to be cast as an int for it to get converted to the - // correct tamplate type. - data.put("value", (int) vote.value()); - result.add(data); - } - return result; - } - - private String getCommentTimestamp() { - // Grouping is currently done by timestamp. - return MailUtil.rfcDateformatter.format( - ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC"))); - } - - @Override - protected boolean supportsHtml() { - return true; - } -} |