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