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