244 lines · 8.8 KB · Text
RawDownload
#!/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']) == '':        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