##// END OF EJS Templates
use git tags in github_stats...
MinRK -
Show More
@@ -1,154 +1,189
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 urllib import urlopen
16 from urllib import urlopen
16
17
17 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
18 # Globals
19 # Globals
19 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
20
21
21 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
22 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
22 PER_PAGE = 100
23 PER_PAGE = 100
23
24
24 element_pat = re.compile(r'<(.+?)>')
25 element_pat = re.compile(r'<(.+?)>')
25 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
26 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
26
27
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28 # Functions
29 # Functions
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
30
31
31 def parse_link_header(headers):
32 def parse_link_header(headers):
32 link_s = headers.get('link', '')
33 link_s = headers.get('link', '')
33 urls = element_pat.findall(link_s)
34 urls = element_pat.findall(link_s)
34 rels = rel_pat.findall(link_s)
35 rels = rel_pat.findall(link_s)
35 d = {}
36 d = {}
36 for rel,url in zip(rels, urls):
37 for rel,url in zip(rels, urls):
37 d[rel] = url
38 d[rel] = url
38 return d
39 return d
39
40
40 def get_paged_request(url):
41 def get_paged_request(url):
41 """get a full list, handling APIv3's paging"""
42 """get a full list, handling APIv3's paging"""
42 results = []
43 results = []
43 while url:
44 while url:
44 print("fetching %s" % url, file=sys.stderr)
45 print("fetching %s" % url, file=sys.stderr)
45 f = urlopen(url)
46 f = urlopen(url)
46 results.extend(json.load(f))
47 results.extend(json.load(f))
47 links = parse_link_header(f.headers)
48 links = parse_link_header(f.headers)
48 url = links.get('next')
49 url = links.get('next')
49 return results
50 return results
50
51
51 def get_issues(project="ipython/ipython", state="closed", pulls=False):
52 def get_issues(project="ipython/ipython", state="closed", pulls=False):
52 """Get a list of the issues from the Github API."""
53 """Get a list of the issues from the Github API."""
53 which = 'pulls' if pulls else 'issues'
54 which = 'pulls' if pulls else 'issues'
54 url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % (project, which, state, PER_PAGE)
55 url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % (project, which, state, PER_PAGE)
55 return get_paged_request(url)
56 return get_paged_request(url)
56
57
57
58
58 def _parse_datetime(s):
59 def _parse_datetime(s):
59 """Parse dates in the format returned by the Github API."""
60 """Parse dates in the format returned by the Github API."""
60 if s:
61 if s:
61 return datetime.strptime(s, ISO8601)
62 return datetime.strptime(s, ISO8601)
62 else:
63 else:
63 return datetime.fromtimestamp(0)
64 return datetime.fromtimestamp(0)
64
65
65
66
66 def issues2dict(issues):
67 def issues2dict(issues):
67 """Convert a list of issues to a dict, keyed by issue number."""
68 """Convert a list of issues to a dict, keyed by issue number."""
68 idict = {}
69 idict = {}
69 for i in issues:
70 for i in issues:
70 idict[i['number']] = i
71 idict[i['number']] = i
71 return idict
72 return idict
72
73
73
74
74 def is_pull_request(issue):
75 def is_pull_request(issue):
75 """Return True if the given issue is a pull request."""
76 """Return True if the given issue is a pull request."""
76 return 'pull_request_url' in issue
77 return 'pull_request_url' in issue
77
78
78
79
79 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
80 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
80 """Get all issues closed since a particular point in time. period
81 """Get all issues closed since a particular point in time. period
81 can either be a datetime object, or a timedelta object. In the
82 can either be a datetime object, or a timedelta object. In the
82 latter case, it is used as a time before the present."""
83 latter case, it is used as a time before the present."""
83
84
84 which = 'pulls' if pulls else 'issues'
85 which = 'pulls' if pulls else 'issues'
85
86
86 if isinstance(period, timedelta):
87 if isinstance(period, timedelta):
87 period = datetime.now() - period
88 period = datetime.now() - period
88 url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, period.strftime(ISO8601), PER_PAGE)
89 url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, period.strftime(ISO8601), PER_PAGE)
89 allclosed = get_paged_request(url)
90 allclosed = get_paged_request(url)
90 # allclosed = get_issues(project=project, state='closed', pulls=pulls, since=period)
91 # allclosed = get_issues(project=project, state='closed', pulls=pulls, since=period)
91 filtered = [i for i in allclosed if _parse_datetime(i['closed_at']) > period]
92 filtered = [i for i in allclosed if _parse_datetime(i['closed_at']) > period]
92
93
93 # exclude rejected PRs
94 # exclude rejected PRs
94 if pulls:
95 if pulls:
95 filtered = [ pr for pr in filtered if pr['merged_at'] ]
96 filtered = [ pr for pr in filtered if pr['merged_at'] ]
96
97
97 return filtered
98 return filtered
98
99
99
100
100 def sorted_by_field(issues, field='closed_at', reverse=False):
101 def sorted_by_field(issues, field='closed_at', reverse=False):
101 """Return a list of issues sorted by closing date date."""
102 """Return a list of issues sorted by closing date date."""
102 return sorted(issues, key = lambda i:i[field], reverse=reverse)
103 return sorted(issues, key = lambda i:i[field], reverse=reverse)
103
104
104
105
105 def report(issues, show_urls=False):
106 def report(issues, show_urls=False):
106 """Summary report about a list of issues, printing number and title.
107 """Summary report about a list of issues, printing number and title.
107 """
108 """
108 # titles may have unicode in them, so we must encode everything below
109 # titles may have unicode in them, so we must encode everything below
109 if show_urls:
110 if show_urls:
110 for i in issues:
111 for i in issues:
111 role = 'ghpull' if 'merged_at' in i else 'ghissue'
112 role = 'ghpull' if 'merged_at' in i else 'ghissue'
112 print('* :%s:`%d`: %s' % (role, i['number'],
113 print('* :%s:`%d`: %s' % (role, i['number'],
113 i['title'].encode('utf-8')))
114 i['title'].encode('utf-8')))
114 else:
115 else:
115 for i in issues:
116 for i in issues:
116 print('* %d: %s' % (i['number'], i['title'].encode('utf-8')))
117 print('* %d: %s' % (i['number'], i['title'].encode('utf-8')))
117
118
118 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
119 # Main script
120 # Main script
120 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
121
122
122 if __name__ == "__main__":
123 if __name__ == "__main__":
123 # Whether to add reST urls for all issues in printout.
124 # Whether to add reST urls for all issues in printout.
124 show_urls = True
125 show_urls = True
125
126
126 # By default, search one month back
127 # By default, search one month back
128 tag = None
127 if len(sys.argv) > 1:
129 if len(sys.argv) > 1:
130 try:
128 days = int(sys.argv[1])
131 days = int(sys.argv[1])
132 except:
133 tag = sys.argv[1]
129 else:
134 else:
130 days = 30
135 tag = check_output(['git', 'describe', '--abbrev=0']).strip()
131
136
137 if tag:
138 cmd = ['git', 'log', '-1', '--format=%ai', tag]
139 tagday, tz = check_output(cmd).strip().rsplit(' ', 1)
140 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
141 else:
142 since = datetime.now() - timedelta(days=days)
143
144 print("fetching GitHub stats since %s (tag: %s)" % (since, tag), file=sys.stderr)
132 # turn off to play interactively without redownloading, use %run -i
145 # turn off to play interactively without redownloading, use %run -i
133 if 1:
146 if 1:
134 issues = issues_closed_since(timedelta(days=days), pulls=False)
147 issues = issues_closed_since(since, pulls=False)
135 pulls = issues_closed_since(timedelta(days=days), pulls=True)
148 pulls = issues_closed_since(since, pulls=True)
136
149
137 # For regular reports, it's nice to show them in reverse chronological order
150 # For regular reports, it's nice to show them in reverse chronological order
138 issues = sorted_by_field(issues, reverse=True)
151 issues = sorted_by_field(issues, reverse=True)
139 pulls = sorted_by_field(pulls, reverse=True)
152 pulls = sorted_by_field(pulls, reverse=True)
140
153
141 n_issues, n_pulls = map(len, (issues, pulls))
154 n_issues, n_pulls = map(len, (issues, pulls))
142 n_total = n_issues + n_pulls
155 n_total = n_issues + n_pulls
143
156
144 # Print summary report we can directly include into release notes.
157 # Print summary report we can directly include into release notes.
145 print("GitHub stats for the last %d days." % days)
158 print()
146 print("We closed a total of %d issues, %d pull requests and %d regular \n"
159 since_day = since.strftime("%Y/%m/%d")
147 "issues; this is the full list (generated with the script \n"
160 today = datetime.today().strftime("%Y/%m/%d")
148 "`tools/github_stats.py`):" % (n_total, n_pulls, n_issues))
161 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
162 print()
163 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
164 print()
165 if tag:
166 # print git info, in addition to GitHub info:
167 since_tag = tag+'..'
168 cmd = ['git', 'log', '--oneline', since_tag]
169 ncommits = len(check_output(cmd).splitlines())
170
171 author_cmd = ['git', 'log', '--format=* %aN', since_tag]
172 all_authors = check_output(author_cmd).splitlines()
173 unique_authors = sorted(set(all_authors))
174
175 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
176 print()
177 print('\n'.join(unique_authors))
178 print()
179
180 print()
181 print("We closed a total of %d issues, %d pull requests and %d regular issues;\n"
182 "this is the full list (generated with the script \n"
183 ":file:`tools/github_stats.py`):" % (n_total, n_pulls, n_issues))
149 print()
184 print()
150 print('Pull Requests (%d):\n' % n_pulls)
185 print('Pull Requests (%d):\n' % n_pulls)
151 report(pulls, show_urls)
186 report(pulls, show_urls)
152 print()
187 print()
153 print('Issues (%d):\n' % n_issues)
188 print('Issues (%d):\n' % n_issues)
154 report(issues, show_urls)
189 report(issues, show_urls)
General Comments 0
You need to be logged in to leave comments. Login now