| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2019 Intel Corporation |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| # Lists all closes issues since a given date |
| |
| import argparse |
| import sys |
| import os |
| import re |
| import time |
| import threading |
| import requests |
| |
| |
| args = None |
| |
| |
| class Spinner: |
| busy = False |
| delay = 0.1 |
| |
| @staticmethod |
| def spinning_cursor(): |
| while 1: |
| for cursor in '|/-\\': |
| yield cursor |
| |
| def __init__(self, delay=None): |
| self.spinner_generator = self.spinning_cursor() |
| if delay and float(delay): |
| self.delay = delay |
| |
| def spinner_task(self): |
| while self.busy: |
| sys.stdout.write(next(self.spinner_generator)) |
| sys.stdout.flush() |
| time.sleep(self.delay) |
| sys.stdout.write('\b') |
| sys.stdout.flush() |
| |
| def __enter__(self): |
| self.busy = True |
| threading.Thread(target=self.spinner_task).start() |
| |
| def __exit__(self, exception, value, tb): |
| self.busy = False |
| time.sleep(self.delay) |
| if exception is not None: |
| return False |
| |
| |
| class Issues: |
| def __init__(self, org, repo, token): |
| self.repo = repo |
| self.org = org |
| self.issues_url = "https://github.com/%s/%s/issues" % ( |
| self.org, self.repo) |
| self.github_url = 'https://api.github.com/repos/%s/%s' % ( |
| self.org, self.repo) |
| |
| self.api_token = token |
| self.headers = {} |
| self.headers['Authorization'] = 'token %s' % self.api_token |
| self.headers['Accept'] = 'application/vnd.github.golden-comet-preview+json' |
| self.items = [] |
| |
| def get_pull(self, pull_nr): |
| url = ("%s/pulls/%s" % (self.github_url, pull_nr)) |
| response = requests.get("%s" % (url), headers=self.headers) |
| if response.status_code != 200: |
| raise RuntimeError( |
| "Failed to get issue due to unexpected HTTP status code: {}".format( |
| response.status_code) |
| ) |
| item = response.json() |
| return item |
| |
| def get_issue(self, issue_nr): |
| url = ("%s/issues/%s" % (self.github_url, issue_nr)) |
| response = requests.get("%s" % (url), headers=self.headers) |
| if response.status_code != 200: |
| return None |
| |
| item = response.json() |
| return item |
| |
| def list_issues(self, url): |
| response = requests.get("%s" % (url), headers=self.headers) |
| if response.status_code != 200: |
| raise RuntimeError( |
| "Failed to get issue due to unexpected HTTP status code: {}".format( |
| response.status_code) |
| ) |
| self.items = self.items + response.json() |
| |
| try: |
| print("Getting more items...") |
| next_issues = response.links["next"] |
| if next_issues: |
| next_url = next_issues['url'] |
| self.list_issues(next_url) |
| except KeyError: |
| pass |
| |
| def issues_since(self, date, state="closed"): |
| self.list_issues("%s/issues?state=%s&since=%s" % |
| (self.github_url, state, date)) |
| |
| def pull_requests(self, base='v1.14-branch', state='closed'): |
| self.list_issues("%s/pulls?state=%s&base=%s" % |
| (self.github_url, state, base)) |
| |
| |
| def parse_args(): |
| global args |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| |
| parser.add_argument("-o", "--org", default="zephyrproject-rtos", |
| help="Github organisation") |
| |
| parser.add_argument("-r", "--repo", default="zephyr", |
| help="Github repository") |
| |
| parser.add_argument("-f", "--file", required=True, |
| help="Name of output file.") |
| |
| parser.add_argument("-s", "--issues-since", |
| help="""List issues since date where date |
| is in the format 2019-09-01.""") |
| |
| parser.add_argument("-b", "--issues-in-pulls", |
| help="List issues in pulls for a given branch") |
| |
| parser.add_argument("-c", "--commits-file", |
| help="""File with all commits (git log a..b) to |
| be parsed for fixed bugs.""") |
| |
| args = parser.parse_args() |
| |
| |
| def main(): |
| parse_args() |
| |
| token = os.environ.get('GH_TOKEN', None) |
| if not token: |
| sys.exit("""Github token not set in environment, |
| set the env. variable GH_TOKEN please and retry.""") |
| |
| i = Issues(args.org, args.repo, token) |
| |
| if args.issues_since: |
| i.issues_since(args.issues_since) |
| count = 0 |
| with open(args.file, "w") as f: |
| for issue in i.items: |
| if 'pull_request' not in issue: |
| # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail |
| f.write("* :github:`{}` - {}\n".format( |
| issue['number'], issue['title'])) |
| count = count + 1 |
| elif args.issues_in_pulls: |
| i.pull_requests(base=args.issues_in_pulls) |
| count = 0 |
| |
| bugs = set() |
| backports = [] |
| for issue in i.items: |
| if not isinstance(issue['body'], str): |
| continue |
| match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| issue['body'], re.MULTILINE) |
| if match: |
| for mm in match: |
| bugs.add(mm[1]) |
| else: |
| match = re.findall( |
| r"Backport #([0-9]+)", issue['body'], re.MULTILINE) |
| if match: |
| backports.append(match[0]) |
| |
| # follow PRs to their origin (backports) |
| with Spinner(): |
| for p in backports: |
| item = i.get_pull(p) |
| match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| item['body'], re.MULTILINE) |
| for mm in match: |
| bugs.add(mm[1]) |
| |
| # now open commits |
| if args.commits_file: |
| print("Open commits file and parse for fixed bugs...") |
| with open(args.commits_file, "r") as commits: |
| content = commits.read() |
| match = re.findall(r"(Fixes|Closes|Fixed|close):? #([0-9]+)", |
| str(content), re.MULTILINE) |
| for mm in match: |
| bugs.add(mm[1]) |
| |
| print("Create output file...") |
| with Spinner(): |
| with open(args.file, "w") as f: |
| for m in sorted(bugs): |
| item = i.get_issue(m) |
| if item: |
| # * :github:`8193` - STM32 config BUILD_OUTPUT_HEX fail |
| f.write("* :github:`{}` - {}\n".format( |
| item['number'], item['title'])) |
| |
| if __name__ == '__main__': |
| main() |