~ubuntu-branches/debian/sid/subversion/sid

« back to all changes in this revision

Viewing changes to tools/dist/backport.pl

  • Committer: Package Import Robot
  • Author(s): James McCoy
  • Date: 2015-08-07 21:32:47 UTC
  • mfrom: (0.2.15) (4.1.7 experimental)
  • Revision ID: package-import@ubuntu.com-20150807213247-ozyewtmgsr6tkewl
Tags: 1.9.0-1
* Upload to unstable
* New upstream release.
  + Security fixes
    - CVE-2015-3184: Mixed anonymous/authenticated path-based authz with
      httpd 2.4
    - CVE-2015-3187: svn_repos_trace_node_locations() reveals paths hidden
      by authz
* Add >= 2.7 requirement for python-all-dev Build-Depends, needed to run
  tests.
* Remove Build-Conflicts against ruby-test-unit.  (Closes: #791844)
* Remove patches/apache_module_dependency in favor of expressing the
  dependencies in authz_svn.load/dav_svn.load.
* Build-Depend on apache2-dev (>= 2.4.16) to ensure ap_some_authn_required()
  is available when building mod_authz_svn and Depend on apache2-bin (>=
  2.4.16) for runtime support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/perl -l
 
1
#!/usr/bin/perl
2
2
use warnings;
3
3
use strict;
4
4
use feature qw/switch say/;
5
5
 
 
6
#no warnings 'experimental::smartmatch';
 
7
 
6
8
# Licensed to the Apache Software Foundation (ASF) under one
7
9
# or more contributor license agreements.  See the NOTICE file
8
10
# distributed with this work for additional information
20
22
# specific language governing permissions and limitations
21
23
# under the License.
22
24
 
 
25
use Carp qw/croak confess carp cluck/;
 
26
use Digest ();
23
27
use Term::ReadKey qw/ReadMode ReadKey/;
 
28
use File::Basename qw/basename dirname/;
 
29
use File::Copy qw/copy move/;
24
30
use File::Temp qw/tempfile/;
25
 
use POSIX qw/ctermid/;
26
 
 
 
31
use IO::Select ();
 
32
use IPC::Open3 qw/open3/;
 
33
use POSIX qw/ctermid strftime isprint isspace/;
 
34
use Text::Wrap qw/wrap/;
 
35
use Tie::File ();
 
36
 
 
37
############### Start of reading values from environment ###############
 
38
 
 
39
# Programs we use.
 
40
#
 
41
# TODO: document which are interpreted by sh and which should point to binary.
27
42
my $SVN = $ENV{SVN} || 'svn'; # passed unquoted to sh
 
43
my $SHELL = $ENV{SHELL} // '/bin/sh';
28
44
my $VIM = 'vim';
 
45
my $EDITOR = $ENV{SVN_EDITOR} // $ENV{VISUAL} // $ENV{EDITOR} // 'ed';
 
46
my $PAGER = $ENV{PAGER} // 'less' // 'cat';
 
47
 
 
48
# Mode flags.
 
49
package Mode {
 
50
  use constant {
 
51
    AutoCommitApproveds => 1, # used by nightly commits (svn-role)
 
52
    Conflicts => 2,           # used by the hourly conflicts-detection buildbot
 
53
    Interactive => 3,
 
54
  };
 
55
};
 
56
my $YES = ($ENV{YES} // "0") =~ /^(1|yes|true)$/i; # batch mode: eliminate prompts, add sleeps
 
57
my $MAY_COMMIT = ($ENV{MAY_COMMIT} // "false") =~ /^(1|yes|true)$/i;
 
58
my $MODE = ($YES ? ($MAY_COMMIT ? Mode::AutoCommitApproveds : Mode::Conflicts )
 
59
                 : Mode::Interactive );
 
60
 
 
61
# Other knobs.
 
62
my $VERBOSE = 0;
 
63
my $DEBUG = (exists $ENV{DEBUG}); # 'set -x', etc
 
64
 
 
65
# Force all these knobs to be usable via @sh.
 
66
my @sh = qw/false true/;
 
67
die if grep { ($sh[$_] eq 'true') != !!$_ } $DEBUG, $MAY_COMMIT, $VERBOSE, $YES;
 
68
 
 
69
# Username for entering votes.
 
70
my $SVN_A_O_REALM = '<https://svn.apache.org:443> ASF Committers';
 
71
my ($AVAILID) = $ENV{AVAILID} // do {
 
72
  local $_ = `$SVN auth svn.apache.org:443 2>/dev/null`; # TODO: pass $SVN_A_O_REALM
 
73
  ($? == 0 && /Auth.*realm: \Q$SVN_A_O_REALM\E\nUsername: (.*)/) ? $1 : undef
 
74
} // do {
 
75
  local $/; # slurp mode
 
76
  my $fh;
 
77
  my $dir = "$ENV{HOME}/.subversion/auth/svn.simple/";
 
78
  my $filename = Digest->new("MD5")->add($SVN_A_O_REALM)->hexdigest;
 
79
  open $fh, '<', "$dir/$filename"
 
80
  and <$fh> =~ /K 8\nusername\nV \d+\n(.*)/
 
81
  ? $1
 
82
  : undef
 
83
};
 
84
 
 
85
unless (defined $AVAILID) {
 
86
  unless ($MODE == Mode::Conflicts) {
 
87
    warn "Username for commits (of votes/merges) not found; "
 
88
         ."it will be possible to review nominations but not to commit votes "
 
89
         ."or merges.\n";
 
90
    warn "Press the 'any' key to continue...\n";
 
91
    die if $MODE == Mode::AutoCommitApproveds; # unattended mode; can't prompt.
 
92
    ReadMode 'cbreak';
 
93
    ReadKey 0;
 
94
    ReadMode 'restore';
 
95
  }
 
96
}
 
97
 
 
98
############## End of reading values from the environment ##############
 
99
 
 
100
# Constants.
29
101
my $STATUS = './STATUS';
 
102
my $STATEFILE = './.backports1';
30
103
my $BRANCHES = '^/subversion/branches';
31
 
 
32
 
my $YES = $ENV{YES}; # batch mode: eliminate prompts, add sleeps
33
 
my $WET_RUN = qw[false true][1]; # don't commit
34
 
my $DEBUG = qw[false true][0]; # 'set -x', etc
35
 
 
36
 
# derived values
 
104
my $TRUNK = '^/subversion/trunk';
 
105
$ENV{LC_ALL} = "C";  # since we parse 'svn info' output and use isprint()
 
106
 
 
107
# Globals.
 
108
my %ERRORS = ();
 
109
# TODO: can $MERGED_SOMETHING be removed and references to it replaced by scalar(@MERGES_TODAY) ?
 
110
#       alternately, does @MERGES_TODAY need to be purged whenever $MERGED_SOMETHING is reset?
 
111
#       The scalar is only used in interactive runs, but the array is used in
 
112
#       svn-role batch mode too.
 
113
my @MERGES_TODAY;
 
114
my $MERGED_SOMETHING = 0;
37
115
my $SVNq;
38
116
 
 
117
# Derived values.
 
118
my $SVNvsn = do {
 
119
  my ($major, $minor, $patch) = `$SVN --version -q` =~ /^(\d+)\.(\d+)\.(\d+)/;
 
120
  1e6*$major + 1e3*$minor + $patch;
 
121
};
39
122
$SVN .= " --non-interactive" if $YES or not defined ctermid;
40
123
$SVNq = "$SVN -q ";
41
 
$SVNq =~ s/-q// if $DEBUG eq 'true';
 
124
$SVNq =~ s/-q// if $DEBUG;
42
125
 
43
 
sub usage {
44
 
  my $basename = $0;
45
 
  $basename =~ s#.*/##;
 
126
 
 
127
sub backport_usage {
 
128
  my $basename = basename $0;
46
129
  print <<EOF;
47
 
Run this from the root of your release branch (e.g., 1.6.x) working copy.
48
 
 
49
 
For each entry in STATUS, you will be prompted whether to merge it.
50
 
 
51
 
WARNING:
52
 
If you accept the prompt, $basename will revert all local changes and will
53
 
commit the merge immediately.
 
130
backport.pl: a tool for reviewing, merging, and voting on STATUS entries.
 
131
 
 
132
Normally, invoke this with CWD being the root of the stable branch (e.g.,
 
133
1.8.x):
 
134
 
 
135
    Usage: test -e \$d/STATUS && cd \$d && \\
 
136
           backport.pl [PATTERN]
 
137
    (where \$d is a working copy of branches/1.8.x)
 
138
 
 
139
Alternatively, invoke this via a symlink named "b" placed at the same directory
 
140
as the STATUS file, in which case the CWD doesn't matter (the script will cd):
 
141
 
 
142
    Usage: ln -s /path/to/backport.pl \$d/b && \\
 
143
           \$d/b [PATTERN]
 
144
    (where \$d is a working copy of branches/1.8.x)
 
145
 
 
146
In either case, the ./STATUS file should be at HEAD.  If it has local mods,
 
147
they will be preserved through 'revert' operations but included in 'commit'
 
148
operations.
 
149
 
 
150
If PATTERN is provided, only entries which match PATTERN are considered.  The
 
151
sense of "match" is either substring (fgrep) or Perl regexp (with /msi).
 
152
 
 
153
In interactive mode (the default), you will be prompted once per STATUS entry.
 
154
At a prompt, you have the following options:
 
155
 
 
156
y:   Run a merge.  It will not be committed.
 
157
     WARNING: This will run 'update' and 'revert -R ./'.
 
158
l:   Show logs for the entries being nominated.
 
159
v:   Show the full entry (the prompt only shows an abridged version).
 
160
q:   Quit the "for each nomination" loop.
 
161
±1:  Enter a +1 or -1 vote
 
162
     You will be prompted to commit your vote at the end.
 
163
±0:  Enter a +0 or -0 vote
 
164
     You will be prompted to commit your vote at the end.
 
165
a:   Move the entry to the "Approved changes" section.
 
166
     When both approving and voting on an entry, approve first: for example,
 
167
     to enter a third +1 vote, type "a" "+" "1".
 
168
e:   Edit the entry in $EDITOR.
 
169
     You will be prompted to commit your edits at the end.
 
170
N:   Move to the next entry.  Cache the entry in '$STATEFILE' and do not
 
171
     prompt for it again (even across runs) until it is changed.
 
172
 :   Move to the next entry, without adding the current one to the cache.
 
173
     (That's a space character, ASCII 0x20.)
 
174
 
 
175
After running a merge, you have the following options:
 
176
 
 
177
y:   Open a shell.
 
178
d:   View a diff.
 
179
N:   Move to the next entry.
 
180
 
 
181
To commit a merge, you have two options: either answer 'y' to the second prompt
 
182
to open a shell, and manually run 'svn commit' therein; or set \$MAY_COMMIT=1
 
183
in the environment before running the script, in which case answering 'y'
 
184
to the first prompt will not only run the merge but also commit it.
 
185
 
 
186
There are two batch modes.  The first mode is used by the nightly svn-role
 
187
mergebot.  It is enabled by setting \$YES and \$MAY_COMMIT to '1' in the
 
188
environment.  In this mode, the script will iterate the "Approved changes:"
 
189
section and merge and commit each entry therein.  To prevent an entry from
 
190
being auto-merged, veto it or move it to a new section named "Approved, but
 
191
merge manually:".
 
192
 
 
193
The second batch mode is used by the hourly conflicts detector bot.  It is
 
194
triggered by having \$YES defined in the environment to '1' and \$MAY_COMMIT
 
195
undefined.  In this mode, the script will locally merge every nomination
 
196
(including unapproved and vetoed ones), and complain to stderr if the merge
 
197
failed due to a conflict.  This mode never commits anything.
 
198
 
 
199
The hourly conflicts detector bot turns red if any entry produced a merge
 
200
conflict.  When entry A depends on entry B for a clean merge, put a "Depends:"
 
201
header on entry A to instruct the bot not to turn red due to A.  (The header
 
202
is not parsed; only its presence or absence matters.)
 
203
 
 
204
Both batch modes also perform a basic sanity-check on entries that declare
 
205
backport branches (via the "Branch:" header): if a backport branch is used, but
 
206
at least one of the revisions enumerated in the entry title had not been merged
 
207
from $TRUNK to the branch root, the hourly bot will turn red and 
 
208
nightly bot will skip the entry and email its admins.  (The nightly bot does
 
209
not email the list on failure, since it doesn't use buildbot.)
54
210
 
55
211
The 'svn' binary defined by the environment variable \$SVN, or otherwise the
56
212
'svn' found in \$PATH, will be used to manage the working copy.
57
213
EOF
58
214
}
59
215
 
 
216
sub nominate_usage {
 
217
  my $availid = $AVAILID // "(your username)";
 
218
  my $basename = basename $0;
 
219
  print <<EOF;
 
220
nominate.pl: a tool for adding entries to STATUS.
 
221
 
 
222
Usage: $0 "foo r42 bar r43 qux 45." "\$Some_justification"
 
223
 
 
224
Will add:
 
225
 * r42, r43, r45
 
226
   (log message of r42)
 
227
   Justification:
 
228
     \$Some_justification
 
229
   Votes:
 
230
     +1: $availid
 
231
to STATUS.  Backport branches are detected automatically.
 
232
 
 
233
The STATUS file in the current directory is used (unless argv[0] is "n", in
 
234
which case the STATUS file in the directory of argv[0] is used; the intent
 
235
is to create a symlink named "n" in the branch wc root).
 
236
 
 
237
EOF
 
238
# TODO: Optionally add a "Notes" section.
 
239
# TODO: Look for backport branches named after issues.
 
240
# TODO: Do a dry-run merge on added entries.
 
241
# TODO: Do a dry-run merge on interactively-edited entries in backport.pl
 
242
}
 
243
 
 
244
# If $AVAILID is undefined, warn about it and return true.
 
245
# Else return false.
 
246
#
 
247
# $_[0] is a string for inclusion in generated error messages.
 
248
sub warned_cannot_commit {
 
249
  my $caller_error_string = shift;
 
250
  return 0 if defined $AVAILID;
 
251
 
 
252
  warn "$0: $caller_error_string: unable to determine your username via \$AVAILID or svnauth(1) or ~/.subversion/auth/";
 
253
  return 1;
 
254
}
 
255
 
 
256
sub digest_string {
 
257
  Digest->new("MD5")->add(@_)->hexdigest
 
258
}
 
259
 
 
260
sub digest_entry($) {
 
261
  # Canonicalize the number of trailing EOLs to two.  This matters when there's
 
262
  # on empty line after the last entry in Approved, for example.
 
263
  local $_ = shift;
 
264
  s/\n*\z// and $_ .= "\n\n";
 
265
  digest_string($_)
 
266
}
 
267
 
60
268
sub prompt {
61
 
  local $\; # disable 'perl -l' effects
62
 
  print "Go ahead? ";
63
 
 
64
 
  # TODO: this part was written by trial-and-error
65
 
  ReadMode 'cbreak';
66
 
  my $answer = (ReadKey 0);
67
 
  print $answer, "\n";
68
 
  return ($answer =~ /^y/i) ? 1 : 0;
69
 
}
70
 
 
 
269
  print $_[0]; shift;
 
270
  my %args = @_;
 
271
  my $getchar = sub {
 
272
    my $answer;
 
273
    do {
 
274
      ReadMode 'cbreak';
 
275
      $answer = (ReadKey 0);
 
276
      ReadMode 'normal';
 
277
      die if $@ or not defined $answer;
 
278
      # Swallow terminal escape codes (e.g., arrow keys).
 
279
      unless (isprint $answer or isspace $answer) {
 
280
        $answer = (ReadKey -1) while defined $answer;
 
281
        # TODO: provide an indication that the keystroke was sensed and ignored.
 
282
      }
 
283
    } until defined $answer and (isprint $answer or isspace $answer);
 
284
    print $answer;
 
285
    return $answer;
 
286
  };
 
287
 
 
288
  die "$0: called prompt() in non-interactive mode!" if $YES;
 
289
  my $answer = $getchar->();
 
290
  $answer .= $getchar->() if exists $args{extra} and $answer =~ $args{extra};
 
291
  say "" unless $args{dontprint};
 
292
  return $args{verbose}
 
293
         ? $answer
 
294
         : ($answer =~ /^y/i) ? 1 : 0;
 
295
}
 
296
 
 
297
# Bourne-escape a string.
 
298
# Example:
 
299
#     >>> shell_escape(q[foo'bar]) eq q['foo'\''bar']
 
300
#     True
 
301
sub shell_escape {
 
302
  my (@reply) = map {
 
303
    local $_ = $_; # the LHS $_ is mutable; the RHS $_ may not be.
 
304
    s/\x27/'\\\x27'/g;
 
305
    "'$_'"
 
306
  } @_;
 
307
  wantarray ? @reply : $reply[0]
 
308
}
 
309
 
 
310
sub shell_safe_path_or_url($) {
 
311
  local $_ = shift;
 
312
  return (m{^[A-Za-z0-9._:+/-]+$} and !/^-|^[+]/);
 
313
}
 
314
 
 
315
# Shell-safety-validating wrapper for File::Temp::tempfile
 
316
sub my_tempfile {
 
317
  my ($fh, $fn) = tempfile();
 
318
  croak "Tempfile name '$fn' not shell-safe; aborting"
 
319
        unless shell_safe_path_or_url $fn;
 
320
  return ($fh, $fn);
 
321
}
 
322
 
 
323
# The first argument is a shell script.  Run it and return the shell's
 
324
# exit code, and stdout and stderr as references to arrays of lines.
 
325
sub run_in_shell($) {
 
326
  my $script = shift;
 
327
  my $pid = open3 \*SHELL_IN, \*SHELL_OUT, \*SHELL_ERR, qw#/bin/sh#;
 
328
  # open3 raises exception when it fails; no need to error check
 
329
 
 
330
  print SHELL_IN $script;
 
331
  close SHELL_IN;
 
332
 
 
333
  # Read loop: tee stdout,stderr to arrays.
 
334
  my $select = IO::Select->new(\*SHELL_OUT, \*SHELL_ERR);
 
335
  my (@readable, $outlines, $errlines);
 
336
  while (@readable = $select->can_read) {
 
337
    for my $fh (@readable) {
 
338
      my $line = <$fh>;
 
339
      $select->remove($fh) if eof $fh or not defined $line;
 
340
      next unless defined $line;
 
341
 
 
342
      if ($fh == \*SHELL_OUT) {
 
343
        push @$outlines, $line;
 
344
        print STDOUT $line;
 
345
      }
 
346
      if ($fh == \*SHELL_ERR) {
 
347
        push @$errlines, $line;
 
348
        print STDERR $line;
 
349
      }
 
350
    }
 
351
  }
 
352
  waitpid $pid, 0; # sets $?
 
353
  return $?, $outlines, $errlines;
 
354
}
 
355
 
 
356
 
 
357
# EXPECTED_ERROR_P is subref called with EXIT_CODE, OUTLINES, ERRLINES,
 
358
# expected to return TRUE if the error should be considered fatal (cause
 
359
# backport.pl to exit non-zero) or not.  It may be undef for default behaviour.
71
360
sub merge {
72
 
  my %entry = @_;
73
 
 
74
 
  my ($logmsg_fh, $logmsg_filename) = tempfile();
75
 
  my ($mergeargs, $pattern);
76
 
 
77
 
  my $backupfile = "backport_pl.$$.tmp";
 
361
  my %entry = %{ +shift };
 
362
  my $expected_error_p = shift // sub { 0 }; # by default, errors are unexpected
 
363
  my $parno = $entry{parno} - scalar grep { $_->{parno} < $entry{parno} } @MERGES_TODAY;
 
364
 
 
365
  my ($logmsg_fh, $logmsg_filename) = my_tempfile();
 
366
  my (@mergeargs);
 
367
 
 
368
  my $shell_escaped_branch = shell_escape($entry{branch})
 
369
    if defined($entry{branch});
78
370
 
79
371
  if ($entry{branch}) {
80
 
    # NOTE: This doesn't escape the branch into the pattern.
81
 
    $pattern = sprintf '\V\(%s branch(es)?\|branches\/%s\|Branch(es)?:\n *%s\)', $entry{branch}, $entry{branch}, $entry{branch};
82
 
    $mergeargs = "--reintegrate $BRANCHES/$entry{branch}";
83
 
    print $logmsg_fh "Reintegrate the $entry{header}:";
84
 
    print $logmsg_fh "";
 
372
    if ($SVNvsn >= 1_008_000) {
 
373
      @mergeargs = shell_escape "$BRANCHES/$entry{branch}";
 
374
      say $logmsg_fh "Merge $entry{header}:";
 
375
    } else {
 
376
      @mergeargs = shell_escape qw/--reintegrate/, "$BRANCHES/$entry{branch}";
 
377
      say $logmsg_fh "Reintegrate $entry{header}:";
 
378
    }
 
379
    say $logmsg_fh "";
85
380
  } elsif (@{$entry{revisions}}) {
86
 
    $pattern = '^ [*] \V' . 'r' . $entry{revisions}->[0];
87
 
    $mergeargs = join " ", (map { "-c$_" } @{$entry{revisions}}), '^/subversion/trunk';
88
 
    if (@{$entry{revisions}} > 1) {
89
 
      print $logmsg_fh "Merge the $entry{header} from trunk:";
90
 
      print $logmsg_fh "";
91
 
    } else {
92
 
      print $logmsg_fh "Merge r$entry{revisions}->[0] from trunk:";
93
 
      print $logmsg_fh "";
94
 
    }
 
381
    @mergeargs = shell_escape(
 
382
      ($entry{accept} ? "--accept=$entry{accept}" : ()),
 
383
      (map { "-c$_" } @{$entry{revisions}}),
 
384
      '--',
 
385
      '^/subversion/trunk',
 
386
    );
 
387
    say $logmsg_fh
 
388
      "Merge $entry{header} from trunk",
 
389
      $entry{accept} ? ", with --accept=$entry{accept}" : "",
 
390
      ":";
 
391
    say $logmsg_fh "";
95
392
  } else {
96
393
    die "Don't know how to call $entry{header}";
97
394
  }
98
 
  print $logmsg_fh $_ for @{$entry{entry}};
 
395
  say $logmsg_fh $_ for @{$entry{entry}};
99
396
  close $logmsg_fh or die "Can't close $logmsg_filename: $!";
100
397
 
 
398
  my $reintegrated_word = ($SVNvsn >= 1_008_000) ? "merged" : "reintegrated";
101
399
  my $script = <<"EOF";
102
400
#!/bin/sh
103
401
set -e
104
 
if $DEBUG; then
 
402
if $sh[$DEBUG]; then
105
403
  set -x
106
404
fi
107
 
$SVN diff > $backupfile
108
 
$SVNq revert -R .
109
405
$SVNq up
110
 
$SVNq merge $mergeargs
111
 
$VIM -e -s -n -N -i NONE -u NONE -c '/$pattern/normal! dap' -c wq $STATUS
112
 
if $WET_RUN; then
 
406
$SVNq merge @mergeargs
 
407
if [ "`$SVN status -q | wc -l`" -eq 1 ]; then
 
408
  if [ -n "`$SVN diff | perl -lne 'print if s/^(Added|Deleted|Modified): //' | grep -vx svn:mergeinfo`" ]; then
 
409
    # This check detects STATUS entries that name non-^/subversion/ revnums.
 
410
    # ### Q: What if we actually commit a mergeinfo fix to trunk and then want
 
411
    # ###    to backport it?
 
412
    # ### A: We don't merge it using the script.
 
413
    echo "Bogus merge: includes only svn:mergeinfo changes!" >&2
 
414
    exit 2
 
415
  fi
 
416
fi
 
417
if $sh[$MAY_COMMIT]; then
 
418
  # Remove the approved entry.  The sentinel is important when the entry being
 
419
  # removed is the very last one in STATUS, and in that case it has two effects:
 
420
  # (1) keeps STATUS from ending in a run of multiple empty lines;
 
421
  # (2) makes the \x{7d}k motion behave the same as in all other cases.
 
422
  #
 
423
  # Use a tempfile because otherwise backport_main() would see the "sentinel paragraph".
 
424
  # Since backport_main() has an open descriptor, it will continue to see
 
425
  # the STATUS inode that existed when control flow entered backport_main();
 
426
  # since we replace the file on disk, when this block of code runs in the
 
427
  # next iteration, it will see the new contents.
 
428
  cp $STATUS $STATUS.t
 
429
  (echo; echo; echo "sentinel paragraph") >> $STATUS.t
 
430
  $VIM -e -s -n -N -i NONE -u NONE -c ':0normal! $parno\x{7d}kdap' -c wq $STATUS.t
 
431
  $VIM -e -s -n -N -i NONE -u NONE -c '\$normal! dap' -c wq $STATUS.t
 
432
  mv $STATUS.t $STATUS
113
433
  $SVNq commit -F $logmsg_filename
114
 
else
115
 
  echo "Committing:"
 
434
elif ! $sh[$YES]; then
 
435
  echo "Would have committed:"
 
436
  echo '[[['
116
437
  $SVN status -q
 
438
  echo 'M       STATUS (not shown in the diff)'
117
439
  cat $logmsg_filename
 
440
  echo ']]]'
118
441
fi
119
442
EOF
120
443
 
 
444
  if ($MAY_COMMIT) {
 
445
    # STATUS has been edited and the change has been committed
 
446
    push @MERGES_TODAY, \%entry;
 
447
  }
 
448
 
121
449
  $script .= <<"EOF" if $entry{branch};
122
450
reinteg_rev=\`$SVN info $STATUS | sed -ne 's/Last Changed Rev: //p'\`
123
 
if $WET_RUN; then
 
451
if $sh[$MAY_COMMIT]; then
124
452
  # Sleep to avoid out-of-order commit notifications
125
 
  if [ -n "\$YES" ]; then sleep 15; fi
126
 
  $SVNq rm $BRANCHES/$entry{branch} -m "Remove the '$entry{branch}' branch, reintegrated in r\$reinteg_rev."
127
 
  if [ -n "\$YES" ]; then sleep 1; fi
128
 
else
129
 
  echo "Removing reintegrated '$entry{branch}' branch"
 
453
  if $sh[$YES]; then sleep 15; fi
 
454
  $SVNq rm $BRANCHES/$shell_escaped_branch -m "Remove the '"$shell_escaped_branch"' branch, $reintegrated_word in r\$reinteg_rev."
 
455
  if $sh[$YES]; then sleep 1; fi
 
456
elif ! $sh[$YES]; then
 
457
  echo "Would remove $reintegrated_word '"$shell_escaped_branch"' branch"
130
458
fi
131
459
EOF
132
460
 
133
 
  open SHELL, '|-', qw#/bin/sh# or die $!;
134
 
  print SHELL $script;
135
 
  close SHELL or warn "$0: sh($?): $!";
136
 
 
137
 
  unlink $backupfile if -z $backupfile;
138
 
  unlink $logmsg_filename unless $? or $!;
 
461
  # Include the time so it's easier to find the interesting backups.
 
462
  my $backupfile = strftime "backport_pl.%Y%m%d-%H%M%S.$$.tmp", localtime;
 
463
  die if -s $backupfile;
 
464
  system("$SVN diff > $backupfile") == 0
 
465
    or die "Saving a backup diff ($backupfile) failed ($?): $!";
 
466
  if (-z $backupfile) {
 
467
    unlink $backupfile;
 
468
  } else {
 
469
    warn "Local mods saved to '$backupfile'\n";
 
470
  }
 
471
 
 
472
  # If $MAY_COMMIT, then $script will edit STATUS anyway.
 
473
  revert(verbose => 0, discard_STATUS => $MAY_COMMIT);
 
474
 
 
475
  $MERGED_SOMETHING++;
 
476
  my ($exit_code, $outlines, $errlines) = run_in_shell $script;
 
477
  unless ($! == 0) {
 
478
    die "system() failed to spawn subshell ($!); aborting";
 
479
  }
 
480
  unless ($exit_code == 0) {
 
481
    warn "$0: subshell exited with code $exit_code (in '$entry{header}') "
 
482
        ."(maybe due to 'set -e'?)";
 
483
 
 
484
    # If we're committing, don't attempt to guess the problem and gracefully
 
485
    # continue; just abort.
 
486
    if ($MAY_COMMIT) {
 
487
      die "Lost track of paragraph numbers; aborting";
 
488
    }
 
489
 
 
490
    # Record the error, unless the caller wants not to.
 
491
    $ERRORS{$entry{id}} = [\%entry, "subshell exited with code $exit_code"]
 
492
      unless $expected_error_p->($exit_code, $outlines, $errlines);
 
493
  }
 
494
 
 
495
  unlink $logmsg_filename unless $exit_code;
139
496
}
140
497
 
 
498
# Input formats:
 
499
#    "1.8.x-r42",
 
500
#    "branches/1.8.x-r42",
 
501
#    "branches/1.8.x-r42/",
 
502
#    "subversion/branches/1.8.x-r42",
 
503
#    "subversion/branches/1.8.x-r42/",
 
504
#    "^/subversion/branches/1.8.x-r42",
 
505
#    "^/subversion/branches/1.8.x-r42/",
 
506
# Return value:
 
507
#    "1.8.x-r42"
 
508
# Works for any branch name that doesn't include slashes.
141
509
sub sanitize_branch {
142
510
  local $_ = shift;
143
 
  s#.*/##;
144
511
  s/^\s*//;
145
512
  s/\s*$//;
 
513
  s#/*$##;
 
514
  s#.*/##;
146
515
  return $_;
147
516
}
148
517
 
 
518
sub logsummarysummary {
 
519
  my $entry = shift;
 
520
  join "",
 
521
    $entry->{logsummary}->[0], ('[...]' x (0 < $#{$entry->{logsummary}}))
 
522
}
 
523
 
149
524
# TODO: may need to parse other headers too?
150
525
sub parse_entry {
 
526
  my $raw = shift;
 
527
  my $parno = shift;
151
528
  my @lines = @_;
 
529
  my $depends;
 
530
  my $accept;
152
531
  my (@revisions, @logsummary, $branch, @votes);
153
532
  # @lines = @_;
154
533
 
155
 
  # strip first three spaces
156
 
  $_[0] =~ s/^ \* /   /;
157
 
  s/^   // for @_;
 
534
  # strip spaces to match up with the indention
 
535
  $_[0] =~ s/^( *)\* //;
 
536
  my $indentation = ' ' x (length($1) + 2);
 
537
  s/^$indentation// for @_;
 
538
 
 
539
  # Ignore trailing spaces: it is not significant on any field, and makes the
 
540
  # regexes simpler.
 
541
  s/\s*$// for @_;
158
542
 
159
543
  # revisions
160
 
  $branch = sanitize_branch $1 if $_[0] =~ /^(\S*) branch$/;
161
 
  while ($_[0] =~ /^r/) {
162
 
    while ($_[0] =~ s/^r(\d+)(?:$|[,; ]+)//) {
163
 
      push @revisions, $1;
164
 
    }
 
544
  $branch = sanitize_branch $1
 
545
    and shift
 
546
    if $_[0] =~ /^(\S*) branch$/ or $_[0] =~ m#branches/(\S+)#;
 
547
  while ($_[0] =~ /^(?:r?\d+[,; ]*)+$/) {
 
548
    push @revisions, ($_[0] =~ /(\d+)/g);
165
549
    shift;
166
550
  }
167
551
 
168
552
  # summary
169
 
  push @logsummary, shift until $_[0] =~ /^\s*\w+:/ or not defined $_[0];
 
553
  do {
 
554
    push @logsummary, shift
 
555
  } until $_[0] =~ /^\s*[][\w]+:/ or not defined $_[0];
170
556
 
171
557
  # votes
172
558
  unshift @votes, pop until $_[-1] =~ /^\s*Votes:/ or not defined $_[-1];
173
559
  pop;
174
560
 
175
 
  # branch
 
561
  # depends, branch, notes
 
562
  # Ignored headers: Changes[*]
176
563
  while (@_) {
177
 
    shift and next unless $_[0] =~ s/^\s*Branch(es)?:\s*//;
178
 
    $branch = sanitize_branch (shift || shift || die "Branch header found without value");
 
564
    given (shift) {
 
565
      when (/^Depends:/) {
 
566
        $depends++;
 
567
      }
 
568
      if (s/^Branch:\s*//) {
 
569
        $branch = sanitize_branch ($_ || shift || die "Branch header found without value");
 
570
      }
 
571
      if (s/^Notes:\s*//) {
 
572
        my $notes = $_;
 
573
        $notes .= shift while @_ and $_[0] !~ /^\w/;
 
574
        my %accepts = map { $_ => 1 } ($notes =~ /--accept[ =]([a-z-]+)/g);
 
575
        given (scalar keys %accepts) {
 
576
          when (0) { }
 
577
          when (1) { $accept = [keys %accepts]->[0]; }
 
578
          default  {
 
579
            warn "Too many --accept values at '",
 
580
                 logsummarysummary({ logsummary => [@logsummary] }),
 
581
                 "'";
 
582
          }
 
583
        }
 
584
      }
 
585
    }
179
586
  }
180
587
 
181
588
  # Compute a header.
182
 
  my $header;
183
 
  $header = "r$revisions[0] group" if @revisions;
184
 
  $header = "$branch branch" if $branch;
185
 
  warn "No header for [@lines]" unless $header;
 
589
  my ($header, $id);
 
590
  if ($branch) {
 
591
    $header = "the $branch branch";
 
592
    $id = $branch;
 
593
  } elsif (@revisions == 1) {
 
594
    $header = "r$revisions[0]";
 
595
    $id = "r$revisions[0]";
 
596
  } elsif (@revisions) {
 
597
    $header = "the r$revisions[0] group";
 
598
    $id = "r$revisions[0]";
 
599
  } else {
 
600
    die "Entry '$raw' has neither revisions nor branch";
 
601
  }
 
602
  my $header_start = ($header =~ /^the/ ? ucfirst($header) : $header);
 
603
 
 
604
  warn "Entry has both branch '$branch' and --accept=$accept specified\n"
 
605
    if $branch and $accept;
186
606
 
187
607
  return (
188
608
    revisions => [@revisions],
189
609
    logsummary => [@logsummary],
190
610
    branch => $branch,
191
611
    header => $header,
 
612
    header_start => $header_start,
 
613
    depends => $depends,
 
614
    id => $id,
192
615
    votes => [@votes],
193
616
    entry => [@lines],
 
617
    accept => $accept,
 
618
    raw => $raw,
 
619
    digest => digest_entry($raw),
 
620
    parno => $parno, # $. from backport_main()
194
621
  );
195
622
}
196
623
 
 
624
sub edit_string {
 
625
  # Edits $_[0] in an editor.
 
626
  # $_[1] is used in error messages.
 
627
  die "$0: called edit_string() in non-interactive mode!" if $YES;
 
628
  my $string = shift;
 
629
  my $name = shift;
 
630
  my %args = @_;
 
631
  my $trailing_eol = $args{trailing_eol};
 
632
  my ($fh, $fn) = my_tempfile();
 
633
  print $fh $string;
 
634
  $fh->flush or die $!;
 
635
  system("$EDITOR -- $fn") == 0
 
636
    or warn "\$EDITOR failed editing $name: $! ($?); "
 
637
           ."edit results ($fn) ignored.";
 
638
  my $rv = `cat $fn`;
 
639
  $rv =~ s/\n*\z// and $rv .= ("\n" x $trailing_eol) if defined $trailing_eol;
 
640
  $rv;
 
641
}
 
642
 
 
643
sub vote {
 
644
  my ($state, $approved, $votes) = @_;
 
645
  # TODO: use votesarray instead of votescheck
 
646
  my (%approvedcheck, %votescheck);
 
647
  my $raw_approved = "";
 
648
  my @votesarray;
 
649
  return unless %$approved or %$votes;
 
650
 
 
651
  # If $AVAILID is undef, we can only process 'edit' pseudovotes; handle_entry() is
 
652
  # supposed to prevent numeric (±1,±0) votes from getting to this point.
 
653
  die "Assertion failed" if not defined $AVAILID
 
654
                            and grep { $_ ne 'edit' } map { $_->[0] } values %$votes;
 
655
 
 
656
  my $had_empty_line;
 
657
 
 
658
  $. = 0;
 
659
  open STATUS, "<", $STATUS;
 
660
  open VOTES, ">", "$STATUS.$$.tmp";
 
661
  while (<STATUS>) {
 
662
    $had_empty_line = /\n\n\z/;
 
663
    my $key = digest_entry $_;
 
664
 
 
665
    $approvedcheck{$key}++ if exists $approved->{$key};
 
666
    $votescheck{$key}++ if exists $votes->{$key};
 
667
 
 
668
    unless (exists $votes->{$key} or exists $approved->{$key}) {
 
669
      print VOTES;
 
670
      next;
 
671
    }
 
672
 
 
673
    unless (exists $votes->{$key}) {
 
674
      push @votesarray, {
 
675
        entry => $approved->{$key},
 
676
        approval => 1,
 
677
        digest => $key,
 
678
      };
 
679
      $raw_approved .= $_;
 
680
      next;
 
681
    }
 
682
 
 
683
    # We have a vote, and potentially an approval.
 
684
 
 
685
    my ($vote, $entry) = @{$votes->{$key}};
 
686
    push @votesarray, {
 
687
      entry => $entry,
 
688
      vote => $vote,
 
689
      approval => (exists $approved->{$key}),
 
690
      digest => $key,
 
691
    };
 
692
 
 
693
    if ($vote eq 'edit') {
 
694
      local $_ = $entry->{raw};
 
695
      $votesarray[-1]->{digest} = digest_entry $_;
 
696
      (exists $approved->{$key}) ? ($raw_approved .= $_) : (print VOTES);
 
697
      next;
 
698
    }
 
699
 
 
700
    s/^(\s*\Q$vote\E:.*)/"$1, $AVAILID"/me
 
701
    or s/(.*\w.*?\n)/"$1     $vote: $AVAILID\n"/se;
 
702
    $_ = edit_string $_, $entry->{header}, trailing_eol => 2
 
703
        if $vote ne '+1';
 
704
    $votesarray[-1]->{digest} = digest_entry $_;
 
705
    (exists $approved->{$key}) ? ($raw_approved .= $_) : (print VOTES);
 
706
  }
 
707
  close STATUS;
 
708
  print VOTES "\n" if $raw_approved and !$had_empty_line;
 
709
  print VOTES $raw_approved;
 
710
  close VOTES;
 
711
  warn "Some vote chunks weren't found: ",
 
712
    join ',',
 
713
    map $votes->{$_}->[1]->{id},
 
714
    grep { !$votescheck{$_} } keys %$votes
 
715
    if scalar(keys %$votes) != scalar(keys %votescheck);
 
716
  warn "Some approval chunks weren't found: ",
 
717
    join ',',
 
718
    map $approved->{$_}->{id},
 
719
    grep { !$approvedcheck{$_} } keys %$approved
 
720
    if scalar(keys %$approved) != scalar(keys %approvedcheck);
 
721
  prompt "Press the 'any' key to continue...\n", dontprint => 1
 
722
    if scalar(keys %$approved) != scalar(keys %approvedcheck)
 
723
    or scalar(keys %$votes) != scalar(keys %votescheck);
 
724
  move "$STATUS.$$.tmp", $STATUS;
 
725
 
 
726
  my $logmsg = do {
 
727
    my @sentences = map {
 
728
       my $words_vote = ", approving" x $_->{approval};
 
729
       my $words_edit = " and approve" x $_->{approval};
 
730
       exists $_->{vote}
 
731
       ? (
 
732
         ( $_->{vote} eq 'edit'
 
733
           ? "Edit$words_edit the $_->{entry}->{id} entry"
 
734
           : "Vote $_->{vote} on $_->{entry}->{header}$words_vote"
 
735
         )
 
736
         . "."
 
737
         )
 
738
      : # exists only in $approved
 
739
        "Approve $_->{entry}->{header}."
 
740
      } @votesarray;
 
741
    (@sentences == 1)
 
742
    ? $sentences[0]
 
743
    : "* STATUS:\n" . join "", map "  $_\n", @sentences;
 
744
  };
 
745
 
 
746
  system "$SVN diff -- $STATUS";
 
747
  printf "[[[\n%s%s]]]\n", $logmsg, ("\n" x ($logmsg !~ /\n\z/));
 
748
  if (prompt "Commit these votes? ") {
 
749
    my ($logmsg_fh, $logmsg_filename) = my_tempfile();
 
750
    print $logmsg_fh $logmsg;
 
751
    close $logmsg_fh;
 
752
    system("$SVN commit -F $logmsg_filename -- $STATUS") == 0
 
753
        or warn("Committing the votes failed($?): $!") and return;
 
754
    unlink $logmsg_filename;
 
755
 
 
756
    # Add to state votes that aren't '+0' or 'edit'
 
757
    $state->{$_->{digest}}++ for grep
 
758
                                   +{ qw/-1 t -0 t +1 t/ }->{$_->{vote}},
 
759
                                 @votesarray;
 
760
  }
 
761
}
 
762
 
 
763
sub check_local_mods_to_STATUS {
 
764
  if (`$SVN status -q $STATUS`) {
 
765
    die  "Local mods to STATUS file $STATUS" if $YES;
 
766
    warn "Local mods to STATUS file $STATUS";
 
767
    system "$SVN diff -- $STATUS";
 
768
    prompt "Press the 'any' key to continue...\n", dontprint => 1;
 
769
    return 1;
 
770
  }
 
771
  return 0;
 
772
}
 
773
 
 
774
sub renormalize_STATUS {
 
775
  my $vimscript = <<'EOVIM';
 
776
:"" Strip trailing whitespace before entries and section headers, but not
 
777
:"" inside entries (e.g., multi-paragraph Notes: fields).
 
778
:""
 
779
:"" Since an entry is always followed by another entry, section header, or EOF,
 
780
:"" there is no need to separately strip trailing whitespace from lines following
 
781
:"" entries.
 
782
:%s/\v\s+\n(\s*\n)*\ze(\s*[*]|\w)/\r\r/g
 
783
 
 
784
:"" Ensure there is exactly one blank line around each entry and header.
 
785
:""
 
786
:"" First, inject a new empty line above and below each entry and header; then,
 
787
:"" squeeze runs of empty lines together.
 
788
:0/^=/,$ g/^ *[*]/normal! O
 
789
:g/^=/normal! o
 
790
:g/^=/-normal! O
 
791
:
 
792
:%s/\n\n\n\+/\r\r/g
 
793
 
 
794
:"" Save.
 
795
:wq
 
796
EOVIM
 
797
  open VIM, '|-', $VIM, qw/-e -s -n -N -i NONE -u NONE --/, $STATUS
 
798
    or die "Can't renormalize STATUS: $!";
 
799
  print VIM $vimscript;
 
800
  close VIM or warn "$0: renormalize_STATUS failed ($?): $!)";
 
801
 
 
802
  system("$SVN commit -m '* STATUS: Whitespace changes only.' -- $STATUS") == 0
 
803
    or die "$0: Can't renormalize STATUS ($?): $!"
 
804
    if $MAY_COMMIT;
 
805
}
 
806
 
 
807
sub revert {
 
808
  my %args = @_;
 
809
  die "Bug: \$args{verbose} undefined" unless exists $args{verbose};
 
810
  die "Bug: unknown argument" if grep !/^(?:verbose|discard_STATUS)$/, keys %args;
 
811
 
 
812
  copy $STATUS, "$STATUS.$$.tmp"        unless $args{discard_STATUS};
 
813
  system("$SVN revert -q $STATUS") == 0
 
814
    or die "revert failed ($?): $!";
 
815
  system("$SVN revert -R ./" . (" -q" x !$args{verbose})) == 0
 
816
    or die "revert failed ($?): $!";
 
817
  move "$STATUS.$$.tmp", $STATUS        unless $args{discard_STATUS};
 
818
  $MERGED_SOMETHING = 0;
 
819
}
 
820
 
 
821
sub maybe_revert {
 
822
  # This is both a SIGINT handler, and the tail end of main() in normal runs.
 
823
  # @_ is 'INT' in the former case and () in the latter.
 
824
  delete $SIG{INT} unless @_;
 
825
  revert verbose => 1 if !$YES and $MERGED_SOMETHING and prompt 'Revert? ';
 
826
  (@_ ? exit : return);
 
827
}
 
828
 
 
829
sub signal_handler {
 
830
  my $sig = shift;
 
831
 
 
832
  # Clean up after prompt()
 
833
  ReadMode 'normal';
 
834
 
 
835
  # Fall back to default action
 
836
  delete $SIG{$sig};
 
837
  kill $sig, $$;
 
838
}
 
839
 
 
840
sub warning_summary {
 
841
  return unless %ERRORS;
 
842
 
 
843
  warn "Warning summary\n";
 
844
  warn "===============\n";
 
845
  warn "\n";
 
846
  for my $id (keys %ERRORS) {
 
847
    my $title = logsummarysummary $ERRORS{$id}->[0];
 
848
    warn "$id ($title): $ERRORS{$id}->[1]\n";
 
849
  }
 
850
}
 
851
 
 
852
sub read_state {
 
853
  # die "$0: called read_state() in non-interactive mode!" if $YES;
 
854
 
 
855
  open my $fh, '<', $STATEFILE or do {
 
856
    return {} if $!{ENOENT};
 
857
    die "Can't read statefile: $!";
 
858
  };
 
859
 
 
860
  my %rv;
 
861
  while (<$fh>) {
 
862
    chomp;
 
863
    $rv{$_}++;
 
864
  }
 
865
  return \%rv;
 
866
}
 
867
 
 
868
sub write_state {
 
869
  my $state = shift;
 
870
  open STATE, '>', $STATEFILE or warn("Can't write state: $!"), return;
 
871
  say STATE for keys %$state;
 
872
  close STATE;
 
873
}
 
874
 
 
875
sub exit_stage_left {
 
876
  my $state = shift;
 
877
  maybe_revert;
 
878
  warning_summary if $YES;
 
879
  vote $state, @_;
 
880
  write_state $state;
 
881
  exit scalar keys %ERRORS;
 
882
}
 
883
 
 
884
# Given an ENTRY, check whether all ENTRY->{revisions} have been merged
 
885
# into ENTRY->{branch}, if it has one.  If revisions are missing, record
 
886
# a warning in $ERRORS.  Return TRUE If the entry passed the validation
 
887
# and FALSE otherwise.
 
888
sub validate_branch_contains_named_revisions {
 
889
  my %entry = @_;
 
890
  return 1 unless defined $entry{branch};
 
891
  my %present;
 
892
 
 
893
  return "Why are you running so old versions?" # true in boolean context
 
894
    if $SVNvsn < 1_005_000;     # doesn't have the 'mergeinfo' subcommand
 
895
 
 
896
  my $shell_escaped_branch = shell_escape($entry{branch});
 
897
  %present = do {
 
898
    my @present = `$SVN mergeinfo --show-revs=merged -- $TRUNK $BRANCHES/$shell_escaped_branch`;
 
899
    chomp @present;
 
900
    @present = map /(\d+)/g, @present;
 
901
    map +($_ => 1), @present;
 
902
  };
 
903
 
 
904
  my @absent = grep { not exists $present{$_} } @{$entry{revisions}};
 
905
 
 
906
  if (@absent) {
 
907
    $ERRORS{$entry{id}} //= [\%entry,
 
908
      sprintf("Revisions '%s' nominated but not included in branch",
 
909
              (join ", ", map { "r$_" } @absent)),
 
910
    ];
 
911
  }
 
912
  return @absent ? 0 : 1;
 
913
}
 
914
 
197
915
sub handle_entry {
198
 
  my %entry = parse_entry @_;
199
 
  my @vetoes = grep { /^  -1:/ } @{$entry{votes}};
 
916
  my $in_approved = shift;
 
917
  my $approved = shift;
 
918
  my $votes = shift;
 
919
  my $state = shift;
 
920
  my $raw = shift;
 
921
  my $parno = shift;
 
922
  my $skip = shift;
 
923
  my %entry = parse_entry $raw, $parno, @_;
 
924
  my @vetoes = grep /^\s*-1:/, @{$entry{votes}};
 
925
 
 
926
  my $match = defined($skip) ? ($raw =~ /\Q$skip\E/ or $raw =~ /$skip/msi) : 0
 
927
              unless $YES;
200
928
 
201
929
  if ($YES) {
202
 
    merge %entry unless @vetoes;
 
930
    # Run a merge if:
 
931
    unless (@vetoes) {
 
932
      if ($MAY_COMMIT and $in_approved) {
 
933
        # svn-role mode
 
934
        merge \%entry if validate_branch_contains_named_revisions %entry;
 
935
      } elsif (!$MAY_COMMIT) {
 
936
        # Scan-for-conflicts mode
 
937
 
 
938
        # First, sanity-check the entry.  We ignore the result; even if it
 
939
        # failed, we do want to check for conflicts, in the remainder of this
 
940
        # block.
 
941
        validate_branch_contains_named_revisions %entry;
 
942
 
 
943
        # E155015 is SVN_ERR_WC_FOUND_CONFLICT
 
944
        my $expected_error_p = sub {
 
945
          my ($exit_code, $outlines, $errlines) = @_;
 
946
          ($exit_code == 0)
 
947
            or
 
948
          (grep /svn: E155015:/, @$errlines)
 
949
        };
 
950
        merge \%entry, ($entry{depends} ? $expected_error_p : undef);
 
951
 
 
952
        my $output = `$SVN status`;
 
953
 
 
954
        # Pre-1.6 svn's don't have the 7th column, so fake it.
 
955
        $output =~ s/^(......)/$1 /mg if $SVNvsn < 1_006_000;
 
956
 
 
957
        my (@conflicts) = ($output =~ m#^(?:C......|.C.....|......C)\s(.*)#mg);
 
958
        if (@conflicts and !$entry{depends}) {
 
959
          $ERRORS{$entry{id}} //= [\%entry,
 
960
                                   sprintf "Conflicts on %s%s%s",
 
961
                                     '[' x !!$#conflicts,
 
962
                                     (join ', ',
 
963
                                      map { basename $_ }
 
964
                                      @conflicts),
 
965
                                     ']' x !!$#conflicts,
 
966
                                  ];
 
967
          say STDERR "Conflicts merging $entry{header}!";
 
968
          say STDERR "";
 
969
          say STDERR $output;
 
970
          system "$SVN diff -- " . join ' ', shell_escape @conflicts;
 
971
        } elsif (!@conflicts and $entry{depends}) {
 
972
          # Not a warning since svn-role may commit the dependency without
 
973
          # also committing the dependent in the same pass.
 
974
          print "No conflicts merging $entry{header}, but conflicts were "
 
975
              ."expected ('Depends:' header set)\n";
 
976
        } elsif (@conflicts) {
 
977
          say "Conflicts found merging $entry{header}, as expected.";
 
978
        }
 
979
        revert verbose => 0;
 
980
      }
 
981
    }
 
982
  } elsif (defined($skip) ? not $match : $state->{$entry{digest}}) {
 
983
    print "\n\n";
 
984
    my $reason = defined($skip) ? "doesn't match pattern"
 
985
                                : "remove $STATEFILE to reset";
 
986
    say "Skipping $entry{header} ($reason):";
 
987
    say logsummarysummary \%entry;
 
988
  } elsif ($match or not defined $skip) {
 
989
    # This loop is just a hack because 'goto' panics.  The goto should be where
 
990
    # the "next PROMPT;" is; there's a "last;" at the end of the loop body.
 
991
    PROMPT: while (1) {
 
992
    say "";
 
993
    say "\n>>> $entry{header_start}:";
 
994
    say join ", ", map { "r$_" } @{$entry{revisions}} if @{$entry{revisions}};
 
995
    say "$BRANCHES/$entry{branch}" if $entry{branch};
 
996
    say "--accept=$entry{accept}" if $entry{accept};
 
997
    say "";
 
998
    say for @{$entry{logsummary}};
 
999
    say "";
 
1000
    say for @{$entry{votes}};
 
1001
    say "";
 
1002
    say "Vetoes found!" if @vetoes;
 
1003
 
 
1004
    # See above for why the while(1).
 
1005
    QUESTION: while (1) {
 
1006
    my $key = $entry{digest};
 
1007
    given (prompt 'Run a merge? [y,l,v,±1,±0,q,e,a, ,N] ',
 
1008
                   verbose => 1, extra => qr/[+-]/) {
 
1009
      when (/^y/i) {
 
1010
        #validate_branch_contains_named_revisions %entry;
 
1011
        merge \%entry;
 
1012
        while (1) {
 
1013
          given (prompt "Shall I open a subshell? [ydN] ", verbose => 1) {
 
1014
            when (/^y/i) {
 
1015
              # TODO: if $MAY_COMMIT, save the log message to a file (say,
 
1016
              #       backport.logmsg in the wcroot).
 
1017
              system($SHELL) == 0
 
1018
                or warn "Creating an interactive subshell failed ($?): $!"
 
1019
            }
 
1020
            when (/^d/) {
 
1021
              system("$SVN diff | $PAGER") == 0
 
1022
                or warn "diff failed ($?): $!";
 
1023
              next;
 
1024
            }
 
1025
            when (/^N/i) {
 
1026
              # fall through.
 
1027
            }
 
1028
            default {
 
1029
              next;
 
1030
            }
 
1031
          }
 
1032
          revert verbose => 1;
 
1033
          next PROMPT;
 
1034
        }
 
1035
        # NOTREACHED
 
1036
      }
 
1037
      when (/^l/i) {
 
1038
        if ($entry{branch}) {
 
1039
            system "$SVN log --stop-on-copy -v -g -r 0:HEAD -- "
 
1040
                   .shell_escape("$BRANCHES/$entry{branch}")." "
 
1041
                   ."| $PAGER";
 
1042
        } elsif (@{$entry{revisions}}) {
 
1043
            system "$SVN log ".(join ' ', map { "-r$_" } @{$entry{revisions}})
 
1044
                   ." -- ^/subversion | $PAGER";
 
1045
        } else {
 
1046
            die "Assertion failed: entry has neither branch nor revisions:\n",
 
1047
                '[[[', (join ';;', %entry), ']]]';
 
1048
        }
 
1049
        next PROMPT;
 
1050
      }
 
1051
      when (/^v/i) {
 
1052
        say "";
 
1053
        say for @{$entry{entry}};
 
1054
        say "";
 
1055
        next QUESTION;
 
1056
      }
 
1057
      when (/^q/i) {
 
1058
        exit_stage_left $state, $approved, $votes;
 
1059
      }
 
1060
      when (/^a/i) {
 
1061
        $approved->{$key} = \%entry;
 
1062
        next PROMPT;
 
1063
      }
 
1064
      when (/^([+-][01])\s*$/i) {
 
1065
        next QUESTION if warned_cannot_commit "Entering a vote failed";
 
1066
        $votes->{$key} = [$1, \%entry];
 
1067
        say "Your '$1' vote has been recorded." if $VERBOSE;
 
1068
        last PROMPT;
 
1069
      }
 
1070
      when (/^e/i) {
 
1071
        prompt "Press the 'any' key to continue...\n"
 
1072
            if warned_cannot_commit "Committing this edit later on may fail";
 
1073
        my $original = $entry{raw};
 
1074
        $entry{raw} = edit_string $entry{raw}, $entry{header},
 
1075
                        trailing_eol => 2;
 
1076
        # TODO: parse the edited entry (empty lines, logsummary+votes, etc.)
 
1077
        $votes->{$key} = ['edit', \%entry] # marker for the 2nd pass
 
1078
            if $original ne $entry{raw};
 
1079
        last PROMPT;
 
1080
      }
 
1081
      when (/^N/i) {
 
1082
        $state->{$entry{digest}}++;
 
1083
        last PROMPT;
 
1084
      }
 
1085
      when (/^\x20/) {
 
1086
        last PROMPT; # Fall off the end of the given/when block.
 
1087
      }
 
1088
      default {
 
1089
        say "Please use one of the options in brackets (q to quit)!";
 
1090
        next QUESTION;
 
1091
      }
 
1092
    }
 
1093
    last; } # QUESTION
 
1094
    last; } # PROMPT
203
1095
  } else {
204
 
    print "";
205
 
    print "\n>>> The $entry{header}:";
206
 
    print join ", ", map { "r$_" } @{$entry{revisions}};
207
 
    print "$BRANCHES/$entry{branch}" if $entry{branch};
208
 
    print "";
209
 
    print for @{$entry{logsummary}};
210
 
    print "";
211
 
    print for @{$entry{votes}};
212
 
    print "";
213
 
    print "Vetoes found!" if @vetoes;
214
 
 
215
 
    merge %entry if prompt;
 
1096
    # NOTREACHED
 
1097
    die "Unreachable code reached.";
216
1098
  }
217
1099
 
218
 
  # TODO: merge() changes ./STATUS, which we're reading below, but
219
 
  #       on my system the loop in main() doesn't seem to care.
220
 
 
221
1100
  1;
222
1101
}
223
1102
 
224
 
sub main {
225
 
  usage, exit 0 if @ARGV;
226
 
 
227
 
  open STATUS, "<", $STATUS or (usage, exit 1);
 
1103
 
 
1104
sub backport_main {
 
1105
  my %approved;
 
1106
  my %votes;
 
1107
  my $state = read_state;
 
1108
  my $renormalize;
 
1109
 
 
1110
  if (@ARGV && $ARGV[0] eq '--renormalize') {
 
1111
    $renormalize = 1;
 
1112
    shift;
 
1113
  }
 
1114
 
 
1115
  backport_usage, exit 0 if @ARGV > ($YES ? 0 : 1) or grep /^--help$/, @ARGV;
 
1116
  backport_usage, exit 0 if grep /^(?:-h|-\?|--help|help)$/, @ARGV;
 
1117
  my $skip = shift; # maybe undef
 
1118
  # assert not defined $skip if $YES;
 
1119
 
 
1120
  open STATUS, "<", $STATUS or (backport_usage, exit 1);
228
1121
 
229
1122
  # Because we use the ':normal' command in Vim...
230
 
  die "A vim with the +ex_extra feature is required"
231
 
      if `${VIM} --version` !~ /[+]ex_extra/;
 
1123
  die "A vim with the +ex_extra feature is required for --renormalize and "
 
1124
      ."\$MAY_COMMIT modes"
 
1125
      if ($renormalize or $MAY_COMMIT) and `${VIM} --version` !~ /[+]ex_extra/;
232
1126
 
233
1127
  # ### TODO: need to run 'revert' here
234
1128
  # ### TODO: both here and in merge(), unlink files that previous merges added
235
 
  die "Local mods to STATUS file $STATUS" if `$SVN status -q $STATUS`;
 
1129
  # When running from cron, there shouldn't be local mods.  (For interactive
 
1130
  # usage, we preserve local mods to STATUS.)
 
1131
  system("$SVN info $STATUS >/dev/null") == 0
 
1132
    or die "$0: svn error; point \$SVN to an appropriate binary";
 
1133
 
 
1134
  check_local_mods_to_STATUS;
 
1135
  renormalize_STATUS if $renormalize;
236
1136
 
237
1137
  # Skip most of the file
238
 
  while (<STATUS>) {
239
 
    last if /^Approved changes/;
240
 
  }
241
 
  while (<STATUS>) {
242
 
    last unless /^=+$/;
243
 
  }
244
1138
  $/ = ""; # paragraph mode
245
 
 
246
 
  while (<STATUS>) {
 
1139
  while (<STATUS>) {
 
1140
    last if /^Status of \d+\.\d+/;
 
1141
  }
 
1142
 
 
1143
  $SIG{INT} = \&maybe_revert unless $YES;
 
1144
  $SIG{TERM} = \&signal_handler unless $YES;
 
1145
 
 
1146
  my $in_approved = 0;
 
1147
  while (<STATUS>) {
 
1148
    my $lines = $_;
247
1149
    my @lines = split /\n/;
248
1150
 
249
1151
    given ($lines[0]) {
250
1152
      # Section header
251
1153
      when (/^[A-Z].*:$/i) {
252
 
        print "\n\n=== $lines[0]" unless $YES;
 
1154
        say "\n\n=== $lines[0]" unless $YES;
 
1155
        $in_approved = $lines[0] =~ /^Approved changes/;
 
1156
      }
 
1157
      # Comment
 
1158
      when (/^[#\x5b]/i) {
 
1159
        next;
253
1160
      }
254
1161
      # Separator after section header
255
1162
      when (/^=+$/i) {
256
1163
        break;
257
1164
      }
258
1165
      # Backport entry?
259
 
      when (/^ \*/) {
 
1166
      when (/^ *\*/) {
260
1167
        warn "Too many bullets in $lines[0]" and next
261
 
          if grep /^ \*/, @lines[1..$#lines];
262
 
        handle_entry @lines;
 
1168
          if grep /^ *\*/, @lines[1..$#lines];
 
1169
        handle_entry $in_approved, \%approved, \%votes, $state, $lines, $.,
 
1170
                     $skip,
 
1171
                     @lines;
263
1172
      }
264
1173
      default {
265
 
        warn "Unknown entry '$lines[0]' at $ARGV:$.\n";
 
1174
        warn "Unknown entry '$lines[0]'";
266
1175
      }
267
1176
    }
268
1177
  }
269
 
}
270
 
 
271
 
&main
 
1178
 
 
1179
  exit_stage_left $state, \%approved, \%votes;
 
1180
}
 
1181
 
 
1182
sub nominate_main {
 
1183
  my $had_local_mods;
 
1184
 
 
1185
  local $Text::Wrap::columns = 79;
 
1186
 
 
1187
  $had_local_mods = check_local_mods_to_STATUS;
 
1188
 
 
1189
  # Argument parsing.
 
1190
  nominate_usage, exit 0 if @ARGV != 2;
 
1191
  my (@revnums) = (+shift) =~ /(\d+)/g;
 
1192
  my $justification = shift;
 
1193
 
 
1194
  die "Unable to proceed." if warned_cannot_commit "Nominating failed";
 
1195
 
 
1196
  @revnums = sort { $a <=> $b } keys %{{ map { $_ => 1 } @revnums }};
 
1197
  die "No revision numbers specified" unless @revnums;
 
1198
 
 
1199
  # Determine whether a backport branch exists
 
1200
  my ($URL) = `$SVN info` =~ /^URL: (.*)$/m;
 
1201
  die "Can't retrieve URL of cwd" unless $URL;
 
1202
 
 
1203
  die unless shell_safe_path_or_url $URL;
 
1204
  system "$SVN info -- $URL-r$revnums[0] 2>/dev/null";
 
1205
  my $branch = ($? == 0) ? basename("$URL-r$revnums[0]") : undef;
 
1206
 
 
1207
  # Construct entry.
 
1208
  my $logmsg = `$SVN propget --revprop -r $revnums[0] --strict svn:log '^/'`;
 
1209
  die "Can't fetch log message of r$revnums[0]: $!" unless $logmsg;
 
1210
 
 
1211
  unless ($logmsg =~ s/^(.*?)\n\n.*/$1/s) {
 
1212
    # "* file\n  (symbol): Log message."
 
1213
 
 
1214
    # Strip before and after the first symbol's log message.
 
1215
    $logmsg =~ s/^.*?: //s;
 
1216
    $logmsg =~ s/^  \x28.*//ms;
 
1217
 
 
1218
    # Undo line wrapping.  (We'll re-do it later.)
 
1219
    $logmsg =~ s/\s*\n\s+/ /g;
 
1220
  }
 
1221
 
 
1222
  my @lines;
 
1223
  warn "Wrapping [$logmsg]\n";
 
1224
  push @lines, wrap " * ", ' 'x3, join ', ', map "r$_", @revnums;
 
1225
  push @lines, wrap ' 'x3, ' 'x3, split /\n/, $logmsg;
 
1226
  push @lines, "   Justification:";
 
1227
  push @lines, wrap ' 'x5, ' 'x5, $justification;
 
1228
  push @lines, "   Branch: $branch" if defined $branch;
 
1229
  push @lines, "   Votes:";
 
1230
  push @lines, "     +1: $AVAILID";
 
1231
  push @lines, "";
 
1232
  my $raw = join "", map "$_\n", @lines;
 
1233
 
 
1234
  # Open the file in line-mode (not paragraph-mode).
 
1235
  my @STATUS;
 
1236
  tie @STATUS, "Tie::File", $STATUS, recsep => "\n";
 
1237
  my ($index) = grep { $STATUS[$_] =~ /^Veto/ } (0..$#STATUS);
 
1238
  die "Couldn't find where to add an entry" unless $index;
 
1239
 
 
1240
  # Add an empty line if needed.
 
1241
  if ($STATUS[$index-1] =~ /\S/) {
 
1242
    splice @STATUS, $index, 0, "";
 
1243
    $index++;
 
1244
  }
 
1245
 
 
1246
  # Add the entry.
 
1247
  splice @STATUS, $index, 0, @lines;
 
1248
 
 
1249
  # Save.
 
1250
  untie @STATUS;
 
1251
 
 
1252
  # Done!
 
1253
  system "$SVN diff -- $STATUS";
 
1254
  if (prompt "Commit this nomination? ") {
 
1255
    system "$SVN commit -m 'Nominate r$revnums[0].' -- $STATUS";
 
1256
    exit $?;
 
1257
  }
 
1258
  elsif (!$had_local_mods or prompt "Revert STATUS (destroying local mods)? ") {
 
1259
    # TODO: we could be smarter and just un-splice the lines we'd added.
 
1260
    system "$SVN revert -- $STATUS";
 
1261
    exit $?;
 
1262
  }
 
1263
 
 
1264
  exit 0;
 
1265
}
 
1266
 
 
1267
# Dispatch to the appropriate main().
 
1268
given (basename($0)) {
 
1269
  when (/^b$|backport/) {
 
1270
    chdir dirname $0 or die "Can't chdir: $!" if /^b$/;
 
1271
    &backport_main(@ARGV);
 
1272
  }
 
1273
  when (/^n$|nominate/) {
 
1274
    chdir dirname $0 or die "Can't chdir: $!" if /^n$/;
 
1275
    &nominate_main(@ARGV);
 
1276
  }
 
1277
  default {
 
1278
    &backport_main(@ARGV);
 
1279
  }
 
1280
}