File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / rsync / packaging / branch-from-patch
Revision 1.1.1.2 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Wed Mar 17 00:32:36 2021 UTC (3 years, 3 months ago) by misho
Branches: rsync, MAIN
CVS tags: v3_2_3, HEAD
rsync 3.2.3

#!/usr/bin/env -S python3 -B

# This script turns one or more diff files in the patches dir (which is
# expected to be a checkout of the rsync-patches git repo) into a branch
# in the main rsync git checkout. This allows the applied patch to be
# merged with the latest rsync changes and tested.  To update the diff
# with the resulting changes, see the patch-update script.

import os, sys, re, argparse, glob

sys.path = ['packaging'] + sys.path

from pkglib import *

def main():
    global created, info, local_branch

    cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)

    local_branch = get_patch_branches(args.base_branch)

    if args.delete_local_branches:
        for name in sorted(local_branch):
            branch = f"patch/{args.base_branch}/{name}"
            cmd_chk(['git', 'branch', '-D', branch])
        local_branch = set()

    if args.add_missing:
        for fn in sorted(glob.glob(f"{args.patches_dir}/*.diff")):
            name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
            if name not in local_branch and fn not in args.patch_files:
                args.patch_files.append(fn)

    if not args.patch_files:
        return

    for fn in args.patch_files:
        if not fn.endswith('.diff'):
            die(f"Filename is not a .diff file: {fn}")
        if not os.path.isfile(fn):
            die(f"File not found: {fn}")

    scanned = set()
    info = { }

    patch_list = [ ]
    for fn in args.patch_files:
        m = re.match(r'^(?P<dir>.*?)(?P<name>[^/]+)\.diff$', fn)
        patch = argparse.Namespace(**m.groupdict())
        if patch.name in scanned:
            continue
        patch.fn = fn

        lines = [ ]
        commit_hash = None
        with open(patch.fn, 'r', encoding='utf-8') as fh:
            for line in fh:
                m = re.match(r'^based-on: (\S+)', line)
                if m:
                    commit_hash = m[1]
                    break
                if (re.match(r'^index .*\.\..* \d', line)
                 or re.match(r'^diff --git ', line)
                 or re.match(r'^--- (old|a)/', line)):
                    break
                lines.append(re.sub(r'\s*\Z', "\n", line, 1))
        info_txt = ''.join(lines).strip() + "\n"
        lines = None

        parent = args.base_branch
        patches = re.findall(r'patch -p1 <%s/(\S+)\.diff' % args.patches_dir, info_txt)
        if patches:
            last = patches.pop()
            if last != patch.name:
                warn(f"No identity patch line in {patch.fn}")
                patches.append(last)
            if patches:
                parent = patches.pop()
                if parent not in scanned:
                    diff_fn = patch.dir + parent + '.diff'
                    if not os.path.isfile(diff_fn):
                        die(f"Failed to find parent of {patch.fn}: {parent}")
                    # Add parent to args.patch_files so that we will look for the
                    # parent's parent.  Any duplicates will be ignored.
                    args.patch_files.append(diff_fn)
        else:
            warn(f"No patch lines found in {patch.fn}")

        info[patch.name] = [ parent, info_txt, commit_hash ]

        patch_list.append(patch)

    created = set()
    for patch in patch_list:
        create_branch(patch)

    cmd_chk(['git', 'checkout', args.base_branch])


def create_branch(patch):
    if patch.name in created:
        return
    created.add(patch.name)

    parent, info_txt, commit_hash = info[patch.name]
    parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff')

    if parent.name == args.base_branch:
        parent_branch = commit_hash if commit_hash else args.base_branch
    else:
        create_branch(parent)
        parent_branch = '/'.join(['patch', args.base_branch, parent.name])

    branch = '/'.join(['patch', args.base_branch, patch.name])
    print("\n" + '=' * 64)
    print(f"Processing {branch} ({parent_branch})")

    if patch.name in local_branch:
        cmd_chk(['git', 'branch', '-D', branch])

    cmd_chk(['git', 'checkout', '-b', branch, parent_branch])

    info_fn = 'PATCH.' + patch.name
    with open(info_fn, 'w', encoding='utf-8') as fh:
        fh.write(info_txt)
    cmd_chk(['git', 'add', info_fn])

    with open(patch.fn, 'r', encoding='utf-8') as fh:
        patch_txt = fh.read()

    cmd_run('patch -p1'.split(), input=patch_txt)

    for fn in glob.glob('*.orig') + glob.glob('*/*.orig'):
        os.unlink(fn)

    pos = 0
    new_file_re = re.compile(r'\nnew file mode (?P<mode>\d+)\s+--- /dev/null\s+\+\+\+ b/(?P<fn>.+)')
    while True:
        m = new_file_re.search(patch_txt, pos)
        if not m:
            break
        os.chmod(m['fn'], int(m['mode'], 8))
        cmd_chk(['git', 'add', m['fn']])
        pos = m.end()

    while True:
        cmd_chk('git status'.split())
        ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ')
        if ans == '':
            break
        cmd_chk("git add " + ans, shell=True)

    while True:
        s = cmd_run(['git', 'commit', '-a', '-m', f"Creating branch from {patch.name}.diff."])
        if not s.returncode:
            break
        s = cmd_run(['/bin/zsh'])
        if s.returncode:
            die('Aborting due to shell error code')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Create a git patch branch from an rsync patch file.", 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('--add-missing', '-a', action='store_true', help="Add a branch for every patches/*.diff that doesn't have a branch.")
    parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
    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.")
    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()
    main()

# vim: sw=4 et ft=python

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>