backport_pr.py
192 lines
| 5.6 KiB
| text/x-python
|
PythonLexer
/ tools / backport_pr.py
MinRK
|
r8347 | #!/usr/bin/env python | ||
MinRK
|
r8346 | """ | ||
Backport pull requests to a particular branch. | ||||
Jonathan Frederic
|
r21985 | Usage: backport_pr.py [org/repository] branch [PR] [PR2] | ||
MinRK
|
r8346 | |||
e.g.: | ||||
MinRK
|
r16761 | python tools/backport_pr.py 0.13.1 123 155 | ||
MinRK
|
r8346 | |||
Jonathan Frederic
|
r21985 | to backport PRs #123 and #155 onto branch 0.13.1 | ||
MinRK
|
r8346 | |||
MinRK
|
r12424 | or | ||
MinRK
|
r16167 | python tools/backport_pr.py 2.1 | ||
MinRK
|
r12424 | |||
MinRK
|
r16167 | to see what PRs are marked for backport with milestone=2.1 that have yet to be applied | ||
Jonathan Frederic
|
r21985 | to branch 2.x | ||
or | ||||
python tools/backport_pr.py jupyter/notebook 0.13.1 123 155 | ||||
to backport PRs #123 and #155 of the `jupyter/notebook` repo onto branch 0.13.1 | ||||
of that repo. | ||||
MinRK
|
r12424 | |||
MinRK
|
r8346 | """ | ||
import os | ||||
MinRK
|
r12424 | import re | ||
MinRK
|
r8346 | import sys | ||
MinRK
|
r12424 | |||
MinRK
|
r8346 | from subprocess import Popen, PIPE, check_call, check_output | ||
Thomas Kluyver
|
r16178 | try: | ||
from urllib.request import urlopen | ||||
except: | ||||
MinRK
|
r16761 | from urllib import urlopen | ||
MinRK
|
r8346 | |||
MinRK
|
r12424 | from gh_api import ( | ||
get_issues_list, | ||||
get_pull_request, | ||||
get_pull_request_files, | ||||
is_pull_request, | ||||
Skipper Seabold
|
r13169 | get_milestone_id, | ||
MinRK
|
r12424 | ) | ||
MinRK
|
r8346 | |||
def find_rejects(root='.'): | ||||
for dirname, dirs, files in os.walk(root): | ||||
for fname in files: | ||||
if fname.endswith('.rej'): | ||||
yield os.path.join(dirname, fname) | ||||
def get_current_branch(): | ||||
branches = check_output(['git', 'branch']) | ||||
for branch in branches.splitlines(): | ||||
Thomas Kluyver
|
r16178 | if branch.startswith(b'*'): | ||
return branch[1:].strip().decode('utf-8') | ||||
MinRK
|
r8346 | |||
def backport_pr(branch, num, project='ipython/ipython'): | ||||
current_branch = get_current_branch() | ||||
if branch != current_branch: | ||||
check_call(['git', 'checkout', branch]) | ||||
Thomas Kluyver
|
r12276 | check_call(['git', 'pull']) | ||
MinRK
|
r12129 | pr = get_pull_request(project, num, auth=True) | ||
files = get_pull_request_files(project, num, auth=True) | ||||
MinRK
|
r8346 | patch_url = pr['patch_url'] | ||
title = pr['title'] | ||||
description = pr['body'] | ||||
fname = "PR%i.patch" % num | ||||
if os.path.exists(fname): | ||||
print("using patch from {fname}".format(**locals())) | ||||
Min RK
|
r18786 | with open(fname, 'rb') as f: | ||
MinRK
|
r8346 | patch = f.read() | ||
else: | ||||
req = urlopen(patch_url) | ||||
patch = req.read() | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r16762 | lines = description.splitlines() | ||
if len(lines) > 5: | ||||
lines = lines[:5] + ['...'] | ||||
description = '\n'.join(lines) | ||||
MinRK
|
r8346 | msg = "Backport PR #%i: %s" % (num, title) + '\n\n' + description | ||
check = Popen(['git', 'apply', '--check', '--verbose'], stdin=PIPE) | ||||
a,b = check.communicate(patch) | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r8346 | if check.returncode: | ||
print("patch did not apply, saving to {fname}".format(**locals())) | ||||
print("edit {fname} until `cat {fname} | git apply --check` succeeds".format(**locals())) | ||||
print("then run tools/backport_pr.py {num} again".format(**locals())) | ||||
if not os.path.exists(fname): | ||||
with open(fname, 'wb') as f: | ||||
f.write(patch) | ||||
return 1 | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r8346 | p = Popen(['git', 'apply'], stdin=PIPE) | ||
a,b = p.communicate(patch) | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r12129 | filenames = [ f['filename'] for f in files ] | ||
MinRK
|
r8346 | |||
MinRK
|
r12341 | check_call(['git', 'add'] + filenames) | ||
Skipper Seabold
|
r13169 | |||
MinRK
|
r12341 | check_call(['git', 'commit', '-m', msg]) | ||
Skipper Seabold
|
r13169 | |||
MinRK
|
r12341 | print("PR #%i applied, with msg:" % num) | ||
print() | ||||
print(msg) | ||||
print() | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r8346 | if branch != current_branch: | ||
check_call(['git', 'checkout', current_branch]) | ||||
Skipper Seabold
|
r13169 | |||
MinRK
|
r8346 | return 0 | ||
Min RK
|
r20874 | backport_re = re.compile(r"(?:[Bb]ackport|[Mm]erge).*#(\d+)") | ||
MinRK
|
r12424 | |||
def already_backported(branch, since_tag=None): | ||||
"""return set of PRs that have been backported already""" | ||||
if since_tag is None: | ||||
since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip() | ||||
cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline'] | ||||
lines = check_output(cmd).decode('utf8') | ||||
return set(int(num) for num in backport_re.findall(lines)) | ||||
Jonathan Frederic
|
r21984 | def should_backport(labels=None, milestone=None, project='ipython/ipython'): | ||
MinRK
|
r12424 | """return set of PRs marked for backport""" | ||
Skipper Seabold
|
r13169 | if labels is None and milestone is None: | ||
raise ValueError("Specify one of labels or milestone.") | ||||
elif labels is not None and milestone is not None: | ||||
raise ValueError("Specify only one of labels or milestone.") | ||||
if labels is not None: | ||||
Jonathan Frederic
|
r21984 | issues = get_issues_list(project, | ||
Skipper Seabold
|
r13169 | labels=labels, | ||
state='closed', | ||||
auth=True, | ||||
) | ||||
else: | ||||
Jonathan Frederic
|
r21984 | milestone_id = get_milestone_id(project, milestone, | ||
Skipper Seabold
|
r13169 | auth=True) | ||
Jonathan Frederic
|
r21984 | issues = get_issues_list(project, | ||
Skipper Seabold
|
r13169 | milestone=milestone_id, | ||
state='closed', | ||||
auth=True, | ||||
) | ||||
MinRK
|
r12424 | should_backport = set() | ||
for issue in issues: | ||||
if not is_pull_request(issue): | ||||
continue | ||||
Jonathan Frederic
|
r21984 | pr = get_pull_request(project, issue['number'], | ||
Skipper Seabold
|
r13169 | auth=True) | ||
MinRK
|
r12424 | if not pr['merged']: | ||
print ("Marked PR closed without merge: %i" % pr['number']) | ||||
continue | ||||
MinRK
|
r16673 | if pr['base']['ref'] != 'master': | ||
continue | ||||
MinRK
|
r12424 | should_backport.add(pr['number']) | ||
return should_backport | ||||
MinRK
|
r8346 | if __name__ == '__main__': | ||
Jonathan Frederic
|
r21985 | project = 'ipython/ipython' | ||
Matthias Bussonnier
|
r22897 | |||
luz.paz
|
r24132 | print("DEPRECATE: backport_pr.py is deprecated and it is now recommended" | ||
Matthias Bussonnier
|
r22897 | "to install `ghpro` from PyPI.", file=sys.stderr) | ||
Jonathan Frederic
|
r21985 | args = list(sys.argv) | ||
if len(args) >= 2: | ||||
if '/' in args[1]: | ||||
project = args[1] | ||||
del args[1] | ||||
if len(args) < 2: | ||||
MinRK
|
r8346 | print(__doc__) | ||
sys.exit(1) | ||||
Skipper Seabold
|
r13169 | |||
Jonathan Frederic
|
r21985 | if len(args) < 3: | ||
milestone = args[1] | ||||
MinRK
|
r16167 | branch = milestone.split('.')[0] + '.x' | ||
MinRK
|
r12424 | already = already_backported(branch) | ||
Jonathan Frederic
|
r21985 | should = should_backport(milestone=milestone, project=project) | ||
MinRK
|
r12424 | print ("The following PRs should be backported:") | ||
MinRK
|
r13952 | for pr in sorted(should.difference(already)): | ||
MinRK
|
r12424 | print (pr) | ||
sys.exit(0) | ||||
Jonathan Frederic
|
r21985 | |||
for prno in map(int, args[2:]): | ||||
MinRK
|
r16761 | print("Backporting PR #%i" % prno) | ||
Jonathan Frederic
|
r21985 | rc = backport_pr(args[1], prno, project=project) | ||
MinRK
|
r16761 | if rc: | ||
print("Backporting PR #%i failed" % prno) | ||||
sys.exit(rc) | ||||