version 1.1, 2013/10/14 07:51:14
|
version 1.1.1.2, 2021/03/17 00:32:36
|
Line 1
|
Line 1
|
#!/usr/bin/perl | #!/usr/bin/env -S python3 -B |
|
|
use strict; | # This script turns one or more diff files in the patches dir (which is |
use warnings; | # expected to be a checkout of the rsync-patches git repo) into a branch |
use Getopt::Long; | # 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. |
|
|
&Getopt::Long::Configure('bundling'); | import os, sys, re, argparse, glob |
&usage if !&GetOptions( | |
'branch|b=s' => \( my $master_branch = 'master' ), | |
'skip-check' => \( my $skip_branch_check ), | |
'delete' => \( my $delete_local_branches ), | |
'help|h' => \( my $help_opt ), | |
); | |
&usage if $help_opt; | |
|
|
require 'packaging/git-status.pl'; | sys.path = ['packaging'] + sys.path |
check_git_state($master_branch, !$skip_branch_check, 1); | |
|
|
my %local_branch; | from pkglib import * |
open PIPE, '-|', 'git branch -l' or die "Unable to fork: $!\n"; | |
while (<PIPE>) { | |
if (m# patch/\Q$master_branch\E/(.*)#o) { | |
$local_branch{$1} = 1; | |
} | |
} | |
close PIPE; | |
|
|
if ($delete_local_branches) { | def main(): |
foreach my $name (sort keys %local_branch) { | global created, info, local_branch |
my $branch = "patch/$master_branch/$name"; | |
system 'git', 'branch', '-D', $branch and exit 1; | |
} | |
%local_branch = ( ); | |
} | |
|
|
my @patch_list; | cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir) |
foreach (@ARGV) { | |
if (!-f $_) { | |
die "File not found: $_\n"; | |
} | |
die "Filename is not a .diff file: $_\n" unless /\.diff$/; | |
push @patch_list, $_; | |
} | |
|
|
exit unless @patch_list; | local_branch = get_patch_branches(args.base_branch) |
|
|
my(%scanned, %created, %info); | 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() |
|
|
foreach my $patch (@patch_list) { | if args.add_missing: |
my($where, $name) = $patch =~ m{^(.*?)([^/]+)\.diff$}; | for fn in sorted(glob.glob(f"{args.patches_dir}/*.diff")): |
next if $scanned{$name}++; | 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) |
|
|
open IN, '<', $patch or die "Unable to open $patch: $!\n"; | if not args.patch_files: |
| return |
|
|
my $info = ''; | for fn in args.patch_files: |
my $commit; | if not fn.endswith('.diff'): |
while (<IN>) { | die(f"Filename is not a .diff file: {fn}") |
if (m#^based-on: (\S+)#) { | if not os.path.isfile(fn): |
$commit = $1; | die(f"File not found: {fn}") |
last; | |
} | |
last if m#^index .*\.\..* \d#; | |
last if m#^diff --git #; | |
last if m#^--- (old|a)/#; | |
$info .= $_; | |
} | |
close IN; | |
|
|
$info =~ s/\s+\Z/\n/; | scanned = set() |
| info = { } |
|
|
my $parent = $master_branch; | patch_list = [ ] |
my @patches = $info =~ m#patch -p1 <patches/(\S+)\.diff#g; | for fn in args.patch_files: |
if (@patches) { | m = re.match(r'^(?P<dir>.*?)(?P<name>[^/]+)\.diff$', fn) |
if ($patches[-1] eq $name) { | patch = argparse.Namespace(**m.groupdict()) |
pop @patches; | if patch.name in scanned: |
} else { | continue |
warn "No identity patch line in $patch\n"; | patch.fn = fn |
} | |
if (@patches) { | |
$parent = pop @patches; | |
if (!$scanned{$parent}) { | |
unless (-f "$where$parent.diff") { | |
die "Unknown parent of $patch: $parent\n"; | |
} | |
# Add parent to @patch_list so that we will look for the | |
# parent's parent. Any duplicates will just be ignored. | |
push @patch_list, "$where$parent.diff"; | |
} | |
} | |
} else { | |
warn "No patch lines found in $patch\n"; | |
} | |
|
|
$info{$name} = [ $parent, $info, $commit ]; | 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 |
|
|
foreach my $patch (@patch_list) { | parent = args.base_branch |
create_branch($patch); | 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}") |
|
|
system 'git', 'checkout', $master_branch and exit 1; | info[patch.name] = [ parent, info_txt, commit_hash ] |
|
|
exit; | patch_list.append(patch) |
|
|
sub create_branch | created = set() |
{ | for patch in patch_list: |
my($patch) = @_; | create_branch(patch) |
my($where, $name) = $patch =~ m{^(.*?)([^/]+)\.diff$}; | |
|
|
return if $created{$name}++; | cmd_chk(['git', 'checkout', args.base_branch]) |
|
|
my $ref = $info{$name}; |
|
my($parent, $info, $commit) = @$ref; |
|
|
|
my $parent_branch; | def create_branch(patch): |
if ($parent eq $master_branch) { | if patch.name in created: |
$parent_branch = $master_branch; | return |
$parent_branch = $commit if defined $commit; | created.add(patch.name) |
} else { | |
create_branch("$where/$parent.diff"); | |
$parent_branch = "patch/$master_branch/$parent"; | |
} | |
|
|
my $branch = "patch/$master_branch/$name"; | parent, info_txt, commit_hash = info[patch.name] |
print "\n", '=' x 64, "\nProcessing $branch ($parent_branch)\n"; | parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff') |
|
|
if ($local_branch{$name}) { | if parent.name == args.base_branch: |
system 'git', 'branch', '-D', $branch and exit 1; | parent_branch = commit_hash if commit_hash else args.base_branch |
} | else: |
| create_branch(parent) |
| parent_branch = '/'.join(['patch', args.base_branch, parent.name]) |
|
|
system 'git', 'checkout', '-b', $branch, $parent_branch and exit 1; | branch = '/'.join(['patch', args.base_branch, patch.name]) |
| print("\n" + '=' * 64) |
| print(f"Processing {branch} ({parent_branch})") |
|
|
open OUT, '>', "PATCH.$name" or die $!; | if patch.name in local_branch: |
print OUT $info; | cmd_chk(['git', 'branch', '-D', branch]) |
close OUT; | |
system 'git', 'add', "PATCH.$name" and exit 1; | |
|
|
open IN, '<', $patch or die "Unable to open $patch: $!\n"; | cmd_chk(['git', 'checkout', '-b', branch, parent_branch]) |
$_ = join('', <IN>); | |
close IN; | |
|
|
open PIPE, '|-', 'patch -p1' or die $!; | info_fn = 'PATCH.' + patch.name |
print PIPE $_; | with open(info_fn, 'w', encoding='utf-8') as fh: |
close PIPE; | fh.write(info_txt) |
| cmd_chk(['git', 'add', info_fn]) |
|
|
system 'rm -f *.orig */*.orig'; | with open(patch.fn, 'r', encoding='utf-8') as fh: |
| patch_txt = fh.read() |
|
|
while (m#\nnew file mode (\d+)\s+--- /dev/null\s+\Q+++\E b/(.*)#g) { | cmd_run('patch -p1'.split(), input=patch_txt) |
chmod oct($1), $2; | |
system 'git', 'add', $2; | |
} | |
|
|
while (1) { | for fn in glob.glob('*.orig') + glob.glob('*/*.orig'): |
system 'git status'; | os.unlink(fn) |
print 'Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: '; | |
$_ = <STDIN>; | |
last if /^$/; | |
chomp; | |
system "git add $_"; | |
} | |
|
|
while (system 'git', 'commit', '-a', '-m', "Creating branch from $name.diff.") { | pos = 0 |
exit 1 if system '/bin/zsh'; | 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() |
|
|
sub usage | while True: |
{ | cmd_chk('git status'.split()) |
die <<EOT; | ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ') |
Usage branch-from-patch [OPTIONS] patches/DIFF... | if ans == '': |
| break |
| cmd_chk("git add " + ans, shell=True) |
|
|
Options: | while True: |
-b, --branch=BRANCH Create branches relative to BRANCH if no "based-on" | s = cmd_run(['git', 'commit', '-a', '-m', f"Creating branch from {patch.name}.diff."]) |
header was found in the patch file. | if not s.returncode: |
--skip-check Skip the check that ensures starting with a clean branch. | break |
--delete Delete all the local patch/BASE/* branches, not just the ones | s = cmd_run(['/bin/zsh']) |
that are being recreated. | if s.returncode: |
-h, --help Output this help message. | die('Aborting due to shell error code') |
EOT | |
} | |
| 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 |