blob: df6aa4257ecd52ccd76342a5481825cefaefe1f5 [file] [log] [blame]
Anas Nashife645d9f2019-10-04 10:41:07 -04001#!/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('GH_TOKEN', None)
151 if not token:
152 sys.exit("""Github token not set in environment,
153set 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
216if __name__ == '__main__':
217 main()