1: #!/usr/bin/env -S python3 -B
2:
3: # This script is used to turn one or more of the "patch/BASE/*" branches
4: # into one or more diffs in the "patches" directory. Pass the option
5: # --gen if you want generated files in the diffs. Pass the name of
6: # one or more diffs if you want to just update a subset of all the
7: # diffs.
8:
9: import os, sys, re, argparse, time, shutil
10:
11: sys.path = ['packaging'] + sys.path
12:
13: from pkglib import *
14:
15: MAKE_GEN_CMDS = [
16: './prepare-source'.split(),
17: 'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi',
18: 'make -C build gen'.split(),
19: ]
20: TMP_DIR = "patches.gen"
21:
22: os.environ['GIT_MERGE_AUTOEDIT'] = 'no'
23:
24: def main():
25: global master_commit, parent_patch, description, completed, last_touch
26:
27: if not os.path.isdir(args.patches_dir):
28: die(f'No "{args.patches_dir}" directory was found.')
29: if not os.path.isdir('.git'):
30: die('No ".git" directory present in the current dir.')
31:
32: starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)
33:
34: master_commit = latest_git_hash(args.base_branch)
35:
36: if cmd_txt_chk(['packaging/prep-auto-dir']) == '':
37: die('You must setup an auto-build-save dir to use this script.')
38:
39: if args.gen:
40: if os.path.lexists(TMP_DIR):
41: die(f'"{TMP_DIR}" must not exist in the current directory.')
42: gen_files = get_gen_files()
43: os.mkdir(TMP_DIR, 0o700)
44: for cmd in MAKE_GEN_CMDS:
45: cmd_chk(cmd)
46: cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])
47:
48: last_touch = int(time.time())
49:
50: # Start by finding all patches so that we can load all possible parents.
51: patches = sorted(list(get_patch_branches(args.base_branch)))
52:
53: parent_patch = { }
54: description = { }
55:
56: for patch in patches:
57: branch = f"patch/{args.base_branch}/{patch}"
58: desc = ''
59: proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"])
60: in_diff = False
61: for line in proc.stdout:
62: if in_diff:
63: if not re.match(r'^[ +]', line):
64: continue
65: line = line[1:]
66: m = re.search(r'patch -p1 <patches/(\S+)\.diff', line)
67: if m and m[1] != patch:
68: parpat = parent_patch[patch] = m[1]
69: if not parpat in patches:
70: die(f"Parent of {patch} is not a local branch: {parpat}")
71: desc += line
72: elif re.match(r'^@@ ', line):
73: in_diff = True
74: description[patch] = desc
75: proc.communicate()
76:
77: if args.patch_files: # Limit the list of patches to actually process
78: valid_patches = patches
79: patches = [ ]
80: for fn in args.patch_files:
81: name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
82: if name not in valid_patches:
83: die(f"Local branch not available for patch: {name}")
84: patches.append(name)
85:
86: completed = set()
87:
88: for patch in patches:
89: if patch in completed:
90: continue
91: if not update_patch(patch):
92: break
93:
94: if args.gen:
95: shutil.rmtree(TMP_DIR)
96:
97: while last_touch >= int(time.time()):
98: time.sleep(1)
99: cmd_chk(['git', 'checkout', starting_branch])
100: cmd_chk(['packaging/prep-auto-dir'], discard='output')
101:
102:
103: def update_patch(patch):
104: global last_touch
105:
106: completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.
107:
108: parent = parent_patch.get(patch, None)
109: if parent:
110: if parent not in completed:
111: if not update_patch(parent):
112: return 0
113: based_on = parent = f"patch/{args.base_branch}/{parent}"
114: else:
115: parent = args.base_branch
116: based_on = master_commit
117:
118: print(f"======== {patch} ========")
119:
120: while args.gen and last_touch >= int(time.time()):
121: time.sleep(1)
122:
123: branch = f"patch/{args.base_branch}/{patch}"
124: s = cmd_run(['git', 'checkout', branch])
125: if s.returncode != 0:
126: return 0
127:
128: s = cmd_run(['git', 'merge', based_on])
129: ok = s.returncode == 0
130: skip_shell = False
131: if not ok or args.cmd or args.make or args.shell:
132: cmd_chk(['packaging/prep-auto-dir'], discard='output')
133: if not ok:
134: print(f'"git merge {based_on}" incomplete -- please fix.')
135: if not run_a_shell(parent, patch):
136: return 0
137: if not args.make and not args.cmd:
138: skip_shell = True
139: if args.make:
140: if cmd_run(['packaging/smart-make']).returncode != 0:
141: if not run_a_shell(parent, patch):
142: return 0
143: if not args.cmd:
144: skip_shell = True
145: if args.cmd:
146: if cmd_run(args.cmd).returncode != 0:
147: if not run_a_shell(parent, patch):
148: return 0
149: skip_shell = True
150: if args.shell and not skip_shell:
151: if not run_a_shell(parent, patch):
152: return 0
153:
154: with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh:
155: fh.write(description[patch])
156: fh.write(f"\nbased-on: {based_on}\n")
157:
158: if args.gen:
159: gen_files = get_gen_files()
160: for cmd in MAKE_GEN_CMDS:
161: cmd_chk(cmd)
162: cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"])
163: else:
164: gen_files = [ ]
165: last_touch = int(time.time())
166:
167: proc = cmd_pipe(['git', 'diff', based_on])
168: skipping = False
169: for line in proc.stdout:
170: if skipping:
171: if not re.match(r'^diff --git a/', line):
172: continue
173: skipping = False
174: elif re.match(r'^diff --git a/PATCH', line):
175: skipping = True
176: continue
177: if not re.match(r'^index ', line):
178: fh.write(line)
179: proc.communicate()
180:
181: if args.gen:
182: e_tmp_dir = re.escape(TMP_DIR)
183: diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))
184: minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
185: plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
186:
187: if parent == args.base_branch:
188: parent_dir = 'master'
189: else:
190: m = re.search(r'([^/]+)$', parent)
191: parent_dir = m[1]
192:
193: proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"])
194: for line in proc.stdout:
195: line = diff_re.sub(r'\1 a/\2 b/\3', line)
196: line = minus_re.sub(r'--- a/\1', line)
197: line = plus_re.sub(r'+++ b/\1', line)
198: fh.write(line)
199: proc.communicate()
200:
201: return 1
202:
203:
204: def run_a_shell(parent, patch):
205: m = re.search(r'([^/]+)$', parent)
206: parent_dir = m[1]
207: os.environ['PS1'] = f"[{parent_dir}] {patch}: "
208:
209: while True:
210: s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
211: if s.returncode != 0:
212: ans = input("Abort? [n/y] ")
213: if re.match(r'^y', ans, flags=re.I):
214: return False
215: continue
216: cur_branch, is_clean, status_txt = check_git_status(0)
217: if is_clean:
218: break
219: print(status_txt, end='')
220:
221: cmd_run('rm -f build/*.o build/*/*.o')
222:
223: return True
224:
225:
226: if __name__ == '__main__':
227: parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False)
228: parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
229: parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
230: parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.")
231: parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.")
232: parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.")
233: parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.')
234: parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
235: parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
236: parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
237: args = parser.parse_args()
238: if args.gen == '':
239: args.gen = args.patches_dir
240: elif args.gen is not None:
241: args.patches_dir = args.gen
242: main()
243:
244: # vim: sw=4 et ft=python
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>