| 1 | # given a PR number, get all contributers and the summary from |
|---|
| 2 | # GitHub's API |
|---|
| 3 | |
|---|
| 4 | import sys |
|---|
| 5 | import json |
|---|
| 6 | import base64 |
|---|
| 7 | |
|---|
| 8 | from twisted.internet.task import react |
|---|
| 9 | from twisted.internet.defer import inlineCallbacks, returnValue |
|---|
| 10 | |
|---|
| 11 | import treq |
|---|
| 12 | |
|---|
| 13 | base_pr_url = "https://api.github.com/repos/tahoe-lafs/tahoe-lafs/pulls/{}" |
|---|
| 14 | ignore_handles = ('codecov-io', ) |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | def _find_pull_request_numbers(): |
|---|
| 18 | """ |
|---|
| 19 | This returns a list of Pull Request numbers that are |
|---|
| 20 | interesting. It first assumes any command-line arguments are PR |
|---|
| 21 | numbers. Failing that, it reads stdin and looks for works starting |
|---|
| 22 | with 'PR' |
|---|
| 23 | """ |
|---|
| 24 | if len(sys.argv) < 2: |
|---|
| 25 | data = sys.stdin.read() |
|---|
| 26 | if not len(data): |
|---|
| 27 | print("put some PR numbers on the command-line") |
|---|
| 28 | raise SystemExit(1) |
|---|
| 29 | else: |
|---|
| 30 | all_prs = set() |
|---|
| 31 | for word in data.split(): |
|---|
| 32 | word = word.strip() |
|---|
| 33 | if word.startswith('PR'): |
|---|
| 34 | all_prs.add(word[2:]) |
|---|
| 35 | all_prs = list(all_prs) |
|---|
| 36 | print("Found {} PRs in stdin text".format(len(all_prs))) |
|---|
| 37 | else: |
|---|
| 38 | all_prs = sys.argv[1:] |
|---|
| 39 | return all_prs |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | def _read_github_token(fname='token'): |
|---|
| 43 | """ |
|---|
| 44 | read a secret github token; a 'token' file contains two lines: |
|---|
| 45 | username, github token. |
|---|
| 46 | |
|---|
| 47 | If the token can't be found, SystemExit is raised |
|---|
| 48 | """ |
|---|
| 49 | try: |
|---|
| 50 | with open(fname, 'r') as f: |
|---|
| 51 | data = f.read().strip() |
|---|
| 52 | username, token = data.split('\n', 1) |
|---|
| 53 | except (IOError, EnvironmentError) as e: |
|---|
| 54 | print("Couldn't open or parse 'token' file: {}".format(e)) |
|---|
| 55 | raise SystemExit(1) |
|---|
| 56 | except ValueError: |
|---|
| 57 | print("'token' should contain two lines: username, github token") |
|---|
| 58 | raise SystemExit(1) |
|---|
| 59 | return username, token |
|---|
| 60 | |
|---|
| 61 | |
|---|
| 62 | def _initialize_headers(username, token): |
|---|
| 63 | """ |
|---|
| 64 | Create the base headers for all requests. |
|---|
| 65 | |
|---|
| 66 | :return dict: the headers dict |
|---|
| 67 | """ |
|---|
| 68 | return { |
|---|
| 69 | "User-Agent": "treq", |
|---|
| 70 | "Authorization": "Basic {}".format(base64.b64encode("{}:{}".format(username, token))), |
|---|
| 71 | } |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | @inlineCallbacks |
|---|
| 75 | def _report_authors(data, headers): |
|---|
| 76 | print("Commits:") |
|---|
| 77 | commits_resp = yield treq.get(data['commits_url'], headers=headers) |
|---|
| 78 | commits_data = yield commits_resp.text() |
|---|
| 79 | commits = json.loads(commits_data) |
|---|
| 80 | authors = set() |
|---|
| 81 | for commit in commits: |
|---|
| 82 | if commit['author'] is None: |
|---|
| 83 | print(" {}: no author!".format(commit['sha'])) |
|---|
| 84 | else: |
|---|
| 85 | author = commit['author']['login'] |
|---|
| 86 | print(" {}: {}".format(commit['sha'], author)) |
|---|
| 87 | if author not in ignore_handles: |
|---|
| 88 | authors.add(author) |
|---|
| 89 | returnValue(authors) |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | @inlineCallbacks |
|---|
| 93 | def _report_helpers(data, headers): |
|---|
| 94 | helpers = set() |
|---|
| 95 | print("Comments:") |
|---|
| 96 | comments_resp = yield treq.get(data['comments_url'], headers=headers) |
|---|
| 97 | comments_data = yield comments_resp.text() |
|---|
| 98 | comments = json.loads(comments_data) |
|---|
| 99 | for comment in comments: |
|---|
| 100 | author = comment['user']['login'] |
|---|
| 101 | if author not in ignore_handles: |
|---|
| 102 | helpers.add(author) |
|---|
| 103 | print(" {}: {}".format(author, comment['body'].replace('\n', ' ')[:60])) |
|---|
| 104 | returnValue(helpers) |
|---|
| 105 | |
|---|
| 106 | @inlineCallbacks |
|---|
| 107 | def _request_pr_information(username, token, headers, all_prs): |
|---|
| 108 | """ |
|---|
| 109 | Download PR information from GitHub. |
|---|
| 110 | |
|---|
| 111 | :return dict: mapping PRs to a 2-tuple of "contributers" and |
|---|
| 112 | "helpers" to the PR. Contributers are nicks of people who |
|---|
| 113 | commited to the PR, and "helpers" either reviewed or commented |
|---|
| 114 | on the PR. |
|---|
| 115 | """ |
|---|
| 116 | pr_info = dict() |
|---|
| 117 | |
|---|
| 118 | for pr in all_prs: |
|---|
| 119 | print("Fetching PR{}".format(pr)) |
|---|
| 120 | resp = yield treq.get( |
|---|
| 121 | base_pr_url.format(pr), |
|---|
| 122 | headers=headers, |
|---|
| 123 | ) |
|---|
| 124 | raw_data = yield resp.text() |
|---|
| 125 | data = json.loads(raw_data) |
|---|
| 126 | |
|---|
| 127 | code_handles = yield _report_authors(data, headers) |
|---|
| 128 | help_handles = yield _report_helpers(data, headers) |
|---|
| 129 | |
|---|
| 130 | pr_info[pr] = ( |
|---|
| 131 | code_handles, |
|---|
| 132 | help_handles - help_handles.intersection(code_handles), |
|---|
| 133 | ) |
|---|
| 134 | returnValue(pr_info) |
|---|
| 135 | |
|---|
| 136 | |
|---|
| 137 | #async def main(reactor): |
|---|
| 138 | @inlineCallbacks |
|---|
| 139 | def main(reactor): |
|---|
| 140 | """ |
|---|
| 141 | Fetch Pull Request (PR) information from GitHub. |
|---|
| 142 | |
|---|
| 143 | Either pass a list of PR numbers on the command-line, or pipe text |
|---|
| 144 | containing references like: "There is a PR123 somewhere" from |
|---|
| 145 | which instances of "PRxxx" are extrated. From GitHub's API we get |
|---|
| 146 | all author information and anyone who disucced the PR and print a |
|---|
| 147 | summary afterwards. |
|---|
| 148 | |
|---|
| 149 | You need a 'token' file containing two lines: your username, and |
|---|
| 150 | access token (get this from the GitHub Web UI). |
|---|
| 151 | """ |
|---|
| 152 | |
|---|
| 153 | username, token = _read_github_token() |
|---|
| 154 | pr_info = yield _request_pr_information( |
|---|
| 155 | username, token, |
|---|
| 156 | _initialize_headers(username, token), |
|---|
| 157 | _find_pull_request_numbers(), |
|---|
| 158 | ) |
|---|
| 159 | |
|---|
| 160 | unique_handles = set() |
|---|
| 161 | for pr, (code_handles, help_handles) in sorted(pr_info.items()): |
|---|
| 162 | coders = ', '.join('`{}`_'.format(c) for c in code_handles) |
|---|
| 163 | helpers = ', '.join('`{}`_'.format(c) for c in help_handles) |
|---|
| 164 | if helpers: |
|---|
| 165 | print("`PR{}`_: {} (with {})".format(pr, coders, helpers)) |
|---|
| 166 | else: |
|---|
| 167 | print("`PR{}`_: {}".format(pr, coders)) |
|---|
| 168 | for h in code_handles.union(help_handles): |
|---|
| 169 | unique_handles.add(h) |
|---|
| 170 | |
|---|
| 171 | for pr in sorted(pr_info.keys()): |
|---|
| 172 | print(".. _PR{}: https://github.com/tahoe-lafs/tahoe-lafs/pull/{}".format(pr, pr)) |
|---|
| 173 | for h in sorted(unique_handles): |
|---|
| 174 | print(".. _{}: https://github.com/{}".format(h, h)) |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | if __name__ == "__main__": |
|---|
| 178 | #react(lambda r: ensureDeferred(main(r))) |
|---|
| 179 | react(main) |
|---|