##// END OF EJS Templates
Start making backport_pr work on Python 3
Thomas Kluyver -
Show More
@@ -1,161 +1,164 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 branch [PR]
5 Usage: backport_pr.py branch [PR]
6
6
7 e.g.:
7 e.g.:
8
8
9 python tools/backport_pr.py 0.13.1 123
9 python tools/backport_pr.py 0.13.1 123
10
10
11 to backport PR #123 onto branch 0.13.1
11 to backport PR #123 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 """
20 """
21
21
22 from __future__ import print_function
22 from __future__ import print_function
23
23
24 import os
24 import os
25 import re
25 import re
26 import sys
26 import sys
27
27
28 from subprocess import Popen, PIPE, check_call, check_output
28 from subprocess import Popen, PIPE, check_call, check_output
29 from urllib import urlopen
29 try:
30 from urllib.request import urlopen
31 except:
32 from urllib import urlopen
30
33
31 from gh_api import (
34 from gh_api import (
32 get_issues_list,
35 get_issues_list,
33 get_pull_request,
36 get_pull_request,
34 get_pull_request_files,
37 get_pull_request_files,
35 is_pull_request,
38 is_pull_request,
36 get_milestone_id,
39 get_milestone_id,
37 )
40 )
38
41
39 def find_rejects(root='.'):
42 def find_rejects(root='.'):
40 for dirname, dirs, files in os.walk(root):
43 for dirname, dirs, files in os.walk(root):
41 for fname in files:
44 for fname in files:
42 if fname.endswith('.rej'):
45 if fname.endswith('.rej'):
43 yield os.path.join(dirname, fname)
46 yield os.path.join(dirname, fname)
44
47
45 def get_current_branch():
48 def get_current_branch():
46 branches = check_output(['git', 'branch'])
49 branches = check_output(['git', 'branch'])
47 for branch in branches.splitlines():
50 for branch in branches.splitlines():
48 if branch.startswith('*'):
51 if branch.startswith(b'*'):
49 return branch[1:].strip()
52 return branch[1:].strip().decode('utf-8')
50
53
51 def backport_pr(branch, num, project='ipython/ipython'):
54 def backport_pr(branch, num, project='ipython/ipython'):
52 current_branch = get_current_branch()
55 current_branch = get_current_branch()
53 if branch != current_branch:
56 if branch != current_branch:
54 check_call(['git', 'checkout', branch])
57 check_call(['git', 'checkout', branch])
55 check_call(['git', 'pull'])
58 check_call(['git', 'pull'])
56 pr = get_pull_request(project, num, auth=True)
59 pr = get_pull_request(project, num, auth=True)
57 files = get_pull_request_files(project, num, auth=True)
60 files = get_pull_request_files(project, num, auth=True)
58 patch_url = pr['patch_url']
61 patch_url = pr['patch_url']
59 title = pr['title']
62 title = pr['title']
60 description = pr['body']
63 description = pr['body']
61 fname = "PR%i.patch" % num
64 fname = "PR%i.patch" % num
62 if os.path.exists(fname):
65 if os.path.exists(fname):
63 print("using patch from {fname}".format(**locals()))
66 print("using patch from {fname}".format(**locals()))
64 with open(fname) as f:
67 with open(fname) as f:
65 patch = f.read()
68 patch = f.read()
66 else:
69 else:
67 req = urlopen(patch_url)
70 req = urlopen(patch_url)
68 patch = req.read()
71 patch = req.read()
69
72
70 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
73 msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description
71 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
74 check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE)
72 a,b = check.communicate(patch)
75 a,b = check.communicate(patch)
73
76
74 if check.returncode:
77 if check.returncode:
75 print("patch did not apply, saving to {fname}".format(**locals()))
78 print("patch did not apply, saving to {fname}".format(**locals()))
76 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
79 print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals()))
77 print("then run tools/backport_pr.py {num} again".format(**locals()))
80 print("then run tools/backport_pr.py {num} again".format(**locals()))
78 if not os.path.exists(fname):
81 if not os.path.exists(fname):
79 with open(fname, 'wb') as f:
82 with open(fname, 'wb') as f:
80 f.write(patch)
83 f.write(patch)
81 return 1
84 return 1
82
85
83 p = Popen(['git', 'apply'], stdin=PIPE)
86 p = Popen(['git', 'apply'], stdin=PIPE)
84 a,b = p.communicate(patch)
87 a,b = p.communicate(patch)
85
88
86 filenames = [ f['filename'] for f in files ]
89 filenames = [ f['filename'] for f in files ]
87
90
88 check_call(['git', 'add'] + filenames)
91 check_call(['git', 'add'] + filenames)
89
92
90 check_call(['git', 'commit', '-m', msg])
93 check_call(['git', 'commit', '-m', msg])
91
94
92 print("PR #%i applied, with msg:" % num)
95 print("PR #%i applied, with msg:" % num)
93 print()
96 print()
94 print(msg)
97 print(msg)
95 print()
98 print()
96
99
97 if branch != current_branch:
100 if branch != current_branch:
98 check_call(['git', 'checkout', current_branch])
101 check_call(['git', 'checkout', current_branch])
99
102
100 return 0
103 return 0
101
104
102 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
105 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
103
106
104 def already_backported(branch, since_tag=None):
107 def already_backported(branch, since_tag=None):
105 """return set of PRs that have been backported already"""
108 """return set of PRs that have been backported already"""
106 if since_tag is None:
109 if since_tag is None:
107 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
110 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
108 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
111 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
109 lines = check_output(cmd).decode('utf8')
112 lines = check_output(cmd).decode('utf8')
110 return set(int(num) for num in backport_re.findall(lines))
113 return set(int(num) for num in backport_re.findall(lines))
111
114
112 def should_backport(labels=None, milestone=None):
115 def should_backport(labels=None, milestone=None):
113 """return set of PRs marked for backport"""
116 """return set of PRs marked for backport"""
114 if labels is None and milestone is None:
117 if labels is None and milestone is None:
115 raise ValueError("Specify one of labels or milestone.")
118 raise ValueError("Specify one of labels or milestone.")
116 elif labels is not None and milestone is not None:
119 elif labels is not None and milestone is not None:
117 raise ValueError("Specify only one of labels or milestone.")
120 raise ValueError("Specify only one of labels or milestone.")
118 if labels is not None:
121 if labels is not None:
119 issues = get_issues_list("ipython/ipython",
122 issues = get_issues_list("ipython/ipython",
120 labels=labels,
123 labels=labels,
121 state='closed',
124 state='closed',
122 auth=True,
125 auth=True,
123 )
126 )
124 else:
127 else:
125 milestone_id = get_milestone_id("ipython/ipython", milestone,
128 milestone_id = get_milestone_id("ipython/ipython", milestone,
126 auth=True)
129 auth=True)
127 issues = get_issues_list("ipython/ipython",
130 issues = get_issues_list("ipython/ipython",
128 milestone=milestone_id,
131 milestone=milestone_id,
129 state='closed',
132 state='closed',
130 auth=True,
133 auth=True,
131 )
134 )
132
135
133 should_backport = set()
136 should_backport = set()
134 for issue in issues:
137 for issue in issues:
135 if not is_pull_request(issue):
138 if not is_pull_request(issue):
136 continue
139 continue
137 pr = get_pull_request("ipython/ipython", issue['number'],
140 pr = get_pull_request("ipython/ipython", issue['number'],
138 auth=True)
141 auth=True)
139 if not pr['merged']:
142 if not pr['merged']:
140 print ("Marked PR closed without merge: %i" % pr['number'])
143 print ("Marked PR closed without merge: %i" % pr['number'])
141 continue
144 continue
142 should_backport.add(pr['number'])
145 should_backport.add(pr['number'])
143 return should_backport
146 return should_backport
144
147
145 if __name__ == '__main__':
148 if __name__ == '__main__':
146
149
147 if len(sys.argv) < 2:
150 if len(sys.argv) < 2:
148 print(__doc__)
151 print(__doc__)
149 sys.exit(1)
152 sys.exit(1)
150
153
151 if len(sys.argv) < 3:
154 if len(sys.argv) < 3:
152 milestone = sys.argv[1]
155 milestone = sys.argv[1]
153 branch = milestone.split('.')[0] + '.x'
156 branch = milestone.split('.')[0] + '.x'
154 already = already_backported(branch)
157 already = already_backported(branch)
155 should = should_backport(milestone=milestone)
158 should = should_backport(milestone=milestone)
156 print ("The following PRs should be backported:")
159 print ("The following PRs should be backported:")
157 for pr in sorted(should.difference(already)):
160 for pr in sorted(should.difference(already)):
158 print (pr)
161 print (pr)
159 sys.exit(0)
162 sys.exit(0)
160
163
161 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
164 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
General Comments 0
You need to be logged in to leave comments. Login now