1import re 2from argparse import ArgumentParser 3from os.path import dirname, abspath, join 4from subprocess import check_output, call 5 6 7def git(command, repo): 8 return check_output('git '+command, cwd=repo, shell=True).decode() 9 10 11def 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 19def 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 25def find_initial_cpython_rev(): 26 with open('lastsync.txt') as source: 27 return source.read().strip() 28 29 30def 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 that may need backporting') 36 return revs 37 38 39def 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 48def 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 53def 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 (r'(a|b)/Lib/unittest/test/testmock/(\S+)', r'\1/mock/tests/\2'), 61 ('(a|b)/Misc/NEWS', r'\1/NEWS'), 62 ('(a|b)/NEWS.d/next/[^/]+/(.+\.rst)', r'\1/NEWS.d/\2'), 63 ): 64 patch = re.sub(pattern, sub, patch) 65 return patch 66 67 68def 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 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) 79 80 81def 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 86 87def rev_from_mock_patch(text): 88 match = re.search('Backports: ([a-z0-9]+)', text) 89 return match.group(1) 90 91 92def 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) 98 call(f'git commit -m "Backports: {rev}, skipped: {reason}" lastsync.txt', shell=True, cwd=mock_repo) 99 cleanup_old_patches(mock_repo) 100 101 102def 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 108def main(): 109 args = parse_args() 110 111 if args.skip_current: 112 return skip_current(args.mock, args.skip_reason) 113 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): 125 update_last_sync(args.mock, rev) 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 133 else: 134 commit_last_sync(revs, args.mock) 135 136 137def parse_args(): 138 parser = ArgumentParser() 139 parser.add_argument('--cpython', default='../cpython') 140 parser.add_argument('--mock', default=abspath(dirname(__file__))) 141 parser.add_argument('--skip-current', action='store_true') 142 parser.add_argument('--skip-reason', default='it has no changes needed here.') 143 return parser.parse_args() 144 145 146if __name__ == '__main__': 147 main() 148