##// END OF EJS Templates
recommend GhPro as a replacemetn for custom tools
Matthias Bussonnier -
Show More
@@ -1,189 +1,193 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """
2 """
3 Backport pull requests to a particular branch.
3 Backport pull requests to a particular branch.
4
4
5 Usage: backport_pr.py [org/repository] branch [PR] [PR2]
5 Usage: backport_pr.py [org/repository] branch [PR] [PR2]
6
6
7 e.g.:
7 e.g.:
8
8
9 python tools/backport_pr.py 0.13.1 123 155
9 python tools/backport_pr.py 0.13.1 123 155
10
10
11 to backport PRs #123 and #155 onto branch 0.13.1
11 to backport PRs #123 and #155 onto branch 0.13.1
12
12
13 or
13 or
14
14
15 python tools/backport_pr.py 2.1
15 python tools/backport_pr.py 2.1
16
16
17 to see what PRs are marked for backport with milestone=2.1 that have yet to be applied
17 to see what PRs are marked for backport with milestone=2.1 that have yet to be applied
18 to branch 2.x
18 to branch 2.x
19
19
20 or
20 or
21
21
22 python tools/backport_pr.py jupyter/notebook 0.13.1 123 155
22 python tools/backport_pr.py jupyter/notebook 0.13.1 123 155
23
23
24 to backport PRs #123 and #155 of the `jupyter/notebook` repo onto branch 0.13.1
24 to backport PRs #123 and #155 of the `jupyter/notebook` repo onto branch 0.13.1
25 of that repo.
25 of that repo.
26
26
27 """
27 """
28
28
29 from __future__ import print_function
29 from __future__ import print_function
30
30
31 import os
31 import os
32 import re
32 import re
33 import sys
33 import sys
34
34
35 from subprocess import Popen, PIPE, check_call, check_output
35 from subprocess import Popen, PIPE, check_call, check_output
36 try:
36 try:
37 from urllib.request import urlopen
37 from urllib.request import urlopen
38 except:
38 except:
39 from urllib import urlopen
39 from urllib import urlopen
40
40
41 from gh_api import (
41 from gh_api import (
42 get_issues_list,
42 get_issues_list,
43 get_pull_request,
43 get_pull_request,
44 get_pull_request_files,
44 get_pull_request_files,
45 is_pull_request,
45 is_pull_request,
46 get_milestone_id,
46 get_milestone_id,
47 )
47 )
48
48
49 def find_rejects(root='.'):
49 def find_rejects(root='.'):
50 for dirname, dirs, files in os.walk(root):
50 for dirname, dirs, files in os.walk(root):
51 for fname in files:
51 for fname in files:
52 if fname.endswith('.rej'):
52 if fname.endswith('.rej'):
53 yield os.path.join(dirname, fname)
53 yield os.path.join(dirname, fname)
54
54
55 def get_current_branch():
55 def get_current_branch():
56 branches = check_output(['git', 'branch'])
56 branches = check_output(['git', 'branch'])
57 for branch in branches.splitlines():
57 for branch in branches.splitlines():
58 if branch.startswith(b'*'):
58 if branch.startswith(b'*'):
59 return branch[1:].strip().decode('utf-8')
59 return branch[1:].strip().decode('utf-8')
60
60
61 def backport_pr(branch, num, project='ipython/ipython'):
61 def backport_pr(branch, num, project='ipython/ipython'):
62 current_branch = get_current_branch()
62 current_branch = get_current_branch()
63 if branch != current_branch:
63 if branch != current_branch:
64 check_call(['git', 'checkout', branch])
64 check_call(['git', 'checkout', branch])
65 check_call(['git', 'pull'])
65 check_call(['git', 'pull'])
66 pr = get_pull_request(project, num, auth=True)
66 pr = get_pull_request(project, num, auth=True)
67 files = get_pull_request_files(project, num, auth=True)
67 files = get_pull_request_files(project, num, auth=True)
68 patch_url = pr['patch_url']
68 patch_url = pr['patch_url']
69 title = pr['title']
69 title = pr['title']
70 description = pr['body']
70 description = pr['body']
71 fname = "PR%i.patch" % num
71 fname = "PR%i.patch" % num
72 if os.path.exists(fname):
72 if os.path.exists(fname):
73 print("using patch from {fname}".format(**locals()))
73 print("using patch from {fname}".format(**locals()))
74 with open(fname, 'rb') as f:
74 with open(fname, 'rb') as f:
75 patch = f.read()
75 patch = f.read()
76 else:
76 else:
77 req = urlopen(patch_url)
77 req = urlopen(patch_url)
78 patch = req.read()
78 patch = req.read()
79
79
80 lines = description.splitlines()
80 lines = description.splitlines()
81 if len(lines) > 5:
81 if len(lines) > 5:
82 lines = lines[:5] + ['...']
82 lines = lines[:5] + ['...']
83 description = '\n'.join(lines)
83 description = '\n'.join(lines)
84
84
85 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
85 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
86 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
86 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
87 a,b = check.communicate(patch)
87 a,b = check.communicate(patch)
88
88
89 if check.returncode:
89 if check.returncode:
90 print("patch did not apply, saving to {fname}".format(**locals()))
90 print("patch did not apply, saving to {fname}".format(**locals()))
91 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
91 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
92 print("then run tools/backport_pr.py {num} again".format(**locals()))
92 print("then run tools/backport_pr.py {num} again".format(**locals()))
93 if not os.path.exists(fname):
93 if not os.path.exists(fname):
94 with open(fname, 'wb') as f:
94 with open(fname, 'wb') as f:
95 f.write(patch)
95 f.write(patch)
96 return 1
96 return 1
97
97
98 p = Popen(['git', 'apply'], stdin=PIPE)
98 p = Popen(['git', 'apply'], stdin=PIPE)
99 a,b = p.communicate(patch)
99 a,b = p.communicate(patch)
100
100
101 filenames = [ f['filename'] for f in files ]
101 filenames = [ f['filename'] for f in files ]
102
102
103 check_call(['git', 'add'] + filenames)
103 check_call(['git', 'add'] + filenames)
104
104
105 check_call(['git', 'commit', '-m', msg])
105 check_call(['git', 'commit', '-m', msg])
106
106
107 print("PR #%i applied, with msg:" % num)
107 print("PR #%i applied, with msg:" % num)
108 print()
108 print()
109 print(msg)
109 print(msg)
110 print()
110 print()
111
111
112 if branch != current_branch:
112 if branch != current_branch:
113 check_call(['git', 'checkout', current_branch])
113 check_call(['git', 'checkout', current_branch])
114
114
115 return 0
115 return 0
116
116
117 backport_re = re.compile(r"(?:[Bb]ackport|[Mm]erge).*#(\d+)")
117 backport_re = re.compile(r"(?:[Bb]ackport|[Mm]erge).*#(\d+)")
118
118
119 def already_backported(branch, since_tag=None):
119 def already_backported(branch, since_tag=None):
120 """return set of PRs that have been backported already"""
120 """return set of PRs that have been backported already"""
121 if since_tag is None:
121 if since_tag is None:
122 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
122 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
123 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
123 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
124 lines = check_output(cmd).decode('utf8')
124 lines = check_output(cmd).decode('utf8')
125 return set(int(num) for num in backport_re.findall(lines))
125 return set(int(num) for num in backport_re.findall(lines))
126
126
127 def should_backport(labels=None, milestone=None, project='ipython/ipython'):
127 def should_backport(labels=None, milestone=None, project='ipython/ipython'):
128 """return set of PRs marked for backport"""
128 """return set of PRs marked for backport"""
129 if labels is None and milestone is None:
129 if labels is None and milestone is None:
130 raise ValueError("Specify one of labels or milestone.")
130 raise ValueError("Specify one of labels or milestone.")
131 elif labels is not None and milestone is not None:
131 elif labels is not None and milestone is not None:
132 raise ValueError("Specify only one of labels or milestone.")
132 raise ValueError("Specify only one of labels or milestone.")
133 if labels is not None:
133 if labels is not None:
134 issues = get_issues_list(project,
134 issues = get_issues_list(project,
135 labels=labels,
135 labels=labels,
136 state='closed',
136 state='closed',
137 auth=True,
137 auth=True,
138 )
138 )
139 else:
139 else:
140 milestone_id = get_milestone_id(project, milestone,
140 milestone_id = get_milestone_id(project, milestone,
141 auth=True)
141 auth=True)
142 issues = get_issues_list(project,
142 issues = get_issues_list(project,
143 milestone=milestone_id,
143 milestone=milestone_id,
144 state='closed',
144 state='closed',
145 auth=True,
145 auth=True,
146 )
146 )
147
147
148 should_backport = set()
148 should_backport = set()
149 for issue in issues:
149 for issue in issues:
150 if not is_pull_request(issue):
150 if not is_pull_request(issue):
151 continue
151 continue
152 pr = get_pull_request(project, issue['number'],
152 pr = get_pull_request(project, issue['number'],
153 auth=True)
153 auth=True)
154 if not pr['merged']:
154 if not pr['merged']:
155 print ("Marked PR closed without merge: %i" % pr['number'])
155 print ("Marked PR closed without merge: %i" % pr['number'])
156 continue
156 continue
157 if pr['base']['ref'] != 'master':
157 if pr['base']['ref'] != 'master':
158 continue
158 continue
159 should_backport.add(pr['number'])
159 should_backport.add(pr['number'])
160 return should_backport
160 return should_backport
161
161
162 if __name__ == '__main__':
162 if __name__ == '__main__':
163 project = 'ipython/ipython'
163 project = 'ipython/ipython'
164
165 print("DEPRECATE: backport_pr.py is deprecated and is is now recommended"
166 "to install `ghpro` from PyPI.", file=sys.stderr)
167
164 args = list(sys.argv)
168 args = list(sys.argv)
165 if len(args) >= 2:
169 if len(args) >= 2:
166 if '/' in args[1]:
170 if '/' in args[1]:
167 project = args[1]
171 project = args[1]
168 del args[1]
172 del args[1]
169
173
170 if len(args) < 2:
174 if len(args) < 2:
171 print(__doc__)
175 print(__doc__)
172 sys.exit(1)
176 sys.exit(1)
173
177
174 if len(args) < 3:
178 if len(args) < 3:
175 milestone = args[1]
179 milestone = args[1]
176 branch = milestone.split('.')[0] + '.x'
180 branch = milestone.split('.')[0] + '.x'
177 already = already_backported(branch)
181 already = already_backported(branch)
178 should = should_backport(milestone=milestone, project=project)
182 should = should_backport(milestone=milestone, project=project)
179 print ("The following PRs should be backported:")
183 print ("The following PRs should be backported:")
180 for pr in sorted(should.difference(already)):
184 for pr in sorted(should.difference(already)):
181 print (pr)
185 print (pr)
182 sys.exit(0)
186 sys.exit(0)
183
187
184 for prno in map(int, args[2:]):
188 for prno in map(int, args[2:]):
185 print("Backporting PR #%i" % prno)
189 print("Backporting PR #%i" % prno)
186 rc = backport_pr(args[1], prno, project=project)
190 rc = backport_pr(args[1], prno, project=project)
187 if rc:
191 if rc:
188 print("Backporting PR #%i failed" % prno)
192 print("Backporting PR #%i failed" % prno)
189 sys.exit(rc)
193 sys.exit(rc)
@@ -1,231 +1,235 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 To generate a report for IPython 2.0, run:
4 To generate a report for IPython 2.0, run:
5
5
6 python github_stats.py --milestone 2.0 --since-tag rel-1.0.0
6 python github_stats.py --milestone 2.0 --since-tag rel-1.0.0
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Imports
9 # Imports
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 import codecs
14 import codecs
15 import sys
15 import sys
16
16
17 from argparse import ArgumentParser
17 from argparse import ArgumentParser
18 from datetime import datetime, timedelta
18 from datetime import datetime, timedelta
19 from subprocess import check_output
19 from subprocess import check_output
20
20
21 from gh_api import (
21 from gh_api import (
22 get_paged_request, make_auth_header, get_pull_request, is_pull_request,
22 get_paged_request, make_auth_header, get_pull_request, is_pull_request,
23 get_milestone_id, get_issues_list, get_authors,
23 get_milestone_id, get_issues_list, get_authors,
24 )
24 )
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Globals
26 # Globals
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
29 ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
30 PER_PAGE = 100
30 PER_PAGE = 100
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Functions
33 # Functions
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 def round_hour(dt):
36 def round_hour(dt):
37 return dt.replace(minute=0,second=0,microsecond=0)
37 return dt.replace(minute=0,second=0,microsecond=0)
38
38
39 def _parse_datetime(s):
39 def _parse_datetime(s):
40 """Parse dates in the format returned by the Github API."""
40 """Parse dates in the format returned by the Github API."""
41 if s:
41 if s:
42 return datetime.strptime(s, ISO8601)
42 return datetime.strptime(s, ISO8601)
43 else:
43 else:
44 return datetime.fromtimestamp(0)
44 return datetime.fromtimestamp(0)
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 def split_pulls(all_issues, project="ipython/ipython"):
53 def split_pulls(all_issues, project="ipython/ipython"):
54 """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"""
55 pulls = []
55 pulls = []
56 issues = []
56 issues = []
57 for i in all_issues:
57 for i in all_issues:
58 if is_pull_request(i):
58 if is_pull_request(i):
59 pull = get_pull_request(project, i['number'], auth=True)
59 pull = get_pull_request(project, i['number'], auth=True)
60 pulls.append(pull)
60 pulls.append(pull)
61 else:
61 else:
62 issues.append(i)
62 issues.append(i)
63 return issues, pulls
63 return issues, pulls
64
64
65
65
66 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
66 def issues_closed_since(period=timedelta(days=365), project="ipython/ipython", pulls=False):
67 """Get all issues closed since a particular point in time. period
67 """Get all issues closed since a particular point in time. period
68 can either be a datetime object, or a timedelta object. In the
68 can either be a datetime object, or a timedelta object. In the
69 latter case, it is used as a time before the present.
69 latter case, it is used as a time before the present.
70 """
70 """
71
71
72 which = 'pulls' if pulls else 'issues'
72 which = 'pulls' if pulls else 'issues'
73
73
74 if isinstance(period, timedelta):
74 if isinstance(period, timedelta):
75 since = round_hour(datetime.utcnow() - period)
75 since = round_hour(datetime.utcnow() - period)
76 else:
76 else:
77 since = period
77 since = period
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)
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 allclosed = get_paged_request(url, headers=make_auth_header())
79 allclosed = get_paged_request(url, headers=make_auth_header())
80
80
81 filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
81 filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
82 if pulls:
82 if pulls:
83 filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
83 filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
84 # filter out PRs not against master (backports)
84 # filter out PRs not against master (backports)
85 filtered = [ i for i in filtered if i['base']['ref'] == 'master' ]
85 filtered = [ i for i in filtered if i['base']['ref'] == 'master' ]
86 else:
86 else:
87 filtered = [ i for i in filtered if not is_pull_request(i) ]
87 filtered = [ i for i in filtered if not is_pull_request(i) ]
88
88
89 return filtered
89 return filtered
90
90
91
91
92 def sorted_by_field(issues, field='closed_at', reverse=False):
92 def sorted_by_field(issues, field='closed_at', reverse=False):
93 """Return a list of issues sorted by closing date date."""
93 """Return a list of issues sorted by closing date date."""
94 return sorted(issues, key = lambda i:i[field], reverse=reverse)
94 return sorted(issues, key = lambda i:i[field], reverse=reverse)
95
95
96
96
97 def report(issues, show_urls=False):
97 def report(issues, show_urls=False):
98 """Summary report about a list of issues, printing number and title."""
98 """Summary report about a list of issues, printing number and title."""
99 if show_urls:
99 if show_urls:
100 for i in issues:
100 for i in issues:
101 role = 'ghpull' if 'merged_at' in i else 'ghissue'
101 role = 'ghpull' if 'merged_at' in i else 'ghissue'
102 print(u'* :%s:`%d`: %s' % (role, i['number'],
102 print(u'* :%s:`%d`: %s' % (role, i['number'],
103 i['title'].replace(u'`', u'``')))
103 i['title'].replace(u'`', u'``')))
104 else:
104 else:
105 for i in issues:
105 for i in issues:
106 print(u'* %d: %s' % (i['number'], i['title'].replace(u'`', u'``')))
106 print(u'* %d: %s' % (i['number'], i['title'].replace(u'`', u'``')))
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # Main script
109 # Main script
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111
111
112 if __name__ == "__main__":
112 if __name__ == "__main__":
113
114 print("DEPRECATE: backport_pr.py is deprecated and is is now recommended"
115 "to install `ghpro` from PyPI.", file=sys.stderr)
116
113 # deal with unicode
117 # deal with unicode
114 if sys.version_info < (3,):
118 if sys.version_info < (3,):
115 sys.stdout = codecs.getwriter('utf8')(sys.stdout)
119 sys.stdout = codecs.getwriter('utf8')(sys.stdout)
116
120
117 # Whether to add reST urls for all issues in printout.
121 # Whether to add reST urls for all issues in printout.
118 show_urls = True
122 show_urls = True
119
123
120 parser = ArgumentParser()
124 parser = ArgumentParser()
121 parser.add_argument('--since-tag', type=str,
125 parser.add_argument('--since-tag', type=str,
122 help="The git tag to use for the starting point (typically the last major release)."
126 help="The git tag to use for the starting point (typically the last major release)."
123 )
127 )
124 parser.add_argument('--milestone', type=str,
128 parser.add_argument('--milestone', type=str,
125 help="The GitHub milestone to use for filtering issues [optional]."
129 help="The GitHub milestone to use for filtering issues [optional]."
126 )
130 )
127 parser.add_argument('--days', type=int,
131 parser.add_argument('--days', type=int,
128 help="The number of days of data to summarize (use this or --since-tag)."
132 help="The number of days of data to summarize (use this or --since-tag)."
129 )
133 )
130 parser.add_argument('--project', type=str, default="ipython/ipython",
134 parser.add_argument('--project', type=str, default="ipython/ipython",
131 help="The project to summarize."
135 help="The project to summarize."
132 )
136 )
133 parser.add_argument('--links', action='store_true', default=False,
137 parser.add_argument('--links', action='store_true', default=False,
134 help="Include links to all closed Issues and PRs in the output."
138 help="Include links to all closed Issues and PRs in the output."
135 )
139 )
136
140
137 opts = parser.parse_args()
141 opts = parser.parse_args()
138 tag = opts.since_tag
142 tag = opts.since_tag
139
143
140 # set `since` from days or git tag
144 # set `since` from days or git tag
141 if opts.days:
145 if opts.days:
142 since = datetime.utcnow() - timedelta(days=opts.days)
146 since = datetime.utcnow() - timedelta(days=opts.days)
143 else:
147 else:
144 if not tag:
148 if not tag:
145 tag = check_output(['git', 'describe', '--abbrev=0']).strip().decode('utf8')
149 tag = check_output(['git', 'describe', '--abbrev=0']).strip().decode('utf8')
146 cmd = ['git', 'log', '-1', '--format=%ai', tag]
150 cmd = ['git', 'log', '-1', '--format=%ai', tag]
147 tagday, tz = check_output(cmd).strip().decode('utf8').rsplit(' ', 1)
151 tagday, tz = check_output(cmd).strip().decode('utf8').rsplit(' ', 1)
148 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
152 since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
149 h = int(tz[1:3])
153 h = int(tz[1:3])
150 m = int(tz[3:])
154 m = int(tz[3:])
151 td = timedelta(hours=h, minutes=m)
155 td = timedelta(hours=h, minutes=m)
152 if tz[0] == '-':
156 if tz[0] == '-':
153 since += td
157 since += td
154 else:
158 else:
155 since -= td
159 since -= td
156
160
157 since = round_hour(since)
161 since = round_hour(since)
158
162
159 milestone = opts.milestone
163 milestone = opts.milestone
160 project = opts.project
164 project = opts.project
161
165
162 print("fetching GitHub stats since %s (tag: %s, milestone: %s)" % (since, tag, milestone), file=sys.stderr)
166 print("fetching GitHub stats since %s (tag: %s, milestone: %s)" % (since, tag, milestone), file=sys.stderr)
163 if milestone:
167 if milestone:
164 milestone_id = get_milestone_id(project=project, milestone=milestone,
168 milestone_id = get_milestone_id(project=project, milestone=milestone,
165 auth=True)
169 auth=True)
166 issues_and_pulls = get_issues_list(project=project,
170 issues_and_pulls = get_issues_list(project=project,
167 milestone=milestone_id,
171 milestone=milestone_id,
168 state='closed',
172 state='closed',
169 auth=True,
173 auth=True,
170 )
174 )
171 issues, pulls = split_pulls(issues_and_pulls, project=project)
175 issues, pulls = split_pulls(issues_and_pulls, project=project)
172 else:
176 else:
173 issues = issues_closed_since(since, project=project, pulls=False)
177 issues = issues_closed_since(since, project=project, pulls=False)
174 pulls = issues_closed_since(since, project=project, pulls=True)
178 pulls = issues_closed_since(since, project=project, pulls=True)
175
179
176 # For regular reports, it's nice to show them in reverse chronological order
180 # For regular reports, it's nice to show them in reverse chronological order
177 issues = sorted_by_field(issues, reverse=True)
181 issues = sorted_by_field(issues, reverse=True)
178 pulls = sorted_by_field(pulls, reverse=True)
182 pulls = sorted_by_field(pulls, reverse=True)
179
183
180 n_issues, n_pulls = map(len, (issues, pulls))
184 n_issues, n_pulls = map(len, (issues, pulls))
181 n_total = n_issues + n_pulls
185 n_total = n_issues + n_pulls
182
186
183 # Print summary report we can directly include into release notes.
187 # Print summary report we can directly include into release notes.
184
188
185 print()
189 print()
186 since_day = since.strftime("%Y/%m/%d")
190 since_day = since.strftime("%Y/%m/%d")
187 today = datetime.today().strftime("%Y/%m/%d")
191 today = datetime.today().strftime("%Y/%m/%d")
188 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
192 print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
189 print()
193 print()
190 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
194 print("These lists are automatically generated, and may be incomplete or contain duplicates.")
191 print()
195 print()
192
196
193 ncommits = 0
197 ncommits = 0
194 all_authors = []
198 all_authors = []
195 if tag:
199 if tag:
196 # print git info, in addition to GitHub info:
200 # print git info, in addition to GitHub info:
197 since_tag = tag+'..'
201 since_tag = tag+'..'
198 cmd = ['git', 'log', '--oneline', since_tag]
202 cmd = ['git', 'log', '--oneline', since_tag]
199 ncommits += len(check_output(cmd).splitlines())
203 ncommits += len(check_output(cmd).splitlines())
200
204
201 author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
205 author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
202 all_authors.extend(check_output(author_cmd).decode('utf-8', 'replace').splitlines())
206 all_authors.extend(check_output(author_cmd).decode('utf-8', 'replace').splitlines())
203
207
204 pr_authors = []
208 pr_authors = []
205 for pr in pulls:
209 for pr in pulls:
206 pr_authors.extend(get_authors(pr))
210 pr_authors.extend(get_authors(pr))
207 ncommits = len(pr_authors) + ncommits - len(pulls)
211 ncommits = len(pr_authors) + ncommits - len(pulls)
208 author_cmd = ['git', 'check-mailmap'] + pr_authors
212 author_cmd = ['git', 'check-mailmap'] + pr_authors
209 with_email = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
213 with_email = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
210 all_authors.extend([ u'* ' + a.split(' <')[0] for a in with_email ])
214 all_authors.extend([ u'* ' + a.split(' <')[0] for a in with_email ])
211 unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
215 unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
212
216
213 print("We closed %d issues and merged %d pull requests." % (n_issues, n_pulls))
217 print("We closed %d issues and merged %d pull requests." % (n_issues, n_pulls))
214 if milestone:
218 if milestone:
215 print("The full list can be seen `on GitHub <https://github.com/{project}/issues?q=milestone%3A{milestone}+>`__".format(project=project,milestone=milestone)
219 print("The full list can be seen `on GitHub <https://github.com/{project}/issues?q=milestone%3A{milestone}+>`__".format(project=project,milestone=milestone)
216 )
220 )
217
221
218 print()
222 print()
219 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
223 print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
220 print()
224 print()
221 print('\n'.join(unique_authors))
225 print('\n'.join(unique_authors))
222
226
223 if opts.links:
227 if opts.links:
224 print()
228 print()
225 print("GitHub issues and pull requests:")
229 print("GitHub issues and pull requests:")
226 print()
230 print()
227 print('Pull Requests (%d):\n' % n_pulls)
231 print('Pull Requests (%d):\n' % n_pulls)
228 report(pulls, show_urls)
232 report(pulls, show_urls)
229 print()
233 print()
230 print('Issues (%d):\n' % n_issues)
234 print('Issues (%d):\n' % n_issues)
231 report(issues, show_urls)
235 report(issues, show_urls)
General Comments 0
You need to be logged in to leave comments. Login now