-#!/usr/bin/perl
-
-use strict;
-use warnings;
-use Getopt::Long;
-
-&Getopt::Long::Configure('bundling');
-&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;
-
-push @INC, '.';
-
-require 'packaging/git-status.pl';
-check_git_state($master_branch, !$skip_branch_check, 1);
-
-my %local_branch;
-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) {
- foreach my $name (sort keys %local_branch) {
- my $branch = "patch/$master_branch/$name";
- system 'git', 'branch', '-D', $branch and exit 1;
- }
- %local_branch = ( );
-}
-
-my @patch_list;
-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;
-
-my(%scanned, %created, %info);
-
-foreach my $patch (@patch_list) {
- my($where, $name) = $patch =~ m{^(.*?)([^/]+)\.diff$};
- next if $scanned{$name}++;
-
- open IN, '<', $patch or die "Unable to open $patch: $!\n";
-
- my $info = '';
- my $commit;
- while (<IN>) {
- if (m#^based-on: (\S+)#) {
- $commit = $1;
- last;
- }
- last if m#^index .*\.\..* \d#;
- last if m#^diff --git #;
- last if m#^--- (old|a)/#;
- $info .= $_;
- }
- close IN;
-
- $info =~ s/\s+\Z/\n/;
-
- my $parent = $master_branch;
- my @patches = $info =~ m#patch -p1 <patches/(\S+)\.diff#g;
- if (@patches) {
- if ($patches[-1] eq $name) {
- pop @patches;
- } else {
- warn "No identity patch line in $patch\n";
- }
- 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 ];
-}
-
-foreach my $patch (@patch_list) {
- create_branch($patch);
-}
-
-system 'git', 'checkout', $master_branch and exit 1;
-
-exit;
-
-sub create_branch
-{
- my($patch) = @_;
- my($where, $name) = $patch =~ m{^(.*?)([^/]+)\.diff$};
-
- return if $created{$name}++;
-
- my $ref = $info{$name};
- my($parent, $info, $commit) = @$ref;
-
- my $parent_branch;
- if ($parent eq $master_branch) {
- $parent_branch = $master_branch;
- $parent_branch = $commit if defined $commit;
- } else {
- create_branch("$where/$parent.diff");
- $parent_branch = "patch/$master_branch/$parent";
- }
-
- my $branch = "patch/$master_branch/$name";
- print "\n", '=' x 64, "\nProcessing $branch ($parent_branch)\n";
-
- if ($local_branch{$name}) {
- system 'git', 'branch', '-D', $branch and exit 1;
- }
-
- system 'git', 'checkout', '-b', $branch, $parent_branch and exit 1;
-
- open OUT, '>', "PATCH.$name" or die $!;
- print OUT $info;
- close OUT;
- system 'git', 'add', "PATCH.$name" and exit 1;
-
- open IN, '<', $patch or die "Unable to open $patch: $!\n";
- $_ = join('', <IN>);
- close IN;
-
- open PIPE, '|-', 'patch -p1' or die $!;
- print PIPE $_;
- close PIPE;
-
- system 'rm -f *.orig */*.orig';
-
- while (m#\nnew file mode (\d+)\s+--- /dev/null\s+\Q+++\E b/(.*)#g) {
- chmod oct($1), $2;
- system 'git', 'add', $2;
- }
-
- while (1) {
- system 'git status';
- 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.") {
- exit 1 if system '/bin/zsh';
- }
-}
-
-sub usage
-{
- die <<EOT;
-Usage branch-from-patch [OPTIONS] patches/DIFF...
-
-Options:
--b, --branch=BRANCH Create branches relative to BRANCH if no "based-on"
- header was found in the patch file.
- --skip-check Skip the check that ensures starting with a clean branch.
- --delete Delete all the local patch/BASE/* branches, not just the ones
- that are being recreated.
--h, --help Output this help message.
-EOT
-}
+#!/usr/bin/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))
+ 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
+++ /dev/null
-# Do some git-status checking for the current dir and (optionally)
-# the patches dir.
-
-sub check_git_state
-{
- my($master_branch, $fatal_unless_clean, $check_patches_dir) = @_;
-
- my($cur_branch) = check_git_status($fatal_unless_clean);
- (my $branch = $cur_branch) =~ s{^patch/([^/]+)/[^/]+$}{$1}; # change patch/BRANCH/PATCH_NAME into BRANCH
- if ($branch ne $master_branch) {
- print "The checkout is not on the $master_branch branch.\n";
- exit 1 if $master_branch ne 'master';
- print "Do you want me to continue with --branch=$branch? [n] ";
- $_ = <STDIN>;
- exit 1 unless /^y/i;
- $_[0] = $master_branch = $branch; # Updates caller's $master_branch too.
- }
-
- if ($check_patches_dir && -d 'patches/.git') {
- ($branch) = check_git_status($fatal_unless_clean, 'patches');
- if ($branch ne $master_branch) {
- print "The *patches* checkout is on branch $branch, not branch $master_branch.\n";
- print "Do you want to change it to branch $master_branch? [n] ";
- $_ = <STDIN>;
- exit 1 unless /^y/i;
- system "cd patches && git checkout '$master_branch'";
- }
- }
-
- return $cur_branch;
-}
-
-sub check_git_status
-{
- my($fatal_unless_clean, $subdir) = @_;
- $subdir = '.' unless defined $subdir;
- my $status = `cd '$subdir' && git status`;
- my $is_clean = $status =~ /\nnothing to commit.+working (directory|tree) clean/;
-
- my($cur_branch) = $status =~ /^(?:# )?On branch (.+)\n/;
- if ($fatal_unless_clean && !$is_clean) {
- if ($subdir eq '.') {
- $subdir = '';
- } else {
- $subdir = " *$subdir*";
- }
- die "The$subdir checkout is not clean:\n", $status;
- }
- ($cur_branch, $is_clean, $status);
-}
-
-1;
-#!/usr/bin/perl
+#!/usr/bin/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.
-use strict;
-use warnings;
-use Getopt::Long;
-
-my $patches_dir = 'patches';
-my $tmp_dir = "patches.$$";
-my $make_gen_cmd = 'make -f prepare-source.mak conf && ./config.status && make gen';
-
-&Getopt::Long::Configure('bundling');
-&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';
-
-if (defined $incl_generated_files) {
- $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;
-die "No '.git' directory present in the current dir.\n" unless -d '.git';
-
-push @INC, '.';
-
-require 'packaging/git-status.pl';
-my $starting_branch = check_git_state($master_branch, !$skip_branch_check, 1);
-
-my $master_commit;
-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) {
- 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;
-
-# Start by finding all patches so that we can load all possible parents.
-open(PIPE, '-|', 'git', 'branch', '-l') or die $!;
-while (<PIPE>) {
- if (m# patch/\Q$master_branch\E/(.*)#o) {
- $patches{$1} = 1;
- }
-}
-close PIPE;
-
-my @patches = sort keys %patches;
-
-my(%parent, %description);
-foreach my $patch (@patches) {
- 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) {
- # Limit the list of patches to actually process based on @ARGV.
- @patches = ( );
- foreach (@ARGV) {
- s{^patch(es)?/} {};
- s{\.diff$} {};
- if (!$patches{$_}) {
- die "Local branch not available for patch: $_\n";
- }
- push(@patches, $_);
- }
-}
-
-my %completed;
-foreach my $patch (@patches) {
- next if $completed{$patch}++;
- last unless update_patch($patch);
-}
-
-if ($incl_generated_files) {
- system "rm -rf $tmp_dir";
-}
-
-sleep 1 while $last_touch >= time;
-system "git checkout $starting_branch" and exit 1;
-
-exit;
-
-
-sub update_patch
-{
- my($patch) = @_;
-
- 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";
-
- sleep 1 while $incl_generated_files && $last_touch >= time;
- system "git checkout patch/$master_branch/$patch" and return 0;
-
- my $ok = system("git merge $based_on") == 0;
- if (!$ok || $launch_shell) {
- my($parent_dir) = $parent =~ m{([^/]+)$};
- print qq|"git merge $based_on" incomplete -- please fix.\n| if !$ok;
- $ENV{PS1} = "[$parent_dir] $patch: ";
- while (1) {
- if (system($ENV{SHELL}) != 0) {
- print "Abort? [n/y] ";
- $_ = <STDIN>;
- 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 OUT $description{$patch}, "\nbased-on: $based_on\n";
-
- my @extra_files;
- if ($incl_generated_files) {
- @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 $!;
- DIFF: while (<PIPE>) {
- while (m{^diff --git a/PATCH}) {
- while (<PIPE>) {
- last if m{^diff --git a/};
- }
- last DIFF if !defined $_;
- }
- next if /^index /;
- print OUT $_;
- }
- close PIPE;
-
- if ($incl_generated_files) {
- my $parent_dir;
- if ($parent eq $master_branch) {
- $parent_dir = 'master';
- } else {
- ($parent_dir) = $parent =~ m{([^/]+)$};
- }
- open(PIPE, '-|', 'diff', '-Nurp', "$tmp_dir/$parent_dir", "$tmp_dir/$patch") or die $!;
- while (<PIPE>) {
- s#^(diff -Nurp) $tmp_dir/[^/]+/(.*?) $tmp_dir/[^/]+/(.*)#$1 a/$2 b/$3#o;
- s#^\Q---\E $tmp_dir/[^/]+/([^\t]+)\t.*#--- a/$1#o;
- s#^\Q+++\E $tmp_dir/[^/]+/([^\t]+)\t.*#+++ b/$1#o;
- print OUT $_;
- }
- close PIPE;
- unlink @extra_files;
- }
-
- close OUT;
-
- 1;
-}
-
-exit;
-
-sub get_extra_files
-{
- my @extras;
-
- open(IN, '<', 'Makefile.in') or die "Couldn't open Makefile.in: $!\n";
- while (<IN>) {
- if (s/^GENFILES=//) {
- while (s/\\$//) {
- $_ .= <IN>;
- }
- @extras = split(' ', $_);
- last;
- }
- }
- close IN;
-
- return @extras;
-}
-
-sub usage
-{
- die <<EOT;
-Usage: patch-update [OPTIONS] [patches/DIFF...]
-
-Options:
--b, --branch=BRANCH The master branch to merge into the patch/BASE/* branches.
- --gen[=DIR] Include generated files. Optional destination DIR
- arg overrides the default of using the "patches" dir.
- --skip-check Skip the check that ensures starting with a clean branch.
--s, --shell Launch a shell for every patch/BASE/* branch updated, not
- just when a conflict occurs.
--h, --help Output this help message.
-EOT
-}
+import os, sys, re, argparse, time, shutil
+
+sys.path = ['packaging'] + sys.path
+
+from pkglib import *
+
+MAKE_GEN_CMDS = [
+ 'make -f prepare-source.mak conf'.split(),
+ './config.status'.split(),
+ 'make 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 args.gen:
+ if os.path.lexists(TMP_DIR):
+ die(f'"{TMP_DIR}" must not exist in the current directory.')
+ extra_files = get_extra_files()
+ os.mkdir(TMP_DIR, 0o700)
+ for cmd in MAKE_GEN_CMDS:
+ cmd_chk(cmd)
+ cmd_chk(['rsync', '-a', *extra_files, f'{TMP_DIR}/master/'])
+
+ last_touch = 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 >= time.time():
+ time.sleep(1)
+ cmd_chk(['git', 'checkout', starting_branch])
+
+
+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 >= time.time():
+ time.sleep(1)
+ s = cmd_run(f"git checkout patch/{args.base_branch}/{patch}".split())
+ if s.returncode != 0:
+ return 0
+
+ s = cmd_run(['git', 'merge', based_on])
+ ok = s.returncode == 0
+ if not ok or args.shell:
+ m = re.search(r'([^/]+)$', parent)
+ parent_dir = m[1]
+ if not ok:
+ print(f'"git merge {based_on}" incomplete -- please fix.')
+ 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 0
+ continue
+ cur_branch, is_clean, status_txt = check_git_status(0)
+ if is_clean:
+ break
+ print(status_txt, end='')
+
+ 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:
+ extra_files = get_extra_files()
+ for cmd in MAKE_GEN_CMDS:
+ cmd_chk(cmd)
+ cmd_chk(['rsync', '-a', *extra_files, f"{TMP_DIR}/{patch}/"])
+ else:
+ extra_files = [ ]
+ last_touch = 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()
+ for fn in extra_files:
+ os.unlink(fn)
+
+ return 1
+
+
+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('--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
--- /dev/null
+import os, sys, re, subprocess
+
+# This python3 library provides a few helpful routines that are
+# used by the latest packaging scripts.
+
+# Output the msg args to stderr. Accepts all the args that print() accepts.
+def warn(*msg):
+ print(*msg, file=sys.stderr)
+
+
+# Output the msg args to stderr and die with a non-zero return-code.
+# Accepts all the args that print() accepts.
+def die(*msg):
+ warn(*msg)
+ sys.exit(1)
+
+
+def _tweak_opts(cmd, opts):
+ if type(cmd) == str:
+ opts['shell'] = True
+ if not 'encoding' in opts:
+ if opts.get('raw', False):
+ del opts['raw']
+ else:
+ opts['encoding'] = 'utf-8'
+
+
+# This does a normal subprocess.run() with some auto-args added to make life easier.
+def cmd_run(cmd, **opts):
+ _tweak_opts(cmd, opts)
+ return subprocess.run(cmd, **opts)
+
+
+# Works like cmd_run() with a default check=True specified for you.
+def cmd_chk(cmd, **opts):
+ return cmd_run(cmd, check=True, **opts)
+
+
+# Captures stdout & stderr together in a string and returns the (output, return-code) tuple.
+def cmd_txt(cmd, **opts):
+ opts['stdout'] = subprocess.PIPE
+ opts['stderr'] = subprocess.STDOUT
+ _tweak_opts(cmd, opts)
+ proc = subprocess.Popen(cmd, **opts)
+ out = proc.communicate()[0]
+ return (out, proc.returncode)
+
+
+# Captures stdout & stderr together in a string and returns the output if the command has a 0
+# return-code. Otherwise it throws an exception that indicates the return code and all the
+# captured output.
+def cmd_txt_chk(cmd, **opts):
+ out, rc = cmd_txt(cmd, **opts)
+ if rc != 0:
+ cmd_err = f'Command "{cmd}" returned non-zero exit status "{rc}" and output:\n{out}'
+ raise Exception(cmd_err)
+ return out
+
+
+# Starts a piped-output command of just stdout (by default) and leaves it up to you to do
+# any incremental reading of the output and to call communicate() on the returned object.
+def cmd_pipe(cmd, **opts):
+ opts['stdout'] = subprocess.PIPE
+ _tweak_opts(cmd, opts)
+ return subprocess.Popen(cmd, **opts)
+
+
+# Runs a "git status" command and dies if the checkout is not clean (the
+# arg fatal_unless_clean can be used to make that non-fatal. Returns a
+# tuple of the current branch, the is_clean flag, and the status text.
+def check_git_status(fatal_unless_clean=True, subdir='.'):
+ status_txt = cmd_txt_chk(f"cd '{subdir}' && git status")
+ is_clean = re.search(r'\nnothing to commit.+working (directory|tree) clean', status_txt) != None
+
+ if not is_clean and fatal_unless_clean:
+ if subdir == '.':
+ subdir = ''
+ else:
+ subdir = f" *{subdir}*"
+ die(f"The{subdir} checkout is not clean:\n" + status_txt)
+
+ m = re.match(r'^(?:# )?On branch (.+)\n', status_txt)
+ cur_branch = m[1] if m else None
+
+ return (cur_branch, is_clean, status_txt)
+
+
+# Calls check_git_status() on the current git checkout and (optionally) a subdir path's
+# checkout. Use fatal_unless_clean to indicate if an unclean checkout is fatal or not.
+# The master_branch arg indicates what branch we want both checkouts to be using, and
+# if the branch is wrong the user is given the option of either switching to the right
+# branch or aborting.
+def check_git_state(master_branch, fatal_unless_clean=True, check_extra_dir=None):
+ cur_branch = check_git_status(fatal_unless_clean)[0]
+ branch = re.sub(r'^patch/([^/]+)/[^/]+$', r'\1', cur_branch) # change patch/BRANCH/PATCH_NAME into BRANCH
+ if branch != master_branch:
+ print(f"The checkout is not on the {master_branch} branch.")
+ if master_branch != 'master':
+ sys.exit(1)
+ ans = input(f"Do you want me to continue with --branch={branch}? [n] ")
+ if not ans or not re.match(r'^y', ans, flags=re.I):
+ sys.exit(1)
+ master_branch = branch
+
+ if check_extra_dir and os.path.isdir(os.path.join(check_extra_dir, '.git')):
+ branch = check_git_status(fatal_unless_clean, check_extra_dir)[0]
+ if branch != master_branch:
+ print(f"The *{check_extra_dir}* checkout is on branch {branch}, not branch {master_branch}.")
+ ans = input(f"Do you want to change it to branch {master_branch}? [n] ")
+ if not ans or not re.match(r'^y', ans, flags=re.I):
+ sys.exit(1)
+ subdir.check_call(f"cd {check_extra_dir} && git checkout '{master_branch}'", shell=True)
+
+ return (cur_branch, master_branch)
+
+
+# Return the git hash of the most recent commit.
+def latest_git_hash(branch):
+ out = cmd_txt_chk(['git', 'log', '-1', '--no-color', branch])
+ m = re.search(r'^commit (\S+)', out, flags=re.M)
+ if not m:
+ die(f"Unable to determine commit hash for master branch: {branch}")
+ return m[1]
+
+
+# Return a set of all branch names that have the format "patch/BASE_BRANCH/NAME"
+# for the given base_branch string. Just the NAME portion is put into the set.
+def get_patch_branches(base_branch):
+ branches = set()
+ proc = cmd_pipe('git branch -l'.split())
+ for line in proc.stdout:
+ m = re.search(r' patch/([^/]+)/(.+)', line)
+ if m and m[1] == base_branch:
+ branches.add(m[2])
+ proc.communicate()
+ return branches
+
+
+# Snag the GENFILES values out of the Makefile.in file and return them as a list.
+def get_extra_files():
+ cont_re = re.compile(r'\\\n')
+
+ extras = [ ]
+
+ with open('Makefile.in', 'r', encoding='utf-8') as fh:
+ for line in fh:
+ if not extras:
+ chk = re.sub(r'^GENFILES=', '', line)
+ if line == chk:
+ continue
+ line = chk
+ m = re.search(r'\\$', line)
+ line = re.sub(r'^\s+|\s*\\\n?$|\s+$', '', line)
+ extras += line.split()
+ if not m:
+ break
+
+ return extras
+
+# vim: sw=4 et
-#!/usr/bin/perl
+#!/usr/bin/python3 -B
+
# This script expects the directory ~/samba-rsync-ftp to exist and to be a
# copy of the /home/ftp/pub/rsync dir on samba.org. When the script is done,
# the git repository in the current directory will be updated, and the local
# ~/samba-rsync-ftp dir will be ready to be rsynced to samba.org.
-use strict;
-use warnings;
-use Cwd;
-use Getopt::Long;
-use Term::ReadKey;
-use Date::Format;
-
-my $dest = $ENV{HOME} . '/samba-rsync-ftp';
-my $passfile = $ENV{HOME} . '/.rsyncpass';
-my $path = $ENV{PATH};
-my $make_gen_cmd = 'make -f prepare-source.mak conf && ./config.status && make gen';
-
-&Getopt::Long::Configure('bundling');
-&usage if !&GetOptions(
- 'branch|b=s' => \( my $master_branch = 'master' ),
- 'help|h' => \( my $help_opt ),
-);
-&usage if $help_opt;
-
-my $now = time;
-my $cl_today = time2str('* %a %b %d %Y', $now);
-my $year = time2str('%Y', $now);
-my $ztoday = time2str('%d %b %Y', $now);
-(my $today = $ztoday) =~ s/^0//;
-
-my $curdir = Cwd::cwd;
-
-END {
- unlink($passfile);
-}
-
-my @extra_files;
-open(IN, '<', 'Makefile.in') or die "Couldn't open Makefile.in: $!\n";
-while (<IN>) {
- if (s/^GENFILES=//) {
- while (s/\\$//) {
- $_ .= <IN>;
- }
- @extra_files = split(' ', $_);
- last;
- }
-}
-close IN;
-
-my $break = <<EOT;
-==========================================================================
-EOT
-
-print $break, <<EOT, $break, "\n";
+import os, sys, re, argparse, glob, time, shutil, atexit, signal
+from datetime import datetime
+from getpass import getpass
+
+sys.path = ['packaging'] + sys.path
+
+from pkglib import *
+
+dest = os.environ['HOME'] + '/samba-rsync-ftp'
+passfile = os.environ['HOME'] + '/.rsyncpass'
+ORIGINAL_PATH = os.environ['PATH']
+MAKE_GEN_CMDS = [
+ 'make -f prepare-source.mak conf'.split(),
+ './config.status'.split(),
+ 'make gen'.split(),
+ ]
+REL_RE = re.compile(r'^\s+\S{2}\s\S{3}\s\d{4}\s+(?P<ver>\d+\.\d+\.\d+)\s+(?P<pdate>\d{2} \w{3} \d{4}\s+)?(?P<pver>\d+)$')
+
+def main():
+ now = datetime.now()
+ cl_today = now.strftime('* %a %b %d %Y')
+ year = now.strftime('%Y')
+ ztoday = now.strftime('%d %b %Y')
+ today = ztoday.lstrip('0')
+
+ curdir = os.getcwd()
+
+ atexit.register(remove_passfile)
+ signal.signal(signal.SIGINT, signal_handler)
+
+ extra_files = get_extra_files()
+
+ dash_line = '=' * 74
+
+ print(f"""\
+{dash_line}
== This will release a new version of rsync onto an unsuspecting world. ==
-EOT
-
-die "$dest does not exist\n" unless -d $dest;
-die "There is no .git dir in the current directory.\n" unless -d '.git';
-die "'a' must not exist in the current directory.\n" if -e 'a';
-die "'b' must not exist in the current directory.\n" if -e 'b';
-
-require 'packaging/git-status.pl';
-check_git_state($master_branch, 1, 1);
-
-my $confversion;
-open(IN, '<', 'configure.ac') or die $!;
-while (<IN>) {
- if (/^AC_INIT\(\[rsync\],\s*\[(\d.+?)\]/) {
- $confversion = $1;
- last;
- }
-}
-close IN;
-die "Unable to find AC_INIT with version in configure.ac\n" unless defined $confversion;
-
-open(IN, '<', 'OLDNEWS') or die $!;
-$_ = <IN>;
-my($lastversion) = /(\d+\.\d+\.\d+)/;
-my($last_protocol_version, %pdate);
-while (<IN>) {
- if (my($ver,$pdate,$pver) = /^\s+\S\S\s\S\S\S\s\d\d\d\d\s+(\d+\.\d+\.\d+)\s+(\d\d \w\w\w \d\d\d\d\s+)?(\d+)$/) {
- $pdate{$ver} = $pdate if defined $pdate;
- $last_protocol_version = $pver if $ver eq $lastversion;
- }
-}
-close IN;
-die "Unable to determine protocol_version for $lastversion.\n" unless defined $last_protocol_version;
-
-my $protocol_version;
-open(IN, '<', 'rsync.h') or die $!;
-while (<IN>) {
- if (/^#define\s+PROTOCOL_VERSION\s+(\d+)/) {
- $protocol_version = $1;
- last;
- }
-}
-close IN;
-die "Unable to determine the current PROTOCOL_VERSION.\n" unless defined $protocol_version;
-
-my $version = $confversion;
-$version =~ s/dev/pre1/ || $version =~ s/pre(\d+)/ 'pre' . ($1 + 1) /e;
-
-print "Please enter the version number of this release: [$version] ";
-chomp($_ = <STDIN>);
-if ($_ eq '.') {
- $version =~ s/pre\d+//;
-} elsif ($_ ne '') {
- $version = $_;
-}
-die "Invalid version: `$version'\n" unless $version =~ /^[\d.]+(pre\d+)?$/;
-
-if (`git tag -l v$version` ne '') {
- print "Tag v$version already exists.\n\nDelete tag or quit? [q/del] ";
- $_ = <STDIN>;
- exit 1 unless /^del/i;
- system "git tag -d v$version";
-}
-
-if ($version =~ s/[-.]*pre[-.]*/pre/ && $confversion !~ /dev$/) {
- $lastversion = $confversion;
-}
-
-print "Enter the previous version to produce a patch against: [$lastversion] ";
-chomp($_ = <STDIN>);
-$lastversion = $_ if $_ ne '';
-$lastversion =~ s/[-.]*pre[-.]*/pre/;
-
-my $pre = $version =~ /(pre\d+)/ ? $1 : '';
-
-my $release = $pre ? '0.1' : '1';
-print "Please enter the RPM release number of this release: [$release] ";
-chomp($_ = <STDIN>);
-$release = $_ if $_ ne '';
-$release .= ".$pre" if $pre;
-
-(my $finalversion = $version) =~ s/pre\d+//;
-my($proto_changed,$proto_change_date);
-if ($protocol_version eq $last_protocol_version) {
- $proto_changed = 'unchanged';
- $proto_change_date = "\t\t";
-} else {
- $proto_changed = 'changed';
- if (!defined($proto_change_date = $pdate{$finalversion})) {
- while (1) {
- print "On what date did the protocol change to $protocol_version get checked in? (dd Mmm yyyy) ";
- chomp($_ = <STDIN>);
- last if /^\d\d \w\w\w \d\d\d\d$/;
- }
- $proto_change_date = "$_\t";
- }
-}
-
-my($srcdir,$srcdiffdir,$lastsrcdir,$skipping);
-if ($lastversion =~ /pre/) {
- if (!$pre) {
- die "You should not diff a release version against a pre-release version.\n";
- }
- $srcdir = $srcdiffdir = $lastsrcdir = 'src-previews';
- $skipping = ' ** SKIPPING **';
-} elsif ($pre) {
- $srcdir = $srcdiffdir = 'src-previews';
- $lastsrcdir = 'src';
- $skipping = ' ** SKIPPING **';
-} else {
- $srcdir = $lastsrcdir = 'src';
- $srcdiffdir = 'src-diffs';
- $skipping = '';
-}
-
-print "\n", $break, <<EOT;
-\$version is "$version"
-\$lastversion is "$lastversion"
-\$dest is "$dest"
-\$curdir is "$curdir"
-\$srcdir is "$srcdir"
-\$srcdiffdir is "$srcdiffdir"
-\$lastsrcdir is "$lastsrcdir"
-\$release is "$release"
+{dash_line}
+""")
+
+ if not os.path.isdir(dest):
+ die(dest, "dest does not exist")
+ if not os.path.isdir('.git'):
+ die("There is no .git dir in the current directory.")
+ if os.path.lexists('a'):
+ die('"a" must not exist in the current directory.')
+ if os.path.lexists('b'):
+ die('"b" must not exist in the current directory.')
+ if os.path.lexists('patches.gen'):
+ die('"patches.gen" must not exist in the current directory.')
+
+ check_git_state(args.master_branch, True, 'patches')
+
+ confversion = None
+ with open('configure.ac', 'r', encoding='utf-8') as fh:
+ for line in fh:
+ m = re.match(r'^AC_INIT\(\[rsync\],\s*\[(\d.+?)\]', line)
+ if m:
+ confversion = m[1]
+ break
+ if not confversion:
+ die("Unable to find AC_INIT with version in configure.ac")
+
+ lastversion = last_protocol_version = None
+ pdate = { }
+ with open('OLDNEWS', 'r', encoding='utf-8') as fh:
+ for line in fh:
+ if not lastversion:
+ m = re.search(r'(\d+\.\d+\.\d+)', line)
+ if m:
+ lastversion = m[1]
+ m = REL_RE.match(line)
+ if m:
+ if m['pdate']:
+ pdate[m['ver']] = m['pdate']
+ if m['ver'] == lastversion:
+ last_protocol_version = m['pver']
+ if not last_protocol_version:
+ die(f"Unable to determine protocol_version for {lastversion}.")
+
+ protocol_version = None
+ with open('rsync.h', 'r', encoding='utf-8') as fh:
+ for line in fh:
+ m = re.match(r'^#define\s+PROTOCOL_VERSION\s+(\d+)', line)
+ if m:
+ protocol_version = m[1]
+ break
+ if not protocol_version:
+ die("Unable to determine the current PROTOCOL_VERSION.")
+
+ version = confversion
+ m = re.search(r'pre(\d+)', version)
+ if m:
+ version = re.sub(r'pre\d+', 'pre' + str(int(m[1]) + 1), version)
+ else:
+ version = version.replace('dev', 'pre1')
+
+ ans = input(f"Please enter the version number of this release: [{version}] ")
+ if ans == '.':
+ version = re.sub(r'pre\d+', '', version)
+ elif ans != '':
+ version = ans
+ if not re.match(r'^[\d.]+(pre\d+)?$', version):
+ die(f'Invalid version: "{version}"')
+
+ v_ver = 'v' + version
+ rsync_ver = 'rsync-' + version
+ rsync_lastver = 'rsync-' + lastversion
+
+ if os.path.lexists(rsync_ver):
+ die(f'"{rsync_ver}" must not exist in the current directory.')
+ if os.path.lexists(rsync_lastver):
+ die(f'"{rsync_lastver}" must not exist in the current directory.')
+
+ out = cmd_txt_chk(['git', 'tag', '-l', v_ver])
+ if out != '':
+ print(f"Tag {v_ver} already exists.")
+ ans = input("\nDelete tag or quit? [Q/del] ")
+ if not re.match(r'^del', ans, flags=re.I):
+ die("Aborted")
+ cmd_chk(['git', 'tag', '-d', v_ver])
+
+ version = re.sub(r'[-.]*pre[-.]*', 'pre', version)
+ if 'pre' in version and not confversion.endswith('dev'):
+ lastversion = confversion
+
+ ans = input(f"Enter the previous version to produce a patch against: [{lastversion}] ")
+ if ans != '':
+ lastversion = ans
+ lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion)
+
+ m = re.search(r'(pre\d+)', version)
+ pre = m[1] if m else ''
+
+ release = '0.1' if pre else '1'
+ ans = input(f"Please enter the RPM release number of this release: [{release}] ")
+ if ans != '':
+ release = ans
+ if pre:
+ release += '.' + pre
+
+ finalversion = re.sub(r'pre\d+', '', version)
+ if protocol_version == last_protocol_version:
+ proto_changed = 'unchanged'
+ proto_change_date = "\t\t"
+ else:
+ proto_changed = 'changed'
+ if finalversion in pdate:
+ proto_change_date = pdate[finalversion]
+ else:
+ while True:
+ ans = input("On what date did the protocol change to {protocol_version} get checked in? (dd Mmm yyyy) ")
+ if re.match(r'^\d\d \w\w\w \d\d\d\d$', ans):
+ break
+ proto_change_date = ans + "\t"
+
+ if 'pre' in lastversion:
+ if not pre:
+ die("You should not diff a release version against a pre-release version.")
+ srcdir = srcdiffdir = lastsrcdir = 'src-previews'
+ skipping = ' ** SKIPPING **'
+ elif pre:
+ srcdir = srcdiffdir = 'src-previews'
+ lastsrcdir = 'src'
+ skipping = ' ** SKIPPING **'
+ else:
+ srcdir = lastsrcdir = 'src'
+ srcdiffdir = 'src-diffs'
+ skipping = ''
+
+ print(f"""
+{dash_line}
+version is "{version}"
+lastversion is "{lastversion}"
+dest is "{dest}"
+curdir is "{curdir}"
+srcdir is "{srcdir}"
+srcdiffdir is "{srcdiffdir}"
+lastsrcdir is "{lastsrcdir}"
+release is "{release}"
About to:
- tweak SUBPROTOCOL_VERSION in rsync.h, if needed
- tweak the date in the *.yo files and generate the manpages
- generate configure.sh, config.h.in, and proto.h
- page through the differences
-
-EOT
-print "<Press Enter to continue> ";
-$_ = <STDIN>;
-
-my %specvars = ( 'Version:' => $finalversion, 'Release:' => $release,
- '%define fullversion' => "\%{version}$pre", 'Released' => "$version.",
- '%define srcdir' => $srcdir );
-my @tweak_files = ( glob('packaging/*.spec'), glob('packaging/*/*.spec'), glob('*.yo'),
- qw( configure.ac rsync.h NEWS OLDNEWS options.c ) );
-
-foreach my $fn (@tweak_files) {
- open(IN, '<', $fn) or die $!;
- undef $/; $_ = <IN>; $/ = "\n";
- close IN;
- if ($fn =~ /configure/) {
- s/^(AC_INIT\(\[rsync\],\s*\[)\d.+?(\])/$1$version$2/m
- or die "Unable to update AC_INIT with version in $fn\n";
- } elsif ($fn =~ /\.spec/) {
- while (my($str, $val) = each %specvars) {
- s/^\Q$str\E .*/$str $val/m
- or die "Unable to update $str in $fn\n";
- }
- s/^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)/$cl_today $1/m
- or die "Unable to update ChangeLog header in $fn\n";
- } elsif ($fn =~ /\.yo/) {
- s/^(manpage\([^)]+\)\(\d+\)\()[^)]+(\).*)/$1$today$2/m
- or die "Unable to update date in manpage() header in $fn\n";
- s/^(This man ?page is current for version) \S+ (of rsync)/$1 $version $2/m
- or die "Unable to update current version info in $fn\n";
- } elsif ($fn eq 'rsync.h') {
- s{(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)}
- { $1 . ' ' . get_subprotocol_version($2) }e
- or die "Unable to find SUBPROTOCOL_VERSION define in $fn\n";
- } elsif ($fn eq 'NEWS') {
- s{^(NEWS for rsync \Q$finalversion\E )(\(UNRELEASED\))\s*(\nProtocol: )(\d+) (\([^)]+\))\n}
- { $1 . ($pre ? $2 : "($today)") . "$3$protocol_version ($proto_changed)\n" }ei
- or die "The first 2 lines of $fn are not in the right format. They must be:\n"
- . "NEWS for rsync $finalversion (UNRELEASED)\n"
- . "Protocol: $protocol_version ($proto_changed)\n";
- } elsif ($fn eq 'OLDNEWS') {
- s{^(\t\S\S\s\S\S\S\s\d\d\d\d)(\t\Q$finalversion\E\t).*}
- { ($pre ? $1 : "\t$ztoday") . $2 . $proto_change_date . $protocol_version }em
- or die "Unable to find \"?? ??? $year\t$finalversion\" line in $fn\n";
- } elsif ($fn eq 'options.c') {
- if (s/(Copyright \(C\) 2002-)(\d+)( Wayne Davison)/$1$year$3/
- && $2 ne $year) {
- die "Copyright comments need to be updated to $year in all files!\n";
- }
- # Adjust the year in the --version output.
- s/(rprintf\(f, "Copyright \(C\) 1996-)(\d+)/$1$year/
- or die "Unable to find Copyright string in --version output of $fn\n";
- next if $2 eq $year;
- } else {
- die "Unrecognized file in \@tweak_files: $fn\n";
- }
- open(OUT, '>', $fn) or die $!;
- print OUT $_;
- close OUT;
-}
-
-print $break;
-system "git diff --color | less -p '^diff .*'";
-
-my $srctar_name = "rsync-$version.tar.gz";
-my $pattar_name = "rsync-patches-$version.tar.gz";
-my $diff_name = "rsync-$lastversion-$version.diffs.gz";
-my $srctar_file = "$dest/$srcdir/$srctar_name";
-my $pattar_file = "$dest/$srcdir/$pattar_name";
-my $diff_file = "$dest/$srcdiffdir/$diff_name";
-my $news_file = "$dest/$srcdir/rsync-$version-NEWS";
-my $lasttar_file = "$dest/$lastsrcdir/rsync-$lastversion.tar.gz";
-
-print $break, <<EOT;
+""")
+ ans = input("<Press Enter to continue> ")
+
+ specvars = {
+ 'Version:': finalversion,
+ 'Release:': release,
+ '%define fullversion': f'%{{version}}{pre}',
+ 'Released': version + '.',
+ '%define srcdir': srcdir,
+ }
+
+ tweak_files = 'configure.ac rsync.h NEWS OLDNEWS'.split()
+ tweak_files += glob.glob('packaging/*.spec')
+ tweak_files += glob.glob('packaging/*/*.spec')
+ tweak_files += glob.glob('*.yo')
+
+ for fn in tweak_files:
+ with open(fn, 'r', encoding='utf-8') as fh:
+ old_txt = txt = fh.read()
+ if 'configure' in fn:
+ x_re = re.compile(r'^(AC_INIT\(\[rsync\],\s*\[)\d.+?(\])', re.M)
+ txt = replace_or_die(x_re, r'\g<1>%s\2' % version, txt, f"Unable to update AC_INIT with version in {fn}")
+ elif '.spec' in fn:
+ for var, val in specvars.items():
+ x_re = re.compile(r'^%s .*' % re.escape(var), re.M)
+ txt = replace_or_die(x_re, var + ' ' + val, txt, f"Unable to update {var} in {fn}")
+ x_re = re.compile(r'^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)', re.M)
+ txt = replace_or_die(x_re, r'%s \1' % cl_today, txt, f"Unable to update ChangeLog header in {fn}")
+ elif '.yo' in fn:
+ x_re = re.compile(r'^(manpage\([^)]+\)\(\d+\)\()[^)]+(\).*)', re.M)
+ txt = replace_or_die(x_re, r'\g<1>%s\2' % today, txt, f"Unable to update date in manpage() header in {fn}")
+ x_re = re.compile(r'^(This man ?page is current for version) \S+ (of rsync)', re.M)
+ txt = replace_or_die(x_re, r'\1 %s \2' % version, txt, f"Unable to update current version info in {fn}")
+ elif fn == 'rsync.h':
+ x_re = re.compile('(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)')
+ repl = lambda m: m[1] + ' ' + '0' if not pre or proto_changed != 'changed' else 1 if m[2] == '0' else m[2]
+ txt = replace_or_die(x_re, repl, txt, f"Unable to find SUBPROTOCOL_VERSION define in {fn}")
+ elif fn == 'NEWS':
+ x_re = re.compile(
+ r'^(NEWS for rsync %s )(\(UNRELEASED\))\s*(\nProtocol: )(\d+) (\([^)]+\))\n' % re.escape(finalversion),
+ re.I)
+ repl = lambda m: m[1] + (m[2] if pre else f"({today})") + m[3] + f"{protocol_version} ({proto_changed})\n"
+ msg = (f"The first 2 lines of {fn} are not in the right format. They must be:\n"
+ + f"NEWS for rsync {finalversion} (UNRELEASED)\n"
+ + f"Protocol: {protocol_version} ({proto_changed})")
+ txt = replace_or_die(x_re, repl, txt, msg)
+ elif fn == 'OLDNEWS':
+ x_re = re.compile(r'^(\t\S\S\s\S\S\S\s\d\d\d\d)(\t%s\t).*' % re.escape(finalversion), re.M)
+ repl = lambda m: (m[1] if pre else "\t" + ztoday) + m[2] + proto_change_date + protocol_version
+ txt = replace_or_die(x_re, repl, txt, f'Unable to find "?? ??? {year}\t{finalversion}" line in {fn}')
+ else:
+ die(f"Unrecognized file in tweak_files: {fn}")
+
+ if txt != old_txt:
+ print(f"Updating {fn}")
+ with open(fn, 'w', encoding='utf-8') as fh:
+ fh.write(txt)
+
+ cmd_chk(['packaging/year-tweak'])
+
+ print(dash_line)
+ cmd_run("git diff --color | less -p '^diff .*'")
+
+ srctar_name = f"{rsync_ver}.tar.gz"
+ pattar_name = f"rsync-patches-{version}.tar.gz"
+ diff_name = f"{rsync_lastver}-{version}.diffs.gz"
+ srctar_file = f"{dest}/{srcdir}/{srctar_name}"
+ pattar_file = f"{dest}/{srcdir}/{pattar_name}"
+ diff_file = f"{dest}/{srcdiffdir}/{diff_name}"
+ news_file = f"{dest}/{srcdir}/{rsync_ver}-NEWS"
+ lasttar_file = f"{dest}/{lastsrcdir}/{rsync_lastver}.tar.gz"
+
+ print(f"""\
+{dash_line}
About to:
- commit all version changes
- - merge the $master_branch branch into the patch/$master_branch/* branches
+ - merge the {args.master_branch} branch into the patch/{args.master_branch}/* branches
- update the files in the "patches" dir and OPTIONALLY
(if you type 'y') to launch a shell for each patch
+""")
+ ans = input("<Press Enter OR 'y' to continue> ")
-EOT
-print "<Press Enter OR 'y' to continue> ";
-my $ans = <STDIN>;
+ s = cmd_run(['git', 'commit', '-a', '-m', f'Preparing for release of {version}'])
+ if s.returncode:
+ die('Aborting')
-system "git commit -a -m 'Preparing for release of $version'" and exit 1;
+ print(f'Creating any missing patch branches.')
+ s = cmd_run(f'packaging/branch-from-patch --branch={args.master_branch} --add-missing')
+ if s.returncode:
+ die('Aborting')
-print "Updating files in \"patches\" dir ...\n";
-system "packaging/patch-update --branch=$master_branch";
+ print('Updating files in "patches" dir ...')
+ s = cmd_run(f'packaging/patch-update --branch={args.master_branch}')
+ if s.returncode:
+ die('Aborting')
-if ($ans =~ /^y/i) {
- print "\nVisiting all \"patch/$master_branch/*\" branches ...\n";
- system "packaging/patch-update --branch=$master_branch --skip-check --shell";
-}
+ if re.match(r'^y', ans, re.I):
+ print(f'\nVisiting all "patch/{args.master_branch}/*" branches ...')
+ cmd_run(f"packaging/patch-update --branch={args.master_branch} --skip-check --shell")
-if (-d 'patches/.git') {
- system "cd patches && git commit -a -m 'The patches for $version.'" and exit 1;
-}
+ if os.path.isdir('patches/.git'):
+ s = cmd_run(f"cd patches && git commit -a -m 'The patches for {version}.'")
+ if s.returncode:
+ die('Aborting')
-print $break, <<EOT;
+ print(f"""\
+{dash_line}
About to:
- - create signed tag for this release: v$version
- - create release diffs, "$diff_name"
- - create release tar, "$srctar_name"
- - generate rsync-$version/patches/* files
- - create patches tar, "$pattar_name"
+ - create signed tag for this release: {v_ver}
+ - create release diffs, "{diff_name}"
+ - create release tar, "{srctar_name}"
+ - generate {rsync_ver}/patches/* files
+ - create patches tar, "{pattar_name}"
- update top-level README, *NEWS, TODO, and ChangeLog
- update top-level rsync*.html manpages
- gpg-sign the release files
- - update hard-linked top-level release files$skipping
-
-EOT
-print "<Press Enter to continue> ";
-$_ = <STDIN>;
-
-# We want to use our passphrase-providing "gpg" script, so modify the PATH.
-$ENV{PATH} = "$curdir/packaging/bin:$path";
-
-my $passphrase;
-while (1) {
- ReadMode('noecho');
- print "\nEnter your GPG pass-phrase: ";
- chomp($passphrase = <STDIN>);
- ReadMode(0);
- print "\n";
-
- # Briefly create a temp file with the passphrase for git's tagging use.
- my $oldmask = umask 077;
- unlink($passfile);
- open(OUT, '>', $passfile) or die $!;
- print OUT $passphrase, "\n";
- close OUT;
- umask $oldmask;
- $ENV{'GPG_PASSFILE'} = $passfile;
-
- $_ = `git tag -s -m 'Version $version.' v$version 2>&1`;
- print $_;
- next if /bad passphrase/;
- exit 1 if /failed/;
-
- if (-d 'patches/.git') {
- $_ = `cd patches && git tag -s -m 'Version $version.' v$version 2>&1`;
- print $_;
- exit 1 if /bad passphrase|failed/;
- }
-
- unlink($passfile);
- last;
-}
-
-$ENV{PATH} = $path;
-
-# Extract the generated files from the old tar.
-@_ = @extra_files;
-map { s#^#rsync-$lastversion/# } @_;
-system "tar xzf $lasttar_file @_";
-rename("rsync-$lastversion", 'a');
-
-print "Creating $diff_file ...\n";
-system "$make_gen_cmd && rsync -a @extra_files b/" and exit 1;
-my $sed_script = 's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:';
-system "(git diff v$lastversion v$version; diff -upN a b | sed -r '$sed_script') | gzip -9 >$diff_file";
-system "rm -rf a";
-rename('b', "rsync-$version");
-
-print "Creating $srctar_file ...\n";
-system "git archive --format=tar --prefix=rsync-$version/ v$version | tar xf -";
-system "support/git-set-file-times --quiet --prefix=rsync-$version/";
-system "fakeroot tar czf $srctar_file rsync-$version; rm -rf rsync-$version";
-
-print "Updating files in \"rsync-$version/patches\" dir ...\n";
-mkdir("rsync-$version", 0755);
-mkdir("rsync-$version/patches", 0755);
-system "packaging/patch-update --skip-check --branch=$master_branch --gen=rsync-$version/patches";
-
-print "Creating $pattar_file ...\n";
-system "fakeroot tar chzf $pattar_file rsync-$version/patches; rm -rf rsync-$version";
-
-print "Updating the other files in $dest ...\n";
-system "rsync -a README NEWS OLDNEWS TODO $dest";
-unlink($news_file);
-link("$dest/NEWS", $news_file);
-system "git log --name-status | gzip -9 >$dest/ChangeLog.gz";
-
-system "yodl2html -o $dest/rsync.html rsync.yo";
-system "yodl2html -o $dest/rsyncd.conf.html rsyncd.conf.yo";
-
-foreach my $fn ($srctar_file, $pattar_file, $diff_file) {
- unlink("$fn.asc");
- open(GPG, '|-', "gpg --batch --passphrase-fd=0 -ba $fn") or die $!;
- print GPG $passphrase, "\n";
- close GPG;
-}
-
-if (!$pre) {
- system "rm $dest/rsync-*.gz $dest/rsync-*.asc $dest/rsync-*-NEWS $dest/src-previews/rsync-*diffs.gz*";
-
- foreach my $fn ($srctar_file, "$srctar_file.asc",
- $pattar_file, "$pattar_file.asc",
- $diff_file, "$diff_file.asc", $news_file) {
- (my $top_fn = $fn) =~ s#/src(-\w+)?/#/#;
- link($fn, $top_fn);
- }
-}
-
-print $break, <<'EOT';
+ - update hard-linked top-level release files{skipping}
+""")
+ ans = input("<Press Enter to continue> ")
+
+ # We want to use our passphrase-providing "gpg" script, so modify the PATH.
+ os.environ['PATH'] = f"{curdir}/packaging/bin:{ORIGINAL_PATH}"
+
+ while True:
+ passphrase = getpass("\nEnter your GPG pass-phrase: ")
+
+ # Briefly create a temp file with the passphrase for git's tagging use.
+ oldmask = os.umask(0o077)
+ if os.path.lexists(passfile):
+ os.unlink(passfile)
+ with open(passfile, 'w', encoding='utf-8') as fh:
+ fh.write(passphrase + "\n")
+ os.umask(oldmask)
+ os.environ['GPG_PASSFILE'] = passfile
+
+ out = cmd_txt(f"git tag -s -m 'Version {version}.' {v_ver}")[0]
+ print(out, end='')
+ if 'bad passphrase' in out:
+ continue
+ if 'failed' in out:
+ die('Aborting')
+
+ if os.path.isdir('patches/.git'):
+ out = cmd_txt(f"cd patches && git tag -s -m 'Version {version}.' {v_ver}")[0]
+ print(out, end='')
+ if 'bad passphrase' in out or 'failed' in out:
+ die('Aborting')
+
+ os.unlink(passfile)
+ break
+
+ os.environ['PATH'] = ORIGINAL_PATH
+
+ # Extract the generated files from the old tar.
+ tweaked_extra_files = [ f"{rsync_lastver}/{x}" for x in extra_files ]
+ cmd_chk(['tar', 'xzf', lasttar_file, *tweaked_extra_files])
+ os.rename(rsync_lastver, 'a')
+
+ print(f"Creating {diff_file} ...")
+ for cmd in MAKE_GEN_CMDS:
+ cmd_chk(cmd)
+ cmd_chk(['rsync', '-a', *extra_files, 'b/'])
+
+ sed_script = r's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:' # CAUTION: must not contain any single quotes!
+ cmd_chk(f"(git diff v{lastversion} {v_ver}; diff -upN a b | sed -r '{sed_script}') | gzip -9 >{diff_file}")
+ shutil.rmtree('a')
+ os.rename('b', rsync_ver)
+
+ print(f"Creating {srctar_file} ...")
+ cmd_chk(f"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | tar xf -")
+ cmd_chk(f"support/git-set-file-times --quiet --prefix={rsync_ver}/")
+ cmd_chk(['fakeroot', 'tar', 'czf', srctar_file, rsync_ver])
+ shutil.rmtree(rsync_ver)
+
+ print(f'Updating files in "{rsync_ver}/patches" dir ...')
+ os.mkdir(rsync_ver, 0o755)
+ os.mkdir(f"{rsync_ver}/patches", 0o755)
+ cmd_chk(f"packaging/patch-update --skip-check --branch={args.master_branch} --gen={rsync_ver}/patches")
+
+ print(f"Creating {pattar_file} ...")
+ cmd_chk(f"fakeroot tar chzf {pattar_file} {rsync_ver}/patches")
+ shutil.rmtree(rsync_ver)
+
+ print(f"Updating the other files in {dest} ...")
+ cmd_chk('rsync -a README NEWS OLDNEWS TODO'.split() + [dest])
+ if os.path.lexists(news_file):
+ os.unlink(news_file)
+ os.link(f"{dest}/NEWS", news_file)
+ cmd_chk(f"git log --name-status | gzip -9 >{dest}/ChangeLog.gz")
+
+ cmd_chk(f"yodl2html -o {dest}/rsync.html rsync.yo")
+ cmd_chk(f"yodl2html -o {dest}/rsyncd.conf.html rsyncd.conf.yo")
+
+ for fn in (srctar_file, pattar_file, diff_file):
+ asc_fn = fn + '.asc'
+ if os.path.lexists(asc_fn):
+ os.unlink(asc_fn)
+ cmd_chk(['gpg', '--batch', '--passphrase-fd=0', '-ba', fn], input=passphrase)
+
+ if not pre:
+ for find in f'{dest}/rsync-*.gz {dest}/rsync-*.asc {dest}/rsync-*-NEWS {dest}/src-previews/rsync-*diffs.gz*'.split():
+ for fn in glob.glob(find):
+ os.unlink(fn)
+ top_link = [
+ srctar_file, f"{srctar_file}.asc",
+ pattar_file, f"{pattar_file}.asc",
+ diff_file, f"{diff_file}.asc",
+ news_file,
+ ]
+ for fn in top_link:
+ os.link(fn, re.sub(r'/src(-\w+)?/', '/', fn))
+
+ print(f"""\
+{dash_line}
Local changes are done. When you're satisfied, push the git repository
and rsync the release files. Remember to announce the release on *BOTH*
rsync-announce@lists.samba.org and rsync@lists.samba.org (and the web)!
-EOT
-
-exit;
-
-sub get_subprotocol_version
-{
- my($subver) = @_;
- if ($pre && $proto_changed eq 'changed') {
- return $subver == 0 ? 1 : $subver;
- }
- 0;
-}
-
-sub usage
-{
- die <<EOT;
-Usage: release-rsync [OPTIONS]
-
--b, --branch=BRANCH The branch to release (default: master)
--h, --help Display this help message
-EOT
-}
+""")
+
+
+def replace_or_die(regex, repl, txt, die_msg):
+ m = regex.search(txt)
+ if not m:
+ die(die_msg)
+ return regex.sub(repl, txt, 1)
+
+
+def remove_passfile():
+ if passfile and os.path.lexists(passfile):
+ os.unlink(passfile)
+
+
+def signal_handler(sig, frame):
+ die("\nAborting due to SIGINT.")
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="Prepare a new release of rsync in the git repo & ftp dir.", add_help=False)
+ parser.add_argument('--branch', '-b', dest='master_branch', default='master', help="The branch to release. Default: master.")
+ parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
+ args = parser.parse_args()
+ main()
+
+# vim: sw=4 et
def main():
if not args.git_dir:
cmd = 'git rev-parse --show-toplevel 2>/dev/null || echo .'
- top_dir = subprocess.check_output(cmd, shell=True).decode('utf-8').strip()
+ top_dir = subprocess.check_output(cmd, shell=True, encoding='utf-8').strip()
args.git_dir = os.path.join(top_dir, '.git')
if not args.prefix:
os.chdir(top_dir)
else:
cmd = git + 'ls-files -z'.split()
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
- out = proc.communicate()[0].decode('utf-8')
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, encoding='utf-8')
+ out = proc.communicate()[0]
ls = set(out.split('\0'))
ls.discard('')
+ if not args.tree:
+ # All modified files keep their current mtime.
+ proc = subprocess.Popen(git + 'ls-files -m -z'.split(), stdout=subprocess.PIPE, encoding='utf-8')
+ out = proc.communicate()[0]
+ for fn in out.split('\0'):
+ if fn == '':
+ continue
+ if args.list:
+ mtime = os.lstat(fn).st_mtime
+ print_line(fn, mtime, mtime)
+ ls.discard(fn)
+
cmd = git + 'log -r --name-only --no-color --pretty=raw --no-renames -z'.split()
if args.tree:
cmd.append(args.tree)
cmd += ['--'] + args.files
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, encoding='utf-8')
for line in proc.stdout:
- line = line.decode('utf-8').strip()
+ line = line.strip()
m = re.match(r'^committer .*? (\d+) [-+]\d+$', line)
if m:
commit_time = int(m[1])
fn = args.prefix + fn
mtime = os.lstat(fn).st_mtime
if args.list:
- if args.list > 1:
- ts = str(commit_time).rjust(10)
- else:
- ts = datetime.utcfromtimestamp(commit_time).strftime("%Y-%m-%d %H:%M:%S")
- chg = '.' if mtime == commit_time else '*'
- print(chg, ts, fn)
+ print_line(fn, mtime, commit_time)
elif mtime != commit_time:
if not args.quiet:
print(f"Setting {fn}")
proc.communicate()
+def print_line(fn, mtime, commit_time):
+ if args.list > 1:
+ ts = str(commit_time).rjust(10)
+ else:
+ ts = datetime.utcfromtimestamp(commit_time).strftime("%Y-%m-%d %H:%M:%S")
+ chg = '.' if mtime == commit_time else '*'
+ print(chg, ts, fn)
+
+
if __name__ == '__main__':
- parser = argparse.ArgumentParser(description="Set the times of the current git checkout to their last-changed time.")
+ parser = argparse.ArgumentParser(description="Set the times of the current git checkout to their last-changed time.", add_help=False)
parser.add_argument('--git-dir', metavar='GIT_DIR', help="The git dir to query (defaults to affecting the current git checkout).")
parser.add_argument('--tree', metavar='TREE-ISH', help="The tree-ish to query (defaults to the current branch).")
parser.add_argument('--prefix', metavar='PREFIX_STR', help="Prepend the PREFIX_STR to each filename we tweak.")
parser.add_argument('--quiet', '-q', action='store_true', help="Don't output the changed-file information.")
- parser.add_argument('--list', '-l', action='count', help="List the files and their dates instead of changing them. Repeat for Unix Time instead of human reable.")
+ parser.add_argument('--list', '-l', action='count', help="List files & times instead of changing them. Repeat for Unix timestamp instead of human readable.")
parser.add_argument('files', metavar='FILE', nargs='*', help="Specify a subset of checked-out files to tweak.")
+ parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
args = parser.parse_args()
main()