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