in scripts/gha/lint_commenter.py [0:0]
def main():
# This script performs a number of steps:
#
# 1. Get the PR's diff to find the list of affected files and lines in the PR.
#
# 2. Run lint on all files in the PR. For each lint warning, check if it falls
# within the range of lines affected by the diff. Omit that warning if it
# doesn't fall in the affected lines.
#
# 3. Delete any prior lint warning comments posted by previous runs.
#
# 4. Post any lint warnings that fall within the range of the PR's diff.
args = parse_cmdline_args()
if args.repo is None:
args.repo=subprocess.check_output(['git', 'config', '--get', 'remote.origin.url']).decode('utf-8').rstrip('\n').lower()
if args.verbose:
print('autodetected repo: %s' % args.repo)
if not args.repo.startswith('https://github.com/'):
print('Error, only https://github.com/ repositories are allowed.')
exit(2)
(repo_owner, repo_name) = re.match(r'https://github\.com/([^/]+)/([^/.]+)', args.repo).groups()
# Get the head commit for the pull request.
# GET /repos/{owner}/{repo}/pulls/{pull_number}
request_url = 'https://api.github.com/repos/%s/%s/pulls/%s' % (repo_owner, repo_name, args.pr_number)
header = 'Accept: application/vnd.github.VERSION.json'
pr_data = json.loads(subprocess.check_output(
[args.curl,
'-s', '-X', 'GET',
'-H', 'Accept: application/vnd.github.v3+json',
'-H', 'Authorization: token %s' % args.token,
request_url
] + ([] if not args.verbose else ['-v'])).decode('utf-8').rstrip('\n'))
commit_sha = pr_data['head']['sha']
if args.verbose:
print('Commit sha:', commit_sha)
skip_lint = False
if 'labels' in pr_data:
for label in pr_data['labels']:
if label['name'] == LABEL_TO_SKIP_LINT:
skip_lint = True
break
if skip_lint:
print('PR #%s has "%s" label, skipping lint checks' % (args.pr_number, LABEL_TO_SKIP_LINT))
exit(0)
# Get the diff for the pull request.
# GET /repos/{owner}/{repo}/pulls/{pull_number}
request_url = 'https://api.github.com/repos/%s/%s/pulls/%s' % (repo_owner, repo_name, args.pr_number)
header = 'Accept: application/vnd.github.VERSION.diff'
if args.verbose:
print('request_url: %s' % request_url)
pr_diff = subprocess.check_output(
[args.curl,
'-s', '-o', '-', '-w', '\nHTTP status %{http_code}\n',
'-X', 'GET',
'-H', header,
'-H', 'Authorization: token %s' % args.token,
request_url
] + ([] if not args.verbose else ['-v'])).decode('utf-8')
# Parse the diff to determine the whether each source line is touched.
# Only lint lines that refer to parts of files that are diffed will be shown.
# Information on what this means here:
# https://docs.github.com/en/rest/reference/pulls#create-a-review-comment-for-a-pull-request
valid_lines = {}
file_list = []
pr_patch = PatchSet(pr_diff)
for pr_patch_file in pr_patch:
# Skip files that only remove code.
if pr_patch_file.removed and not pr_patch_file.added:
continue
# Skip files that match an EXCLUDE_PATH_REGEX
excluded = False
for exclude_regex in EXCLUDE_PATH_REGEX:
if re.search(exclude_regex, pr_patch_file.path):
excluded = True
break
if excluded: continue
file_list.append(pr_patch_file.path)
valid_lines[pr_patch_file.path] = set()
for hunk in pr_patch_file:
if hunk.target_length > 0:
for line_number in range(
hunk.target_start,
hunk.target_start + hunk.target_length):
# This line is modified by the diff, add it to the valid set of lines.
valid_lines[pr_patch_file.path].add(line_number)
# Now we also have a list of files in repo.
try:
lint_results=subprocess.check_output([
args.lint_command,
'--output=emacs',
('--filter=%s' % CPPLINT_FILTER),
('--repository=..')
] + file_list, stderr=subprocess.STDOUT).decode('utf-8').split('\n')
except subprocess.CalledProcessError as e:
# Nothing to do if there is an exception.
lint_results=e.output.decode('utf-8').split('\n')
all_comments = []
for line in lint_results:
# Match an output line from the linter, in this format:
# path/to/file:line#: Lint message goes here [lint type] [confidence#]
m = re.match(r'([^:]+):([0-9]+): *(.*[^ ]) +\[([^]]+)\] \[(\d+)\]$', line)
if m:
all_comments.append({
'filename': m.group(1),
'line': int(m.group(2)),
'text': m.group(3),
'type': m.group(4),
'confidence': int(m.group(5)),
'original_line': line})
pr_comments = []
for comment in all_comments:
if comment['filename'] in valid_lines:
if (comment['line'] in valid_lines[comment['filename']] and
comment['confidence'] >= MINIMUM_CONFIDENCE):
pr_comments.append(comment)
if args.verbose:
print('Got %d relevant lint comments' % len(pr_comments))
# Next, get all existing review comments that we posted on the PR and delete them.
comments_to_delete = []
page = 1
per_page=100
keep_reading = True
while keep_reading:
if args.verbose:
print('Read page %d of comments' % page)
request_url = 'https://api.github.com/repos/%s/%s/pulls/%s/comments?per_page=%d&page=%d' % (repo_owner, repo_name, args.pr_number, per_page, page)
comments = json.loads(subprocess.check_output([args.curl,
'-s', '-X', 'GET',
'-H', 'Accept: application/vnd.github.v3+json',
'-H', 'Authorization: token %s' % args.token,
request_url]).decode('utf-8').rstrip('\n'))
for comment in comments:
if HIDDEN_COMMENT_TAG in comment['body']:
comments_to_delete.append(comment['id'])
page = page + 1
if len(comments) < per_page:
# Stop once we're read less than a full page of comments.
keep_reading = False
if comments_to_delete:
print('Delete previous lint comments:', comments_to_delete)
for comment_id in comments_to_delete:
# Delete all of these comments.
# DELETE /repos/{owner}/{repo}/pulls/{pull_number}/comments
request_url = 'https://api.github.com/repos/%s/%s/pulls/comments/%d' % (repo_owner, repo_name, comment_id)
delete_output = subprocess.check_output([args.curl,
'-s', '-X', 'DELETE',
'-H', 'Accept: application/vnd.github.v3+json',
'-H', 'Authorization: token %s' % args.token,
request_url]).decode('utf-8').rstrip('\n')
if len(pr_comments) > 0:
comments_to_send = []
for pr_comment in pr_comments:
# Post each comment.
# POST /repos/{owner}/{repo}/pulls/{pull_number}/comments
request_url = 'https://api.github.com/repos/%s/%s/pulls/%s/reviews' % (repo_owner, repo_name, args.pr_number)
comments_to_send.append({
'body': (
LINT_COMMENT_HEADER +
pr_comment['text'] +
LINT_COMMENT_FOOTER +
HIDDEN_COMMENT_TAG +
'<hidden value=%s></hidden>' % json.dumps(pr_comment['original_line'].replace('"','"'))
),
'path': pr_comment['filename'],
'line': pr_comment['line'],
})
print(pr_comment['original_line'])
request_body = {
'commit_id': commit_sha,
'event': 'COMMENT',
'comments': comments_to_send
}
json_text = json.dumps(request_body)
run_output = json.loads(subprocess.check_output([args.curl,
'-s', '-X', 'POST',
'-H', 'Accept: application/vnd.github.v3+json',
'-H', 'Authorization: token %s' % args.token,
request_url, '-d', json_text]
+ ([] if not args.verbose else ['-v'])).decode('utf-8').rstrip('\n'))
if 'message' in run_output and 'errors' in run_output:
print('%s error when posting comments:\n%s' %
(run_output['message'], '\n'.join(run_output['errors'])))
if args.in_github_action:
print('::error ::%s error when posting comments:%0A%s' %
(run_output['message'], '%0A'.join(run_output['errors'])))
exit(1)
else:
print('Posted %d lint warnings successfully' % len(pr_comments))
if args.in_github_action:
# Also post a GitHub log comment.
lines = ['Found %d lint warnings:' % len(pr_comments)]
for comment in pr_comments:
lines.append(comment['original_line'])
print('::warning ::%s' % '%0A'.join(lines))
else:
print('No lint warnings found.')
exit(0)