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