blob: b587791f37016339e650cdff744ec02743c17999 [file] [log] [blame]
// 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.Optional;
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.PatchLineCommentsUtil;
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();
private final PatchLineCommentsUtil plcUtil;
@Inject
public CommentSender(EmailArguments ea,
@Assisted NotifyHandling notify,
@Assisted Change c,
PatchLineCommentsUtil plcUtil) {
super(ea, c, "comment");
this.notify = notify;
this.plcUtil = plcUtil;
}
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) {
log.error("Failed to get patch list", e);
}
}
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) {
Optional<PatchLineComment> parent;
PatchLineComment.Key key = new PatchLineComment.Key(
child.getKey().getParentKey(),
child.getParentUuid());
try {
parent = plcUtil.get(args.db.get(), changeData.notes(), key);
} catch (OrmException e) {
log.warn("Could not find the parent of this comment: "
+ child.toString());
parent = Optional.absent();
}
if (parent.isPresent()) {
String msg = parent.get().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;
}
}
}