Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 1 | import re |
| 2 | from argparse import ArgumentParser |
Chris Withers | 871b526 | 2019-04-27 15:04:13 +0100 | [diff] [blame] | 3 | from os.path import dirname, abspath, join |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 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() |
Chris Withers | 76523d3 | 2019-04-27 15:47:35 +0100 | [diff] [blame] | 35 | print(f'{len(revs)} patches that may need backporting') |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 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'), |
Chris Withers | 1879443 | 2019-04-28 19:41:05 +0100 | [diff] [blame] | 60 | (r'(a|b)/Lib/unittest/test/testmock/(\S+)', r'\1/mock/tests/\2'), |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 61 | ('(a|b)/Misc/NEWS', r'\1/NEWS'), |
Chris Withers | 7c85fb2 | 2019-04-29 07:21:04 +0100 | [diff] [blame] | 62 | ('(a|b)/NEWS.d/next/[^/]+/(.+\.rst)', r'\1/NEWS.d/\2'), |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 63 | ): |
| 64 | patch = re.sub(pattern, sub, patch) |
| 65 | return patch |
| 66 | |
| 67 | |
| 68 | def apply_patch(mock_repo, rev, patch): |
| 69 | patch_path = f'/tmp/{rev}.mock.patch' |
| 70 | |
| 71 | with open(patch_path, 'w') as target: |
| 72 | target.write(patch) |
| 73 | print(f'wrote {patch_path}') |
| 74 | |
Chris Withers | 28e7566 | 2019-04-28 19:41:23 +0100 | [diff] [blame] | 75 | call(f'git am -k ' |
| 76 | f'--include "mock/*" --include NEWS --include "NEWS.d/*" ' |
| 77 | f'--reject {patch_path} ', |
| 78 | cwd=mock_repo, shell=True) |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 79 | |
| 80 | |
Chris Withers | 871b526 | 2019-04-27 15:04:13 +0100 | [diff] [blame] | 81 | def update_last_sync(mock_repo, rev): |
| 82 | with open(join(mock_repo, 'lastsync.txt'), 'w') as target: |
| 83 | target.write(rev+'\n') |
| 84 | print(f'update lastsync.txt to {rev}') |
| 85 | |
Chris Withers | a336518 | 2019-04-27 17:35:49 +0100 | [diff] [blame] | 86 | |
| 87 | def rev_from_mock_patch(text): |
| 88 | match = re.search('Backports: ([a-z0-9]+)', text) |
| 89 | return match.group(1) |
| 90 | |
| 91 | |
| 92 | def skip_current(mock_repo, reason): |
| 93 | text = git('am --show-current-patch', repo=mock_repo) |
| 94 | rev = rev_from_mock_patch(text) |
| 95 | git('am --abort', repo=mock_repo) |
| 96 | print(f'skipping {rev}') |
| 97 | update_last_sync(mock_repo, rev) |
Chris Withers | 4f25a9b | 2019-04-28 15:57:58 +0100 | [diff] [blame] | 98 | call(f'git commit -m "Backports: {rev}, skipped: {reason}" lastsync.txt', shell=True, cwd=mock_repo) |
Chris Withers | a336518 | 2019-04-27 17:35:49 +0100 | [diff] [blame] | 99 | cleanup_old_patches(mock_repo) |
| 100 | |
| 101 | |
Chris Withers | e6fe351 | 2019-04-30 20:33:10 +0100 | [diff] [blame] | 102 | def commit_last_sync(revs, mock_repo): |
| 103 | print('Yay! All caught up!') |
| 104 | if len(revs): |
| 105 | git('commit -m "latest sync point" lastsync.txt', repo=mock_repo) |
| 106 | |
| 107 | |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 108 | def main(): |
| 109 | args = parse_args() |
| 110 | |
Chris Withers | a336518 | 2019-04-27 17:35:49 +0100 | [diff] [blame] | 111 | if args.skip_current: |
| 112 | return skip_current(args.mock, args.skip_reason) |
| 113 | |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 114 | if repo_state_bad(args.mock): |
| 115 | return |
| 116 | |
| 117 | cleanup_old_patches(args.mock) |
| 118 | |
| 119 | initial_cpython_rev = find_initial_cpython_rev() |
| 120 | |
| 121 | revs = cpython_revs_affecting_mock(args.cpython, initial_cpython_rev) |
| 122 | for rev in revs: |
| 123 | |
| 124 | if has_been_backported(args.mock, rev): |
Chris Withers | 871b526 | 2019-04-27 15:04:13 +0100 | [diff] [blame] | 125 | update_last_sync(args.mock, rev) |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 126 | continue |
| 127 | |
| 128 | patch = extract_patch_for(args.cpython, rev) |
| 129 | patch = munge(rev, patch) |
| 130 | apply_patch(args.mock, rev, patch) |
| 131 | break |
| 132 | |
Chris Withers | e6fe351 | 2019-04-30 20:33:10 +0100 | [diff] [blame] | 133 | else: |
| 134 | commit_last_sync(revs, args.mock) |
| 135 | |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 136 | |
| 137 | def parse_args(): |
| 138 | parser = ArgumentParser() |
| 139 | parser.add_argument('--cpython', default='../cpython') |
| 140 | parser.add_argument('--mock', default=abspath(dirname(__file__))) |
Chris Withers | a336518 | 2019-04-27 17:35:49 +0100 | [diff] [blame] | 141 | parser.add_argument('--skip-current', action='store_true') |
| 142 | parser.add_argument('--skip-reason', default='it has no changes needed here.') |
Chris Withers | fb1f5b3 | 2019-04-26 20:29:03 +0100 | [diff] [blame] | 143 | return parser.parse_args() |
| 144 | |
| 145 | |
| 146 | if __name__ == '__main__': |
| 147 | main() |