Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame^] | 1 | import re |
| 2 | from argparse import ArgumentParser |
| 3 | from os.path import dirname, abspath |
| 4 | from subprocess import check_output, call |
| 5 | |
| 6 | |
| 7 | def git(command, repo): |
| 8 | return check_output('git '+command, cwd=repo, shell=True).decode() |
| 9 | |
| 10 | |
| 11 | def repo_state_bad(mock_repo): |
| 12 | status = git('status', mock_repo) |
| 13 | if 'You are in the middle of an am session' in status: |
| 14 | print(f'Mock repo at {mock_repo} needs cleanup:\n') |
| 15 | call('git status', shell=True) |
| 16 | return True |
| 17 | |
| 18 | |
| 19 | def cleanup_old_patches(mock_repo): |
| 20 | print('cleaning up old patches:') |
| 21 | call('rm -vf /tmp/*.mock.patch', shell=True) |
| 22 | call('find . -name "*.rej" -print -delete', shell=True, cwd=mock_repo) |
| 23 | |
| 24 | |
| 25 | def find_initial_cpython_rev(): |
| 26 | with open('lastsync.txt') as source: |
| 27 | return source.read().strip() |
| 28 | |
| 29 | |
| 30 | def cpython_revs_affecting_mock(cpython_repo, start): |
| 31 | revs = git(f'log --no-merges --format=%H {start}.. ' |
| 32 | f'-- Lib/unittest/mock.py Lib/unittest/test/testmock/', |
| 33 | repo=cpython_repo).split() |
| 34 | revs.reverse() |
| 35 | print(f'{len(revs)} patches to backport') |
| 36 | return revs |
| 37 | |
| 38 | |
| 39 | def has_been_backported(mock_repo, cpython_rev): |
| 40 | backport_rev = git(f'log --format=%H --grep "Backports: {cpython_rev}"', |
| 41 | repo=mock_repo).strip() |
| 42 | if backport_rev: |
| 43 | print(f'{cpython_rev} backported in {backport_rev}') |
| 44 | return True |
| 45 | print(f'{cpython_rev} has not been backported') |
| 46 | |
| 47 | |
| 48 | def extract_patch_for(cpython_repo, rev): |
| 49 | return git(f'format-patch -1 --no-stat --keep-subject --signoff --stdout {rev}', |
| 50 | repo=cpython_repo) |
| 51 | |
| 52 | |
| 53 | def munge(rev, patch): |
| 54 | |
| 55 | sign_off = 'Signed-off-by:' |
| 56 | patch = patch.replace(sign_off, f'Backports: {rev}\n{sign_off}', 1) |
| 57 | |
| 58 | for pattern, sub in ( |
| 59 | ('(a|b)/Lib/unittest/mock.py', r'\1/mock/mock.py'), |
| 60 | ('(a|b)/Lib/unittest/test/testmock/(.+)', r'\1/mock/tests/\2'), |
| 61 | ('(a|b)/Misc/NEWS', r'\1/NEWS'), |
| 62 | ): |
| 63 | patch = re.sub(pattern, sub, patch) |
| 64 | return patch |
| 65 | |
| 66 | |
| 67 | def apply_patch(mock_repo, rev, patch): |
| 68 | patch_path = f'/tmp/{rev}.mock.patch' |
| 69 | |
| 70 | with open(patch_path, 'w') as target: |
| 71 | target.write(patch) |
| 72 | print(f'wrote {patch_path}') |
| 73 | |
| 74 | call(f'git am -k --reject {patch_path}', cwd=mock_repo, shell=True) |
| 75 | |
| 76 | |
| 77 | def main(): |
| 78 | args = parse_args() |
| 79 | |
| 80 | if repo_state_bad(args.mock): |
| 81 | return |
| 82 | |
| 83 | cleanup_old_patches(args.mock) |
| 84 | |
| 85 | initial_cpython_rev = find_initial_cpython_rev() |
| 86 | |
| 87 | revs = cpython_revs_affecting_mock(args.cpython, initial_cpython_rev) |
| 88 | for rev in revs: |
| 89 | |
| 90 | if has_been_backported(args.mock, rev): |
| 91 | continue |
| 92 | |
| 93 | patch = extract_patch_for(args.cpython, rev) |
| 94 | patch = munge(rev, patch) |
| 95 | apply_patch(args.mock, rev, patch) |
| 96 | break |
| 97 | |
| 98 | |
| 99 | def parse_args(): |
| 100 | parser = ArgumentParser() |
| 101 | parser.add_argument('--cpython', default='../cpython') |
| 102 | parser.add_argument('--mock', default=abspath(dirname(__file__))) |
| 103 | return parser.parse_args() |
| 104 | |
| 105 | |
| 106 | if __name__ == '__main__': |
| 107 | main() |