Anas Nashif | e645d9f | 2019-10-04 10:41:07 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2019 Intel Corporation |
| 4 | # |
| 5 | # SPDX-License-Identifier: Apache-2.0 |
| 6 | |
| 7 | # Lists all closes issues since a given date |
| 8 | |
| 9 | import argparse |
| 10 | import sys |
| 11 | import os |
| 12 | import re |
| 13 | import time |
| 14 | import threading |
| 15 | import requests |
| 16 | |
| 17 | |
| 18 | args = None |
| 19 | |
| 20 | |
| 21 | class Spinner: |
| 22 | busy = False |
| 23 | delay = 0.1 |
| 24 | |
| 25 | @staticmethod |
| 26 | def spinning_cursor(): |
| 27 | while 1: |
| 28 | for cursor in '|/-\\': |
| 29 | yield cursor |
| 30 | |
| 31 | def __init__(self, delay=None): |
| 32 | self.spinner_generator = self.spinning_cursor() |
| 33 | if delay and float(delay): |
| 34 | self.delay = delay |
| 35 | |
| 36 | def spinner_task(self): |
| 37 | while self.busy: |
| 38 | sys.stdout.write(next(self.spinner_generator)) |
| 39 | sys.stdout.flush() |
| 40 | time.sleep(self.delay) |
| 41 | sys.stdout.write('\b') |
| 42 | sys.stdout.flush() |
| 43 | |
| 44 | def __enter__(self): |
| 45 | self.busy = True |
| 46 | threading.Thread(target=self.spinner_task).start() |
| 47 | |
| 48 | def __exit__(self, exception, value, tb): |
| 49 | self.busy = False |
| 50 | time.sleep(self.delay) |
| 51 | if exception is not None: |
| 52 | return False |
| 53 | |
| 54 | |
| 55 | class Issues: |
| 56 | def __init__(self, org, repo, token): |
| 57 | self.repo = repo |
| 58 | self.org = org |
| 59 | self.issues_url = "https://github.com/%s/%s/issues" % ( |
| 60 | self.org, self.repo) |
| 61 | self.github_url = 'https://api.github.com/repos/%s/%s' % ( |
| 62 | self.org, self.repo) |
| 63 | |
| 64 | self.api_token = token |
| 65 | self.headers = {} |
| 66 | self.headers['Authorization'] = 'token %s' % self.api_token |
| 67 | self.headers['Accept'] = 'application/vnd.github.golden-comet-preview+json' |
| 68 | self.items = [] |
| 69 | |
| 70 | def get_pull(self, pull_nr): |
| 71 | url = ("%s/pulls/%s" % (self.github_url, pull_nr)) |
| 72 | response = requests.get("%s" % (url), headers=self.headers) |
| 73 | if response.status_code != 200: |
| 74 | raise RuntimeError( |
| 75 | "Failed to get issue due to unexpected HTTP status code: {}".format( |
| 76 | response.status_code) |
| 77 | ) |
| 78 | item = response.json() |
| 79 | return item |
| 80 | |
| 81 | def get_issue(self, issue_nr): |
| 82 | url = ("%s/issues/%s" % (self.github_url, issue_nr)) |
| 83 | response = requests.get("%s" % (url), headers=self.headers) |
| 84 | if response.status_code != 200: |
| 85 | return None |
| 86 | |
| 87 | item = response.json() |
| 88 | return item |
| 89 | |
| 90 | def list_issues(self, url): |
| 91 | response = requests.get("%s" % (url), headers=self.headers) |
| 92 | if response.status_code != 200: |
| 93 | raise RuntimeError( |
| 94 | "Failed to get issue due to unexpected HTTP status code: {}".format( |
| 95 | response.status_code) |
| 96 | ) |
| 97 | self.items = self.items + response.json() |
| 98 | |
| 99 | try: |
| 100 | print("Getting more items...") |
| 101 | next_issues = response.links["next"] |
| 102 | if next_issues: |
| 103 | next_url = next_issues['url'] |
| 104 | self.list_issues(next_url) |
| 105 | except KeyError: |
| 106 | pass |
| 107 | |
| 108 | def issues_since(self, date, state="closed"): |
| 109 | self.list_issues("%s/issues?state=%s&since=%s" % |
| 110 | (self.github_url, state, date)) |
| 111 | |
| 112 | def pull_requests(self, base='v1.14-branch', state='closed'): |
| 113 | self.list_issues("%s/pulls?state=%s&base=%s" % |
| 114 | (self.github_url, state, base)) |
| 115 | |
| 116 | |
| 117 | def parse_args(): |
| 118 | global args |
| 119 | |
| 120 | parser = argparse.ArgumentParser( |
| 121 | description=__doc__, |
| 122 | formatter_class=argparse.RawDescriptionHelpFormatter) |
| 123 | |
| 124 | parser.add_argument("-o", "--org", default="zephyrproject-rtos", |
| 125 | help="Github organisation") |
| 126 | |
| 127 | parser.add_argument("-r", "--repo", default="zephyr", |
| 128 | help="Github repository") |
| 129 | |
| 130 | parser.add_argument("-f", "--file", required=True, |
| 131 | help="Name of output file.") |
| 132 | |
| 133 | parser.add_argument("-s", "--issues-since", |
| 134 | help="""List issues since date where date |
| 135 | is in the format 2019-09-01.""") |
| 136 | |
| 137 | parser.add_argument("-b", "--issues-in-pulls", |
| 138 | help="List issues in pulls for a given branch") |
| 139 | |
| 140 | parser.add_argument("-c", "--commits-file", |
| 141 | help="""File with all commits (git log a..b) to |
| 142 | be parsed for fixed bugs.""") |
| 143 | |
| 144 | args = parser.parse_args() |
| 145 | |
| 146 | |
| 147 | def main(): |
| 148 | parse_args() |
| 149 | |
| 150 | token = os.environ.get('GH_TOKEN', None) |
| 151 | if not token: |
| 152 | sys.exit("""Github token not set in environment, |
| 153 | set the env. variable GH_TOKEN please and retry.""") |
| 154 | |
| 155 | i = Issues(args.org, args.repo, token) |
| 156 | |
| 157 | if args.issues_since: |
| 158 | i.issues_since(args.issues_since) |
| 159 | count = 0 |
| 160 | with open(args.file, "w") as f: |
| 161 | for issue in i.items: |
| 162 | if 'pull_request' not in issue: |
| 163 | # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail |
| 164 | f.write("* :github:`{}` - {}\n".format( |
| 165 | issue['number'], issue['title'])) |
| 166 | count = count + 1 |
| 167 | elif args.issues_in_pulls: |
| 168 | i.pull_requests(base=args.issues_in_pulls) |
| 169 | count = 0 |
| 170 | |
| 171 | bugs = set() |
| 172 | backports = [] |
| 173 | for issue in i.items: |
| 174 | if not isinstance(issue['body'], str): |
| 175 | continue |
| 176 | match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| 177 | issue['body'], re.MULTILINE) |
| 178 | if match: |
| 179 | for mm in match: |
| 180 | bugs.add(mm[1]) |
| 181 | else: |
| 182 | match = re.findall( |
| 183 | r"Backport #([0-9]+)", issue['body'], re.MULTILINE) |
| 184 | if match: |
| 185 | backports.append(match[0]) |
| 186 | |
| 187 | # follow PRs to their origin (backports) |
| 188 | with Spinner(): |
| 189 | for p in backports: |
| 190 | item = i.get_pull(p) |
| 191 | match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| 192 | item['body'], re.MULTILINE) |
| 193 | for mm in match: |
| 194 | bugs.add(mm[1]) |
| 195 | |
| 196 | # now open commits |
| 197 | if args.commits_file: |
| 198 | print("Open commits file and parse for fixed bugs...") |
| 199 | with open(args.commits_file, "r") as commits: |
| 200 | content = commits.read() |
| 201 | match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| 202 | str(content), re.MULTILINE) |
| 203 | for mm in match: |
| 204 | bugs.add(mm[1]) |
| 205 | |
| 206 | print("Create output file...") |
| 207 | with Spinner(): |
| 208 | with open(args.file, "w") as f: |
| 209 | for m in sorted(bugs): |
| 210 | item = i.get_issue(m) |
| 211 | if item: |
| 212 | # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail |
| 213 | f.write("* :github:`{}` - {}\n".format( |
| 214 | item['number'], item['title'])) |
| 215 | |
| 216 | if __name__ == '__main__': |
| 217 | main() |