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