def change_authors(pull_request, commits): ''' Changes may have multiple authors in some cases, if multiple people created commits. We order the authors according to the number of commits they created. Sometimes there is not a github user associated with a commit; in these cases, we fall back to the PR author. ''' commits_per_author = defaultdict(lambda: 0) authors = {} num_commits_with_no_author = 0 for commit in commits: if commit.author: author = commit.author commits_per_author[author.id] += 1 authors[author.id] = author else: num_commits_with_no_author += 1 if num_commits_with_no_author > 0: msg = '{} commits had no author in {}' print_warning(msg.format(num_commits_with_no_author, pull_request.html_url)) author_commits = list(commits_per_author.items()) sorted_author_commits = sorted(author_commits, key=lambda ac: ac[1], reverse=True) authors = [authors[aid] for (aid, c) in sorted_author_commits] if len(authors) > 0: return authors else: msg = 'No commits have an author for {}, using pull request author instead' print_warning(msg.format(pull_request.html_url)) return [pull_request.user]
def change_approvals(system, pull_request): ''' Sometimes it makes sense to have third-parties who may not have access to GitHub perform reviews. When this occurs, the pull request is tagged with the `external-review` label. It is assumed that the person doing the review is mentioned in the body of the review. In this case, their may be no explicit approvals, but also no warnings will be logged. If there are no github review with the "approval" status, then we fall back to using github reviews with the "comment" status. ''' external_review = 'external-review' in [ l.name for l in pull_request.labels ] if ('reviews_required' in system): reviews_required = system['reviews_required'] else: reviews_required = True if external_review or not reviews_required: return [] github_reviews = [r for r in pull_request.get_reviews()] approvals = [ build_approval(r) for r in github_reviews if r.state == 'APPROVED' ] if approvals: return approvals # Responses to review comments (oddly) show up in github as reviews; we # apply some extra filtering here to attempt to remove these. github_comments = [ r for r in github_reviews if r.state == 'COMMENTED' and r.user != pull_request.user ] if github_comments: msg = 'No "approved" github reviews for pull request {}, using last "comment" instead' print_warning(msg.format(pull_request.html_url)) return [build_approval(github_comments[-1])] else: msg = 'No reviews for pull request {}' print_warning(msg.format(pull_request.html_url)) return []
def extract_change_requests(pull_request, commits): ''' Deduce which change request a given "change" (i.e., pull request) applies to. Look through the commits in each pull request, and keep track of all of the issues that these commits reference. ''' change_requests = set() for commit in commits: commit_issue_numbers = extract_issue_numbers_from_commit_message(commit.commit.message) change_requests.update(commit_issue_numbers) body_issue_numbers = extract_issue_numbers_from_commit_message(pull_request.body) change_requests.update(body_issue_numbers) if len(change_requests) == 0: branch_name = pull_request.head.ref msg = 'Unable to associate pull request (branch {}) with a change request; {}' print_warning(msg.format(branch_name, pull_request.html_url)) return list(change_requests)
def build_change(system, pull_request): commits = pull_request.get_commits() approvals = change_approvals(system, pull_request) authors = change_authors(pull_request, commits) if authors[0] in approvals: msg = 'Primary author {} is also a reviewer for pull request {}' print_warning(msg.format(authors[0], pull_request.html_url)) return OrderedDict([ ('id', str(pull_request.number)), ('content', change_body(pull_request.body)), ('approvals', approvals), ('authors', authors), ('change_requests', extract_change_requests(pull_request, commits)), ('url', pull_request.html_url), ])
def attach_changes(changes, change_requests): ''' We want to store the connection between change requests and changes with the change requests, because when we generate the templates, we display each change, and within it, we display each change request. But, in GitHub, the connection is stored with the changes. This function mutates the changes and change requests, moving the connection from the former to the latter. ''' change_request_id_to_changes = defaultdict(lambda: []) for change in changes: for change_request_id in change['change_requests']: change_request_id_to_changes[change_request_id].append(change['id']) del change['change_requests'] for change_request in change_requests: if change_request['id'] in change_request_id_to_changes: change_request['change_ids'] = change_request_id_to_changes[change_request['id']] elif not change_request['is_problem_report']: msg = 'No changes implemented for change request {}' print_warning(msg.format(change_request['url']))
def check_user(user): if user.login not in seen_users: seen_users.add(user.login) if user.name is None: msg = 'GitHub User {id} {login} does not have a name; using {login} in place of a name' print_warning(msg.format(id=user.id, login=user.login))
def check_user(user): if user.login not in seen_users: seen_users.add(user.login) if user.name is None: msg = 'GitHub User {} {} does not have a name' print_warning(msg.format(user.id, user.login))