use of com.google.gerrit.entities.HumanComment in project gerrit by GerritCodeReview.
the class Comments method parse.
@Override
public HumanCommentResource parse(RevisionResource rev, IdString id) throws ResourceNotFoundException {
String uuid = id.get();
ChangeNotes notes = rev.getNotes();
for (HumanComment c : commentsUtil.publishedByPatchSet(notes, rev.getPatchSet().id())) {
if (uuid.equals(c.key.uuid)) {
return new HumanCommentResource(rev, c);
}
}
throw new ResourceNotFoundException(id);
}
use of com.google.gerrit.entities.HumanComment in project gerrit by GerritCodeReview.
the class CommentPorter method collectMetrics.
/**
* Collect metrics from the original and ported comments.
*
* @param portMap map of the ported comments. The keys contain a {@link PositionedEntity} of the
* original comment, and the values contain the transformed comments.
*/
private void collectMetrics(ImmutableMap<PositionedEntity<HumanComment>, HumanComment> portMap) {
for (Map.Entry<PositionedEntity<HumanComment>, HumanComment> entry : portMap.entrySet()) {
HumanComment original = entry.getKey().getEntity();
HumanComment transformed = entry.getValue();
if (!Patch.isMagic(original.key.filename)) {
if (Patch.PATCHSET_LEVEL.equals(transformed.key.filename)) {
metrics.portedAsPatchsetLevel.increment();
} else if (extractLineRange(original).isPresent()) {
if (extractLineRange(transformed).isPresent()) {
metrics.portedAsRangeComments.increment();
} else {
// line range was present in the original comment, but the ported comment is a file
// level comment.
metrics.portedAsFileLevel.increment();
}
}
}
}
}
use of com.google.gerrit.entities.HumanComment in project gerrit by GerritCodeReview.
the class CommentPorter method portComments.
/**
* Ports the given comments to the target patchset.
*
* <p>Not all given comments are ported. Only those fulfilling some criteria (e.g. before target
* patchset) are considered eligible for porting.
*
* <p>The returned comments represent the ported version. They don't bear any indication to which
* patchset they were ported. This is intentional as the target patchset should be obvious from
* the API or the used REST resources. The returned comments still have the patchset field filled.
* It contains the reference to the patchset on which the comment was originally left. That
* patchset number can vary among the returned comments as all comments before the target patchset
* are potentially eligible for porting.
*
* <p>The number of returned comments can be smaller (-> only eligible ones are ported!) or larger
* compared to the provided comments. The latter happens when files appear as copied in the target
* patchset. In such a situation, the same comment UUID will occur more than once in the returned
* comments.
*
* @param changeNotes the {@link ChangeNotes} of the change to which the comments belong
* @param targetPatchset the patchset to which the comments should be ported
* @param comments the original comments
* @param filters additional filters to apply to the comments before porting. Only the remaining
* comments will be ported.
* @return the ported comments, in no particular order
*/
public ImmutableList<HumanComment> portComments(ChangeNotes changeNotes, PatchSet targetPatchset, List<HumanComment> comments, List<HumanCommentFilter> filters) {
try (TraceTimer ignored = TraceContext.newTimer("Porting comments", Metadata.builder().patchSetId(targetPatchset.number()).build())) {
ImmutableList<HumanCommentFilter> allFilters = addDefaultFilters(filters, targetPatchset);
ImmutableList<HumanComment> relevantComments = filter(comments, allFilters);
return port(changeNotes, targetPatchset, relevantComments);
}
}
use of com.google.gerrit.entities.HumanComment in project gerrit by GerritCodeReview.
the class TextParser method parse.
/**
* Parses comments from plaintext email.
*
* @param email the message as received from the email service
* @param comments list of {@link HumanComment}s previously persisted on the change that caused
* the original notification email to be sent out. Ordering must be the same as in the
* outbound email
* @param changeUrl canonical change url that points to the change on this Gerrit instance.
* Example: https://go-review.googlesource.com/#/c/91570
* @return list of MailComments parsed from the plaintext part of the email
*/
public static List<MailComment> parse(MailMessage email, Collection<HumanComment> comments, String changeUrl) {
String body = email.textContent();
// Replace CR-LF by \n
body = body.replace("\r\n", "\n");
List<MailComment> parsedComments = new ArrayList<>();
// Some email clients (like GMail) use >> for enquoting text when there are
// inline comments that the users typed. These will then be enquoted by a
// single >. We sanitize this by unifying it into >. Inline comments typed
// by the user will not be enquoted.
//
// Example:
// Some comment
// >> Quoted Text
// >> Quoted Text
// > A comment typed in the email directly
String singleQuotePattern = "\n> ";
String doubleQuotePattern = "\n>> ";
if (countOccurrences(body, doubleQuotePattern) > countOccurrences(body, singleQuotePattern)) {
body = body.replace(doubleQuotePattern, singleQuotePattern);
}
PeekingIterator<HumanComment> iter = Iterators.peekingIterator(comments.iterator());
MailComment currentComment = null;
String lastEncounteredFileName = null;
HumanComment lastEncounteredComment = null;
for (String line : Splitter.on('\n').split(body)) {
if (line.equals(">")) {
// Skip empty lines
continue;
}
if (line.startsWith("> ")) {
line = line.substring("> ".length()).trim();
// add previous comment to list if applicable
if (currentComment != null) {
if (currentComment.type == MailComment.CommentType.PATCHSET_LEVEL) {
currentComment.message = ParserUtil.trimQuotation(currentComment.message);
}
if (!Strings.isNullOrEmpty(currentComment.message)) {
ParserUtil.appendOrAddNewComment(currentComment, parsedComments);
}
currentComment = null;
}
if (!iter.hasNext()) {
continue;
}
HumanComment perspectiveComment = iter.peek();
if (line.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) {
if (lastEncounteredFileName == null || !lastEncounteredFileName.equals(perspectiveComment.key.filename)) {
// This is the annotation of a file
lastEncounteredFileName = perspectiveComment.key.filename;
lastEncounteredComment = null;
} else if (perspectiveComment.lineNbr == 0) {
// This was originally a file-level comment
lastEncounteredComment = perspectiveComment;
iter.next();
}
} else if (ParserUtil.isCommentUrl(line, changeUrl, perspectiveComment)) {
lastEncounteredComment = perspectiveComment;
iter.next();
}
} else {
// create a new comment.
if (currentComment == null) {
// Start new comment
currentComment = new MailComment();
currentComment.message = line;
if (lastEncounteredComment == null) {
if (lastEncounteredFileName == null) {
// Change message
currentComment.type = MailComment.CommentType.PATCHSET_LEVEL;
} else {
// File comment not sent in reply to another comment
currentComment.type = MailComment.CommentType.FILE_COMMENT;
currentComment.fileName = lastEncounteredFileName;
}
} else {
// Comment sent in reply to another comment
currentComment.inReplyTo = lastEncounteredComment;
currentComment.type = MailComment.CommentType.INLINE_COMMENT;
}
} else {
// Attach to previous comment
currentComment.message += "\n" + line;
}
}
}
return parsedComments;
}
use of com.google.gerrit.entities.HumanComment in project gerrit by GerritCodeReview.
the class HtmlParser method parse.
/**
* Parses comments from html email.
*
* <p>This parser goes though all html elements in the email and checks for matching patterns. It
* keeps track of the last file and comments it encountered to know in which context a parsed
* comment belongs. It uses the href attributes of <a> tags to identify comments sent out by
* Gerrit as these are generally more reliable then the text captions.
*
* @param email the message as received from the email service
* @param comments a specific set of comments as sent out in the original notification email.
* Comments are expected to be in the same order as they were sent out to in the email.
* @param changeUrl canonical change URL that points to the change on this Gerrit instance.
* Example: https://go-review.googlesource.com/#/c/91570
* @return list of MailComments parsed from the html part of the email
*/
public static List<MailComment> parse(MailMessage email, Collection<HumanComment> comments, String changeUrl) {
// TODO(hiesel) Add support for Gmail Mobile
// TODO(hiesel) Add tests for other popular email clients
// This parser goes though all html elements in the email and checks for
// matching patterns. It keeps track of the last file and comments it
// encountered to know in which context a parsed comment belongs.
// It uses the href attributes of <a> tags to identify comments sent out by
// Gerrit as these are generally more reliable then the text captions.
List<MailComment> parsedComments = new ArrayList<>();
Document d = Jsoup.parse(email.htmlContent());
PeekingIterator<HumanComment> iter = Iterators.peekingIterator(comments.iterator());
String lastEncounteredFileName = null;
HumanComment lastEncounteredComment = null;
for (Element e : d.body().getAllElements()) {
String elementName = e.tagName();
boolean isInBlockQuote = e.parents().stream().anyMatch(p -> p.tagName().equals("blockquote") || MAIL_PROVIDER_EXTRAS.contains(p.className()));
if (elementName.equals("a")) {
String href = e.attr("href");
// this <a> tag
if (!iter.hasNext()) {
continue;
}
HumanComment perspectiveComment = iter.peek();
if (href.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) {
if (lastEncounteredFileName == null || !lastEncounteredFileName.equals(perspectiveComment.key.filename)) {
// Not a file-level comment, but users could have typed a comment
// right after this file annotation to create a new file-level
// comment. If this file has a file-level comment, we have already
// set lastEncounteredComment to that file-level comment when we
// encountered the file link and should not reset it now.
lastEncounteredFileName = perspectiveComment.key.filename;
lastEncounteredComment = null;
} else if (perspectiveComment.lineNbr == 0) {
// This was originally a file-level comment
lastEncounteredComment = perspectiveComment;
iter.next();
}
continue;
} else if (ParserUtil.isCommentUrl(href, changeUrl, perspectiveComment)) {
// This is a regular inline comment
lastEncounteredComment = perspectiveComment;
iter.next();
continue;
}
}
if (isInBlockQuote) {
// There is no user-input in quoted text
continue;
}
if (!ALLOWED_HTML_TAGS.contains(elementName)) {
// We only accept a set of allowed tags that can contain user input
continue;
}
if (elementName.equals("a") && e.attr("href").startsWith("mailto:")) {
// (User<user@gmail.com> wrote: ...)
continue;
}
// This is a comment typed by the user
// Replace non-breaking spaces and trim string
String content = e.ownText().replace('\u00a0', ' ').trim();
boolean isLink = elementName.equals("a");
if (!Strings.isNullOrEmpty(content)) {
if (lastEncounteredComment == null && lastEncounteredFileName == null) {
// Remove quotation line, email signature and
// "Sent from my xyz device"
content = ParserUtil.trimQuotation(content);
// TODO(hiesel) Add more sanitizer
if (!Strings.isNullOrEmpty(content)) {
ParserUtil.appendOrAddNewComment(new MailComment(content, null, null, MailComment.CommentType.PATCHSET_LEVEL, isLink), parsedComments);
}
} else if (lastEncounteredComment == null) {
ParserUtil.appendOrAddNewComment(new MailComment(content, lastEncounteredFileName, null, MailComment.CommentType.FILE_COMMENT, isLink), parsedComments);
} else {
ParserUtil.appendOrAddNewComment(new MailComment(content, null, lastEncounteredComment, MailComment.CommentType.INLINE_COMMENT, isLink), parsedComments);
}
}
}
return parsedComments;
}
Aggregations