]>
Commit | Line | Data |
---|---|---|
27e88dec | 1 | #!/usr/bin/env python3 -B |
2d0c7adb WD |
2 | |
3 | # This script turns one or more diff files in the patches dir (which is | |
4 | # expected to be a checkout of the rsync-patches git repo) into a branch | |
5 | # in the main rsync git checkout. This allows the applied patch to be | |
6 | # merged with the latest rsync changes and tested. To update the diff | |
7 | # with the resulting changes, see the patch-update script. | |
8 | ||
9 | import os, sys, re, argparse, glob | |
10 | ||
11 | sys.path = ['packaging'] + sys.path | |
12 | ||
13 | from pkglib import * | |
14 | ||
15 | def main(): | |
16 | global created, info, local_branch | |
17 | ||
18 | cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) | |
19 | ||
20 | local_branch = get_patch_branches(args.base_branch) | |
21 | ||
22 | if args.delete_local_branches: | |
23 | for name in sorted(local_branch): | |
24 | branch = f"patch/{args.base_branch}/{name}" | |
25 | cmd_chk(['git', 'branch', '-D', branch]) | |
26 | local_branch = set() | |
27 | ||
28 | if args.add_missing: | |
29 | for fn in sorted(glob.glob(f"{args.patches_dir}/*.diff")): | |
30 | name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn)) | |
31 | if name not in local_branch and fn not in args.patch_files: | |
32 | args.patch_files.append(fn) | |
33 | ||
34 | if not args.patch_files: | |
35 | return | |
36 | ||
37 | for fn in args.patch_files: | |
38 | if not fn.endswith('.diff'): | |
39 | die(f"Filename is not a .diff file: {fn}") | |
40 | if not os.path.isfile(fn): | |
41 | die(f"File not found: {fn}") | |
42 | ||
43 | scanned = set() | |
44 | info = { } | |
45 | ||
46 | patch_list = [ ] | |
47 | for fn in args.patch_files: | |
48 | m = re.match(r'^(?P<dir>.*?)(?P<name>[^/]+)\.diff$', fn) | |
49 | patch = argparse.Namespace(**m.groupdict()) | |
50 | if patch.name in scanned: | |
51 | continue | |
52 | patch.fn = fn | |
53 | ||
54 | lines = [ ] | |
55 | commit_hash = None | |
56 | with open(patch.fn, 'r', encoding='utf-8') as fh: | |
57 | for line in fh: | |
58 | m = re.match(r'^based-on: (\S+)', line) | |
59 | if m: | |
60 | commit_hash = m[1] | |
61 | break | |
62 | if (re.match(r'^index .*\.\..* \d', line) | |
63 | or re.match(r'^diff --git ', line) | |
64 | or re.match(r'^--- (old|a)/', line)): | |
65 | break | |
364d302b | 66 | lines.append(re.sub(r'\s*\Z', "\n", line, 1)) |
2d0c7adb WD |
67 | info_txt = ''.join(lines).strip() + "\n" |
68 | lines = None | |
69 | ||
70 | parent = args.base_branch | |
71 | patches = re.findall(r'patch -p1 <%s/(\S+)\.diff' % args.patches_dir, info_txt) | |
72 | if patches: | |
73 | last = patches.pop() | |
74 | if last != patch.name: | |
75 | warn(f"No identity patch line in {patch.fn}") | |
76 | patches.append(last) | |
77 | if patches: | |
78 | parent = patches.pop() | |
79 | if parent not in scanned: | |
80 | diff_fn = patch.dir + parent + '.diff' | |
81 | if not os.path.isfile(diff_fn): | |
82 | die(f"Failed to find parent of {patch.fn}: {parent}") | |
83 | # Add parent to args.patch_files so that we will look for the | |
84 | # parent's parent. Any duplicates will be ignored. | |
85 | args.patch_files.append(diff_fn) | |
86 | else: | |
87 | warn(f"No patch lines found in {patch.fn}") | |
88 | ||
89 | info[patch.name] = [ parent, info_txt, commit_hash ] | |
90 | ||
91 | patch_list.append(patch) | |
92 | ||
93 | created = set() | |
94 | for patch in patch_list: | |
95 | create_branch(patch) | |
96 | ||
97 | cmd_chk(['git', 'checkout', args.base_branch]) | |
98 | ||
99 | ||
100 | def create_branch(patch): | |
101 | if patch.name in created: | |
102 | return | |
103 | created.add(patch.name) | |
104 | ||
105 | parent, info_txt, commit_hash = info[patch.name] | |
106 | parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff') | |
107 | ||
108 | if parent.name == args.base_branch: | |
109 | parent_branch = commit_hash if commit_hash else args.base_branch | |
110 | else: | |
111 | create_branch(parent) | |
112 | parent_branch = '/'.join(['patch', args.base_branch, parent.name]) | |
113 | ||
114 | branch = '/'.join(['patch', args.base_branch, patch.name]) | |
115 | print("\n" + '=' * 64) | |
116 | print(f"Processing {branch} ({parent_branch})") | |
117 | ||
118 | if patch.name in local_branch: | |
119 | cmd_chk(['git', 'branch', '-D', branch]) | |
120 | ||
121 | cmd_chk(['git', 'checkout', '-b', branch, parent_branch]) | |
122 | ||
123 | info_fn = 'PATCH.' + patch.name | |
124 | with open(info_fn, 'w', encoding='utf-8') as fh: | |
125 | fh.write(info_txt) | |
126 | cmd_chk(['git', 'add', info_fn]) | |
127 | ||
128 | with open(patch.fn, 'r', encoding='utf-8') as fh: | |
129 | patch_txt = fh.read() | |
130 | ||
131 | cmd_run('patch -p1'.split(), input=patch_txt) | |
132 | ||
133 | for fn in glob.glob('*.orig') + glob.glob('*/*.orig'): | |
134 | os.unlink(fn) | |
135 | ||
136 | pos = 0 | |
137 | new_file_re = re.compile(r'\nnew file mode (?P<mode>\d+)\s+--- /dev/null\s+\+\+\+ b/(?P<fn>.+)') | |
138 | while True: | |
139 | m = new_file_re.search(patch_txt, pos) | |
140 | if not m: | |
141 | break | |
142 | os.chmod(m['fn'], int(m['mode'], 8)) | |
143 | cmd_chk(['git', 'add', m['fn']]) | |
144 | pos = m.end() | |
145 | ||
146 | while True: | |
147 | cmd_chk('git status'.split()) | |
148 | ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ') | |
149 | if ans == '': | |
150 | break | |
151 | cmd_chk("git add " + ans, shell=True) | |
152 | ||
153 | while True: | |
154 | s = cmd_run(['git', 'commit', '-a', '-m', f"Creating branch from {patch.name}.diff."]) | |
155 | if not s.returncode: | |
156 | break | |
157 | s = cmd_run(['/bin/zsh']) | |
158 | if s.returncode: | |
159 | die('Aborting due to shell error code') | |
160 | ||
161 | ||
162 | if __name__ == '__main__': | |
163 | parser = argparse.ArgumentParser(description="Create a git patch branch from an rsync patch file.", add_help=False) | |
164 | parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.") | |
165 | parser.add_argument('--add-missing', '-a', action='store_true', help="Add a branch for every patches/*.diff that doesn't have a branch.") | |
166 | parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.") | |
167 | parser.add_argument('--delete', dest='delete_local_branches', action='store_true', help="Delete all the local patch/BASE/* branches, not just the ones that are being recreated.") | |
168 | parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.") | |
169 | parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.") | |
170 | parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") | |
171 | args = parser.parse_args() | |
172 | main() | |
173 | ||
174 | # vim: sw=4 et |