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