summaryrefslogtreecommitdiff
path: root/tools/dist
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-08-05 16:22:51 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-08-05 16:22:51 +0000
commitcf46733632c7279a9fd0fe6ce26f9185a4ae82a9 (patch)
treeda27775a2161723ef342e91af41a8b51fedef405 /tools/dist
parentbb0ef45f7c46b0ae221b26265ef98a768c33f820 (diff)
downloadsubversion-tarball-cf46733632c7279a9fd0fe6ce26f9185a4ae82a9.tar.gz
Diffstat (limited to 'tools/dist')
-rwxr-xr-xtools/dist/backport.pl1235
-rw-r--r--tools/dist/backport_accept.dump550
-rw-r--r--tools/dist/backport_branches.dump642
-rw-r--r--tools/dist/backport_indented_entry.dump522
-rw-r--r--tools/dist/backport_multirevisions.dump534
-rwxr-xr-xtools/dist/backport_tests.py578
-rw-r--r--tools/dist/backport_two_approveds.dump961
-rwxr-xr-xtools/dist/dist.sh33
-rwxr-xr-xtools/dist/make-deps-tarball.sh121
-rwxr-xr-xtools/dist/nightly.sh6
l---------tools/dist/nominate.pl1
-rwxr-xr-xtools/dist/release.py198
-rw-r--r--tools/dist/templates/download.ezt2
-rw-r--r--tools/dist/templates/rc-news.ezt2
-rw-r--r--tools/dist/templates/rc-release-ann.ezt2
-rw-r--r--tools/dist/templates/stable-news.ezt2
-rw-r--r--tools/dist/templates/stable-release-ann.ezt2
17 files changed, 5067 insertions, 324 deletions
diff --git a/tools/dist/backport.pl b/tools/dist/backport.pl
index ab5c823..0c5f6be 100755
--- a/tools/dist/backport.pl
+++ b/tools/dist/backport.pl
@@ -1,8 +1,10 @@
-#!/usr/bin/perl -l
+#!/usr/bin/perl
use warnings;
use strict;
use feature qw/switch say/;
+#no warnings 'experimental::smartmatch';
+
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -20,252 +22,1259 @@ use feature qw/switch say/;
# specific language governing permissions and limitations
# under the License.
+use Carp qw/croak confess carp cluck/;
+use Digest ();
use Term::ReadKey qw/ReadMode ReadKey/;
+use File::Basename qw/basename dirname/;
+use File::Copy qw/copy move/;
use File::Temp qw/tempfile/;
-use POSIX qw/ctermid/;
+use IO::Select ();
+use IPC::Open3 qw/open3/;
+use POSIX qw/ctermid strftime isprint isspace/;
+use Text::Wrap qw/wrap/;
+use Tie::File ();
+
+############### Start of reading values from environment ###############
+# Programs we use.
+#
+# TODO: document which are interpreted by sh and which should point to binary.
my $SVN = $ENV{SVN} || 'svn'; # passed unquoted to sh
+my $SHELL = $ENV{SHELL} // '/bin/sh';
my $VIM = 'vim';
+my $EDITOR = $ENV{SVN_EDITOR} // $ENV{VISUAL} // $ENV{EDITOR} // 'ed';
+my $PAGER = $ENV{PAGER} // 'less' // 'cat';
+
+# Mode flags.
+package Mode {
+ use constant {
+ AutoCommitApproveds => 1, # used by nightly commits (svn-role)
+ Conflicts => 2, # used by the hourly conflicts-detection buildbot
+ Interactive => 3,
+ };
+};
+my $YES = ($ENV{YES} // "0") =~ /^(1|yes|true)$/i; # batch mode: eliminate prompts, add sleeps
+my $MAY_COMMIT = ($ENV{MAY_COMMIT} // "false") =~ /^(1|yes|true)$/i;
+my $MODE = ($YES ? ($MAY_COMMIT ? Mode::AutoCommitApproveds : Mode::Conflicts )
+ : Mode::Interactive );
+
+# Other knobs.
+my $VERBOSE = 0;
+my $DEBUG = (exists $ENV{DEBUG}); # 'set -x', etc
+
+# Force all these knobs to be usable via @sh.
+my @sh = qw/false true/;
+die if grep { ($sh[$_] eq 'true') != !!$_ } $DEBUG, $MAY_COMMIT, $VERBOSE, $YES;
+
+# Username for entering votes.
+my $SVN_A_O_REALM = '<https://svn.apache.org:443> ASF Committers';
+my ($AVAILID) = $ENV{AVAILID} // do {
+ local $_ = `$SVN auth svn.apache.org:443 2>/dev/null`; # TODO: pass $SVN_A_O_REALM
+ ($? == 0 && /Auth.*realm: \Q$SVN_A_O_REALM\E\nUsername: (.*)/) ? $1 : undef
+} // do {
+ local $/; # slurp mode
+ my $fh;
+ my $dir = "$ENV{HOME}/.subversion/auth/svn.simple/";
+ my $filename = Digest->new("MD5")->add($SVN_A_O_REALM)->hexdigest;
+ open $fh, '<', "$dir/$filename"
+ and <$fh> =~ /K 8\nusername\nV \d+\n(.*)/
+ ? $1
+ : undef
+};
+
+unless (defined $AVAILID) {
+ unless ($MODE == Mode::Conflicts) {
+ warn "Username for commits (of votes/merges) not found; "
+ ."it will be possible to review nominations but not to commit votes "
+ ."or merges.\n";
+ warn "Press the 'any' key to continue...\n";
+ die if $MODE == Mode::AutoCommitApproveds; # unattended mode; can't prompt.
+ ReadMode 'cbreak';
+ ReadKey 0;
+ ReadMode 'restore';
+ }
+}
+
+############## End of reading values from the environment ##############
+
+# Constants.
my $STATUS = './STATUS';
+my $STATEFILE = './.backports1';
my $BRANCHES = '^/subversion/branches';
+my $TRUNK = '^/subversion/trunk';
+$ENV{LC_ALL} = "C"; # since we parse 'svn info' output and use isprint()
-my $YES = $ENV{YES}; # batch mode: eliminate prompts, add sleeps
-my $WET_RUN = qw[false true][1]; # don't commit
-my $DEBUG = qw[false true][0]; # 'set -x', etc
-
-# derived values
+# Globals.
+my %ERRORS = ();
+# TODO: can $MERGED_SOMETHING be removed and references to it replaced by scalar(@MERGES_TODAY) ?
+# alternately, does @MERGES_TODAY need to be purged whenever $MERGED_SOMETHING is reset?
+# The scalar is only used in interactive runs, but the array is used in
+# svn-role batch mode too.
+my @MERGES_TODAY;
+my $MERGED_SOMETHING = 0;
my $SVNq;
+# Derived values.
+my $SVNvsn = do {
+ my ($major, $minor, $patch) = `$SVN --version -q` =~ /^(\d+)\.(\d+)\.(\d+)/;
+ 1e6*$major + 1e3*$minor + $patch;
+};
$SVN .= " --non-interactive" if $YES or not defined ctermid;
$SVNq = "$SVN -q ";
-$SVNq =~ s/-q// if $DEBUG eq 'true';
+$SVNq =~ s/-q// if $DEBUG;
-sub usage {
- my $basename = $0;
- $basename =~ s#.*/##;
+
+sub backport_usage {
+ my $basename = basename $0;
print <<EOF;
-Run this from the root of your release branch (e.g., 1.6.x) working copy.
+backport.pl: a tool for reviewing, merging, and voting on STATUS entries.
+
+Normally, invoke this with CWD being the root of the stable branch (e.g.,
+1.8.x):
-For each entry in STATUS, you will be prompted whether to merge it.
+ Usage: test -e \$d/STATUS && cd \$d && \\
+ backport.pl [PATTERN]
+ (where \$d is a working copy of branches/1.8.x)
-WARNING:
-If you accept the prompt, $basename will revert all local changes and will
-commit the merge immediately.
+Alternatively, invoke this via a symlink named "b" placed at the same directory
+as the STATUS file, in which case the CWD doesn't matter (the script will cd):
+
+ Usage: ln -s /path/to/backport.pl \$d/b && \\
+ \$d/b [PATTERN]
+ (where \$d is a working copy of branches/1.8.x)
+
+In either case, the ./STATUS file should be at HEAD. If it has local mods,
+they will be preserved through 'revert' operations but included in 'commit'
+operations.
+
+If PATTERN is provided, only entries which match PATTERN are considered. The
+sense of "match" is either substring (fgrep) or Perl regexp (with /msi).
+
+In interactive mode (the default), you will be prompted once per STATUS entry.
+At a prompt, you have the following options:
+
+y: Run a merge. It will not be committed.
+ WARNING: This will run 'update' and 'revert -R ./'.
+l: Show logs for the entries being nominated.
+v: Show the full entry (the prompt only shows an abridged version).
+q: Quit the "for each nomination" loop.
+±1: Enter a +1 or -1 vote
+ You will be prompted to commit your vote at the end.
+±0: Enter a +0 or -0 vote
+ You will be prompted to commit your vote at the end.
+a: Move the entry to the "Approved changes" section.
+ When both approving and voting on an entry, approve first: for example,
+ to enter a third +1 vote, type "a" "+" "1".
+e: Edit the entry in $EDITOR.
+ You will be prompted to commit your edits at the end.
+N: Move to the next entry. Cache the entry in '$STATEFILE' and do not
+ prompt for it again (even across runs) until it is changed.
+ : Move to the next entry, without adding the current one to the cache.
+ (That's a space character, ASCII 0x20.)
+
+After running a merge, you have the following options:
+
+y: Open a shell.
+d: View a diff.
+N: Move to the next entry.
+
+To commit a merge, you have two options: either answer 'y' to the second prompt
+to open a shell, and manually run 'svn commit' therein; or set \$MAY_COMMIT=1
+in the environment before running the script, in which case answering 'y'
+to the first prompt will not only run the merge but also commit it.
+
+There are two batch modes. The first mode is used by the nightly svn-role
+mergebot. It is enabled by setting \$YES and \$MAY_COMMIT to '1' in the
+environment. In this mode, the script will iterate the "Approved changes:"
+section and merge and commit each entry therein. To prevent an entry from
+being auto-merged, veto it or move it to a new section named "Approved, but
+merge manually:".
+
+The second batch mode is used by the hourly conflicts detector bot. It is
+triggered by having \$YES defined in the environment to '1' and \$MAY_COMMIT
+undefined. In this mode, the script will locally merge every nomination
+(including unapproved and vetoed ones), and complain to stderr if the merge
+failed due to a conflict. This mode never commits anything.
+
+The hourly conflicts detector bot turns red if any entry produced a merge
+conflict. When entry A depends on entry B for a clean merge, put a "Depends:"
+header on entry A to instruct the bot not to turn red due to A. (The header
+is not parsed; only its presence or absence matters.)
+
+Both batch modes also perform a basic sanity-check on entries that declare
+backport branches (via the "Branch:" header): if a backport branch is used, but
+at least one of the revisions enumerated in the entry title had not been merged
+from $TRUNK to the branch root, the hourly bot will turn red and
+nightly bot will skip the entry and email its admins. (The nightly bot does
+not email the list on failure, since it doesn't use buildbot.)
The 'svn' binary defined by the environment variable \$SVN, or otherwise the
'svn' found in \$PATH, will be used to manage the working copy.
EOF
}
+sub nominate_usage {
+ my $availid = $AVAILID // "(your username)";
+ my $basename = basename $0;
+ print <<EOF;
+nominate.pl: a tool for adding entries to STATUS.
+
+Usage: $0 "foo r42 bar r43 qux 45." "\$Some_justification"
+
+Will add:
+ * r42, r43, r45
+ (log message of r42)
+ Justification:
+ \$Some_justification
+ Votes:
+ +1: $availid
+to STATUS. Backport branches are detected automatically.
+
+The STATUS file in the current directory is used (unless argv[0] is "n", in
+which case the STATUS file in the directory of argv[0] is used; the intent
+is to create a symlink named "n" in the branch wc root).
+
+EOF
+# TODO: Optionally add a "Notes" section.
+# TODO: Look for backport branches named after issues.
+# TODO: Do a dry-run merge on added entries.
+# TODO: Do a dry-run merge on interactively-edited entries in backport.pl
+}
+
+# If $AVAILID is undefined, warn about it and return true.
+# Else return false.
+#
+# $_[0] is a string for inclusion in generated error messages.
+sub warned_cannot_commit {
+ my $caller_error_string = shift;
+ return 0 if defined $AVAILID;
+
+ warn "$0: $caller_error_string: unable to determine your username via \$AVAILID or svnauth(1) or ~/.subversion/auth/";
+ return 1;
+}
+
+sub digest_string {
+ Digest->new("MD5")->add(@_)->hexdigest
+}
+
+sub digest_entry($) {
+ # Canonicalize the number of trailing EOLs to two. This matters when there's
+ # on empty line after the last entry in Approved, for example.
+ local $_ = shift;
+ s/\n*\z// and $_ .= "\n\n";
+ digest_string($_)
+}
+
sub prompt {
- local $\; # disable 'perl -l' effects
- print "Go ahead? ";
-
- # TODO: this part was written by trial-and-error
- ReadMode 'cbreak';
- my $answer = (ReadKey 0);
- print $answer, "\n";
- return ($answer =~ /^y/i) ? 1 : 0;
+ print $_[0]; shift;
+ my %args = @_;
+ my $getchar = sub {
+ my $answer;
+ do {
+ ReadMode 'cbreak';
+ $answer = (ReadKey 0);
+ ReadMode 'normal';
+ die if $@ or not defined $answer;
+ # Swallow terminal escape codes (e.g., arrow keys).
+ unless (isprint $answer or isspace $answer) {
+ $answer = (ReadKey -1) while defined $answer;
+ # TODO: provide an indication that the keystroke was sensed and ignored.
+ }
+ } until defined $answer and (isprint $answer or isspace $answer);
+ print $answer;
+ return $answer;
+ };
+
+ die "$0: called prompt() in non-interactive mode!" if $YES;
+ my $answer = $getchar->();
+ $answer .= $getchar->() if exists $args{extra} and $answer =~ $args{extra};
+ say "" unless $args{dontprint};
+ return $args{verbose}
+ ? $answer
+ : ($answer =~ /^y/i) ? 1 : 0;
+}
+
+# Bourne-escape a string.
+# Example:
+# >>> shell_escape(q[foo'bar]) eq q['foo'\''bar']
+# True
+sub shell_escape {
+ my (@reply) = map {
+ local $_ = $_; # the LHS $_ is mutable; the RHS $_ may not be.
+ s/\x27/'\\\x27'/g;
+ "'$_'"
+ } @_;
+ wantarray ? @reply : $reply[0]
+}
+
+sub shell_safe_path_or_url($) {
+ local $_ = shift;
+ return (m{^[A-Za-z0-9._:+/-]+$} and !/^-|^[+]/);
+}
+
+# Shell-safety-validating wrapper for File::Temp::tempfile
+sub my_tempfile {
+ my ($fh, $fn) = tempfile();
+ croak "Tempfile name '$fn' not shell-safe; aborting"
+ unless shell_safe_path_or_url $fn;
+ return ($fh, $fn);
+}
+
+# The first argument is a shell script. Run it and return the shell's
+# exit code, and stdout and stderr as references to arrays of lines.
+sub run_in_shell($) {
+ my $script = shift;
+ my $pid = open3 \*SHELL_IN, \*SHELL_OUT, \*SHELL_ERR, qw#/bin/sh#;
+ # open3 raises exception when it fails; no need to error check
+
+ print SHELL_IN $script;
+ close SHELL_IN;
+
+ # Read loop: tee stdout,stderr to arrays.
+ my $select = IO::Select->new(\*SHELL_OUT, \*SHELL_ERR);
+ my (@readable, $outlines, $errlines);
+ while (@readable = $select->can_read) {
+ for my $fh (@readable) {
+ my $line = <$fh>;
+ $select->remove($fh) if eof $fh or not defined $line;
+ next unless defined $line;
+
+ if ($fh == \*SHELL_OUT) {
+ push @$outlines, $line;
+ print STDOUT $line;
+ }
+ if ($fh == \*SHELL_ERR) {
+ push @$errlines, $line;
+ print STDERR $line;
+ }
+ }
+ }
+ waitpid $pid, 0; # sets $?
+ return $?, $outlines, $errlines;
}
+
+# EXPECTED_ERROR_P is subref called with EXIT_CODE, OUTLINES, ERRLINES,
+# expected to return TRUE if the error should be considered fatal (cause
+# backport.pl to exit non-zero) or not. It may be undef for default behaviour.
sub merge {
- my %entry = @_;
+ my %entry = %{ +shift };
+ my $expected_error_p = shift // sub { 0 }; # by default, errors are unexpected
+ my $parno = $entry{parno} - scalar grep { $_->{parno} < $entry{parno} } @MERGES_TODAY;
- my ($logmsg_fh, $logmsg_filename) = tempfile();
- my ($mergeargs, $pattern);
+ my ($logmsg_fh, $logmsg_filename) = my_tempfile();
+ my (@mergeargs);
- my $backupfile = "backport_pl.$$.tmp";
+ my $shell_escaped_branch = shell_escape($entry{branch})
+ if defined($entry{branch});
if ($entry{branch}) {
- # NOTE: This doesn't escape the branch into the pattern.
- $pattern = sprintf '\V\(%s branch(es)?\|branches\/%s\|Branch(es)?:\n *%s\)', $entry{branch}, $entry{branch}, $entry{branch};
- $mergeargs = "--reintegrate $BRANCHES/$entry{branch}";
- print $logmsg_fh "Reintegrate the $entry{header}:";
- print $logmsg_fh "";
- } elsif (@{$entry{revisions}}) {
- $pattern = '^ [*] \V' . 'r' . $entry{revisions}->[0];
- $mergeargs = join " ", (map { "-c$_" } @{$entry{revisions}}), '^/subversion/trunk';
- if (@{$entry{revisions}} > 1) {
- print $logmsg_fh "Merge the $entry{header} from trunk:";
- print $logmsg_fh "";
+ if ($SVNvsn >= 1_008_000) {
+ @mergeargs = shell_escape "$BRANCHES/$entry{branch}";
+ say $logmsg_fh "Merge $entry{header}:";
} else {
- print $logmsg_fh "Merge r$entry{revisions}->[0] from trunk:";
- print $logmsg_fh "";
+ @mergeargs = shell_escape qw/--reintegrate/, "$BRANCHES/$entry{branch}";
+ say $logmsg_fh "Reintegrate $entry{header}:";
}
+ say $logmsg_fh "";
+ } elsif (@{$entry{revisions}}) {
+ @mergeargs = shell_escape(
+ ($entry{accept} ? "--accept=$entry{accept}" : ()),
+ (map { "-c$_" } @{$entry{revisions}}),
+ '--',
+ '^/subversion/trunk',
+ );
+ say $logmsg_fh
+ "Merge $entry{header} from trunk",
+ $entry{accept} ? ", with --accept=$entry{accept}" : "",
+ ":";
+ say $logmsg_fh "";
} else {
die "Don't know how to call $entry{header}";
}
- print $logmsg_fh $_ for @{$entry{entry}};
+ say $logmsg_fh $_ for @{$entry{entry}};
close $logmsg_fh or die "Can't close $logmsg_filename: $!";
+ my $reintegrated_word = ($SVNvsn >= 1_008_000) ? "merged" : "reintegrated";
my $script = <<"EOF";
#!/bin/sh
set -e
-if $DEBUG; then
+if $sh[$DEBUG]; then
set -x
fi
-$SVN diff > $backupfile
-$SVNq revert -R .
$SVNq up
-$SVNq merge $mergeargs
-$VIM -e -s -n -N -i NONE -u NONE -c '/$pattern/normal! dap' -c wq $STATUS
-if $WET_RUN; then
+$SVNq merge @mergeargs
+if [ "`$SVN status -q | wc -l`" -eq 1 ]; then
+ if [ -n "`$SVN diff | perl -lne 'print if s/^(Added|Deleted|Modified): //' | grep -vx svn:mergeinfo`" ]; then
+ # This check detects STATUS entries that name non-^/subversion/ revnums.
+ # ### Q: What if we actually commit a mergeinfo fix to trunk and then want
+ # ### to backport it?
+ # ### A: We don't merge it using the script.
+ echo "Bogus merge: includes only svn:mergeinfo changes!" >&2
+ exit 2
+ fi
+fi
+if $sh[$MAY_COMMIT]; then
+ # Remove the approved entry. The sentinel is important when the entry being
+ # removed is the very last one in STATUS, and in that case it has two effects:
+ # (1) keeps STATUS from ending in a run of multiple empty lines;
+ # (2) makes the \x{7d}k motion behave the same as in all other cases.
+ #
+ # Use a tempfile because otherwise backport_main() would see the "sentinel paragraph".
+ # Since backport_main() has an open descriptor, it will continue to see
+ # the STATUS inode that existed when control flow entered backport_main();
+ # since we replace the file on disk, when this block of code runs in the
+ # next iteration, it will see the new contents.
+ cp $STATUS $STATUS.t
+ (echo; echo; echo "sentinel paragraph") >> $STATUS.t
+ $VIM -e -s -n -N -i NONE -u NONE -c ':0normal! $parno\x{7d}kdap' -c wq $STATUS.t
+ $VIM -e -s -n -N -i NONE -u NONE -c '\$normal! dap' -c wq $STATUS.t
+ mv $STATUS.t $STATUS
$SVNq commit -F $logmsg_filename
-else
- echo "Committing:"
+elif ! $sh[$YES]; then
+ echo "Would have committed:"
+ echo '[[['
$SVN status -q
+ echo 'M STATUS (not shown in the diff)'
cat $logmsg_filename
+ echo ']]]'
fi
EOF
+ if ($MAY_COMMIT) {
+ # STATUS has been edited and the change has been committed
+ push @MERGES_TODAY, \%entry;
+ }
+
$script .= <<"EOF" if $entry{branch};
reinteg_rev=\`$SVN info $STATUS | sed -ne 's/Last Changed Rev: //p'\`
-if $WET_RUN; then
+if $sh[$MAY_COMMIT]; then
# Sleep to avoid out-of-order commit notifications
- if [ -n "\$YES" ]; then sleep 15; fi
- $SVNq rm $BRANCHES/$entry{branch} -m "Remove the '$entry{branch}' branch, reintegrated in r\$reinteg_rev."
- if [ -n "\$YES" ]; then sleep 1; fi
-else
- echo "Removing reintegrated '$entry{branch}' branch"
+ if $sh[$YES]; then sleep 15; fi
+ $SVNq rm $BRANCHES/$shell_escaped_branch -m "Remove the '"$shell_escaped_branch"' branch, $reintegrated_word in r\$reinteg_rev."
+ if $sh[$YES]; then sleep 1; fi
+elif ! $sh[$YES]; then
+ echo "Would remove $reintegrated_word '"$shell_escaped_branch"' branch"
fi
EOF
- open SHELL, '|-', qw#/bin/sh# or die $!;
- print SHELL $script;
- close SHELL or warn "$0: sh($?): $!";
+ # Include the time so it's easier to find the interesting backups.
+ my $backupfile = strftime "backport_pl.%Y%m%d-%H%M%S.$$.tmp", localtime;
+ die if -s $backupfile;
+ system("$SVN diff > $backupfile") == 0
+ or die "Saving a backup diff ($backupfile) failed ($?): $!";
+ if (-z $backupfile) {
+ unlink $backupfile;
+ } else {
+ warn "Local mods saved to '$backupfile'\n";
+ }
+
+ # If $MAY_COMMIT, then $script will edit STATUS anyway.
+ revert(verbose => 0, discard_STATUS => $MAY_COMMIT);
+
+ $MERGED_SOMETHING++;
+ my ($exit_code, $outlines, $errlines) = run_in_shell $script;
+ unless ($! == 0) {
+ die "system() failed to spawn subshell ($!); aborting";
+ }
+ unless ($exit_code == 0) {
+ warn "$0: subshell exited with code $exit_code (in '$entry{header}') "
+ ."(maybe due to 'set -e'?)";
+
+ # If we're committing, don't attempt to guess the problem and gracefully
+ # continue; just abort.
+ if ($MAY_COMMIT) {
+ die "Lost track of paragraph numbers; aborting";
+ }
- unlink $backupfile if -z $backupfile;
- unlink $logmsg_filename unless $? or $!;
+ # Record the error, unless the caller wants not to.
+ $ERRORS{$entry{id}} = [\%entry, "subshell exited with code $exit_code"]
+ unless $expected_error_p->($exit_code, $outlines, $errlines);
+ }
+
+ unlink $logmsg_filename unless $exit_code;
}
+# Input formats:
+# "1.8.x-r42",
+# "branches/1.8.x-r42",
+# "branches/1.8.x-r42/",
+# "subversion/branches/1.8.x-r42",
+# "subversion/branches/1.8.x-r42/",
+# "^/subversion/branches/1.8.x-r42",
+# "^/subversion/branches/1.8.x-r42/",
+# Return value:
+# "1.8.x-r42"
+# Works for any branch name that doesn't include slashes.
sub sanitize_branch {
local $_ = shift;
- s#.*/##;
s/^\s*//;
s/\s*$//;
+ s#/*$##;
+ s#.*/##;
return $_;
}
+sub logsummarysummary {
+ my $entry = shift;
+ join "",
+ $entry->{logsummary}->[0], ('[...]' x (0 < $#{$entry->{logsummary}}))
+}
+
# TODO: may need to parse other headers too?
sub parse_entry {
+ my $raw = shift;
+ my $parno = shift;
my @lines = @_;
+ my $depends;
+ my $accept;
my (@revisions, @logsummary, $branch, @votes);
# @lines = @_;
- # strip first three spaces
- $_[0] =~ s/^ \* / /;
- s/^ // for @_;
+ # strip spaces to match up with the indention
+ $_[0] =~ s/^( *)\* //;
+ my $indentation = ' ' x (length($1) + 2);
+ s/^$indentation// for @_;
+
+ # Ignore trailing spaces: it is not significant on any field, and makes the
+ # regexes simpler.
+ s/\s*$// for @_;
# revisions
- $branch = sanitize_branch $1 if $_[0] =~ /^(\S*) branch$/;
- while ($_[0] =~ /^r/) {
- while ($_[0] =~ s/^r(\d+)(?:$|[,; ]+)//) {
- push @revisions, $1;
- }
+ $branch = sanitize_branch $1
+ and shift
+ if $_[0] =~ /^(\S*) branch$/ or $_[0] =~ m#branches/(\S+)#;
+ while ($_[0] =~ /^(?:r?\d+[,; ]*)+$/) {
+ push @revisions, ($_[0] =~ /(\d+)/g);
shift;
}
# summary
- push @logsummary, shift until $_[0] =~ /^\s*\w+:/ or not defined $_[0];
+ do {
+ push @logsummary, shift
+ } until $_[0] =~ /^\s*[][\w]+:/ or not defined $_[0];
# votes
unshift @votes, pop until $_[-1] =~ /^\s*Votes:/ or not defined $_[-1];
pop;
- # branch
+ # depends, branch, notes
+ # Ignored headers: Changes[*]
while (@_) {
- shift and next unless $_[0] =~ s/^\s*Branch(es)?:\s*//;
- $branch = sanitize_branch (shift || shift || die "Branch header found without value");
+ given (shift) {
+ when (/^Depends:/) {
+ $depends++;
+ }
+ if (s/^Branch:\s*//) {
+ $branch = sanitize_branch ($_ || shift || die "Branch header found without value");
+ }
+ if (s/^Notes:\s*//) {
+ my $notes = $_;
+ $notes .= shift while @_ and $_[0] !~ /^\w/;
+ my %accepts = map { $_ => 1 } ($notes =~ /--accept[ =]([a-z-]+)/g);
+ given (scalar keys %accepts) {
+ when (0) { }
+ when (1) { $accept = [keys %accepts]->[0]; }
+ default {
+ warn "Too many --accept values at '",
+ logsummarysummary({ logsummary => [@logsummary] }),
+ "'";
+ }
+ }
+ }
+ }
}
# Compute a header.
- my $header;
- $header = "r$revisions[0] group" if @revisions;
- $header = "$branch branch" if $branch;
- warn "No header for [@lines]" unless $header;
+ my ($header, $id);
+ if ($branch) {
+ $header = "the $branch branch";
+ $id = $branch;
+ } elsif (@revisions == 1) {
+ $header = "r$revisions[0]";
+ $id = "r$revisions[0]";
+ } elsif (@revisions) {
+ $header = "the r$revisions[0] group";
+ $id = "r$revisions[0]";
+ } else {
+ die "Entry '$raw' has neither revisions nor branch";
+ }
+ my $header_start = ($header =~ /^the/ ? ucfirst($header) : $header);
+
+ warn "Entry has both branch '$branch' and --accept=$accept specified\n"
+ if $branch and $accept;
return (
revisions => [@revisions],
logsummary => [@logsummary],
branch => $branch,
header => $header,
+ header_start => $header_start,
+ depends => $depends,
+ id => $id,
votes => [@votes],
entry => [@lines],
+ accept => $accept,
+ raw => $raw,
+ digest => digest_entry($raw),
+ parno => $parno, # $. from backport_main()
);
}
+sub edit_string {
+ # Edits $_[0] in an editor.
+ # $_[1] is used in error messages.
+ die "$0: called edit_string() in non-interactive mode!" if $YES;
+ my $string = shift;
+ my $name = shift;
+ my %args = @_;
+ my $trailing_eol = $args{trailing_eol};
+ my ($fh, $fn) = my_tempfile();
+ print $fh $string;
+ $fh->flush or die $!;
+ system("$EDITOR -- $fn") == 0
+ or warn "\$EDITOR failed editing $name: $! ($?); "
+ ."edit results ($fn) ignored.";
+ my $rv = `cat $fn`;
+ $rv =~ s/\n*\z// and $rv .= ("\n" x $trailing_eol) if defined $trailing_eol;
+ $rv;
+}
+
+sub vote {
+ my ($state, $approved, $votes) = @_;
+ # TODO: use votesarray instead of votescheck
+ my (%approvedcheck, %votescheck);
+ my $raw_approved = "";
+ my @votesarray;
+ return unless %$approved or %$votes;
+
+ # If $AVAILID is undef, we can only process 'edit' pseudovotes; handle_entry() is
+ # supposed to prevent numeric (±1,±0) votes from getting to this point.
+ die "Assertion failed" if not defined $AVAILID
+ and grep { $_ ne 'edit' } map { $_->[0] } values %$votes;
+
+ my $had_empty_line;
+
+ $. = 0;
+ open STATUS, "<", $STATUS;
+ open VOTES, ">", "$STATUS.$$.tmp";
+ while (<STATUS>) {
+ $had_empty_line = /\n\n\z/;
+ my $key = digest_entry $_;
+
+ $approvedcheck{$key}++ if exists $approved->{$key};
+ $votescheck{$key}++ if exists $votes->{$key};
+
+ unless (exists $votes->{$key} or exists $approved->{$key}) {
+ print VOTES;
+ next;
+ }
+
+ unless (exists $votes->{$key}) {
+ push @votesarray, {
+ entry => $approved->{$key},
+ approval => 1,
+ digest => $key,
+ };
+ $raw_approved .= $_;
+ next;
+ }
+
+ # We have a vote, and potentially an approval.
+
+ my ($vote, $entry) = @{$votes->{$key}};
+ push @votesarray, {
+ entry => $entry,
+ vote => $vote,
+ approval => (exists $approved->{$key}),
+ digest => $key,
+ };
+
+ if ($vote eq 'edit') {
+ local $_ = $entry->{raw};
+ $votesarray[-1]->{digest} = digest_entry $_;
+ (exists $approved->{$key}) ? ($raw_approved .= $_) : (print VOTES);
+ next;
+ }
+
+ s/^(\s*\Q$vote\E:.*)/"$1, $AVAILID"/me
+ or s/(.*\w.*?\n)/"$1 $vote: $AVAILID\n"/se;
+ $_ = edit_string $_, $entry->{header}, trailing_eol => 2
+ if $vote ne '+1';
+ $votesarray[-1]->{digest} = digest_entry $_;
+ (exists $approved->{$key}) ? ($raw_approved .= $_) : (print VOTES);
+ }
+ close STATUS;
+ print VOTES "\n" if $raw_approved and !$had_empty_line;
+ print VOTES $raw_approved;
+ close VOTES;
+ warn "Some vote chunks weren't found: ",
+ join ',',
+ map $votes->{$_}->[1]->{id},
+ grep { !$votescheck{$_} } keys %$votes
+ if scalar(keys %$votes) != scalar(keys %votescheck);
+ warn "Some approval chunks weren't found: ",
+ join ',',
+ map $approved->{$_}->{id},
+ grep { !$approvedcheck{$_} } keys %$approved
+ if scalar(keys %$approved) != scalar(keys %approvedcheck);
+ prompt "Press the 'any' key to continue...\n", dontprint => 1
+ if scalar(keys %$approved) != scalar(keys %approvedcheck)
+ or scalar(keys %$votes) != scalar(keys %votescheck);
+ move "$STATUS.$$.tmp", $STATUS;
+
+ my $logmsg = do {
+ my @sentences = map {
+ my $words_vote = ", approving" x $_->{approval};
+ my $words_edit = " and approve" x $_->{approval};
+ exists $_->{vote}
+ ? (
+ ( $_->{vote} eq 'edit'
+ ? "Edit$words_edit the $_->{entry}->{id} entry"
+ : "Vote $_->{vote} on $_->{entry}->{header}$words_vote"
+ )
+ . "."
+ )
+ : # exists only in $approved
+ "Approve $_->{entry}->{header}."
+ } @votesarray;
+ (@sentences == 1)
+ ? $sentences[0]
+ : "* STATUS:\n" . join "", map " $_\n", @sentences;
+ };
+
+ system "$SVN diff -- $STATUS";
+ printf "[[[\n%s%s]]]\n", $logmsg, ("\n" x ($logmsg !~ /\n\z/));
+ if (prompt "Commit these votes? ") {
+ my ($logmsg_fh, $logmsg_filename) = my_tempfile();
+ print $logmsg_fh $logmsg;
+ close $logmsg_fh;
+ system("$SVN commit -F $logmsg_filename -- $STATUS") == 0
+ or warn("Committing the votes failed($?): $!") and return;
+ unlink $logmsg_filename;
+
+ # Add to state votes that aren't '+0' or 'edit'
+ $state->{$_->{digest}}++ for grep
+ +{ qw/-1 t -0 t +1 t/ }->{$_->{vote}},
+ @votesarray;
+ }
+}
+
+sub check_local_mods_to_STATUS {
+ if (`$SVN status -q $STATUS`) {
+ die "Local mods to STATUS file $STATUS" if $YES;
+ warn "Local mods to STATUS file $STATUS";
+ system "$SVN diff -- $STATUS";
+ prompt "Press the 'any' key to continue...\n", dontprint => 1;
+ return 1;
+ }
+ return 0;
+}
+
+sub renormalize_STATUS {
+ my $vimscript = <<'EOVIM';
+:"" Strip trailing whitespace before entries and section headers, but not
+:"" inside entries (e.g., multi-paragraph Notes: fields).
+:""
+:"" Since an entry is always followed by another entry, section header, or EOF,
+:"" there is no need to separately strip trailing whitespace from lines following
+:"" entries.
+:%s/\v\s+\n(\s*\n)*\ze(\s*[*]|\w)/\r\r/g
+
+:"" Ensure there is exactly one blank line around each entry and header.
+:""
+:"" First, inject a new empty line above and below each entry and header; then,
+:"" squeeze runs of empty lines together.
+:0/^=/,$ g/^ *[*]/normal! O
+:g/^=/normal! o
+:g/^=/-normal! O
+:
+:%s/\n\n\n\+/\r\r/g
+
+:"" Save.
+:wq
+EOVIM
+ open VIM, '|-', $VIM, qw/-e -s -n -N -i NONE -u NONE --/, $STATUS
+ or die "Can't renormalize STATUS: $!";
+ print VIM $vimscript;
+ close VIM or warn "$0: renormalize_STATUS failed ($?): $!)";
+
+ system("$SVN commit -m '* STATUS: Whitespace changes only.' -- $STATUS") == 0
+ or die "$0: Can't renormalize STATUS ($?): $!"
+ if $MAY_COMMIT;
+}
+
+sub revert {
+ my %args = @_;
+ die "Bug: \$args{verbose} undefined" unless exists $args{verbose};
+ die "Bug: unknown argument" if grep !/^(?:verbose|discard_STATUS)$/, keys %args;
+
+ copy $STATUS, "$STATUS.$$.tmp" unless $args{discard_STATUS};
+ system("$SVN revert -q $STATUS") == 0
+ or die "revert failed ($?): $!";
+ system("$SVN revert -R ./" . (" -q" x !$args{verbose})) == 0
+ or die "revert failed ($?): $!";
+ move "$STATUS.$$.tmp", $STATUS unless $args{discard_STATUS};
+ $MERGED_SOMETHING = 0;
+}
+
+sub maybe_revert {
+ # This is both a SIGINT handler, and the tail end of main() in normal runs.
+ # @_ is 'INT' in the former case and () in the latter.
+ delete $SIG{INT} unless @_;
+ revert verbose => 1 if !$YES and $MERGED_SOMETHING and prompt 'Revert? ';
+ (@_ ? exit : return);
+}
+
+sub signal_handler {
+ my $sig = shift;
+
+ # Clean up after prompt()
+ ReadMode 'normal';
+
+ # Fall back to default action
+ delete $SIG{$sig};
+ kill $sig, $$;
+}
+
+sub warning_summary {
+ return unless %ERRORS;
+
+ warn "Warning summary\n";
+ warn "===============\n";
+ warn "\n";
+ for my $id (keys %ERRORS) {
+ my $title = logsummarysummary $ERRORS{$id}->[0];
+ warn "$id ($title): $ERRORS{$id}->[1]\n";
+ }
+}
+
+sub read_state {
+ # die "$0: called read_state() in non-interactive mode!" if $YES;
+
+ open my $fh, '<', $STATEFILE or do {
+ return {} if $!{ENOENT};
+ die "Can't read statefile: $!";
+ };
+
+ my %rv;
+ while (<$fh>) {
+ chomp;
+ $rv{$_}++;
+ }
+ return \%rv;
+}
+
+sub write_state {
+ my $state = shift;
+ open STATE, '>', $STATEFILE or warn("Can't write state: $!"), return;
+ say STATE for keys %$state;
+ close STATE;
+}
+
+sub exit_stage_left {
+ my $state = shift;
+ maybe_revert;
+ warning_summary if $YES;
+ vote $state, @_;
+ write_state $state;
+ exit scalar keys %ERRORS;
+}
+
+# Given an ENTRY, check whether all ENTRY->{revisions} have been merged
+# into ENTRY->{branch}, if it has one. If revisions are missing, record
+# a warning in $ERRORS. Return TRUE If the entry passed the validation
+# and FALSE otherwise.
+sub validate_branch_contains_named_revisions {
+ my %entry = @_;
+ return 1 unless defined $entry{branch};
+ my %present;
+
+ return "Why are you running so old versions?" # true in boolean context
+ if $SVNvsn < 1_005_000; # doesn't have the 'mergeinfo' subcommand
+
+ my $shell_escaped_branch = shell_escape($entry{branch});
+ %present = do {
+ my @present = `$SVN mergeinfo --show-revs=merged -- $TRUNK $BRANCHES/$shell_escaped_branch`;
+ chomp @present;
+ @present = map /(\d+)/g, @present;
+ map +($_ => 1), @present;
+ };
+
+ my @absent = grep { not exists $present{$_} } @{$entry{revisions}};
+
+ if (@absent) {
+ $ERRORS{$entry{id}} //= [\%entry,
+ sprintf("Revisions '%s' nominated but not included in branch",
+ (join ", ", map { "r$_" } @absent)),
+ ];
+ }
+ return @absent ? 0 : 1;
+}
+
sub handle_entry {
- my %entry = parse_entry @_;
- my @vetoes = grep { /^ -1:/ } @{$entry{votes}};
+ my $in_approved = shift;
+ my $approved = shift;
+ my $votes = shift;
+ my $state = shift;
+ my $raw = shift;
+ my $parno = shift;
+ my $skip = shift;
+ my %entry = parse_entry $raw, $parno, @_;
+ my @vetoes = grep /^\s*-1:/, @{$entry{votes}};
+
+ my $match = defined($skip) ? ($raw =~ /\Q$skip\E/ or $raw =~ /$skip/msi) : 0
+ unless $YES;
if ($YES) {
- merge %entry unless @vetoes;
+ # Run a merge if:
+ unless (@vetoes) {
+ if ($MAY_COMMIT and $in_approved) {
+ # svn-role mode
+ merge \%entry if validate_branch_contains_named_revisions %entry;
+ } elsif (!$MAY_COMMIT) {
+ # Scan-for-conflicts mode
+
+ # First, sanity-check the entry. We ignore the result; even if it
+ # failed, we do want to check for conflicts, in the remainder of this
+ # block.
+ validate_branch_contains_named_revisions %entry;
+
+ # E155015 is SVN_ERR_WC_FOUND_CONFLICT
+ my $expected_error_p = sub {
+ my ($exit_code, $outlines, $errlines) = @_;
+ ($exit_code == 0)
+ or
+ (grep /svn: E155015:/, @$errlines)
+ };
+ merge \%entry, ($entry{depends} ? $expected_error_p : undef);
+
+ my $output = `$SVN status`;
+
+ # Pre-1.6 svn's don't have the 7th column, so fake it.
+ $output =~ s/^(......)/$1 /mg if $SVNvsn < 1_006_000;
+
+ my (@conflicts) = ($output =~ m#^(?:C......|.C.....|......C)\s(.*)#mg);
+ if (@conflicts and !$entry{depends}) {
+ $ERRORS{$entry{id}} //= [\%entry,
+ sprintf "Conflicts on %s%s%s",
+ '[' x !!$#conflicts,
+ (join ', ',
+ map { basename $_ }
+ @conflicts),
+ ']' x !!$#conflicts,
+ ];
+ say STDERR "Conflicts merging $entry{header}!";
+ say STDERR "";
+ say STDERR $output;
+ system "$SVN diff -- " . join ' ', shell_escape @conflicts;
+ } elsif (!@conflicts and $entry{depends}) {
+ # Not a warning since svn-role may commit the dependency without
+ # also committing the dependent in the same pass.
+ print "No conflicts merging $entry{header}, but conflicts were "
+ ."expected ('Depends:' header set)\n";
+ } elsif (@conflicts) {
+ say "Conflicts found merging $entry{header}, as expected.";
+ }
+ revert verbose => 0;
+ }
+ }
+ } elsif (defined($skip) ? not $match : $state->{$entry{digest}}) {
+ print "\n\n";
+ my $reason = defined($skip) ? "doesn't match pattern"
+ : "remove $STATEFILE to reset";
+ say "Skipping $entry{header} ($reason):";
+ say logsummarysummary \%entry;
+ } elsif ($match or not defined $skip) {
+ # This loop is just a hack because 'goto' panics. The goto should be where
+ # the "next PROMPT;" is; there's a "last;" at the end of the loop body.
+ PROMPT: while (1) {
+ say "";
+ say "\n>>> $entry{header_start}:";
+ say join ", ", map { "r$_" } @{$entry{revisions}} if @{$entry{revisions}};
+ say "$BRANCHES/$entry{branch}" if $entry{branch};
+ say "--accept=$entry{accept}" if $entry{accept};
+ say "";
+ say for @{$entry{logsummary}};
+ say "";
+ say for @{$entry{votes}};
+ say "";
+ say "Vetoes found!" if @vetoes;
+
+ # See above for why the while(1).
+ QUESTION: while (1) {
+ my $key = $entry{digest};
+ given (prompt 'Run a merge? [y,l,v,±1,±0,q,e,a, ,N] ',
+ verbose => 1, extra => qr/[+-]/) {
+ when (/^y/i) {
+ #validate_branch_contains_named_revisions %entry;
+ merge \%entry;
+ while (1) {
+ given (prompt "Shall I open a subshell? [ydN] ", verbose => 1) {
+ when (/^y/i) {
+ # TODO: if $MAY_COMMIT, save the log message to a file (say,
+ # backport.logmsg in the wcroot).
+ system($SHELL) == 0
+ or warn "Creating an interactive subshell failed ($?): $!"
+ }
+ when (/^d/) {
+ system("$SVN diff | $PAGER") == 0
+ or warn "diff failed ($?): $!";
+ next;
+ }
+ when (/^N/i) {
+ # fall through.
+ }
+ default {
+ next;
+ }
+ }
+ revert verbose => 1;
+ next PROMPT;
+ }
+ # NOTREACHED
+ }
+ when (/^l/i) {
+ if ($entry{branch}) {
+ system "$SVN log --stop-on-copy -v -g -r 0:HEAD -- "
+ .shell_escape("$BRANCHES/$entry{branch}")." "
+ ."| $PAGER";
+ } elsif (@{$entry{revisions}}) {
+ system "$SVN log ".(join ' ', map { "-r$_" } @{$entry{revisions}})
+ ." -- ^/subversion | $PAGER";
+ } else {
+ die "Assertion failed: entry has neither branch nor revisions:\n",
+ '[[[', (join ';;', %entry), ']]]';
+ }
+ next PROMPT;
+ }
+ when (/^v/i) {
+ say "";
+ say for @{$entry{entry}};
+ say "";
+ next QUESTION;
+ }
+ when (/^q/i) {
+ exit_stage_left $state, $approved, $votes;
+ }
+ when (/^a/i) {
+ $approved->{$key} = \%entry;
+ next PROMPT;
+ }
+ when (/^([+-][01])\s*$/i) {
+ next QUESTION if warned_cannot_commit "Entering a vote failed";
+ $votes->{$key} = [$1, \%entry];
+ say "Your '$1' vote has been recorded." if $VERBOSE;
+ last PROMPT;
+ }
+ when (/^e/i) {
+ prompt "Press the 'any' key to continue...\n"
+ if warned_cannot_commit "Committing this edit later on may fail";
+ my $original = $entry{raw};
+ $entry{raw} = edit_string $entry{raw}, $entry{header},
+ trailing_eol => 2;
+ # TODO: parse the edited entry (empty lines, logsummary+votes, etc.)
+ $votes->{$key} = ['edit', \%entry] # marker for the 2nd pass
+ if $original ne $entry{raw};
+ last PROMPT;
+ }
+ when (/^N/i) {
+ $state->{$entry{digest}}++;
+ last PROMPT;
+ }
+ when (/^\x20/) {
+ last PROMPT; # Fall off the end of the given/when block.
+ }
+ default {
+ say "Please use one of the options in brackets (q to quit)!";
+ next QUESTION;
+ }
+ }
+ last; } # QUESTION
+ last; } # PROMPT
} else {
- print "";
- print "\n>>> The $entry{header}:";
- print join ", ", map { "r$_" } @{$entry{revisions}};
- print "$BRANCHES/$entry{branch}" if $entry{branch};
- print "";
- print for @{$entry{logsummary}};
- print "";
- print for @{$entry{votes}};
- print "";
- print "Vetoes found!" if @vetoes;
-
- merge %entry if prompt;
+ # NOTREACHED
+ die "Unreachable code reached.";
}
- # TODO: merge() changes ./STATUS, which we're reading below, but
- # on my system the loop in main() doesn't seem to care.
-
1;
}
-sub main {
- usage, exit 0 if @ARGV;
+
+sub backport_main {
+ my %approved;
+ my %votes;
+ my $state = read_state;
+ my $renormalize;
- open STATUS, "<", $STATUS or (usage, exit 1);
+ if (@ARGV && $ARGV[0] eq '--renormalize') {
+ $renormalize = 1;
+ shift;
+ }
+
+ backport_usage, exit 0 if @ARGV > ($YES ? 0 : 1) or grep /^--help$/, @ARGV;
+ backport_usage, exit 0 if grep /^(?:-h|-\?|--help|help)$/, @ARGV;
+ my $skip = shift; # maybe undef
+ # assert not defined $skip if $YES;
+
+ open STATUS, "<", $STATUS or (backport_usage, exit 1);
# Because we use the ':normal' command in Vim...
- die "A vim with the +ex_extra feature is required"
- if `${VIM} --version` !~ /[+]ex_extra/;
+ die "A vim with the +ex_extra feature is required for --renormalize and "
+ ."\$MAY_COMMIT modes"
+ if ($renormalize or $MAY_COMMIT) and `${VIM} --version` !~ /[+]ex_extra/;
# ### TODO: need to run 'revert' here
# ### TODO: both here and in merge(), unlink files that previous merges added
- die "Local mods to STATUS file $STATUS" if `$SVN status -q $STATUS`;
+ # When running from cron, there shouldn't be local mods. (For interactive
+ # usage, we preserve local mods to STATUS.)
+ system("$SVN info $STATUS >/dev/null") == 0
+ or die "$0: svn error; point \$SVN to an appropriate binary";
+
+ check_local_mods_to_STATUS;
+ renormalize_STATUS if $renormalize;
# Skip most of the file
+ $/ = ""; # paragraph mode
while (<STATUS>) {
- last if /^Approved changes/;
- }
- while (<STATUS>) {
- last unless /^=+$/;
+ last if /^Status of \d+\.\d+/;
}
- $/ = ""; # paragraph mode
+ $SIG{INT} = \&maybe_revert unless $YES;
+ $SIG{TERM} = \&signal_handler unless $YES;
+
+ my $in_approved = 0;
while (<STATUS>) {
+ my $lines = $_;
my @lines = split /\n/;
given ($lines[0]) {
# Section header
when (/^[A-Z].*:$/i) {
- print "\n\n=== $lines[0]" unless $YES;
+ say "\n\n=== $lines[0]" unless $YES;
+ $in_approved = $lines[0] =~ /^Approved changes/;
+ }
+ # Comment
+ when (/^[#\x5b]/i) {
+ next;
}
# Separator after section header
when (/^=+$/i) {
break;
}
# Backport entry?
- when (/^ \*/) {
+ when (/^ *\*/) {
warn "Too many bullets in $lines[0]" and next
- if grep /^ \*/, @lines[1..$#lines];
- handle_entry @lines;
+ if grep /^ *\*/, @lines[1..$#lines];
+ handle_entry $in_approved, \%approved, \%votes, $state, $lines, $.,
+ $skip,
+ @lines;
}
default {
- warn "Unknown entry '$lines[0]' at $ARGV:$.\n";
+ warn "Unknown entry '$lines[0]'";
}
}
}
+
+ exit_stage_left $state, \%approved, \%votes;
}
-&main
+sub nominate_main {
+ my $had_local_mods;
+
+ local $Text::Wrap::columns = 79;
+
+ $had_local_mods = check_local_mods_to_STATUS;
+
+ # Argument parsing.
+ nominate_usage, exit 0 if @ARGV != 2;
+ my (@revnums) = (+shift) =~ /(\d+)/g;
+ my $justification = shift;
+
+ die "Unable to proceed." if warned_cannot_commit "Nominating failed";
+
+ @revnums = sort { $a <=> $b } keys %{{ map { $_ => 1 } @revnums }};
+ die "No revision numbers specified" unless @revnums;
+
+ # Determine whether a backport branch exists
+ my ($URL) = `$SVN info` =~ /^URL: (.*)$/m;
+ die "Can't retrieve URL of cwd" unless $URL;
+
+ die unless shell_safe_path_or_url $URL;
+ system "$SVN info -- $URL-r$revnums[0] 2>/dev/null";
+ my $branch = ($? == 0) ? basename("$URL-r$revnums[0]") : undef;
+
+ # Construct entry.
+ my $logmsg = `$SVN propget --revprop -r $revnums[0] --strict svn:log '^/'`;
+ die "Can't fetch log message of r$revnums[0]: $!" unless $logmsg;
+
+ unless ($logmsg =~ s/^(.*?)\n\n.*/$1/s) {
+ # "* file\n (symbol): Log message."
+
+ # Strip before and after the first symbol's log message.
+ $logmsg =~ s/^.*?: //s;
+ $logmsg =~ s/^ \x28.*//ms;
+
+ # Undo line wrapping. (We'll re-do it later.)
+ $logmsg =~ s/\s*\n\s+/ /g;
+ }
+
+ my @lines;
+ warn "Wrapping [$logmsg]\n";
+ push @lines, wrap " * ", ' 'x3, join ', ', map "r$_", @revnums;
+ push @lines, wrap ' 'x3, ' 'x3, split /\n/, $logmsg;
+ push @lines, " Justification:";
+ push @lines, wrap ' 'x5, ' 'x5, $justification;
+ push @lines, " Branch: $branch" if defined $branch;
+ push @lines, " Votes:";
+ push @lines, " +1: $AVAILID";
+ push @lines, "";
+ my $raw = join "", map "$_\n", @lines;
+
+ # Open the file in line-mode (not paragraph-mode).
+ my @STATUS;
+ tie @STATUS, "Tie::File", $STATUS, recsep => "\n";
+ my ($index) = grep { $STATUS[$_] =~ /^Veto/ } (0..$#STATUS);
+ die "Couldn't find where to add an entry" unless $index;
+
+ # Add an empty line if needed.
+ if ($STATUS[$index-1] =~ /\S/) {
+ splice @STATUS, $index, 0, "";
+ $index++;
+ }
+
+ # Add the entry.
+ splice @STATUS, $index, 0, @lines;
+
+ # Save.
+ untie @STATUS;
+
+ # Done!
+ system "$SVN diff -- $STATUS";
+ if (prompt "Commit this nomination? ") {
+ system "$SVN commit -m 'Nominate r$revnums[0].' -- $STATUS";
+ exit $?;
+ }
+ elsif (!$had_local_mods or prompt "Revert STATUS (destroying local mods)? ") {
+ # TODO: we could be smarter and just un-splice the lines we'd added.
+ system "$SVN revert -- $STATUS";
+ exit $?;
+ }
+
+ exit 0;
+}
+
+# Dispatch to the appropriate main().
+given (basename($0)) {
+ when (/^b$|backport/) {
+ chdir dirname $0 or die "Can't chdir: $!" if /^b$/;
+ &backport_main(@ARGV);
+ }
+ when (/^n$|nominate/) {
+ chdir dirname $0 or die "Can't chdir: $!" if /^n$/;
+ &nominate_main(@ARGV);
+ }
+ default {
+ &backport_main(@ARGV);
+ }
+}
diff --git a/tools/dist/backport_accept.dump b/tools/dist/backport_accept.dump
new file mode 100644
index 0000000..9532dc3
--- /dev/null
+++ b/tools/dist/backport_accept.dump
@@ -0,0 +1,550 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 76cee987-25c9-4d6c-ad40-000000000003
+
+Revision-number: 0
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 83
+Content-length: 83
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 27
+Log message for revision 1.
+PROPS-END
+
+Node-path: A
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: d1fa4a3ced98961674a441930a51f2d3
+Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03
+Content-length: 36
+
+PROPS-END
+This is the file 'alpha'.
+
+
+Node-path: A/B/E/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1
+Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3
+Content-length: 35
+
+PROPS-END
+This is the file 'beta'.
+
+
+Node-path: A/B/F
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 27
+Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6
+Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421
+Content-length: 37
+
+PROPS-END
+This is the file 'lambda'.
+
+
+Node-path: A/C
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G/pi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6
+Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415
+Content-length: 33
+
+PROPS-END
+This is the file 'pi'.
+
+
+Node-path: A/D/G/rho
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604
+Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881
+Content-length: 34
+
+PROPS-END
+This is the file 'rho'.
+
+
+Node-path: A/D/G/tau
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 9936e2716e469bb686deb98c280ead58
+Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175
+Content-length: 34
+
+PROPS-END
+This is the file 'tau'.
+
+
+Node-path: A/D/H
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/H/chi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3
+Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4
+Content-length: 34
+
+PROPS-END
+This is the file 'chi'.
+
+
+Node-path: A/D/H/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc
+Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6
+Content-length: 36
+
+PROPS-END
+This is the file 'omega'.
+
+
+Node-path: A/D/H/psi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1
+Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204
+Content-length: 34
+
+PROPS-END
+This is the file 'psi'.
+
+
+Node-path: A/D/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: 412138bd677d64cd1c32fafbffe6245d
+Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c
+Content-length: 36
+
+PROPS-END
+This is the file 'gamma'.
+
+
+Node-path: A/mu
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 33
+
+PROPS-END
+This is the file 'mu'.
+
+
+Node-path: iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 35
+
+PROPS-END
+This is the file 'iota'.
+
+
+Revision-number: 2
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+Create trunk
+PROPS-END
+
+Node-path: subversion
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk/A
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: A
+
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: iota
+Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+
+
+Node-path: A
+Node-action: delete
+
+
+Node-path: iota
+Node-action: delete
+
+
+Revision-number: 3
+Prop-content-length: 87
+Content-length: 87
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 31
+Create branch, with STATUS file
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: subversion/trunk
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 4
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+First change
+PROPS-END
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 5
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Second change
+PROPS-END
+
+Node-path: subversion/trunk/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Revision-number: 6
+Prop-content-length: 82
+Content-length: 82
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 26
+Conflicting change on iota
+PROPS-END
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 53
+Text-content-md5: 0c42f8c8b103bf00045cdf514238cfab
+Text-content-sha1: 440ad0a1673258aea8ba78fef0845e182757f8f9
+Content-length: 53
+
+This is the file 'iota'.
+Conflicts with first change
+
+
+Revision-number: 7
+Prop-content-length: 67
+Content-length: 67
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 11
+Nominate r4
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 284
+Text-content-md5: f1f6d73c681587eba4082139a9f2b724
+Text-content-sha1: 251bb84036790a810b1f4cc7f7a4e64c6a54ce9b
+Content-length: 284
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+* r4
+ default logsummary
+ Notes: Merge with --accept=theirs-conflict.
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 8
+Prop-content-length: 206
+Content-length: 206
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 150
+Merge r4 from trunk, with --accept=theirs-conflict:
+
+* r4
+ default logsummary
+ Notes: Merge with --accept=theirs-conflict.
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 54
+Content-length: 54
+
+K 13
+svn:mergeinfo
+V 19
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
diff --git a/tools/dist/backport_branches.dump b/tools/dist/backport_branches.dump
new file mode 100644
index 0000000..de6c800
--- /dev/null
+++ b/tools/dist/backport_branches.dump
@@ -0,0 +1,642 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 76cee987-25c9-4d6c-ad40-000000000004
+
+Revision-number: 0
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 83
+Content-length: 83
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 27
+Log message for revision 1.
+PROPS-END
+
+Node-path: A
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: d1fa4a3ced98961674a441930a51f2d3
+Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03
+Content-length: 36
+
+PROPS-END
+This is the file 'alpha'.
+
+
+Node-path: A/B/E/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1
+Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3
+Content-length: 35
+
+PROPS-END
+This is the file 'beta'.
+
+
+Node-path: A/B/F
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 27
+Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6
+Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421
+Content-length: 37
+
+PROPS-END
+This is the file 'lambda'.
+
+
+Node-path: A/C
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G/pi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6
+Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415
+Content-length: 33
+
+PROPS-END
+This is the file 'pi'.
+
+
+Node-path: A/D/G/rho
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604
+Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881
+Content-length: 34
+
+PROPS-END
+This is the file 'rho'.
+
+
+Node-path: A/D/G/tau
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 9936e2716e469bb686deb98c280ead58
+Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175
+Content-length: 34
+
+PROPS-END
+This is the file 'tau'.
+
+
+Node-path: A/D/H
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/H/chi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3
+Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4
+Content-length: 34
+
+PROPS-END
+This is the file 'chi'.
+
+
+Node-path: A/D/H/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc
+Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6
+Content-length: 36
+
+PROPS-END
+This is the file 'omega'.
+
+
+Node-path: A/D/H/psi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1
+Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204
+Content-length: 34
+
+PROPS-END
+This is the file 'psi'.
+
+
+Node-path: A/D/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: 412138bd677d64cd1c32fafbffe6245d
+Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c
+Content-length: 36
+
+PROPS-END
+This is the file 'gamma'.
+
+
+Node-path: A/mu
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 33
+
+PROPS-END
+This is the file 'mu'.
+
+
+Node-path: iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 35
+
+PROPS-END
+This is the file 'iota'.
+
+
+Revision-number: 2
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+Create trunk
+PROPS-END
+
+Node-path: subversion
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk/A
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: A
+
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: iota
+Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+
+
+Node-path: A
+Node-action: delete
+
+
+Node-path: iota
+Node-action: delete
+
+
+Revision-number: 3
+Prop-content-length: 87
+Content-length: 87
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 31
+Create branch, with STATUS file
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: subversion/trunk
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 4
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+First change
+PROPS-END
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 5
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Second change
+PROPS-END
+
+Node-path: subversion/trunk/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Revision-number: 6
+Prop-content-length: 82
+Content-length: 82
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 26
+Conflicting change on iota
+PROPS-END
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 52
+Text-content-md5: 2309abeef2762865a65aef15a23bd613
+Text-content-sha1: d3339d12dee6df117675e9abf30ebfa1a1dde889
+Content-length: 52
+
+This is the file 'iota'.
+Conflicts with first change
+
+Revision-number: 7
+Prop-content-length: 80
+Content-length: 80
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 24
+Create a backport branch
+PROPS-END
+
+Node-path: subversion/branches/r4
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 6
+Node-copyfrom-path: branch
+
+
+Revision-number: 8
+Prop-content-length: 85
+Content-length: 85
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 29
+Conflict resolution via mkdir
+PROPS-END
+
+Node-path: subversion/branches/r4
+Node-kind: dir
+Node-action: change
+Prop-content-length: 54
+Content-length: 54
+
+K 13
+svn:mergeinfo
+V 19
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: subversion/branches/r4/A_resolved
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches/r4/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 9
+Text-content-md5: 1d0413d4da6866dae63f902165786614
+Text-content-sha1: e2cb0815ec8f0a8b36c6aa910c1f894ec1487da3
+Content-length: 9
+
+resolved
+
+
+Revision-number: 9
+Prop-content-length: 67
+Content-length: 67
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 11
+Nominate r4
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 256
+Text-content-md5: 76f9bca3ededa2eb3c196ef0bbc9ee1b
+Text-content-sha1: 283a9f7ec716dc64b5ec8e5e1d9739d55e34b2d5
+Content-length: 256
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r4
+ default logsummary
+ Branch: r4
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 10
+Prop-content-length: 146
+Content-length: 146
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 91
+Merge the r4 branch:
+
+ * r4
+ default logsummary
+ Branch: r4
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 82
+Content-length: 82
+
+K 13
+svn:mergeinfo
+V 47
+/subversion/branches/r4:7-9
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: branch/A_resolved
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 9
+Node-copyfrom-path: subversion/branches/r4/A_resolved
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 9
+Text-content-md5: 1d0413d4da6866dae63f902165786614
+Text-content-sha1: e2cb0815ec8f0a8b36c6aa910c1f894ec1487da3
+Content-length: 9
+
+resolved
+
+
+Revision-number: 11
+Prop-content-length: 93
+Content-length: 93
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 38
+Remove the 'r4' branch, merged in r10.
+PROPS-END
+
+Node-path: subversion/branches/r4
+Node-action: delete
+
+
diff --git a/tools/dist/backport_indented_entry.dump b/tools/dist/backport_indented_entry.dump
new file mode 100644
index 0000000..bbc501d
--- /dev/null
+++ b/tools/dist/backport_indented_entry.dump
@@ -0,0 +1,522 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 76cee987-25c9-4d6c-ad40-000000000001
+
+Revision-number: 0
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 83
+Content-length: 83
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 27
+Log message for revision 1.
+PROPS-END
+
+Node-path: A
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: d1fa4a3ced98961674a441930a51f2d3
+Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03
+Content-length: 36
+
+PROPS-END
+This is the file 'alpha'.
+
+
+Node-path: A/B/E/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1
+Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3
+Content-length: 35
+
+PROPS-END
+This is the file 'beta'.
+
+
+Node-path: A/B/F
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 27
+Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6
+Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421
+Content-length: 37
+
+PROPS-END
+This is the file 'lambda'.
+
+
+Node-path: A/C
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G/pi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6
+Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415
+Content-length: 33
+
+PROPS-END
+This is the file 'pi'.
+
+
+Node-path: A/D/G/rho
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604
+Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881
+Content-length: 34
+
+PROPS-END
+This is the file 'rho'.
+
+
+Node-path: A/D/G/tau
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 9936e2716e469bb686deb98c280ead58
+Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175
+Content-length: 34
+
+PROPS-END
+This is the file 'tau'.
+
+
+Node-path: A/D/H
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/H/chi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3
+Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4
+Content-length: 34
+
+PROPS-END
+This is the file 'chi'.
+
+
+Node-path: A/D/H/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc
+Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6
+Content-length: 36
+
+PROPS-END
+This is the file 'omega'.
+
+
+Node-path: A/D/H/psi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1
+Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204
+Content-length: 34
+
+PROPS-END
+This is the file 'psi'.
+
+
+Node-path: A/D/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: 412138bd677d64cd1c32fafbffe6245d
+Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c
+Content-length: 36
+
+PROPS-END
+This is the file 'gamma'.
+
+
+Node-path: A/mu
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 33
+
+PROPS-END
+This is the file 'mu'.
+
+
+Node-path: iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 35
+
+PROPS-END
+This is the file 'iota'.
+
+
+Revision-number: 2
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+Create trunk
+PROPS-END
+
+Node-path: subversion
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk/A
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: A
+
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: iota
+Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+
+
+Node-path: A
+Node-action: delete
+
+
+Node-path: iota
+Node-action: delete
+
+
+Revision-number: 3
+Prop-content-length: 87
+Content-length: 87
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 31
+Create branch, with STATUS file
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: subversion/trunk
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 4
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+First change
+PROPS-END
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 5
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Second change
+PROPS-END
+
+Node-path: subversion/trunk/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Revision-number: 6
+Prop-content-length: 67
+Content-length: 67
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 11
+Nominate r4
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 238
+Text-content-md5: d746b12362ddd59c13d39f291710b25b
+Text-content-sha1: aafcdde209c276ffd2d63d6cd4c4b5ab35b36c27
+Content-length: 238
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+* r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 7
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 73
+Merge r4 from trunk:
+
+* r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 54
+Content-length: 54
+
+K 13
+svn:mergeinfo
+V 19
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
diff --git a/tools/dist/backport_multirevisions.dump b/tools/dist/backport_multirevisions.dump
new file mode 100644
index 0000000..d04c850
--- /dev/null
+++ b/tools/dist/backport_multirevisions.dump
@@ -0,0 +1,534 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 76cee987-25c9-4d6c-ad40-000000000005
+
+Revision-number: 0
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 83
+Content-length: 83
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 27
+Log message for revision 1.
+PROPS-END
+
+Node-path: A
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: d1fa4a3ced98961674a441930a51f2d3
+Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03
+Content-length: 36
+
+PROPS-END
+This is the file 'alpha'.
+
+
+Node-path: A/B/E/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1
+Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3
+Content-length: 35
+
+PROPS-END
+This is the file 'beta'.
+
+
+Node-path: A/B/F
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 27
+Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6
+Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421
+Content-length: 37
+
+PROPS-END
+This is the file 'lambda'.
+
+
+Node-path: A/C
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G/pi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6
+Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415
+Content-length: 33
+
+PROPS-END
+This is the file 'pi'.
+
+
+Node-path: A/D/G/rho
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604
+Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881
+Content-length: 34
+
+PROPS-END
+This is the file 'rho'.
+
+
+Node-path: A/D/G/tau
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 9936e2716e469bb686deb98c280ead58
+Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175
+Content-length: 34
+
+PROPS-END
+This is the file 'tau'.
+
+
+Node-path: A/D/H
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/H/chi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3
+Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4
+Content-length: 34
+
+PROPS-END
+This is the file 'chi'.
+
+
+Node-path: A/D/H/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc
+Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6
+Content-length: 36
+
+PROPS-END
+This is the file 'omega'.
+
+
+Node-path: A/D/H/psi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1
+Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204
+Content-length: 34
+
+PROPS-END
+This is the file 'psi'.
+
+
+Node-path: A/D/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: 412138bd677d64cd1c32fafbffe6245d
+Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c
+Content-length: 36
+
+PROPS-END
+This is the file 'gamma'.
+
+
+Node-path: A/mu
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 33
+
+PROPS-END
+This is the file 'mu'.
+
+
+Node-path: iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 35
+
+PROPS-END
+This is the file 'iota'.
+
+
+Revision-number: 2
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+Create trunk
+PROPS-END
+
+Node-path: subversion
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk/A
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: A
+
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: iota
+Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+
+
+Node-path: A
+Node-action: delete
+
+
+Node-path: iota
+Node-action: delete
+
+
+Revision-number: 3
+Prop-content-length: 87
+Content-length: 87
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 31
+Create branch, with STATUS file
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: subversion/trunk
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 4
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+First change
+PROPS-END
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 5
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Second change
+PROPS-END
+
+Node-path: subversion/trunk/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Revision-number: 6
+Prop-content-length: 73
+Content-length: 73
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 17
+Nominate a group.
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 246
+Text-content-md5: 50068058cd9700828164f97c8bc9e44e
+Text-content-sha1: 02f8ed7e3256e1eabd302b8f5b6e35000e2d4ce8
+Content-length: 246
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r4, r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 7
+Prop-content-length: 146
+Content-length: 146
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 91
+Merge the r4 group from trunk:
+
+ * r4, r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 56
+Content-length: 56
+
+K 13
+svn:mergeinfo
+V 21
+/subversion/trunk:4-5
+PROPS-END
+
+
+Node-path: branch/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
diff --git a/tools/dist/backport_tests.py b/tools/dist/backport_tests.py
new file mode 100755
index 0000000..e2b4862
--- /dev/null
+++ b/tools/dist/backport_tests.py
@@ -0,0 +1,578 @@
+#!/usr/bin/env python
+# py:encoding=utf-8
+#
+# backport_tests.py: Test backport.pl
+#
+# Subversion is a tool for revision control.
+# See http://subversion.apache.org for more information.
+#
+# ====================================================================
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+######################################################################
+
+# General modules
+import contextlib
+import functools
+import os
+import re
+import sys
+
+@contextlib.contextmanager
+def chdir(dir):
+ try:
+ saved_dir = os.getcwd()
+ os.chdir(dir)
+ yield
+ finally:
+ os.chdir(saved_dir)
+
+# Our testing module
+# HACK: chdir to cause svntest.main.svn_binary to be set correctly
+sys.path.insert(0, os.path.abspath('../../subversion/tests/cmdline'))
+with chdir('../../subversion/tests/cmdline'):
+ import svntest
+
+# (abbreviations)
+Skip = svntest.testcase.Skip_deco
+SkipUnless = svntest.testcase.SkipUnless_deco
+XFail = svntest.testcase.XFail_deco
+Issues = svntest.testcase.Issues_deco
+Issue = svntest.testcase.Issue_deco
+Wimp = svntest.testcase.Wimp_deco
+
+######################################################################
+# Helper functions
+
+BACKPORT_PL = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ 'backport.pl'))
+STATUS = 'branch/STATUS'
+
+class BackportTest(object):
+ """Decorator. See self.__call__()."""
+
+ def __init__(self, uuid):
+ """The argument is the UUID embedded in the dump file.
+ If the argument is None, then there is no dump file."""
+ self.uuid = uuid
+
+ def __call__(self, test_func):
+ """Return a decorator that: builds TEST_FUNC's sbox, creates
+ ^/subversion/trunk, and calls TEST_FUNC, then compare its output to the
+ expected dump file named after TEST_FUNC."""
+
+ # .wraps() propagates the wrappee's docstring to the wrapper.
+ @functools.wraps(test_func)
+ def wrapped_test_func(sbox):
+ expected_dump_file = './%s.dump' % (test_func.func_name,)
+
+ sbox.build()
+
+ # r2: prepare ^/subversion/ tree
+ sbox.simple_mkdir('subversion', 'subversion/trunk')
+ sbox.simple_mkdir('subversion/tags', 'subversion/branches')
+ sbox.simple_move('A', 'subversion/trunk')
+ sbox.simple_move('iota', 'subversion/trunk')
+ sbox.simple_commit(message='Create trunk')
+
+ # r3: branch
+ sbox.simple_copy('subversion/trunk', 'branch')
+ sbox.simple_append('branch/STATUS', '')
+ sbox.simple_add('branch/STATUS')
+ sbox.simple_commit(message='Create branch, with STATUS file')
+
+ # r4: random change on trunk
+ sbox.simple_append('subversion/trunk/iota', 'First change\n')
+ sbox.simple_commit(message='First change')
+
+ # r5: random change on trunk
+ sbox.simple_append('subversion/trunk/A/mu', 'Second change\n')
+ sbox.simple_commit(message='Second change')
+
+ # Do the work.
+ test_func(sbox)
+
+ # Verify it.
+ verify_backport(sbox, expected_dump_file, self.uuid)
+ return wrapped_test_func
+
+def make_entry(revisions=None, logsummary=None, notes=None, branch=None,
+ depends=None, votes=None):
+ assert revisions
+ if logsummary is None:
+ logsummary = "default logsummary"
+ if votes is None:
+ votes = {+1 : ['jrandom']}
+
+ entry = {
+ 'revisions': revisions,
+ 'logsummary': logsummary,
+ 'notes': notes,
+ 'branch': branch,
+ 'depends': depends,
+ 'votes': votes,
+ }
+
+ return entry
+
+def serialize_entry(entry):
+ return ''.join([
+
+ # revisions,
+ ' * %s\n'
+ % (", ".join("r%ld" % revision for revision in entry['revisions'])),
+
+ # logsummary
+ ' %s\n' % (entry['logsummary'],),
+
+ # notes
+ ' Notes: %s\n' % (entry['notes'],) if entry['notes'] else '',
+
+ # branch
+ ' Branch: %s\n' % (entry['branch'],) if entry['branch'] else '',
+
+ # depends
+ ' Depends: %s\n' % (entry['depends'],) if entry['depends'] else '',
+
+ # votes
+ ' Votes:\n',
+ ''.join(' '
+ '%s: %s\n' % ({1: '+1', 0: '+0', -1: '-1', -0: '-0'}[vote],
+ ", ".join(entry['votes'][vote]))
+ for vote in entry['votes']),
+
+ '\n', # empty line after entry
+ ])
+
+def serialize_STATUS(approveds,
+ serialize_entry=serialize_entry):
+ """Construct and return the contents of a STATUS file.
+
+ APPROVEDS is an iterable of ENTRY dicts. The dicts are defined
+ to have the following keys: 'revisions', a list of revision numbers (ints);
+ 'logsummary'; and 'votes', a dict mapping ±1/±0 (int) to list of voters.
+ """
+
+ strings = []
+ strings.append("Status of 1.8.x:\n\n")
+
+ strings.append("Candidate changes:\n")
+ strings.append("==================\n\n")
+
+ strings.append("Random new subheading:\n")
+ strings.append("======================\n\n")
+
+ strings.append("Veto-blocked changes:\n")
+ strings.append("=====================\n\n")
+
+ strings.append("Approved changes:\n")
+ strings.append("=================\n\n")
+
+ strings.extend(map(serialize_entry, approveds))
+
+ return "".join(strings)
+
+def run_backport(sbox, error_expected=False, extra_env=[]):
+ """Run backport.pl. EXTRA_ENV is a list of key=value pairs (str) to set in
+ the child's environment. ERROR_EXPECTED is propagated to run_command()."""
+ # TODO: if the test is run in verbose mode, pass DEBUG=1 in the environment,
+ # and pass error_expected=True to run_command() to not croak on
+ # stderr output from the child (because it uses 'sh -x').
+ args = [
+ '/usr/bin/env',
+ 'SVN=' + svntest.main.svn_binary,
+ 'YES=1', 'MAY_COMMIT=1', 'AVAILID=jrandom',
+ ] + list(extra_env) + [
+ 'perl', BACKPORT_PL,
+ ]
+ with chdir(sbox.ospath('branch')):
+ return svntest.main.run_command(args[0], error_expected, False, *(args[1:]))
+
+def verify_backport(sbox, expected_dump_file, uuid):
+ """Compare the contents of the SBOX repository with EXPECTED_DUMP_FILE.
+ Set the UUID of SBOX to UUID beforehand.
+ Based on svnsync_tests.py:verify_mirror."""
+
+ if uuid is None:
+ # There is no expected dump file.
+ return
+
+ # Remove some SVNSync-specific housekeeping properties from the
+ # mirror repository in preparation for the comparison dump.
+ svntest.actions.enable_revprop_changes(sbox.repo_dir)
+ for revnum in range(0, 1+int(sbox.youngest())):
+ svntest.actions.run_and_verify_svnadmin([], [],
+ "delrevprop", "-r", revnum, sbox.repo_dir, "svn:date")
+
+ # Create a dump file from the mirror repository.
+ dest_dump = open(expected_dump_file).readlines()
+ svntest.actions.run_and_verify_svnadmin(None, [],
+ 'setuuid', '--', sbox.repo_dir, uuid)
+ src_dump = svntest.actions.run_and_verify_dump(sbox.repo_dir)
+
+ svntest.verify.compare_dump_files(
+ "Dump files", "DUMP", src_dump, dest_dump)
+
+######################################################################
+# Tests
+#
+# Each test must return on success or raise on failure.
+
+#----------------------------------------------------------------------
+@BackportTest('76cee987-25c9-4d6c-ad40-000000000001')
+def backport_indented_entry(sbox):
+ "parsing of entries with nonstandard indentation"
+
+ # r6: nominate r4
+ approved_entries = [
+ make_entry([4]),
+ ]
+ def reindenting_serialize_entry(*args, **kwargs):
+ entry = serialize_entry(*args, **kwargs)
+ return ('\n' + entry).replace('\n ', '\n')[1:]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries,
+ serialize_entry=reindenting_serialize_entry))
+ sbox.simple_commit(message='Nominate r4')
+
+ # Run it.
+ run_backport(sbox)
+
+
+#----------------------------------------------------------------------
+@BackportTest('76cee987-25c9-4d6c-ad40-000000000002')
+def backport_two_approveds(sbox):
+ "backport with two approveds"
+
+ # r6: Enter votes
+ approved_entries = [
+ make_entry([4]),
+ make_entry([5]),
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate r4. Nominate r5.')
+
+ # r7, r8: Run it.
+ run_backport(sbox)
+
+ # Now back up and do three entries.
+ # r9: revert r7, r8
+ svntest.actions.run_and_verify_svnlook(["8\n"], [],
+ 'youngest', sbox.repo_dir)
+ sbox.simple_update()
+ svntest.main.run_svn(None, 'merge', '-r8:6',
+ '^/branch', sbox.ospath('branch'))
+ sbox.simple_commit(message='Revert the merges.')
+
+ # r10: Another change on trunk.
+ # (Note that this change must be merged after r5.)
+ sbox.simple_rm('subversion/trunk/A')
+ sbox.simple_commit(message='Third change on trunk.')
+
+ # r11: Nominate r10.
+ sbox.simple_append(STATUS, serialize_entry(make_entry([10])))
+ sbox.simple_commit(message='Nominate r10.')
+
+ # r12, r13, r14: Run it.
+ run_backport(sbox)
+
+
+
+#----------------------------------------------------------------------
+@BackportTest('76cee987-25c9-4d6c-ad40-000000000003')
+def backport_accept(sbox):
+ "test --accept parsing"
+
+ # r6: conflicting change on branch
+ sbox.simple_append('branch/iota', 'Conflicts with first change\n')
+ sbox.simple_commit(message="Conflicting change on iota")
+
+ # r7: nominate r4 with --accept (because of r6)
+ approved_entries = [
+ make_entry([4], notes="Merge with --accept=theirs-conflict."),
+ ]
+ def reindenting_serialize_entry(*args, **kwargs):
+ entry = serialize_entry(*args, **kwargs)
+ return ('\n' + entry).replace('\n ', '\n')[1:]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries,
+ serialize_entry=reindenting_serialize_entry))
+ sbox.simple_commit(message='Nominate r4')
+
+ # Run it.
+ run_backport(sbox)
+
+
+#----------------------------------------------------------------------
+@BackportTest('76cee987-25c9-4d6c-ad40-000000000004')
+def backport_branches(sbox):
+ "test branches"
+
+ # r6: conflicting change on branch
+ sbox.simple_append('branch/iota', 'Conflicts with first change')
+ sbox.simple_commit(message="Conflicting change on iota")
+
+ # r7: backport branch
+ sbox.simple_update()
+ sbox.simple_copy('branch', 'subversion/branches/r4')
+ sbox.simple_commit(message='Create a backport branch')
+
+ # r8: merge into backport branch
+ sbox.simple_update()
+ svntest.main.run_svn(None, 'merge', '--record-only', '-c4',
+ '^/subversion/trunk', sbox.ospath('subversion/branches/r4'))
+ sbox.simple_mkdir('subversion/branches/r4/A_resolved')
+ sbox.simple_append('subversion/branches/r4/iota', "resolved\n", truncate=1)
+ sbox.simple_commit(message='Conflict resolution via mkdir')
+
+ # r9: nominate r4 with branch
+ approved_entries = [
+ make_entry([4], branch="r4")
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate r4')
+
+ # Run it.
+ run_backport(sbox)
+
+ # This also serves as the 'success mode' part of backport_branch_contains().
+
+
+#----------------------------------------------------------------------
+@BackportTest('76cee987-25c9-4d6c-ad40-000000000005')
+def backport_multirevisions(sbox):
+ "test multirevision entries"
+
+ # r6: nominate r4,r5
+ approved_entries = [
+ make_entry([4,5])
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate a group.')
+
+ # Run it.
+ run_backport(sbox)
+
+
+#----------------------------------------------------------------------
+@BackportTest(None) # would be 000000000006
+def backport_conflicts_detection(sbox):
+ "test the conflicts detector"
+
+ # r6: conflicting change on branch
+ sbox.simple_append('branch/iota', 'Conflicts with first change\n')
+ sbox.simple_commit(message="Conflicting change on iota")
+
+ # r7: nominate r4, but without the requisite --accept
+ approved_entries = [
+ make_entry([4], notes="This will conflict."),
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate r4')
+
+ # Run it.
+ exit_code, output, errput = run_backport(sbox, True,
+ # Choose conflicts mode:
+ ["MAY_COMMIT=0"])
+
+ # Verify the conflict is detected.
+ expected_output = svntest.verify.RegexOutput(
+ 'Index: iota',
+ match_all=False,
+ )
+ expected_errput = (
+ r'(?ms)' # re.MULTILINE | re.DOTALL
+ r'.*Warning summary.*'
+ r'^r4 [(]default logsummary[)]: Conflicts on iota.*'
+ )
+ expected_errput = svntest.verify.RegexListOutput(
+ [
+ r'Warning summary',
+ r'===============',
+ r'r4 [(]default logsummary[)]: Conflicts on iota',
+ ],
+ match_all=False)
+ svntest.verify.verify_outputs(None, output, errput,
+ expected_output, expected_errput)
+ svntest.verify.verify_exit_code(None, exit_code, 1)
+
+ ## Now, let's test the "Depends:" annotation silences the error.
+
+ # Re-nominate.
+ approved_entries = [
+ make_entry([4], depends="World peace."),
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries), truncate=True)
+ sbox.simple_commit(message='Re-nominate r4')
+
+ # Detect conflicts.
+ exit_code, output, errput = run_backport(sbox, extra_env=["MAY_COMMIT=0"])
+
+ # Verify stdout. (exit_code and errput were verified by run_backport().)
+ svntest.verify.verify_outputs(None, output, errput,
+ "Conflicts found.*, as expected.", [])
+
+
+#----------------------------------------------------------------------
+@BackportTest(None) # would be 000000000007
+def backport_branch_contains(sbox):
+ "branch must contain the revisions"
+
+ # r6: conflicting change on branch
+ sbox.simple_append('branch/iota', 'Conflicts with first change')
+ sbox.simple_commit(message="Conflicting change on iota")
+
+ # r7: backport branch
+ sbox.simple_update()
+ sbox.simple_copy('branch', 'subversion/branches/r4')
+ sbox.simple_commit(message='Create a backport branch')
+
+ # r8: merge into backport branch
+ sbox.simple_update()
+ svntest.main.run_svn(None, 'merge', '--record-only', '-c4',
+ '^/subversion/trunk', sbox.ospath('subversion/branches/r4'))
+ sbox.simple_mkdir('subversion/branches/r4/A_resolved')
+ sbox.simple_append('subversion/branches/r4/iota', "resolved\n", truncate=1)
+ sbox.simple_commit(message='Conflict resolution via mkdir')
+
+ # r9: nominate r4,r5 with branch that contains not all of them
+ approved_entries = [
+ make_entry([4,5], branch="r4")
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate r4')
+
+ # Run it.
+ exit_code, output, errput = run_backport(sbox, error_expected=True)
+
+ # Verify the error message.
+ expected_errput = svntest.verify.RegexOutput(
+ ".*Revisions 'r5' nominated but not included in branch",
+ match_all=False,
+ )
+ svntest.verify.verify_outputs(None, output, errput,
+ [], expected_errput)
+ svntest.verify.verify_exit_code(None, exit_code, 1)
+
+ # Verify no commit occurred.
+ svntest.actions.run_and_verify_svnlook(["9\n"], [],
+ 'youngest', sbox.repo_dir)
+
+ # Verify the working copy has been reverted.
+ svntest.actions.run_and_verify_svn([], [], 'status', '-q',
+ sbox.repo_dir)
+
+ # The sibling test backport_branches() verifies the success mode.
+
+
+
+
+#----------------------------------------------------------------------
+@BackportTest(None) # would be 000000000008
+def backport_double_conflict(sbox):
+ "two-revisioned entry with two conflicts"
+
+ # r6: conflicting change on branch
+ sbox.simple_append('branch/iota', 'Conflicts with first change')
+ sbox.simple_commit(message="Conflicting change on iota")
+
+ # r7: further conflicting change to same file
+ sbox.simple_update()
+ sbox.simple_append('subversion/trunk/iota', 'Third line\n')
+ sbox.simple_commit(message="iota's third line")
+
+ # r8: nominate
+ approved_entries = [
+ make_entry([4,7], depends="World peace.")
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries))
+ sbox.simple_commit(message='Nominate the r4 group')
+
+ # Run it, in conflicts mode.
+ exit_code, output, errput = run_backport(sbox, True, ["MAY_COMMIT=0"])
+
+ # Verify the failure mode: "merge conflict" error on stderr, but backport.pl
+ # itself exits with code 0, since conflicts were confined to Depends:-ed
+ # entries.
+ #
+ # The error only happens with multi-pass merges where the first pass
+ # conflicts and the second pass touches the conflict victim.
+ #
+ # The error would be:
+ # subversion/libsvn_client/merge.c:5499: (apr_err=SVN_ERR_WC_FOUND_CONFLICT)
+ # svn: E155015: One or more conflicts were produced while merging r3:4
+ # into '/tmp/stw/working_copies/backport_tests-8/branch' -- resolve all
+ # conflicts and rerun the merge to apply the remaining unmerged revisions
+ # ...
+ # Warning summary
+ # ===============
+ #
+ # r4 (default logsummary): subshell exited with code 256
+ # And backport.pl would exit with exit code 1.
+
+ expected_output = 'Conflicts found.*, as expected.'
+ expected_errput = svntest.verify.RegexOutput(
+ ".*svn: E155015:.*", # SVN_ERR_WC_FOUND_CONFLICT
+ match_all=False,
+ )
+ svntest.verify.verify_outputs(None, output, errput,
+ expected_output, expected_errput)
+ svntest.verify.verify_exit_code(None, exit_code, 0)
+ if any("Warning summary" in line for line in errput):
+ raise svntest.verify.SVNUnexpectedStderr(errput)
+
+ ## Now, let's ensure this does get detected if not silenced.
+ # r9: Re-nominate
+ approved_entries = [
+ make_entry([4,7]) # no depends=
+ ]
+ sbox.simple_append(STATUS, serialize_STATUS(approved_entries), truncate=True)
+ sbox.simple_commit(message='Re-nominate the r4 group')
+
+ exit_code, output, errput = run_backport(sbox, True, ["MAY_COMMIT=0"])
+
+ # [1-9]\d+ matches non-zero exit codes
+ expected_errput = r'r4 .*: subshell exited with code (?:[1-9]\d+)'
+ svntest.verify.verify_exit_code(None, exit_code, 1)
+ svntest.verify.verify_outputs(None, output, errput,
+ svntest.verify.AnyOutput, expected_errput)
+
+
+
+#----------------------------------------------------------------------
+
+########################################################################
+# Run the tests
+
+# list all tests here, starting with None:
+test_list = [ None,
+ backport_indented_entry,
+ backport_two_approveds,
+ backport_accept,
+ backport_branches,
+ backport_multirevisions,
+ backport_conflicts_detection,
+ backport_branch_contains,
+ backport_double_conflict,
+ # When adding a new test, include the test number in the last
+ # 6 bytes of the UUID.
+ ]
+
+if __name__ == '__main__':
+ svntest.main.run_tests(test_list)
+ # NOTREACHED
+
+
+### End of file.
diff --git a/tools/dist/backport_two_approveds.dump b/tools/dist/backport_two_approveds.dump
new file mode 100644
index 0000000..c4349b2
--- /dev/null
+++ b/tools/dist/backport_two_approveds.dump
@@ -0,0 +1,961 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 76cee987-25c9-4d6c-ad40-000000000002
+
+Revision-number: 0
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 83
+Content-length: 83
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 27
+Log message for revision 1.
+PROPS-END
+
+Node-path: A
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/E/alpha
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: d1fa4a3ced98961674a441930a51f2d3
+Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03
+Content-length: 36
+
+PROPS-END
+This is the file 'alpha'.
+
+
+Node-path: A/B/E/beta
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1
+Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3
+Content-length: 35
+
+PROPS-END
+This is the file 'beta'.
+
+
+Node-path: A/B/F
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/B/lambda
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 27
+Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6
+Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421
+Content-length: 37
+
+PROPS-END
+This is the file 'lambda'.
+
+
+Node-path: A/C
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/G/pi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6
+Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415
+Content-length: 33
+
+PROPS-END
+This is the file 'pi'.
+
+
+Node-path: A/D/G/rho
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604
+Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881
+Content-length: 34
+
+PROPS-END
+This is the file 'rho'.
+
+
+Node-path: A/D/G/tau
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 9936e2716e469bb686deb98c280ead58
+Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175
+Content-length: 34
+
+PROPS-END
+This is the file 'tau'.
+
+
+Node-path: A/D/H
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: A/D/H/chi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3
+Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4
+Content-length: 34
+
+PROPS-END
+This is the file 'chi'.
+
+
+Node-path: A/D/H/omega
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc
+Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6
+Content-length: 36
+
+PROPS-END
+This is the file 'omega'.
+
+
+Node-path: A/D/H/psi
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 24
+Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1
+Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204
+Content-length: 34
+
+PROPS-END
+This is the file 'psi'.
+
+
+Node-path: A/D/gamma
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 26
+Text-content-md5: 412138bd677d64cd1c32fafbffe6245d
+Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c
+Content-length: 36
+
+PROPS-END
+This is the file 'gamma'.
+
+
+Node-path: A/mu
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 33
+
+PROPS-END
+This is the file 'mu'.
+
+
+Node-path: iota
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 35
+
+PROPS-END
+This is the file 'iota'.
+
+
+Revision-number: 2
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+Create trunk
+PROPS-END
+
+Node-path: subversion
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: subversion/trunk/A
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: A
+
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: iota
+Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+
+
+Node-path: A
+Node-action: delete
+
+
+Node-path: iota
+Node-action: delete
+
+
+Revision-number: 3
+Prop-content-length: 87
+Content-length: 87
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 31
+Create branch, with STATUS file
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: subversion/trunk
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 4
+Prop-content-length: 68
+Content-length: 68
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 12
+First change
+PROPS-END
+
+Node-path: subversion/trunk/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 5
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Second change
+PROPS-END
+
+Node-path: subversion/trunk/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Revision-number: 6
+Prop-content-length: 82
+Content-length: 82
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 26
+Nominate r4. Nominate r5.
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 298
+Text-content-md5: 4ebc11d7e1ec3a5cb75d3cfdcf0c1399
+Text-content-sha1: 86dd246b9072d6baeaac50f58ee2fa6444f6f889
+Content-length: 298
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 7
+Prop-content-length: 132
+Content-length: 132
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 77
+Merge r4 from trunk:
+
+ * r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 54
+Content-length: 54
+
+K 13
+svn:mergeinfo
+V 19
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 241
+Text-content-md5: cd8d55451e22cd8f83599bc64e67b515
+Text-content-sha1: 6b54b54b2711d0de2f252f34c26f2ac8f222ce35
+Content-length: 241
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 8
+Prop-content-length: 132
+Content-length: 132
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 77
+Merge r5 from trunk:
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 56
+Content-length: 56
+
+K 13
+svn:mergeinfo
+V 21
+/subversion/trunk:4-5
+PROPS-END
+
+
+Node-path: branch/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Revision-number: 9
+Prop-content-length: 74
+Content-length: 74
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 18
+Revert the merges.
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: branch/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 23
+Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8
+Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7
+Content-length: 23
+
+This is the file 'mu'.
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 298
+Text-content-md5: 4ebc11d7e1ec3a5cb75d3cfdcf0c1399
+Text-content-sha1: 86dd246b9072d6baeaac50f58ee2fa6444f6f889
+Content-length: 298
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 25
+Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc
+Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2
+Content-length: 25
+
+This is the file 'iota'.
+
+
+Revision-number: 10
+Prop-content-length: 78
+Content-length: 78
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 22
+Third change on trunk.
+PROPS-END
+
+Node-path: subversion/trunk/A
+Node-action: delete
+
+
+Revision-number: 11
+Prop-content-length: 69
+Content-length: 69
+
+K 10
+svn:author
+V 7
+jrandom
+K 7
+svn:log
+V 13
+Nominate r10.
+PROPS-END
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 355
+Text-content-md5: cc8dd910efc8d555f5dc51e5c331b403
+Text-content-sha1: c67ec7e762d8f7dfa6d2b876e540a6038781171f
+Content-length: 355
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+ * r10
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+
+Revision-number: 12
+Prop-content-length: 132
+Content-length: 132
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 77
+Merge r4 from trunk:
+
+ * r4
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 54
+Content-length: 54
+
+K 13
+svn:mergeinfo
+V 19
+/subversion/trunk:4
+PROPS-END
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 298
+Text-content-md5: 41e1f764781ee0b7874dc92607e9b9f6
+Text-content-sha1: 19e57ad83073cc50d86033ab0f03d3b8574c68fc
+Content-length: 298
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+ * r10
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+Node-path: branch/iota
+Node-kind: file
+Node-action: change
+Text-content-length: 38
+Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847
+Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26
+Content-length: 38
+
+This is the file 'iota'.
+First change
+
+
+Revision-number: 13
+Prop-content-length: 132
+Content-length: 132
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 77
+Merge r5 from trunk:
+
+ * r5
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 56
+Content-length: 56
+
+K 13
+svn:mergeinfo
+V 21
+/subversion/trunk:4-5
+PROPS-END
+
+
+Node-path: branch/A/mu
+Node-kind: file
+Node-action: change
+Text-content-length: 37
+Text-content-md5: eab751301b4e650c83324dfef4aad667
+Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99
+Content-length: 37
+
+This is the file 'mu'.
+Second change
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 242
+Text-content-md5: 30f964a922fe4e9f01b25a274c0a8efb
+Text-content-sha1: f1180ea711cbbbbfb2af52cac509da15313ca319
+Content-length: 242
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+ * r10
+ default logsummary
+ Votes:
+ +1: jrandom
+
+
+Revision-number: 14
+Prop-content-length: 134
+Content-length: 134
+
+K 10
+svn:author
+V 6
+daniel
+K 7
+svn:log
+V 79
+Merge r10 from trunk:
+
+ * r10
+ default logsummary
+ Votes:
+ +1: jrandom
+
+PROPS-END
+
+Node-path: branch
+Node-kind: dir
+Node-action: change
+Prop-content-length: 59
+Content-length: 59
+
+K 13
+svn:mergeinfo
+V 24
+/subversion/trunk:4-5,10
+PROPS-END
+
+
+Node-path: branch/STATUS
+Node-kind: file
+Node-action: change
+Text-content-length: 185
+Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9
+Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7
+Content-length: 185
+
+Status of 1.8.x:
+
+Candidate changes:
+==================
+
+Random new subheading:
+======================
+
+Veto-blocked changes:
+=====================
+
+Approved changes:
+=================
+
+
+Node-path: branch/A
+Node-action: delete
+
+
diff --git a/tools/dist/dist.sh b/tools/dist/dist.sh
index 03d5c39..676db68 100755
--- a/tools/dist/dist.sh
+++ b/tools/dist/dist.sh
@@ -22,7 +22,7 @@
# USAGE: ./dist.sh -v VERSION -r REVISION -pr REPOS-PATH
# [-alpha ALPHA_NUM|-beta BETA_NUM|-rc RC_NUM|pre PRE_NUM]
-# [-apr PATH-TO-APR ] [-apru PATH-TO-APR-UTIL]
+# [-apr PATH-TO-APR ] [-apru PATH-TO-APR-UTIL]
# [-apri PATH-TO-APR-ICONV] [-neon PATH-TO-NEON]
# [-serf PATH-TO-SERF] [-zlib PATH-TO-ZLIB]
# [-sqlite PATH-TO-SQLITE] [-zip] [-sign]
@@ -47,13 +47,13 @@
# working copy, so you may wish to create a dist-resources directory
# containing the apr/, apr-util/, neon/, serf/, zlib/ and sqlite/
# dependencies, and run dist.sh from that.
-#
+#
# When building alpha, beta or rc tarballs pass the appropriate flag
# followed by a number. For example "-alpha 5", "-beta 3", "-rc 2".
-#
+#
# If neither an -alpha, -beta, -pre or -rc option is specified, a release
# tarball will be built.
-#
+#
# To build a Windows zip file package, additionally pass -zip and the
# path to apr-iconv with -apri.
@@ -119,7 +119,7 @@ if [ -n "$ALPHA" ] && [ -n "$BETA" ] && [ -n "$NIGHTLY" ] && [ -n "$PRE" ] ||
exit 1
elif [ -n "$ALPHA" ] ; then
VER_TAG="Alpha $ALPHA"
- VER_NUMTAG="-alpha$ALPHA"
+ VER_NUMTAG="-alpha$ALPHA"
elif [ -n "$BETA" ] ; then
VER_TAG="Beta $BETA"
VER_NUMTAG="-beta$BETA"
@@ -183,20 +183,6 @@ if [ $? -ne 0 ] && [ -z "$ZIP" ]; then
exit 1
fi
-# Default to 'wget', but allow 'curl' to be used if available.
-HTTP_FETCH=wget
-HTTP_FETCH_OUTPUT="-O"
-type wget > /dev/null 2>&1
-if [ $? -ne 0 ]; then
- type curl > /dev/null 2>&1
- if [ $? -ne 0 ]; then
- echo "Neither curl or wget found."
- exit 2
- fi
- HTTP_FETCH=curl
- HTTP_FETCH_OUTPUT="-o"
-fi
-
DISTNAME="subversion-${VERSION}${VER_NUMTAG}"
DIST_SANDBOX=.dist_sandbox
DISTPATH="$DIST_SANDBOX/$DISTNAME"
@@ -306,6 +292,15 @@ if [ -z "$ZIP" ] ; then
(cd "$DISTPATH" && ./autogen.sh --release) || exit 1
fi
+# Generate the .pot file, for use by translators.
+echo "Running po-update.sh in sandbox, to create subversion.pot..."
+# Can't use the po-update.sh in the packaged export since it might have CRLF
+# line endings, in which case it won't run. So first we export it again.
+${svn:-svn} export -q -r "$REVISION" \
+ "http://svn.apache.org/repos/asf/subversion/$REPOS_PATH/tools/po/po-update.sh" \
+ --username none --password none "$DIST_SANDBOX/po-update.sh"
+(cd "$DISTPATH" && ../po-update.sh pot) || exit 1
+
# Pre-translate the various sql-derived header files
echo "Generating SQL-derived headers..."
for f in `find "$DISTPATH/subversion" -name '*.sql'`; do
diff --git a/tools/dist/make-deps-tarball.sh b/tools/dist/make-deps-tarball.sh
deleted file mode 100755
index 318adc6..0000000
--- a/tools/dist/make-deps-tarball.sh
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/bin/sh
-#
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-#
-set -e
-
-APR=apr-1.4.6
-APR_UTIL=apr-util-1.4.1
-NEON=neon-0.29.6
-SERF=serf-0.3.1
-ZLIB=zlib-1.2.7
-SQLITE_VERSION=3071400
-SQLITE=sqlite-amalgamation-$SQLITE_VERSION
-
-HTTPD=httpd-2.2.22
-HTTPD_OOPS=
-APR_ICONV=apr-iconv-1.2.1
-APR_ICONV_OOPS=
-
-WIN32_APR_VIA_HTTPD=1
-
-BASEDIR=`pwd`
-TEMPDIR=$BASEDIR/temp
-
-APACHE_MIRROR=http://archive.apache.org/dist
-
-create_deps() {
- SVN_VERSION="$1"
- set -x
-
- mkdir -p $TEMPDIR
- cd $TEMPDIR
- wget -qnc $APACHE_MIRROR/apr/$APR.tar.bz2
- wget -qnc $APACHE_MIRROR/apr/$APR_UTIL.tar.bz2
- if [ -n "$WIN32_APR_VIA_HTTPD" ]; then
- wget -qnc $APACHE_MIRROR/httpd/$HTTPD-win32-src$HTTPD_OOPS.zip
- else
- wget -qnc $APACHE_MIRROR/apr/$APR-win32-src.zip
- wget -qnc $APACHE_MIRROR/apr/$APR_UTIL-win32-src.zip
- wget -qnc $APACHE_MIRROR/apr/$APR_ICONV-win32-src$APR_ICONV_OOPS.zip
- fi
- wget -qnc http://webdav.org/neon/$NEON.tar.gz
- wget -qnc http://serf.googlecode.com/files/$SERF.tar.bz2
- wget -qnc http://www.zlib.net/$ZLIB.tar.bz2
- wget -qnc http://www.sqlite.org/$SQLITE.zip
-
- mkdir $BASEDIR/unix-dependencies
- cd $BASEDIR/unix-dependencies
- tar zxf $TEMPDIR/$NEON.tar.gz
- tar jxf $TEMPDIR/$ZLIB.tar.bz2
- tar jxf $TEMPDIR/$SERF.tar.bz2
- unzip -q $TEMPDIR/$SQLITE.zip
- mv $NEON neon
- mv $ZLIB zlib
- mv $SERF serf
- mv $SQLITE sqlite-amalgamation
- tar jxf $TEMPDIR/$APR.tar.bz2
- tar jxf $TEMPDIR/$APR_UTIL.tar.bz2
- mv $APR apr
- mv $APR_UTIL apr-util
- cd $TEMPDIR
-
- mkdir $BASEDIR/win32-dependencies
- cd $BASEDIR/win32-dependencies
- tar zxf $TEMPDIR/$NEON.tar.gz
- tar jxf $TEMPDIR/$ZLIB.tar.bz2
- tar jxf $TEMPDIR/$SERF.tar.bz2
- unzip -q $TEMPDIR/$SQLITE.zip
- mv $NEON neon
- mv $ZLIB zlib
- mv $SERF serf
- mv $SQLITE sqlite-amalgamation
- if [ -n "$WIN32_APR_VIA_HTTPD" ]; then
- unzip -q $TEMPDIR/$HTTPD-win32-src$HTTPD_OOPS.zip
- for i in apr apr-util apr-iconv; do
- mv $HTTPD/srclib/$i .
- done
- rm -rf $HTTPD
- else
- unzip -q $TEMPDIR/$APR-win32-src.zip
- unzip -q $TEMPDIR/$APR_UTIL-win32-src.zip
- unzip -q $TEMPDIR/$APR_ICONV-win32-src$APR_ICONV_OOPS.zip
- mv $APR apr
- mv $APR_UTIL apr-util
- mv $APR_ICONV apr-iconv
- fi
-
- cd $BASEDIR
- mv unix-dependencies subversion-$SVN_VERSION
- tar jcf subversion-deps-$SVN_VERSION.tar.bz2 subversion-$SVN_VERSION
- tar zcf subversion-deps-$SVN_VERSION.tar.gz subversion-$SVN_VERSION
- rm -rf subversion-$SVN_VERSION
- mv win32-dependencies subversion-$SVN_VERSION
- zip -qr subversion-deps-$SVN_VERSION.zip subversion-$SVN_VERSION
- rm -rf subversion-$SVN_VERSION
-}
-
-if [ -z "$1" ]; then
- echo "Please provide a Subversion release number."
- echo "Example: ./`basename $0` 1.6.19"
- exit 1
-fi
-
-create_deps "$1"
diff --git a/tools/dist/nightly.sh b/tools/dist/nightly.sh
index 0f2f991..b167ab3 100755
--- a/tools/dist/nightly.sh
+++ b/tools/dist/nightly.sh
@@ -54,7 +54,7 @@ head=`$svn info $repo/trunk | grep '^Revision' | cut -d ' ' -f 2`
# Get the latest versions of the rolling scripts
for i in release.py dist.sh
-do
+do
$svn export --force -r $head $repo/trunk/tools/dist/$i@$head $dir/$i
done
# We also need ezt
@@ -63,11 +63,11 @@ $svn export --force -r $head $repo/trunk/build/generator/ezt.py@$head $dir/ezt.p
# Create the environment
cd roll
echo '----------------building environment------------------'
-../release.py --base-dir ${abscwd}/roll build-env trunk-nightly
+../release.py --verbose --base-dir ${abscwd}/roll build-env trunk-nightly
# Roll the tarballs
echo '-------------------rolling tarball--------------------'
-../release.py --base-dir ${abscwd}/roll roll --branch trunk trunk-nightly $head
+../release.py --verbose --base-dir ${abscwd}/roll roll --branch trunk trunk-nightly $head
cd ..
# Create the information page
diff --git a/tools/dist/nominate.pl b/tools/dist/nominate.pl
new file mode 120000
index 0000000..411377e
--- /dev/null
+++ b/tools/dist/nominate.pl
@@ -0,0 +1 @@
+backport.pl \ No newline at end of file
diff --git a/tools/dist/release.py b/tools/dist/release.py
index bc80549..30a1f0b 100755
--- a/tools/dist/release.py
+++ b/tools/dist/release.py
@@ -66,16 +66,42 @@ except ImportError:
import ezt
+try:
+ subprocess.check_output
+except AttributeError:
+ def check_output(cmd):
+ proc = subprocess.Popen(['svn', 'list', dist_dev_url],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (stdout, stderr) = proc.communicate()
+ rc = proc.wait()
+ if rc or stderr:
+ logging.error('%r failed with stderr %r', cmd, stderr)
+ raise subprocess.CalledProcessError(rc, cmd)
+ return stdout
+ subprocess.check_output = check_output
+ del check_output
+
# Our required / recommended release tool versions by release branch
tool_versions = {
'trunk' : {
- 'autoconf' : '2.68',
- 'libtool' : '2.4',
- 'swig' : '2.0.4',
+ 'autoconf' : '2.69',
+ 'libtool' : '2.4.3',
+ 'swig' : '3.0.0',
+ },
+ '1.9' : {
+ 'autoconf' : '2.69',
+ 'libtool' : '2.4.3',
+ 'swig' : '3.0.0'
+ },
+ '1.8' : {
+ 'autoconf' : '2.69',
+ 'libtool' : '2.4.3',
+ 'swig' : '2.0.9',
},
'1.7' : {
'autoconf' : '2.68',
- 'libtool' : '2.4',
+ 'libtool' : '2.4.3',
'swig' : '2.0.4',
},
'1.6' : {
@@ -85,6 +111,9 @@ tool_versions = {
},
}
+# The version that is our current recommended release
+recommended_release = '1.8'
+
# Some constants
repos = 'http://svn.apache.org/repos/asf/subversion'
secure_repos = 'https://svn.apache.org/repos/asf/subversion'
@@ -99,7 +128,7 @@ extns = ['zip', 'tar.gz', 'tar.bz2']
# Utility functions
class Version(object):
- regex = re.compile('(\d+).(\d+).(\d+)(?:-(?:(rc|alpha|beta)(\d+)))?')
+ regex = re.compile(r'(\d+).(\d+).(\d+)(?:-(?:(rc|alpha|beta)(\d+)))?')
def __init__(self, ver_str):
# Special case the 'trunk-nightly' version
@@ -135,6 +164,18 @@ class Version(object):
def is_prerelease(self):
return self.pre != None
+ def is_recommended(self):
+ return self.branch == recommended_release
+
+ def get_download_anchor(self):
+ if self.is_prerelease():
+ return 'pre-releases'
+ else:
+ if self.is_recommended():
+ return 'recommended-release'
+ else:
+ return 'supported-releases'
+
def __lt__(self, that):
if self.major < that.major: return True
if self.major > that.major: return False
@@ -155,7 +196,7 @@ class Version(object):
else:
return self.pre_num < that.pre_num
- def __str(self):
+ def __str__(self):
if self.pre:
if self.pre == 'nightly':
return 'nightly'
@@ -168,11 +209,7 @@ class Version(object):
def __repr__(self):
- return "Version('%s')" % self.__str()
-
- def __str__(self):
- return self.__str()
-
+ return "Version(%s)" % repr(str(self))
def get_prefix(base_dir):
return os.path.join(base_dir, 'prefix')
@@ -183,6 +220,13 @@ def get_tempdir(base_dir):
def get_deploydir(base_dir):
return os.path.join(base_dir, 'deploy')
+def get_target(args):
+ "Return the location of the artifacts"
+ if args.target:
+ return args.target
+ else:
+ return get_deploydir(args.base_dir)
+
def get_tmpldir():
return os.path.join(os.path.abspath(sys.path[0]), 'templates')
@@ -194,8 +238,7 @@ def get_tmplfile(filename):
return urllib2.urlopen(repos + '/trunk/tools/dist/templates/' + filename)
def get_nullfile():
- # This is certainly not cross platform
- return open('/dev/null', 'w')
+ return open(os.path.devnull, 'w')
def run_script(verbose, script):
if verbose:
@@ -371,12 +414,7 @@ def compare_changes(repos, branch, revision):
mergeinfo_cmd = ['svn', 'mergeinfo', '--show-revs=eligible',
repos + '/trunk/CHANGES',
repos + '/' + branch + '/' + 'CHANGES']
- proc = subprocess.Popen(mergeinfo_cmd, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- (stdout, stderr) = proc.communicate()
- rc = proc.wait()
- if stderr:
- raise RuntimeError('svn mergeinfo failed: %s' % stderr)
+ stdout = subprocess.check_output(mergeinfo_cmd)
if stdout:
# Treat this as a warning since we are now putting entries for future
# minor releases in CHANGES on trunk.
@@ -463,15 +501,11 @@ def sign_candidates(args):
def sign_file(filename):
asc_file = open(filename + '.asc', 'a')
logging.info("Signing %s" % filename)
- proc = subprocess.Popen(['gpg', '-ba', '-o', '-', filename],
- stdout=asc_file)
- proc.wait()
+ proc = subprocess.check_call(['gpg', '-ba', '-o', '-', filename],
+ stdout=asc_file)
asc_file.close()
- if args.target:
- target = args.target
- else:
- target = get_deploydir(args.base_dir)
+ target = get_target(args)
for e in extns:
filename = os.path.join(target, 'subversion-%s.%s' % (args.version, e))
@@ -488,17 +522,17 @@ def sign_candidates(args):
def post_candidates(args):
'Post candidate artifacts to the dist development directory.'
+ target = get_target(args)
+
logging.info('Importing tarballs to %s' % dist_dev_url)
svn_cmd = ['svn', 'import', '-m',
'Add %s candidate release artifacts' % args.version.base,
'--auto-props', '--config-option',
'config:auto-props:*.asc=svn:eol-style=native;svn:mime-type=text/plain',
- get_deploydir(args.base_dir), dist_dev_url]
+ target, dist_dev_url]
if (args.username):
svn_cmd += ['--username', args.username]
- proc = subprocess.Popen(svn_cmd)
- (stdout, stderr) = proc.communicate()
- proc.wait()
+ subprocess.check_call(svn_cmd)
#----------------------------------------------------------------------
# Create tag
@@ -513,6 +547,7 @@ def create_tag(args):
else:
branch = secure_repos + '/branches/%d.%d.x' % (args.version.major,
args.version.minor)
+ target = get_target(args)
tag = secure_repos + '/tags/' + str(args.version)
@@ -521,13 +556,63 @@ def create_tag(args):
if (args.username):
svnmucc_cmd += ['--username', args.username]
svnmucc_cmd += ['cp', str(args.revnum), branch, tag]
- svnmucc_cmd += ['put', os.path.join(get_deploydir(args.base_dir),
- 'svn_version.h.dist'),
+ svnmucc_cmd += ['put', os.path.join(target, 'svn_version.h.dist' + '-' +
+ str(args.version)),
tag + '/subversion/include/svn_version.h']
# don't redirect stdout/stderr since svnmucc might ask for a password
- proc = subprocess.Popen(svnmucc_cmd)
- proc.wait()
+ subprocess.check_call(svnmucc_cmd)
+
+ if not args.version.is_prerelease():
+ logging.info('Bumping revisions on the branch')
+ def replace_in_place(fd, startofline, flat, spare):
+ """In file object FD, replace FLAT with SPARE in the first line
+ starting with STARTOFLINE."""
+
+ fd.seek(0, os.SEEK_SET)
+ lines = fd.readlines()
+ for i, line in enumerate(lines):
+ if line.startswith(startofline):
+ lines[i] = line.replace(flat, spare)
+ break
+ else:
+ raise RuntimeError('Definition of %r not found' % startofline)
+
+ fd.seek(0, os.SEEK_SET)
+ fd.writelines(lines)
+ fd.truncate() # for current callers, new value is never shorter.
+
+ new_version = Version('%d.%d.%d' %
+ (args.version.major, args.version.minor,
+ args.version.patch + 1))
+
+ def file_object_for(relpath):
+ fd = tempfile.NamedTemporaryFile()
+ url = branch + '/' + relpath
+ fd.url = url
+ subprocess.check_call(['svn', 'cat', '%s@%d' % (url, args.revnum)],
+ stdout=fd)
+ return fd
+
+ svn_version_h = file_object_for('subversion/include/svn_version.h')
+ replace_in_place(svn_version_h, '#define SVN_VER_PATCH ',
+ str(args.version.patch), str(new_version.patch))
+
+ STATUS = file_object_for('STATUS')
+ replace_in_place(STATUS, 'Status of ',
+ str(args.version), str(new_version))
+
+ svn_version_h.seek(0, os.SEEK_SET)
+ STATUS.seek(0, os.SEEK_SET)
+ subprocess.check_call(['svnmucc', '-r', str(args.revnum),
+ '-m', 'Post-release housekeeping: '
+ 'bump the %s branch to %s.'
+ % (branch.split('/')[-1], str(new_version)),
+ 'put', svn_version_h.name, svn_version_h.url,
+ 'put', STATUS.name, STATUS.url,
+ ])
+ del svn_version_h
+ del STATUS
#----------------------------------------------------------------------
# Clean dist
@@ -535,13 +620,7 @@ def create_tag(args):
def clean_dist(args):
'Clean the distribution directory of all but the most recent artifacts.'
- proc = subprocess.Popen(['svn', 'list', dist_release_url],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- (stdout, stderr) = proc.communicate()
- proc.wait()
- if stderr:
- raise RuntimeError(stderr)
+ stdout = subprocess.check_output(['svn', 'list', dist_release_url])
filenames = stdout.split('\n')
tar_gz_archives = []
@@ -570,8 +649,7 @@ def clean_dist(args):
svnmucc_cmd += ['rm', dist_release_url + '/' + filename]
# don't redirect stdout/stderr since svnmucc might ask for a password
- proc = subprocess.Popen(svnmucc_cmd)
- proc.wait()
+ subprocess.check_call(svnmucc_cmd)
#----------------------------------------------------------------------
# Move to dist
@@ -579,13 +657,7 @@ def clean_dist(args):
def move_to_dist(args):
'Move candidate artifacts to the distribution directory.'
- proc = subprocess.Popen(['svn', 'list', dist_dev_url],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- (stdout, stderr) = proc.communicate()
- proc.wait()
- if stderr:
- raise RuntimeError(stderr)
+ stdout = subprocess.check_output(['svn', 'list', dist_dev_url])
filenames = []
for entry in stdout.split('\n'):
@@ -603,8 +675,7 @@ def move_to_dist(args):
# don't redirect stdout/stderr since svnmucc might ask for a password
logging.info('Moving release artifacts to %s' % dist_release_url)
- proc = subprocess.Popen(svnmucc_cmd)
- proc.wait()
+ subprocess.check_call(svnmucc_cmd)
#----------------------------------------------------------------------
# Write announcements
@@ -613,9 +684,10 @@ def write_news(args):
'Write text for the Subversion website.'
data = { 'date' : datetime.date.today().strftime('%Y%m%d'),
'date_pres' : datetime.date.today().strftime('%Y-%m-%d'),
- 'major-minor' : '%d.%d' % (args.version.major, args.version.minor),
+ 'major-minor' : args.version.branch,
'version' : str(args.version),
'version_base' : args.version.base,
+ 'anchor': args.version.get_download_anchor(),
}
if args.version.is_prerelease():
@@ -631,10 +703,7 @@ def write_news(args):
def get_sha1info(args, replace=False):
'Return a list of sha1 info for the release'
- if args.target:
- target = args.target
- else:
- target = get_deploydir(args.base_dir)
+ target = get_target(args)
sha1s = glob.glob(os.path.join(target, 'subversion*-%s*.sha1' % args.version))
@@ -665,9 +734,9 @@ def write_announcement(args):
data = { 'version' : str(args.version),
'sha1info' : sha1info,
'siginfo' : siginfo,
- 'major-minor' : '%d.%d' % (args.version.major,
- args.version.minor),
+ 'major-minor' : args.version.branch,
'major-minor-patch' : args.version.base,
+ 'anchor' : args.version.get_download_anchor(),
}
if args.version.is_prerelease():
@@ -708,10 +777,7 @@ def get_siginfo(args, quiet=False):
import _gnupg as gnupg
gpg = gnupg.GPG()
- if args.target:
- target = args.target
- else:
- target = get_deploydir(args.base_dir)
+ target = get_target(args)
good_sigs = {}
fingerprints = {}
@@ -842,6 +908,9 @@ def main():
help='''The release label, such as '1.7.0-alpha1'.''')
subparser.add_argument('--username',
help='''Username for ''' + dist_repos + '''.''')
+ subparser.add_argument('--target',
+ help='''The full path to the directory containing
+ release artifacts.''')
# Setup the parser for the create-tag subcommand
subparser = subparsers.add_parser('create-tag',
@@ -855,6 +924,9 @@ def main():
help='''The branch to base the release on.''')
subparser.add_argument('--username',
help='''Username for ''' + secure_repos + '''.''')
+ subparser.add_argument('--target',
+ help='''The full path to the directory containing
+ release artifacts.''')
# The clean-dist subcommand
subparser = subparsers.add_parser('clean-dist',
diff --git a/tools/dist/templates/download.ezt b/tools/dist/templates/download.ezt
index 601818d..d5fcb54 100644
--- a/tools/dist/templates/download.ezt
+++ b/tools/dist/templates/download.ezt
@@ -1,4 +1,4 @@
-<p style="font-size: 150%; text-align: center;">Subversion [version]</p>
+<p style="font-size: 150%; text-align: center;">Apache Subversion [version]</p>
<table class="centered">
<tr>
<th>File</th>
diff --git a/tools/dist/templates/rc-news.ezt b/tools/dist/templates/rc-news.ezt
index 959735c..704899a 100644
--- a/tools/dist/templates/rc-news.ezt
+++ b/tools/dist/templates/rc-news.ezt
@@ -16,7 +16,7 @@
in the [version_base] release.</p>
<p>To get this release from the nearest mirror, please visit our
- <a href="/download/#pre-releases">download page</a>.</p>
+ <a href="/download/#[anchor]">download page</a>.</p>
</div> <!-- #news-[date] -->
diff --git a/tools/dist/templates/rc-release-ann.ezt b/tools/dist/templates/rc-release-ann.ezt
index f9af5c1..b3085f7 100644
--- a/tools/dist/templates/rc-release-ann.ezt
+++ b/tools/dist/templates/rc-release-ann.ezt
@@ -1,7 +1,7 @@
I'm happy to announce the release of Apache Subversion [version].
Please choose the mirror closest to you by visiting:
- http://subversion.apache.org/download/#pre-releases
+ http://subversion.apache.org/download/#[anchor]
The SHA1 checksums are:
diff --git a/tools/dist/templates/stable-news.ezt b/tools/dist/templates/stable-news.ezt
index aee573f..63ee9da 100644
--- a/tools/dist/templates/stable-news.ezt
+++ b/tools/dist/templates/stable-news.ezt
@@ -13,7 +13,7 @@
>change log</a> for more information about this release.</p>
<p>To get this release from the nearest mirror, please visit our
- <a href="/download/#recommended-release">download page</a>.</p>
+ <a href="/download/#[anchor]">download page</a>.</p>
</div> <!-- #news-[date] -->
diff --git a/tools/dist/templates/stable-release-ann.ezt b/tools/dist/templates/stable-release-ann.ezt
index c865a84..a6ffa9a 100644
--- a/tools/dist/templates/stable-release-ann.ezt
+++ b/tools/dist/templates/stable-release-ann.ezt
@@ -1,7 +1,7 @@
I'm happy to announce the release of Apache Subversion [version].
Please choose the mirror closest to you by visiting:
- http://subversion.apache.org/download/#recommended-release
+ http://subversion.apache.org/download/#[anchor]
The SHA1 checksums are: