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 9import argparse 10import sys 11import os 12import re 13import time 14import threading 15import requests 16 17 18args = None 19 20 21class 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 55class 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 117def 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 147def main(): 148 parse_args() 149 150 token = os.environ.get('GITHUB_TOKEN', None) 151 if not token: 152 sys.exit("""Github token not set in environment, 153set the env. variable GITHUB_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 217if __name__ == '__main__': 218 main() 219