// Copyright (C) 2009 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; import com.google.common.base.Strings; import com.google.common.collect.Ordering; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.CommentRange; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.patch.PatchFile; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** 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 static interface Factory { public CommentSender create(NotifyHandling notify, Change change); } private final NotifyHandling notify; private List<PatchLineComment> inlineComments = Collections.emptyList(); @Inject public CommentSender(EmailArguments ea, @Assisted NotifyHandling notify, @Assisted Change c) { super(ea, c, "comment"); this.notify = notify; } public void setPatchLineComments(final List<PatchLineComment> plc) throws OrmException { inlineComments = plc; Set<String> paths = new HashSet<>(); for (PatchLineComment c : plc) { Patch.Key p = c.getKey().getParentKey(); if (!Patch.COMMIT_MSG.equals(p.getFileName())) { paths.add(p.getFileName()); } } changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths)); } @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); } } @Override public void formatChange() throws EmailException { appendText(velocifyFile("Comment.vm")); } @Override public void formatFooter() throws EmailException { appendText(velocifyFile("CommentFooter.vm")); } public boolean hasInlineComments() { return !inlineComments.isEmpty(); } public String getInlineComments() { return getInlineComments(1); } public String getInlineComments(int lines) { StringBuilder cmts = new StringBuilder(); final Repository repo = getRepository(); try { PatchList patchList = null; if (repo != null) { try { patchList = getPatchList(); } catch (PatchListNotAvailableException e) { patchList = null; } } Patch.Key currentFileKey = null; PatchFile currentFileData = null; for (final PatchLineComment c : inlineComments) { final Patch.Key pk = c.getKey().getParentKey(); if (!pk.equals(currentFileKey)) { String link = makeLink(pk); if (link != null) { cmts.append(link).append('\n'); } if (Patch.COMMIT_MSG.equals(pk.get())) { cmts.append("Commit Message:\n\n"); } else { cmts.append("File ").append(pk.get()).append(":\n\n"); } currentFileKey = pk; if (patchList != null) { try { currentFileData = new PatchFile(repo, patchList, pk.get()); } catch (IOException e) { log.warn(String.format( "Cannot load %s from %s in %s", pk.getFileName(), patchList.getNewId().name(), projectState.getProject().getName()), e); currentFileData = null; } } } if (currentFileData != null) { appendComment(cmts, lines, currentFileData, c); } cmts.append("\n\n"); } } finally { if (repo != null) { repo.close(); } } return cmts.toString(); } private void appendComment(StringBuilder out, int contextLines, PatchFile currentFileData, PatchLineComment comment) { short side = comment.getSide(); CommentRange range = comment.getRange(); if (range != null) { String prefix = String.format("Line %d: ", range.getStartLine()); for (int n = range.getStartLine(); n <= range.getEndLine(); n++) { out.append(n == range.getStartLine() ? prefix : Strings.padStart(": ", prefix.length(), ' ')); try { String s = currentFileData.getLine(side, n); if (n == range.getStartLine() && n == range.getEndLine()) { s = s.substring( Math.min(range.getStartCharacter(), s.length()), Math.min(range.getEndCharacter(), s.length())); } else if (n == range.getStartLine()) { s = s.substring(Math.min(range.getStartCharacter(), s.length())); } else if (n == range.getEndLine()) { s = s.substring(0, Math.min(range.getEndCharacter(), s.length())); } out.append(s); } catch (Throwable e) { // Don't quote the line if we can't safely convert it. } out.append('\n'); } appendQuotedParent(out, comment); out.append(comment.getMessage().trim()).append('\n'); } else { int lineNbr = comment.getLine(); int maxLines; try { maxLines = currentFileData.getLineCount(side); } catch (Throwable e) { maxLines = lineNbr; } final int startLine = Math.max(1, lineNbr - contextLines + 1); final 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.getMessage().trim()).append('\n'); for (int line = lineNbr + 1; line < stopLine; ++line) { appendFileLine(out, currentFileData, side, line); } } } private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) { cmts.append("Line " + line); try { final String lineStr = fileData.getLine(side, line); cmts.append(": "); cmts.append(lineStr); } catch (Throwable e) { // Don't quote the line if we can't safely convert it. } cmts.append("\n"); } private void appendQuotedParent(StringBuilder out, PatchLineComment child) { if (child.getParentUuid() != null) { PatchLineComment parent; try { parent = args.db.get().patchComments().get( new PatchLineComment.Key( child.getKey().getParentKey(), child.getParentUuid())); } catch (OrmException e) { parent = null; } if (parent != null) { String msg = parent.getMessage().trim(); if (msg.length() > 75) { msg = msg.substring(0, 75); } int lf = msg.indexOf('\n'); if (lf > 0) { msg = msg.substring(0, lf); } out.append("> ").append(msg).append('\n'); } } } // Makes a link back to the given patch set and file. private String makeLink(Patch.Key patch) { String url = getGerritUrl(); if (url == null) { return null; } PatchSet.Id ps = patch.getParentKey(); Change.Id c = ps.getParentKey(); return new StringBuilder() .append(url) .append("#/c/").append(c) .append('/').append(ps.get()) .append('/').append(KeyUtil.encode(patch.get())) .toString(); } private Repository getRepository() { try { return args.server.openRepository(projectState.getProject().getNameKey()); } catch (IOException e) { return null; } } }