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