#!/usr/bin/env -S python3 -B # This script is used to turn one or more of the "patch/BASE/*" branches# into one or more diffs in the "patches" directory. Pass the option# --gen if you want generated files in the diffs. Pass the name of# one or more diffs if you want to just update a subset of all the# diffs. import os, sys, re, argparse, time, shutil sys.path = ['packaging'] + sys.path from pkglib import * MAKE_GEN_CMDS = [ './prepare-source'.split(), 'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi', 'make -C build gen'.split(), ]TMP_DIR = "patches.gen" os.environ['GIT_MERGE_AUTOEDIT'] = 'no' def main(): global master_commit, parent_patch, description, completed, last_touch if not os.path.isdir(args.patches_dir): die(f'No "{args.patches_dir}" directory was found.') if not os.path.isdir('.git'): die('No ".git" directory present in the current dir.') starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) master_commit = latest_git_hash(args.base_branch) if cmd_txt_chk(['packaging/prep-auto-dir']).out == '': die('You must setup an auto-build-save dir to use this script.') if args.gen: if os.path.lexists(TMP_DIR): die(f'"{TMP_DIR}" must not exist in the current directory.') gen_files = get_gen_files() os.mkdir(TMP_DIR, 0o700) for cmd in MAKE_GEN_CMDS: cmd_chk(cmd) cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/']) last_touch = int(time.time()) # Start by finding all patches so that we can load all possible parents. patches = sorted(list(get_patch_branches(args.base_branch))) parent_patch = { } description = { } for patch in patches: branch = f"patch/{args.base_branch}/{patch}" desc = '' proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"]) in_diff = False for line in proc.stdout: if in_diff: if not re.match(r'^[ +]', line): continue line = line[1:] m = re.search(r'patch -p1 <patches/(\S+)\.diff', line) if m and m[1] != patch: parpat = parent_patch[patch] = m[1] if not parpat in patches: die(f"Parent of {patch} is not a local branch: {parpat}") desc += line elif re.match(r'^@@ ', line): in_diff = True description[patch] = desc proc.communicate() if args.patch_files: # Limit the list of patches to actually process valid_patches = patches patches = [ ] for fn in args.patch_files: name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn)) if name not in valid_patches: die(f"Local branch not available for patch: {name}") patches.append(name) completed = set() for patch in patches: if patch in completed: continue if not update_patch(patch): break if args.gen: shutil.rmtree(TMP_DIR) while last_touch >= int(time.time()): time.sleep(1) cmd_chk(['git', 'checkout', starting_branch]) cmd_chk(['packaging/prep-auto-dir'], discard='output') def update_patch(patch): global last_touch completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops. parent = parent_patch.get(patch, None) if parent: if parent not in completed: if not update_patch(parent): return 0 based_on = parent = f"patch/{args.base_branch}/{parent}" else: parent = args.base_branch based_on = master_commit print(f"======== {patch} ========") while args.gen and last_touch >= int(time.time()): time.sleep(1) branch = f"patch/{args.base_branch}/{patch}" s = cmd_run(['git', 'checkout', branch]) if s.returncode != 0: return 0 s = cmd_run(['git', 'merge', based_on]) ok = s.returncode == 0 skip_shell = False if not ok or args.cmd or args.make or args.shell: cmd_chk(['packaging/prep-auto-dir'], discard='output') if not ok: print(f'"git merge {based_on}" incomplete -- please fix.') if not run_a_shell(parent, patch): return 0 if not args.make and not args.cmd: skip_shell = True if args.make: if cmd_run(['packaging/smart-make']).returncode != 0: if not run_a_shell(parent, patch): return 0 if not args.cmd: skip_shell = True if args.cmd: if cmd_run(args.cmd).returncode != 0: if not run_a_shell(parent, patch): return 0 skip_shell = True if args.shell and not skip_shell: if not run_a_shell(parent, patch): return 0 with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh: fh.write(description[patch]) fh.write(f"\nbased-on: {based_on}\n") if args.gen: gen_files = get_gen_files() for cmd in MAKE_GEN_CMDS: cmd_chk(cmd) cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"]) else: gen_files = [ ] last_touch = int(time.time()) proc = cmd_pipe(['git', 'diff', based_on]) skipping = False for line in proc.stdout: if skipping: if not re.match(r'^diff --git a/', line): continue skipping = False elif re.match(r'^diff --git a/PATCH', line): skipping = True continue if not re.match(r'^index ', line): fh.write(line) proc.communicate() if args.gen: e_tmp_dir = re.escape(TMP_DIR) diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir)) minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir) plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir) if parent == args.base_branch: parent_dir = 'master' else: m = re.search(r'([^/]+)$', parent) parent_dir = m[1] proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"]) for line in proc.stdout: line = diff_re.sub(r'\1 a/\2 b/\3', line) line = minus_re.sub(r'--- a/\1', line) line = plus_re.sub(r'+++ b/\1', line) fh.write(line) proc.communicate() return 1 def run_a_shell(parent, patch): m = re.search(r'([^/]+)$', parent) parent_dir = m[1] os.environ['PS1'] = f"[{parent_dir}] {patch}: " while True: s = cmd_run([os.environ.get('SHELL', '/bin/sh')]) if s.returncode != 0: ans = input("Abort? [n/y] ") if re.match(r'^y', ans, flags=re.I): return False continue cur_branch, is_clean, status_txt = check_git_status(0) if is_clean: break print(status_txt, end='') cmd_run('rm -f build/*.o build/*/*.o') return True if __name__ == '__main__': parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False) parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.") parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.") parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.") parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.") parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.") parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.') parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.") parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.") parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") args = parser.parse_args() if args.gen == '': args.gen = args.patches_dir elif args.gen is not None: args.patches_dir = args.gen main() # vim: sw=4 et ft=python
#!/usr/bin/env -S python3 -B
# This script is used to turn one or more of the "patch/BASE/*" branches
# into one or more diffs in the "patches" directory. Pass the option
# --gen if you want generated files in the diffs. Pass the name of
# one or more diffs if you want to just update a subset of all the
# diffs.
import os, sys, re, argparse, time, shutil
sys.path = ['packaging'] + sys.path
from pkglib import *
MAKE_GEN_CMDS = [
'./prepare-source'.split(),
'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi',
'make -C build gen'.split(),
]
TMP_DIR = "patches.gen"
os.environ['GIT_MERGE_AUTOEDIT'] = 'no'
def main():
global master_commit, parent_patch, description, completed, last_touch
if not os.path.isdir(args.patches_dir):
die(f'No "{args.patches_dir}" directory was found.')
if not os.path.isdir('.git'):
die('No ".git" directory present in the current dir.')
starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)
master_commit = latest_git_hash(args.base_branch)
if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':
die('You must setup an auto-build-save dir to use this script.')
if args.gen:
if os.path.lexists(TMP_DIR):
die(f'"{TMP_DIR}" must not exist in the current directory.')
gen_files = get_gen_files()
os.mkdir(TMP_DIR, 0o700)
for cmd in MAKE_GEN_CMDS:
cmd_chk(cmd)
cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])
last_touch = int(time.time())
# Start by finding all patches so that we can load all possible parents.
patches = sorted(list(get_patch_branches(args.base_branch)))
parent_patch = { }
description = { }
for patch in patches:
branch = f"patch/{args.base_branch}/{patch}"
desc = ''
proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"])
in_diff = False
for line in proc.stdout:
if in_diff:
if not re.match(r'^[ +]', line):
continue
line = line[1:]
m = re.search(r'patch -p1 <patches/(\S+)\.diff', line)
if m and m[1] != patch:
parpat = parent_patch[patch] = m[1]
if not parpat in patches:
die(f"Parent of {patch} is not a local branch: {parpat}")
desc += line
elif re.match(r'^@@ ', line):
in_diff = True
description[patch] = desc
proc.communicate()
if args.patch_files: # Limit the list of patches to actually process
valid_patches = patches
patches = [ ]
for fn in args.patch_files:
name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
if name not in valid_patches:
die(f"Local branch not available for patch: {name}")
patches.append(name)
completed = set()
for patch in patches:
if patch in completed:
continue
if not update_patch(patch):
break
if args.gen:
shutil.rmtree(TMP_DIR)
while last_touch >= int(time.time()):
time.sleep(1)
cmd_chk(['git', 'checkout', starting_branch])
cmd_chk(['packaging/prep-auto-dir'], discard='output')
def update_patch(patch):
global last_touch
completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.
parent = parent_patch.get(patch, None)
if parent:
if parent not in completed:
if not update_patch(parent):
return 0
based_on = parent = f"patch/{args.base_branch}/{parent}"
else:
parent = args.base_branch
based_on = master_commit
print(f"======== {patch} ========")
while args.gen and last_touch >= int(time.time()):
time.sleep(1)
branch = f"patch/{args.base_branch}/{patch}"
s = cmd_run(['git', 'checkout', branch])
if s.returncode != 0:
return 0
s = cmd_run(['git', 'merge', based_on])
ok = s.returncode == 0
skip_shell = False
if not ok or args.cmd or args.make or args.shell:
cmd_chk(['packaging/prep-auto-dir'], discard='output')
if not ok:
print(f'"git merge {based_on}" incomplete -- please fix.')
if not run_a_shell(parent, patch):
return 0
if not args.make and not args.cmd:
skip_shell = True
if args.make:
if cmd_run(['packaging/smart-make']).returncode != 0:
if not run_a_shell(parent, patch):
return 0
if not args.cmd:
skip_shell = True
if args.cmd:
if cmd_run(args.cmd).returncode != 0:
if not run_a_shell(parent, patch):
return 0
skip_shell = True
if args.shell and not skip_shell:
if not run_a_shell(parent, patch):
return 0
with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh:
fh.write(description[patch])
fh.write(f"\nbased-on: {based_on}\n")
if args.gen:
gen_files = get_gen_files()
for cmd in MAKE_GEN_CMDS:
cmd_chk(cmd)
cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"])
else:
gen_files = [ ]
last_touch = int(time.time())
proc = cmd_pipe(['git', 'diff', based_on])
skipping = False
for line in proc.stdout:
if skipping:
if not re.match(r'^diff --git a/', line):
continue
skipping = False
elif re.match(r'^diff --git a/PATCH', line):
skipping = True
continue
if not re.match(r'^index ', line):
fh.write(line)
proc.communicate()
if args.gen:
e_tmp_dir = re.escape(TMP_DIR)
diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))
minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
if parent == args.base_branch:
parent_dir = 'master'
else:
m = re.search(r'([^/]+)$', parent)
parent_dir = m[1]
proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"])
for line in proc.stdout:
line = diff_re.sub(r'\1 a/\2 b/\3', line)
line = minus_re.sub(r'--- a/\1', line)
line = plus_re.sub(r'+++ b/\1', line)
fh.write(line)
proc.communicate()
return 1
def run_a_shell(parent, patch):
m = re.search(r'([^/]+)$', parent)
parent_dir = m[1]
os.environ['PS1'] = f"[{parent_dir}] {patch}: "
while True:
s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
if s.returncode != 0:
ans = input("Abort? [n/y] ")
if re.match(r'^y', ans, flags=re.I):
return False
continue
cur_branch, is_clean, status_txt = check_git_status(0)
if is_clean:
break
print(status_txt, end='')
cmd_run('rm -f build/*.o build/*/*.o')
return True
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False)
parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.")
parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.")
parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.")
parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.')
parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
args = parser.parse_args()
if args.gen == '':
args.gen = args.patches_dir
elif args.gen is not None:
args.patches_dir = args.gen
main()
# vim: sw=4 et ft=python