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