##// END OF EJS Templates
unrelated lint change
Richard Fung -
Show More
@@ -1,230 +1,230 b''
1 1 #!/usr/bin/env python
2 2 """Simple tools to query github.com and gather stats about issues.
3 3
4 4 To generate a report for IPython 2.0, run:
5 5
6 6 python github_stats.py --milestone 2.0 --since-tag rel-1.0.0
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Imports
10 10 #-----------------------------------------------------------------------------
11 11
12 12
13 13 import sys
14 14
15 15 from argparse import ArgumentParser
16 16 from datetime import datetime, timedelta
17 17 from subprocess import check_output
18 18
19 19 from gh_api import (
20 20 get_paged_request, make_auth_header, get_pull_request, is_pull_request,
21 21 get_milestone_id, get_issues_list, get_authors,
22 22 )
23 23 #-----------------------------------------------------------------------------
24 24 # Globals
25 25 #-----------------------------------------------------------------------------
26 26
27 27 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
28 28 PER_PAGE = 100
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Functions
32 32 #-----------------------------------------------------------------------------
33 33
34 34 def round_hour(dt):
35 35 return dt.replace(minute=0,second=0,microsecond=0)
36 36
37 37 def _parse_datetime(s):
38 38 """Parse dates in the format returned by the Github API."""
39 39 if s:
40 40 return datetime.strptime(s, ISO8601)
41 41 else:
42 42 return datetime.fromtimestamp(0)
43 43
44 44 def issues2dict(issues):
45 45 """Convert a list of issues to a dict, keyed by issue number."""
46 46 idict = {}
47 47 for i in issues:
48 48 idict[i['number']] = i
49 49 return idict
50 50
51 51 def split_pulls(all_issues, project="ipython/ipython"):
52 52 """split a list of closed issues into non-PR Issues and Pull Requests"""
53 53 pulls = []
54 54 issues = []
55 55 for i in all_issues:
56 56 if is_pull_request(i):
57 57 pull = get_pull_request(project, i['number'], auth=True)
58 58 pulls.append(pull)
59 59 else:
60 60 issues.append(i)
61 61 return issues, pulls
62 62
63 63
64 64 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
65 65 """Get all issues closed since a particular point in time. period
66 66 can either be a datetime object, or a timedelta object. In the
67 67 latter case, it is used as a time before the present.
68 68 """
69 69
70 70 which = 'pulls' if pulls else 'issues'
71 71
72 72 if isinstance(period, timedelta):
73 73 since = round_hour(datetime.utcnow() - period)
74 74 else:
75 75 since = period
76 76 url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, since.strftime(ISO8601), PER_PAGE)
77 77 allclosed = get_paged_request(url, headers=make_auth_header())
78 78
79 79 filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
80 80 if pulls:
81 81 filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
82 82 # filter out PRs not against main (backports)
83 filtered = [ i for i in filtered if i['base']['ref'] == 'main' ]
83 filtered = [i for i in filtered if i["base"]["ref"] == "main"]
84 84 else:
85 85 filtered = [ i for i in filtered if not is_pull_request(i) ]
86 86
87 87 return filtered
88 88
89 89
90 90 def sorted_by_field(issues, field='closed_at', reverse=False):
91 91 """Return a list of issues sorted by closing date date."""
92 92 return sorted(issues, key = lambda i:i[field], reverse=reverse)
93 93
94 94
95 95 def report(issues, show_urls=False):
96 96 """Summary report about a list of issues, printing number and title."""
97 97 if show_urls:
98 98 for i in issues:
99 99 role = 'ghpull' if 'merged_at' in i else 'ghissue'
100 100 print(u'* :%s:`%d`: %s' % (role, i['number'],
101 101 i['title'].replace(u'`', u'``')))
102 102 else:
103 103 for i in issues:
104 104 print(u'* %d: %s' % (i['number'], i['title'].replace(u'`', u'``')))
105 105
106 106 #-----------------------------------------------------------------------------
107 107 # Main script
108 108 #-----------------------------------------------------------------------------
109 109
110 110 if __name__ == "__main__":
111 111
112 112 print("DEPRECATE: backport_pr.py is deprecated and it is now recommended"
113 113 "to install `ghpro` from PyPI.", file=sys.stderr)
114 114
115 115
116 116 # Whether to add reST urls for all issues in printout.
117 117 show_urls = True
118 118
119 119 parser = ArgumentParser()
120 120 parser.add_argument('--since-tag', type=str,
121 121 help="The git tag to use for the starting point (typically the last major release)."
122 122 )
123 123 parser.add_argument('--milestone', type=str,
124 124 help="The GitHub milestone to use for filtering issues [optional]."
125 125 )
126 126 parser.add_argument('--days', type=int,
127 127 help="The number of days of data to summarize (use this or --since-tag)."
128 128 )
129 129 parser.add_argument('--project', type=str, default="ipython/ipython",
130 130 help="The project to summarize."
131 131 )
132 132 parser.add_argument('--links', action='store_true', default=False,
133 133 help="Include links to all closed Issues and PRs in the output."
134 134 )
135 135
136 136 opts = parser.parse_args()
137 137 tag = opts.since_tag
138 138
139 139 # set `since` from days or git tag
140 140 if opts.days:
141 141 since = datetime.utcnow() - timedelta(days=opts.days)
142 142 else:
143 143 if not tag:
144 144 tag = check_output(['git', 'describe', '--abbrev=0']).strip().decode('utf8')
145 145 cmd = ['git', 'log', '-1', '--format=%ai', tag]
146 146 tagday, tz = check_output(cmd).strip().decode('utf8').rsplit(' ', 1)
147 147 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
148 148 h = int(tz[1:3])
149 149 m = int(tz[3:])
150 150 td = timedelta(hours=h, minutes=m)
151 151 if tz[0] == '-':
152 152 since += td
153 153 else:
154 154 since -= td
155 155
156 156 since = round_hour(since)
157 157
158 158 milestone = opts.milestone
159 159 project = opts.project
160 160
161 161 print("fetching GitHub stats since %s (tag: %s, milestone: %s)" % (since, tag, milestone), file=sys.stderr)
162 162 if milestone:
163 163 milestone_id = get_milestone_id(project=project, milestone=milestone,
164 164 auth=True)
165 165 issues_and_pulls = get_issues_list(project=project,
166 166 milestone=milestone_id,
167 167 state='closed',
168 168 auth=True,
169 169 )
170 170 issues, pulls = split_pulls(issues_and_pulls, project=project)
171 171 else:
172 172 issues = issues_closed_since(since, project=project, pulls=False)
173 173 pulls = issues_closed_since(since, project=project, pulls=True)
174 174
175 175 # For regular reports, it's nice to show them in reverse chronological order
176 176 issues = sorted_by_field(issues, reverse=True)
177 177 pulls = sorted_by_field(pulls, reverse=True)
178 178
179 179 n_issues, n_pulls = map(len, (issues, pulls))
180 180 n_total = n_issues + n_pulls
181 181
182 182 # Print summary report we can directly include into release notes.
183 183
184 184 print()
185 185 since_day = since.strftime("%Y/%m/%d")
186 186 today = datetime.today().strftime("%Y/%m/%d")
187 187 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
188 188 print()
189 189 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
190 190 print()
191 191
192 192 ncommits = 0
193 193 all_authors = []
194 194 if tag:
195 195 # print git info, in addition to GitHub info:
196 196 since_tag = tag+'..'
197 197 cmd = ['git', 'log', '--oneline', since_tag]
198 198 ncommits += len(check_output(cmd).splitlines())
199 199
200 200 author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
201 201 all_authors.extend(check_output(author_cmd).decode('utf-8', 'replace').splitlines())
202 202
203 203 pr_authors = []
204 204 for pr in pulls:
205 205 pr_authors.extend(get_authors(pr))
206 206 ncommits = len(pr_authors) + ncommits - len(pulls)
207 207 author_cmd = ['git', 'check-mailmap'] + pr_authors
208 208 with_email = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
209 209 all_authors.extend([ u'* ' + a.split(' <')[0] for a in with_email ])
210 210 unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
211 211
212 212 print("We closed %d issues and merged %d pull requests." % (n_issues, n_pulls))
213 213 if milestone:
214 214 print("The full list can be seen `on GitHub <https://github.com/{project}/issues?q=milestone%3A{milestone}>`__".format(project=project,milestone=milestone)
215 215 )
216 216
217 217 print()
218 218 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
219 219 print()
220 220 print('\n'.join(unique_authors))
221 221
222 222 if opts.links:
223 223 print()
224 224 print("GitHub issues and pull requests:")
225 225 print()
226 226 print('Pull Requests (%d):\n' % n_pulls)
227 227 report(pulls, show_urls)
228 228 print()
229 229 print('Issues (%d):\n' % n_issues)
230 230 report(issues, show_urls)
General Comments 0
You need to be logged in to leave comments. Login now