|
version 1.1.1.3, 2016/11/01 09:54:32
|
version 1.1.1.4, 2021/03/17 00:32:36
|
|
Line 1
|
Line 1
|
| #!/usr/bin/perl | #!/usr/bin/env -S python3 -B |
| | |
| # This script is used to turn one or more of the "patch/BASE/*" branches |
# 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 |
# 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 |
# --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 |
# one or more diffs if you want to just update a subset of all the |
| # diffs. |
# diffs. |
| |
|
| use strict; | import os, sys, re, argparse, time, shutil |
| use warnings; | |
| use Getopt::Long; | |
| |
|
| my $patches_dir = 'patches'; | sys.path = ['packaging'] + sys.path |
| my $tmp_dir = "patches.$$"; | |
| my $make_gen_cmd = 'make -f prepare-source.mak conf && ./config.status && make gen'; | |
| |
|
| &Getopt::Long::Configure('bundling'); | from pkglib import * |
| &usage if !&GetOptions( | |
| 'branch|b=s' => \( my $master_branch = 'master' ), | |
| 'skip-check' => \( my $skip_branch_check ), | |
| 'shell|s' => \( my $launch_shell ), | |
| 'gen:s' => \( my $incl_generated_files ), | |
| 'help|h' => \( my $help_opt ), | |
| ); | |
| &usage if $help_opt; | |
| |
|
| $ENV{GIT_MERGE_AUTOEDIT} = 'no'; | 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" |
| |
|
| if (defined $incl_generated_files) { | os.environ['GIT_MERGE_AUTOEDIT'] = 'no' |
| $patches_dir = $incl_generated_files if $incl_generated_files ne ''; | |
| $incl_generated_files = 1; | |
| } | |
| |
|
| die "No '$patches_dir' directory was found.\n" unless -d $patches_dir; | def main(): |
| die "No '.git' directory present in the current dir.\n" unless -d '.git'; | global master_commit, parent_patch, description, completed, last_touch |
| |
|
| require 'packaging/git-status.pl'; | if not os.path.isdir(args.patches_dir): |
| my $starting_branch = check_git_state($master_branch, !$skip_branch_check, 1); | die(f'No "{args.patches_dir}" directory was found.') |
| | if not os.path.isdir('.git'): |
| | die('No ".git" directory present in the current dir.') |
| |
|
| my $master_commit; | starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) |
| open PIPE, '-|', "git log -1 --no-color $master_branch" or die $!; | |
| while (<PIPE>) { | |
| if (/^commit (\S+)/) { | |
| $master_commit = $1; | |
| last; | |
| } | |
| } | |
| close PIPE; | |
| die "Unable to determine commit hash for master branch: $master_branch\n" unless defined $master_commit; | |
| |
|
| if ($incl_generated_files) { | master_commit = latest_git_hash(args.base_branch) |
| my @extra_files = get_extra_files(); | |
| die "'$tmp_dir' must not exist in the current directory.\n" if -e $tmp_dir; | |
| mkdir($tmp_dir, 0700) or die "Unable to mkdir($tmp_dir): $!\n"; | |
| system "$make_gen_cmd && rsync -a @extra_files $tmp_dir/master/" and exit 1; | |
| } | |
| our $last_touch = time; | |
| |
|
| my %patches; | if cmd_txt_chk(['packaging/prep-auto-dir']) == '': |
| | die('You must setup an auto-build-save dir to use this script.') |
| |
|
| # Start by finding all patches so that we can load all possible parents. | if args.gen: |
| open(PIPE, '-|', 'git', 'branch', '-l') or die $!; | if os.path.lexists(TMP_DIR): |
| while (<PIPE>) { | die(f'"{TMP_DIR}" must not exist in the current directory.') |
| if (m# patch/\Q$master_branch\E/(.*)#o) { | gen_files = get_gen_files() |
| $patches{$1} = 1; | os.mkdir(TMP_DIR, 0o700) |
| } | for cmd in MAKE_GEN_CMDS: |
| } | cmd_chk(cmd) |
| close PIPE; | cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/']) |
| |
|
| my @patches = sort keys %patches; | last_touch = int(time.time()) |
| |
|
| my(%parent, %description); | # Start by finding all patches so that we can load all possible parents. |
| foreach my $patch (@patches) { | patches = sorted(list(get_patch_branches(args.base_branch))) |
| my $branch = "patch/$master_branch/$patch"; | |
| my $desc = ''; | |
| open(PIPE, '-|', 'git', 'diff', '-U1000', "$master_branch...$branch", '--', "PATCH.$patch") or die $!; | |
| while (<PIPE>) { | |
| last if /^@@ /; | |
| } | |
| while (<PIPE>) { | |
| next unless s/^[ +]//; | |
| if (m#patch -p1 <patches/(\S+)\.diff# && $1 ne $patch) { | |
| my $parent = $parent{$patch} = $1; | |
| if (!$patches{$parent}) { | |
| die "Parent of $patch is not a local branch: $parent\n"; | |
| } | |
| } | |
| $desc .= $_; | |
| } | |
| close PIPE; | |
| $description{$patch} = $desc; | |
| } | |
| |
|
| if (@ARGV) { | parent_patch = { } |
| # Limit the list of patches to actually process based on @ARGV. | description = { } |
| @patches = ( ); | |
| foreach (@ARGV) { | |
| s{^patch(es)?/} {}; | |
| s{\.diff$} {}; | |
| if (!$patches{$_}) { | |
| die "Local branch not available for patch: $_\n"; | |
| } | |
| push(@patches, $_); | |
| } | |
| } | |
| |
|
| my %completed; | for patch in patches: |
| foreach my $patch (@patches) { | branch = f"patch/{args.base_branch}/{patch}" |
| next if $completed{$patch}++; | desc = '' |
| last unless update_patch($patch); | 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 ($incl_generated_files) { | if args.patch_files: # Limit the list of patches to actually process |
| system "rm -rf $tmp_dir"; | 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) |
| |
|
| sleep 1 while $last_touch >= time; | completed = set() |
| system "git checkout $starting_branch" and exit 1; | |
| |
|
| exit; | for patch in patches: |
| | if patch in completed: |
| | continue |
| | if not update_patch(patch): |
| | break |
| |
|
| |
if args.gen: |
| |
shutil.rmtree(TMP_DIR) |
| |
|
| sub update_patch | while last_touch >= int(time.time()): |
| { | time.sleep(1) |
| my($patch) = @_; | cmd_chk(['git', 'checkout', starting_branch]) |
| | cmd_chk(['packaging/prep-auto-dir'], discard='output') |
| |
|
| my $parent = $parent{$patch}; |
|
| my $based_on; |
|
| if (defined $parent) { |
|
| unless ($completed{$parent}++) { |
|
| update_patch($parent); |
|
| } |
|
| $based_on = $parent = "patch/$master_branch/$parent"; |
|
| } else { |
|
| $parent = $master_branch; |
|
| $based_on = $master_commit; |
|
| } |
|
| |
|
| print "======== $patch ========\n"; | def update_patch(patch): |
| | global last_touch |
| |
|
| sleep 1 while $incl_generated_files && $last_touch >= time; | completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops. |
| system "git checkout patch/$master_branch/$patch" and return 0; | |
| |
|
| my $ok = system("git merge $based_on") == 0; | parent = parent_patch.get(patch, None) |
| if (!$ok || $launch_shell) { | if parent: |
| my($parent_dir) = $parent =~ m{([^/]+)$}; | if parent not in completed: |
| print qq|"git merge $based_on" incomplete -- please fix.\n| if !$ok; | if not update_patch(parent): |
| $ENV{PS1} = "[$parent_dir] $patch: "; | return 0 |
| while (1) { | based_on = parent = f"patch/{args.base_branch}/{parent}" |
| if (system($ENV{SHELL}) != 0) { | else: |
| print "Abort? [n/y] "; | parent = args.base_branch |
| $_ = <STDIN>; | based_on = master_commit |
| next unless /^y/i; | |
| return 0; | |
| } | |
| my($cur_branch, $is_clean, $status) = check_git_status(0); | |
| last if $is_clean; | |
| print $status; | |
| } | |
| } | |
| |
|
| open(OUT, '>', "$patches_dir/$patch.diff") or die $!; | print(f"======== {patch} ========") |
| print OUT $description{$patch}, "\nbased-on: $based_on\n"; | |
| |
|
| my @extra_files; | while args.gen and last_touch >= int(time.time()): |
| if ($incl_generated_files) { | time.sleep(1) |
| @extra_files = get_extra_files(); | |
| system "$make_gen_cmd && rsync -a @extra_files $tmp_dir/$patch/" and exit 1; | |
| } | |
| $last_touch = time; | |
| |
|
| open(PIPE, '-|', 'git', 'diff', $based_on) or die $!; | branch = f"patch/{args.base_branch}/{patch}" |
| DIFF: while (<PIPE>) { | s = cmd_run(['git', 'checkout', branch]) |
| while (m{^diff --git a/PATCH}) { | if s.returncode != 0: |
| while (<PIPE>) { | return 0 |
| last if m{^diff --git a/}; | |
| } | |
| last DIFF if !defined $_; | |
| } | |
| next if /^index /; | |
| print OUT $_; | |
| } | |
| close PIPE; | |
| |
|
| if ($incl_generated_files) { | s = cmd_run(['git', 'merge', based_on]) |
| my $parent_dir; | ok = s.returncode == 0 |
| if ($parent eq $master_branch) { | skip_shell = False |
| $parent_dir = 'master'; | if not ok or args.cmd or args.make or args.shell: |
| } else { | cmd_chk(['packaging/prep-auto-dir'], discard='output') |
| ($parent_dir) = $parent =~ m{([^/]+)$}; | if not ok: |
| } | print(f'"git merge {based_on}" incomplete -- please fix.') |
| open(PIPE, '-|', 'diff', '-Nurp', "$tmp_dir/$parent_dir", "$tmp_dir/$patch") or die $!; | if not run_a_shell(parent, patch): |
| while (<PIPE>) { | return 0 |
| s#^(diff -Nurp) $tmp_dir/[^/]+/(.*?) $tmp_dir/[^/]+/(.*)#$1 a/$2 b/$3#o; | if not args.make and not args.cmd: |
| s#^\Q---\E $tmp_dir/[^/]+/([^\t]+)\t.*#--- a/$1#o; | skip_shell = True |
| s#^\Q+++\E $tmp_dir/[^/]+/([^\t]+)\t.*#+++ b/$1#o; | if args.make: |
| print OUT $_; | if cmd_run(['packaging/smart-make']).returncode != 0: |
| } | if not run_a_shell(parent, patch): |
| close PIPE; | return 0 |
| unlink @extra_files; | 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 |
| |
|
| close OUT; | 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") |
| |
|
| 1; | 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()) |
| |
|
| exit; | 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() |
| |
|
| sub get_extra_files | if args.gen: |
| { | e_tmp_dir = re.escape(TMP_DIR) |
| my @extras; | 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) |
| |
|
| open(IN, '<', 'Makefile.in') or die "Couldn't open Makefile.in: $!\n"; | if parent == args.base_branch: |
| while (<IN>) { | parent_dir = 'master' |
| if (s/^GENFILES=//) { | else: |
| while (s/\\$//) { | m = re.search(r'([^/]+)$', parent) |
| $_ .= <IN>; | parent_dir = m[1] |
| } | |
| @extras = split(' ', $_); | |
| last; | |
| } | |
| } | |
| close IN; | |
| |
|
| return @extras; | 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() |
| |
|
| sub usage | return 1 |
| { | |
| die <<EOT; | |
| Usage: patch-update [OPTIONS] [patches/DIFF...] | |
| |
|
| Options: | |
| -b, --branch=BRANCH The master branch to merge into the patch/BASE/* branches. | def run_a_shell(parent, patch): |
| --gen[=DIR] Include generated files. Optional destination DIR | m = re.search(r'([^/]+)$', parent) |
| arg overrides the default of using the "patches" dir. | parent_dir = m[1] |
| --skip-check Skip the check that ensures starting with a clean branch. | os.environ['PS1'] = f"[{parent_dir}] {patch}: " |
| -s, --shell Launch a shell for every patch/BASE/* branch updated, not | |
| just when a conflict occurs. | while True: |
| -h, --help Output this help message. | s = cmd_run([os.environ.get('SHELL', '/bin/sh')]) |
| EOT | 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 |