##// END OF EJS Templates
allow params in gh_api methods
MinRK -
Show More
@@ -1,254 +1,259 b''
1 """Functions for Github API requests."""
1 """Functions for Github API requests."""
2 from __future__ import print_function
2 from __future__ import print_function
3
3
4 try:
4 try:
5 input = raw_input
5 input = raw_input
6 except NameError:
6 except NameError:
7 pass
7 pass
8
8
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12
12
13 import requests
13 import requests
14 import getpass
14 import getpass
15 import json
15 import json
16
16
17 try:
17 try:
18 import requests_cache
18 import requests_cache
19 except ImportError:
19 except ImportError:
20 print("no cache")
20 print("no cache")
21 else:
21 else:
22 requests_cache.install_cache("gh_api")
22 requests_cache.install_cache("gh_api")
23
23
24 # Keyring stores passwords by a 'username', but we're not storing a username and
24 # Keyring stores passwords by a 'username', but we're not storing a username and
25 # password
25 # password
26 fake_username = 'ipython_tools'
26 fake_username = 'ipython_tools'
27
27
28 class Obj(dict):
28 class Obj(dict):
29 """Dictionary with attribute access to names."""
29 """Dictionary with attribute access to names."""
30 def __getattr__(self, name):
30 def __getattr__(self, name):
31 try:
31 try:
32 return self[name]
32 return self[name]
33 except KeyError:
33 except KeyError:
34 raise AttributeError(name)
34 raise AttributeError(name)
35
35
36 def __setattr__(self, name, val):
36 def __setattr__(self, name, val):
37 self[name] = val
37 self[name] = val
38
38
39 token = None
39 token = None
40 def get_auth_token():
40 def get_auth_token():
41 global token
41 global token
42
42
43 if token is not None:
43 if token is not None:
44 return token
44 return token
45
45
46 import keyring
46 import keyring
47 token = keyring.get_password('github', fake_username)
47 token = keyring.get_password('github', fake_username)
48 if token is not None:
48 if token is not None:
49 return token
49 return token
50
50
51 print("Please enter your github username and password. These are not "
51 print("Please enter your github username and password. These are not "
52 "stored, only used to get an oAuth token. You can revoke this at "
52 "stored, only used to get an oAuth token. You can revoke this at "
53 "any time on Github.")
53 "any time on Github.")
54 user = input("Username: ")
54 user = input("Username: ")
55 pw = getpass.getpass("Password: ")
55 pw = getpass.getpass("Password: ")
56
56
57 auth_request = {
57 auth_request = {
58 "scopes": [
58 "scopes": [
59 "public_repo",
59 "public_repo",
60 "gist"
60 "gist"
61 ],
61 ],
62 "note": "IPython tools",
62 "note": "IPython tools",
63 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
63 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
64 }
64 }
65 response = requests.post('https://api.github.com/authorizations',
65 response = requests.post('https://api.github.com/authorizations',
66 auth=(user, pw), data=json.dumps(auth_request))
66 auth=(user, pw), data=json.dumps(auth_request))
67 response.raise_for_status()
67 response.raise_for_status()
68 token = json.loads(response.text)['token']
68 token = json.loads(response.text)['token']
69 keyring.set_password('github', fake_username, token)
69 keyring.set_password('github', fake_username, token)
70 return token
70 return token
71
71
72 def make_auth_header():
72 def make_auth_header():
73 return {'Authorization': 'token ' + get_auth_token()}
73 return {'Authorization': 'token ' + get_auth_token()}
74
74
75 def post_issue_comment(project, num, body):
75 def post_issue_comment(project, num, body):
76 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
76 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
77 payload = json.dumps({'body': body})
77 payload = json.dumps({'body': body})
78 requests.post(url, data=payload, headers=make_auth_header())
78 requests.post(url, data=payload, headers=make_auth_header())
79
79
80 def post_gist(content, description='', filename='file', auth=False):
80 def post_gist(content, description='', filename='file', auth=False):
81 """Post some text to a Gist, and return the URL."""
81 """Post some text to a Gist, and return the URL."""
82 post_data = json.dumps({
82 post_data = json.dumps({
83 "description": description,
83 "description": description,
84 "public": True,
84 "public": True,
85 "files": {
85 "files": {
86 filename: {
86 filename: {
87 "content": content
87 "content": content
88 }
88 }
89 }
89 }
90 }).encode('utf-8')
90 }).encode('utf-8')
91
91
92 headers = make_auth_header() if auth else {}
92 headers = make_auth_header() if auth else {}
93 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
93 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
94 response.raise_for_status()
94 response.raise_for_status()
95 response_data = json.loads(response.text)
95 response_data = json.loads(response.text)
96 return response_data['html_url']
96 return response_data['html_url']
97
97
98 def get_pull_request(project, num, auth=False):
98 def get_pull_request(project, num, auth=False):
99 """get pull request info by number
99 """get pull request info by number
100 """
100 """
101 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
101 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
102 if auth:
102 if auth:
103 header = make_auth_header()
103 header = make_auth_header()
104 else:
104 else:
105 header = None
105 header = None
106 response = requests.get(url, headers=header)
106 response = requests.get(url, headers=header)
107 response.raise_for_status()
107 response.raise_for_status()
108 return json.loads(response.text, object_hook=Obj)
108 return json.loads(response.text, object_hook=Obj)
109
109
110 def get_pull_request_files(project, num, auth=False):
110 def get_pull_request_files(project, num, auth=False):
111 """get list of files in a pull request"""
111 """get list of files in a pull request"""
112 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
112 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
113 if auth:
113 if auth:
114 header = make_auth_header()
114 header = make_auth_header()
115 else:
115 else:
116 header = None
116 header = None
117 return get_paged_request(url, headers=header)
117 return get_paged_request(url, headers=header)
118
118
119 element_pat = re.compile(r'<(.+?)>')
119 element_pat = re.compile(r'<(.+?)>')
120 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
120 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
121
121
122 def get_paged_request(url, headers=None):
122 def get_paged_request(url, headers=None, **params):
123 """get a full list, handling APIv3's paging"""
123 """get a full list, handling APIv3's paging"""
124 results = []
124 results = []
125 params.setdefault("per_page", 100)
125 while True:
126 while True:
126 print("fetching %s" % url, file=sys.stderr)
127 print("fetching %s with %s" % (url, params), file=sys.stderr)
127 response = requests.get(url, headers=headers)
128 response = requests.get(url, headers=headers, params=params)
128 response.raise_for_status()
129 response.raise_for_status()
129 results.extend(response.json())
130 results.extend(response.json())
130 if 'next' in response.links:
131 if 'next' in response.links:
131 url = response.links['next']['url']
132 url = response.links['next']['url']
132 else:
133 else:
133 break
134 break
134 return results
135 return results
135
136
136 def get_pulls_list(project, state="closed", auth=False):
137 def get_pulls_list(project, auth=False, **params):
137 """get pull request list
138 """get pull request list"""
138 """
139 params.setdefault("state", "closed")
139 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
140 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
140 if auth:
141 if auth:
141 headers = make_auth_header()
142 headers = make_auth_header()
142 else:
143 else:
143 headers = None
144 headers = None
144 pages = get_paged_request(url, headers=headers)
145 pages = get_paged_request(url, headers=headers, params=params)
145 return pages
146 return pages
146
147
147 def get_issues_list(project, state="closed", auth=False):
148 def get_issues_list(project, auth=False, **params):
148 """get pull request list
149 """get issues list"""
149 """
150 params.setdefault("state", "closed")
150 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
151 url = "https://api.github.com/repos/{project}/issues".format(project=project)
151 if auth:
152 if auth:
152 headers = make_auth_header()
153 headers = make_auth_header()
153 else:
154 else:
154 headers = None
155 headers = None
155 pages = get_paged_request(url, headers=headers)
156 pages = get_paged_request(url, headers=headers, **params)
156 return pages
157 return pages
157
158
159 def is_pull_request(issue):
160 """Return True if the given issue is a pull request."""
161 return bool(issue.get('pull_request', {}).get('html_url', None))
162
158 # encode_multipart_formdata is from urllib3.filepost
163 # encode_multipart_formdata is from urllib3.filepost
159 # The only change is to iter_fields, to enforce S3's required key ordering
164 # The only change is to iter_fields, to enforce S3's required key ordering
160
165
161 def iter_fields(fields):
166 def iter_fields(fields):
162 fields = fields.copy()
167 fields = fields.copy()
163 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
168 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
164 'Policy', 'Signature', 'Content-Type', 'file'):
169 'Policy', 'Signature', 'Content-Type', 'file'):
165 yield (key, fields.pop(key))
170 yield (key, fields.pop(key))
166 for (k,v) in fields.items():
171 for (k,v) in fields.items():
167 yield k,v
172 yield k,v
168
173
169 def encode_multipart_formdata(fields, boundary=None):
174 def encode_multipart_formdata(fields, boundary=None):
170 """
175 """
171 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
176 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
172
177
173 :param fields:
178 :param fields:
174 Dictionary of fields or list of (key, value) field tuples. The key is
179 Dictionary of fields or list of (key, value) field tuples. The key is
175 treated as the field name, and the value as the body of the form-data
180 treated as the field name, and the value as the body of the form-data
176 bytes. If the value is a tuple of two elements, then the first element
181 bytes. If the value is a tuple of two elements, then the first element
177 is treated as the filename of the form-data section.
182 is treated as the filename of the form-data section.
178
183
179 Field names and filenames must be unicode.
184 Field names and filenames must be unicode.
180
185
181 :param boundary:
186 :param boundary:
182 If not specified, then a random boundary will be generated using
187 If not specified, then a random boundary will be generated using
183 :func:`mimetools.choose_boundary`.
188 :func:`mimetools.choose_boundary`.
184 """
189 """
185 # copy requests imports in here:
190 # copy requests imports in here:
186 from io import BytesIO
191 from io import BytesIO
187 from requests.packages.urllib3.filepost import (
192 from requests.packages.urllib3.filepost import (
188 choose_boundary, six, writer, b, get_content_type
193 choose_boundary, six, writer, b, get_content_type
189 )
194 )
190 body = BytesIO()
195 body = BytesIO()
191 if boundary is None:
196 if boundary is None:
192 boundary = choose_boundary()
197 boundary = choose_boundary()
193
198
194 for fieldname, value in iter_fields(fields):
199 for fieldname, value in iter_fields(fields):
195 body.write(b('--%s\r\n' % (boundary)))
200 body.write(b('--%s\r\n' % (boundary)))
196
201
197 if isinstance(value, tuple):
202 if isinstance(value, tuple):
198 filename, data = value
203 filename, data = value
199 writer(body).write('Content-Disposition: form-data; name="%s"; '
204 writer(body).write('Content-Disposition: form-data; name="%s"; '
200 'filename="%s"\r\n' % (fieldname, filename))
205 'filename="%s"\r\n' % (fieldname, filename))
201 body.write(b('Content-Type: %s\r\n\r\n' %
206 body.write(b('Content-Type: %s\r\n\r\n' %
202 (get_content_type(filename))))
207 (get_content_type(filename))))
203 else:
208 else:
204 data = value
209 data = value
205 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
210 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
206 % (fieldname))
211 % (fieldname))
207 body.write(b'Content-Type: text/plain\r\n\r\n')
212 body.write(b'Content-Type: text/plain\r\n\r\n')
208
213
209 if isinstance(data, int):
214 if isinstance(data, int):
210 data = str(data) # Backwards compatibility
215 data = str(data) # Backwards compatibility
211 if isinstance(data, six.text_type):
216 if isinstance(data, six.text_type):
212 writer(body).write(data)
217 writer(body).write(data)
213 else:
218 else:
214 body.write(data)
219 body.write(data)
215
220
216 body.write(b'\r\n')
221 body.write(b'\r\n')
217
222
218 body.write(b('--%s--\r\n' % (boundary)))
223 body.write(b('--%s--\r\n' % (boundary)))
219
224
220 content_type = b('multipart/form-data; boundary=%s' % boundary)
225 content_type = b('multipart/form-data; boundary=%s' % boundary)
221
226
222 return body.getvalue(), content_type
227 return body.getvalue(), content_type
223
228
224
229
225 def post_download(project, filename, name=None, description=""):
230 def post_download(project, filename, name=None, description=""):
226 """Upload a file to the GitHub downloads area"""
231 """Upload a file to the GitHub downloads area"""
227 if name is None:
232 if name is None:
228 name = os.path.basename(filename)
233 name = os.path.basename(filename)
229 with open(filename, 'rb') as f:
234 with open(filename, 'rb') as f:
230 filedata = f.read()
235 filedata = f.read()
231
236
232 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
237 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
233
238
234 payload = json.dumps(dict(name=name, size=len(filedata),
239 payload = json.dumps(dict(name=name, size=len(filedata),
235 description=description))
240 description=description))
236 response = requests.post(url, data=payload, headers=make_auth_header())
241 response = requests.post(url, data=payload, headers=make_auth_header())
237 response.raise_for_status()
242 response.raise_for_status()
238 reply = json.loads(response.content)
243 reply = json.loads(response.content)
239 s3_url = reply['s3_url']
244 s3_url = reply['s3_url']
240
245
241 fields = dict(
246 fields = dict(
242 key=reply['path'],
247 key=reply['path'],
243 acl=reply['acl'],
248 acl=reply['acl'],
244 success_action_status=201,
249 success_action_status=201,
245 Filename=reply['name'],
250 Filename=reply['name'],
246 AWSAccessKeyId=reply['accesskeyid'],
251 AWSAccessKeyId=reply['accesskeyid'],
247 Policy=reply['policy'],
252 Policy=reply['policy'],
248 Signature=reply['signature'],
253 Signature=reply['signature'],
249 file=(reply['name'], filedata),
254 file=(reply['name'], filedata),
250 )
255 )
251 fields['Content-Type'] = reply['mime_type']
256 fields['Content-Type'] = reply['mime_type']
252 data, content_type = encode_multipart_formdata(fields)
257 data, content_type = encode_multipart_formdata(fields)
253 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
258 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
254 return s3r
259 return s3r
@@ -1,200 +1,194 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Simple tools to query github.com and gather stats about issues.
2 """Simple tools to query github.com and gather stats about issues.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Imports
5 # Imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 from __future__ import print_function
8 from __future__ import print_function
9
9
10 import json
10 import json
11 import re
11 import re
12 import sys
12 import sys
13
13
14 from datetime import datetime, timedelta
14 from datetime import datetime, timedelta
15 from subprocess import check_output
15 from subprocess import check_output
16 from gh_api import get_paged_request, make_auth_header, get_pull_request
16 from gh_api import get_paged_request, make_auth_header, get_pull_request, is_pull_request
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Globals
19 # Globals
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
22 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
23 PER_PAGE = 100
23 PER_PAGE = 100
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Functions
26 # Functions
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 def get_issues(project="ipython/ipython", state="closed", pulls=False):
29 def get_issues(project="ipython/ipython", state="closed", pulls=False):
30 """Get a list of the issues from the Github API."""
30 """Get a list of the issues from the Github API."""
31 which = 'pulls' if pulls else 'issues'
31 which = 'pulls' if pulls else 'issues'
32 url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % (project, which, state, PER_PAGE)
32 url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % (project, which, state, PER_PAGE)
33 return get_paged_request(url, headers=make_auth_header())
33 return get_paged_request(url, headers=make_auth_header())
34
34
35 def round_hour(dt):
35 def round_hour(dt):
36 return dt.replace(minute=0,second=0,microsecond=0)
36 return dt.replace(minute=0,second=0,microsecond=0)
37
37
38 def _parse_datetime(s):
38 def _parse_datetime(s):
39 """Parse dates in the format returned by the Github API."""
39 """Parse dates in the format returned by the Github API."""
40 if s:
40 if s:
41 return datetime.strptime(s, ISO8601)
41 return datetime.strptime(s, ISO8601)
42 else:
42 else:
43 return datetime.fromtimestamp(0)
43 return datetime.fromtimestamp(0)
44
44
45
45
46 def issues2dict(issues):
46 def issues2dict(issues):
47 """Convert a list of issues to a dict, keyed by issue number."""
47 """Convert a list of issues to a dict, keyed by issue number."""
48 idict = {}
48 idict = {}
49 for i in issues:
49 for i in issues:
50 idict[i['number']] = i
50 idict[i['number']] = i
51 return idict
51 return idict
52
52
53
54 def is_pull_request(issue):
55 """Return True if the given issue is a pull request."""
56 return bool(issue.get('pull_request', {}).get('html_url', None))
57
58
59 def split_pulls(all_issues, project="ipython/ipython"):
53 def split_pulls(all_issues, project="ipython/ipython"):
60 """split a list of closed issues into non-PR Issues and Pull Requests"""
54 """split a list of closed issues into non-PR Issues and Pull Requests"""
61 pulls = []
55 pulls = []
62 issues = []
56 issues = []
63 for i in all_issues:
57 for i in all_issues:
64 if is_pull_request(i):
58 if is_pull_request(i):
65 pull = get_pull_request(project, i['number'], auth=True)
59 pull = get_pull_request(project, i['number'], auth=True)
66 pulls.append(pull)
60 pulls.append(pull)
67 else:
61 else:
68 issues.append(i)
62 issues.append(i)
69 return issues, pulls
63 return issues, pulls
70
64
71
65
72
66
73 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
67 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
74 """Get all issues closed since a particular point in time. period
68 """Get all issues closed since a particular point in time. period
75 can either be a datetime object, or a timedelta object. In the
69 can either be a datetime object, or a timedelta object. In the
76 latter case, it is used as a time before the present.
70 latter case, it is used as a time before the present.
77 """
71 """
78
72
79 which = 'pulls' if pulls else 'issues'
73 which = 'pulls' if pulls else 'issues'
80
74
81 if isinstance(period, timedelta):
75 if isinstance(period, timedelta):
82 since = round_hour(datetime.utcnow() - period)
76 since = round_hour(datetime.utcnow() - period)
83 else:
77 else:
84 since = period
78 since = period
85 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 url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, since.strftime(ISO8601), PER_PAGE)
86 allclosed = get_paged_request(url, headers=make_auth_header())
80 allclosed = get_paged_request(url, headers=make_auth_header())
87
81
88 filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
82 filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
89 if pulls:
83 if pulls:
90 filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
84 filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
91 # filter out PRs not against master (backports)
85 # filter out PRs not against master (backports)
92 filtered = [ i for i in filtered if i['base']['ref'] == 'master' ]
86 filtered = [ i for i in filtered if i['base']['ref'] == 'master' ]
93 else:
87 else:
94 filtered = [ i for i in filtered if not is_pull_request(i) ]
88 filtered = [ i for i in filtered if not is_pull_request(i) ]
95
89
96 return filtered
90 return filtered
97
91
98
92
99 def sorted_by_field(issues, field='closed_at', reverse=False):
93 def sorted_by_field(issues, field='closed_at', reverse=False):
100 """Return a list of issues sorted by closing date date."""
94 """Return a list of issues sorted by closing date date."""
101 return sorted(issues, key = lambda i:i[field], reverse=reverse)
95 return sorted(issues, key = lambda i:i[field], reverse=reverse)
102
96
103
97
104 def report(issues, show_urls=False):
98 def report(issues, show_urls=False):
105 """Summary report about a list of issues, printing number and title.
99 """Summary report about a list of issues, printing number and title.
106 """
100 """
107 # titles may have unicode in them, so we must encode everything below
101 # titles may have unicode in them, so we must encode everything below
108 if show_urls:
102 if show_urls:
109 for i in issues:
103 for i in issues:
110 role = 'ghpull' if 'merged_at' in i else 'ghissue'
104 role = 'ghpull' if 'merged_at' in i else 'ghissue'
111 print('* :%s:`%d`: %s' % (role, i['number'],
105 print('* :%s:`%d`: %s' % (role, i['number'],
112 i['title'].encode('utf-8')))
106 i['title'].encode('utf-8')))
113 else:
107 else:
114 for i in issues:
108 for i in issues:
115 print('* %d: %s' % (i['number'], i['title'].encode('utf-8')))
109 print('* %d: %s' % (i['number'], i['title'].encode('utf-8')))
116
110
117 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
118 # Main script
112 # Main script
119 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
120
114
121 if __name__ == "__main__":
115 if __name__ == "__main__":
122 # deal with unicode
116 # deal with unicode
123 import codecs
117 import codecs
124 sys.stdout = codecs.getwriter('utf8')(sys.stdout)
118 sys.stdout = codecs.getwriter('utf8')(sys.stdout)
125
119
126 # Whether to add reST urls for all issues in printout.
120 # Whether to add reST urls for all issues in printout.
127 show_urls = True
121 show_urls = True
128
122
129 # By default, search one month back
123 # By default, search one month back
130 tag = None
124 tag = None
131 if len(sys.argv) > 1:
125 if len(sys.argv) > 1:
132 try:
126 try:
133 days = int(sys.argv[1])
127 days = int(sys.argv[1])
134 except:
128 except:
135 tag = sys.argv[1]
129 tag = sys.argv[1]
136 else:
130 else:
137 tag = check_output(['git', 'describe', '--abbrev=0']).strip()
131 tag = check_output(['git', 'describe', '--abbrev=0']).strip()
138
132
139 if tag:
133 if tag:
140 cmd = ['git', 'log', '-1', '--format=%ai', tag]
134 cmd = ['git', 'log', '-1', '--format=%ai', tag]
141 tagday, tz = check_output(cmd).strip().rsplit(' ', 1)
135 tagday, tz = check_output(cmd).strip().rsplit(' ', 1)
142 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
136 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
143 h = int(tz[1:3])
137 h = int(tz[1:3])
144 m = int(tz[3:])
138 m = int(tz[3:])
145 td = timedelta(hours=h, minutes=m)
139 td = timedelta(hours=h, minutes=m)
146 if tz[0] == '-':
140 if tz[0] == '-':
147 since += td
141 since += td
148 else:
142 else:
149 since -= td
143 since -= td
150 else:
144 else:
151 since = datetime.utcnow() - timedelta(days=days)
145 since = datetime.utcnow() - timedelta(days=days)
152
146
153 since = round_hour(since)
147 since = round_hour(since)
154
148
155 print("fetching GitHub stats since %s (tag: %s)" % (since, tag), file=sys.stderr)
149 print("fetching GitHub stats since %s (tag: %s)" % (since, tag), file=sys.stderr)
156 # turn off to play interactively without redownloading, use %run -i
150 # turn off to play interactively without redownloading, use %run -i
157 if 1:
151 if 1:
158 issues = issues_closed_since(since, pulls=False)
152 issues = issues_closed_since(since, pulls=False)
159 pulls = issues_closed_since(since, pulls=True)
153 pulls = issues_closed_since(since, pulls=True)
160
154
161 # For regular reports, it's nice to show them in reverse chronological order
155 # For regular reports, it's nice to show them in reverse chronological order
162 issues = sorted_by_field(issues, reverse=True)
156 issues = sorted_by_field(issues, reverse=True)
163 pulls = sorted_by_field(pulls, reverse=True)
157 pulls = sorted_by_field(pulls, reverse=True)
164
158
165 n_issues, n_pulls = map(len, (issues, pulls))
159 n_issues, n_pulls = map(len, (issues, pulls))
166 n_total = n_issues + n_pulls
160 n_total = n_issues + n_pulls
167
161
168 # Print summary report we can directly include into release notes.
162 # Print summary report we can directly include into release notes.
169
163
170 print()
164 print()
171 since_day = since.strftime("%Y/%m/%d")
165 since_day = since.strftime("%Y/%m/%d")
172 today = datetime.today().strftime("%Y/%m/%d")
166 today = datetime.today().strftime("%Y/%m/%d")
173 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
167 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
174 print()
168 print()
175 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
169 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
176 print()
170 print()
177 if tag:
171 if tag:
178 # print git info, in addition to GitHub info:
172 # print git info, in addition to GitHub info:
179 since_tag = tag+'..'
173 since_tag = tag+'..'
180 cmd = ['git', 'log', '--oneline', since_tag]
174 cmd = ['git', 'log', '--oneline', since_tag]
181 ncommits = len(check_output(cmd).splitlines())
175 ncommits = len(check_output(cmd).splitlines())
182
176
183 author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
177 author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
184 all_authors = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
178 all_authors = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
185 unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
179 unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
186 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
180 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
187 print()
181 print()
188 print('\n'.join(unique_authors))
182 print('\n'.join(unique_authors))
189 print()
183 print()
190
184
191 print()
185 print()
192 print("We closed a total of %d issues, %d pull requests and %d regular issues;\n"
186 print("We closed a total of %d issues, %d pull requests and %d regular issues;\n"
193 "this is the full list (generated with the script \n"
187 "this is the full list (generated with the script \n"
194 ":file:`tools/github_stats.py`):" % (n_total, n_pulls, n_issues))
188 ":file:`tools/github_stats.py`):" % (n_total, n_pulls, n_issues))
195 print()
189 print()
196 print('Pull Requests (%d):\n' % n_pulls)
190 print('Pull Requests (%d):\n' % n_pulls)
197 report(pulls, show_urls)
191 report(pulls, show_urls)
198 print()
192 print()
199 print('Issues (%d):\n' % n_issues)
193 print('Issues (%d):\n' % n_issues)
200 report(issues, show_urls)
194 report(issues, show_urls)
General Comments 0
You need to be logged in to leave comments. Login now