• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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