Skip to content

Commit

Permalink
Handle PR reviews. (#134)
Browse files Browse the repository at this point in the history
The main comment on a review wasn't being handled, and this change interleaves it with the PR comments. GitHub treats these as different entities.

I'm hiding reviews with no top-level comment; these didn't seem worth including empty comments.

Links are being added to PR comments to make them easier to find. This means indenting the content, so now all comments (PR or review thread) are consistently indented, removing a parameter. Also, I'm adjusting the formatting of review threads to match.

snippets for comparison:
```
#83 (comment)
  googlebot: All (the pull request submitter and all commit authors) CLAs are...

#83 (review)
  chandlerc: Overall, I think this is a pretty significant improved direction...
```

and from a review thread:
```
https://github.com/carbon-language/carbon-lang/pull/83/files/9863da8#diff-8f28081abb21e65fba5a0b0b29404c78R16
  - line 16; unresolved
  - diff: https://github.com/carbon-language/carbon-lang/pull/83/files/9863da8..HEAD#diff-8f28081abb21e65fba5a0b0b29404c78L16
  josh11b: ```suggestion¶ - Block comments begin with a line starting with `/...
  jonmeow: "begin with a line starting with" phrasing is a little confusing t...
  josh11b: As I understand @zygoloid 's lexing proposal, there may be additio...
  jonmeow: Rewritten with example
```
  • Loading branch information
jonmeow authored Jul 31, 2020
1 parent 388b2de commit e208a98
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 98 deletions.
164 changes: 107 additions & 57 deletions src/scripts/pr_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
author {
login
}
createdAt
title
%(comments)s
%(reviews)s
%(review_threads)s
}
}
Expand All @@ -47,6 +49,23 @@
}
"""

_QUERY_REVIEWS = """
reviews(first: 100%s) {
nodes {
author {
login
}
body
createdAt
url
}
pageInfo {
endCursor
hasNextPage
}
}
"""

_QUERY_REVIEW_THREADS = """
reviewThreads(first: 100%s) {
nodes {
Expand Down Expand Up @@ -98,8 +117,8 @@ def from_raw_comment(raw_comment):
)

@staticmethod
def _rewrap(content, indent):
"""Rewraps a comment to fit in 80 columns with an optional indent."""
def _rewrap(content):
"""Rewraps a comment to fit in 80 columns with an indent."""
lines = []
for line in content.split("\n"):
lines.extend(
Expand All @@ -108,31 +127,49 @@ def _rewrap(content, indent):
for x in textwrap.wrap(
line,
width=80,
initial_indent=" " * indent,
subsequent_indent=" " * indent,
initial_indent=" " * 4,
subsequent_indent=" " * 4,
)
]
)
return "\n".join(lines)

def format(self, long, indent):
def format(self, long):
"""Formats the comment."""
if long:
return "%s%s at %s:\n%s" % (
" " * indent,
" " * 2,
self.author,
self.timestamp.strftime("%Y-%m-%d %H:%M"),
self._rewrap(self.body, indent + 2),
self._rewrap(self.body),
)
else:
# Compact newlines down into pilcrows, leaving a space after.
body = self.body.replace("\r", "").replace("\n", "¶ ")
while "¶ ¶" in body:
body = body.replace("¶ ¶", "¶¶")
line = "%s%s: %s" % (" " * indent, self.author, body)
line = "%s%s: %s" % (" " * 2, self.author, body)
return line if len(line) <= 80 else line[:77] + "..."


class _PRComment(_Comment):
"""A comment on the top-level PR."""

def __init__(self, raw_comment):
super().__init__(
raw_comment["author"]["login"],
raw_comment["createdAt"],
raw_comment["body"],
)
self.url = raw_comment["url"]

def __lt__(self, other):
return self.timestamp < other.timestamp

def format(self, long):
return "%s\n%s" % (self.url, super().format(long))


class _Thread(object):
"""A review thread on a line of code."""

Expand All @@ -144,36 +181,29 @@ def __init__(self, parsed_args, thread):
self.line = first_comment["originalPosition"]
self.path = first_comment["path"]

# Link to the comment in the commit if possible; GitHub features work
# better there than in the conversation view. The diff_url allows
# viewing changes since the comment, although the comment won't be
# visible there.
if "originalCommit" in first_comment:
template = (
"https://github.com/carbon-language/%(repo)s/pull/%(pr_num)s/"
"files/%(oid)s%(head)s#diff-%(path_md5)s%(line_side)s%(line)s"
)
# GitHub uses an md5 of the file's path for the link.
path_md5 = hashlib.md5()
path_md5.update(bytearray(self.path, "utf-8"))
format_dict = {
"head": "",
"line_side": "R",
"line": self.line,
"oid": first_comment["originalCommit"]["abbreviatedOid"],
"path_md5": path_md5.hexdigest(),
"pr_num": parsed_args.pr_num,
"repo": parsed_args.repo,
}
self.comment_url = template % format_dict
format_dict["head"] = "..HEAD"
format_dict["line_side"] = "L"
self.diff_url = template % format_dict
self.conversation_url = None
else:
self.conversation_url = first_comment["url"]
self.comment_url = None
self.diff_url = None
# Link to the comment in the commit; GitHub features work better there
# than in the conversation view. The diff_url allows viewing changes
# since the comment, although the comment won't be visible there.
template = (
"https://github.com/carbon-language/%(repo)s/pull/%(pr_num)s/"
"files/%(oid)s%(head)s#diff-%(path_md5)s%(line_side)s%(line)s"
)
# GitHub uses an md5 of the file's path for the link.
path_md5 = hashlib.md5()
path_md5.update(bytearray(self.path, "utf-8"))
format_dict = {
"head": "",
"line_side": "R",
"line": self.line,
"oid": first_comment["originalCommit"]["abbreviatedOid"],
"path_md5": path_md5.hexdigest(),
"pr_num": parsed_args.pr_num,
"repo": parsed_args.repo,
}
self.url = template % format_dict
format_dict["head"] = "..HEAD"
format_dict["line_side"] = "L"
self.diff_url = template % format_dict

self.comments = [
_Comment.from_raw_comment(comment)
Expand All @@ -198,16 +228,17 @@ def format(self, long):
"""Formats the review thread with comments."""
lines = []
lines.append(
"line %d; %s"
% (self.line, ("resolved" if self.is_resolved else "unresolved"),)
"%s\n - line %d; %s"
% (
self.url,
self.line,
("resolved" if self.is_resolved else "unresolved"),
)
)
if self.diff_url:
lines.append(" COMMENT: %s" % self.comment_url)
lines.append(" CHANGES: %s" % self.diff_url)
else:
lines.append(" %s" % self.conversation_url)
lines.append(" - diff: %s" % self.diff_url)
for comment in self.comments:
lines.append(comment.format(long, 2))
lines.append(comment.format(long))
return "\n".join(lines)

def has_comment_from(self, comments_from):
Expand Down Expand Up @@ -285,6 +316,7 @@ def _query(parsed_args, client, field_name=None, field=None):
"repo": parsed_args.repo,
"comments": "",
"review_threads": "",
"reviews": "",
}
if field:
# Use a cursor for pagination of the field.
Expand All @@ -293,19 +325,25 @@ def _query(parsed_args, client, field_name=None, field=None):
format_inputs["comments"] = _QUERY_COMMENTS % cursor
elif field_name == "reviewThreads":
format_inputs["review_threads"] = _QUERY_REVIEW_THREADS % cursor
elif field_name == "reviews":
format_inputs["reviews"] = _QUERY_REVIEWS % cursor
else:
raise ValueError("Unexpected field_name: %s" % field_name)
else:
# Fetch the first page of both fields.
# Fetch the first page of all fields.
format_inputs["comments"] = _QUERY_COMMENTS % ""
format_inputs["review_threads"] = _QUERY_REVIEW_THREADS % ""
format_inputs["reviews"] = _QUERY_REVIEWS % ""
return client.execute(gql.gql(_QUERY % format_inputs))


def _accumulate_comments(parsed_args, comments, raw_comments):
"""Collects top-level comments."""
def _accumulate_pr_comments(parsed_args, comments, raw_comments):
"""Collects top-level comments and reviews."""
for raw_comment in raw_comments:
comments.append(_Comment.from_raw_comment(raw_comment))
# Elide reviews that have no top-level comment body.
if not raw_comment["body"]:
continue
comments.append(_PRComment(raw_comment))


def _accumulate_threads(parsed_args, threads_by_path, raw_threads):
Expand Down Expand Up @@ -373,11 +411,20 @@ def _fetch_comments(parsed_args):
threads_result = _query(parsed_args, client)
pull_request = threads_result["repository"]["pullRequest"]

# Paginate comments and review threads.
# Paginate comments, reviews, and review threads.
comments = []
_paginate(
"comments",
_accumulate_comments,
_accumulate_pr_comments,
parsed_args,
client,
pull_request,
comments,
)
# Combine reviews into comments for interleaving.
_paginate(
"reviews",
_accumulate_pr_comments,
parsed_args,
client,
pull_request,
Expand All @@ -394,20 +441,23 @@ def _fetch_comments(parsed_args):
)

# Now that loading is done (no more progress indicators), print the header.
print(
"\n '%s' by %s"
% (pull_request["title"], pull_request["author"]["login"])
print()
pr_desc = _Comment(
pull_request["author"]["login"],
pull_request["createdAt"],
pull_request["title"],
)
print(pr_desc.format(parsed_args.long))
return comments, threads_by_path


def main():
parsed_args = _parse_args()
comments, threads_by_path = _fetch_comments(parsed_args)

print()
for comment in comments:
print(comment.format(parsed_args.long, 0))
for comment in sorted(comments):
print()
print(comment.format(parsed_args.long))

for path, threads in sorted(threads_by_path.items()):
# Print a header for each path.
Expand Down
Loading

0 comments on commit e208a98

Please sign in to comment.