Search in sources :

Example 51 with HumanComment

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);
}
Also used : HumanCommentResource(com.google.gerrit.server.change.HumanCommentResource) IdString(com.google.gerrit.extensions.restapi.IdString) ChangeNotes(com.google.gerrit.server.notedb.ChangeNotes) ResourceNotFoundException(com.google.gerrit.extensions.restapi.ResourceNotFoundException) HumanComment(com.google.gerrit.entities.HumanComment)

Example 52 with HumanComment

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();
                }
            }
        }
    }
}
Also used : PositionedEntity(com.google.gerrit.server.patch.GitPositionTransformer.PositionedEntity) Map(java.util.Map) ImmutableMap(com.google.common.collect.ImmutableMap) HumanComment(com.google.gerrit.entities.HumanComment)

Example 53 with HumanComment

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);
    }
}
Also used : TraceTimer(com.google.gerrit.server.logging.TraceContext.TraceTimer) HumanComment(com.google.gerrit.entities.HumanComment)

Example 54 with HumanComment

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;
}
Also used : ArrayList(java.util.ArrayList) HumanComment(com.google.gerrit.entities.HumanComment)

Example 55 with HumanComment

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;
}
Also used : Element(org.jsoup.nodes.Element) ArrayList(java.util.ArrayList) Document(org.jsoup.nodes.Document) HumanComment(com.google.gerrit.entities.HumanComment)

Aggregations

HumanComment (com.google.gerrit.entities.HumanComment)87 Test (org.junit.Test)53 Change (com.google.gerrit.entities.Change)39 PatchSet (com.google.gerrit.entities.PatchSet)39 ObjectId (org.eclipse.jgit.lib.ObjectId)39 CommentRange (com.google.gerrit.entities.CommentRange)25 Instant (java.time.Instant)20 ChangeNotes (com.google.gerrit.server.notedb.ChangeNotes)14 Comment (com.google.gerrit.entities.Comment)11 Project (com.google.gerrit.entities.Project)11 Map (java.util.Map)11 ArrayList (java.util.ArrayList)8 RevCommit (org.eclipse.jgit.revwalk.RevCommit)8 HashMap (java.util.HashMap)7 NoteMap (org.eclipse.jgit.notes.NoteMap)7 ImmutableMap (com.google.common.collect.ImmutableMap)6 TraceTimer (com.google.gerrit.server.logging.TraceContext.TraceTimer)6 FluentLogger (com.google.common.flogger.FluentLogger)5 Account (com.google.gerrit.entities.Account)5 BranchNameKey (com.google.gerrit.entities.BranchNameKey)5