#!/usr/bin/perl -w use strict; use Getopt::Long; Getopt::Long::Configure("bundling", "pass_through"); use File::Spec; use File::Temp qw(tempfile tempdir); ################################################################################ # Configure Script # Whether or not to be chatty about what we're doing. # Set this on the command line with --verbose. our $VERBOSE = 1; # A horizontal rule for formatting lines. my $ss = "--------------------------------------------------------------------"; # Whether or not to continue when we encounter a potentially serious problem. # Set this on the command line with --live-dangerously. our $DOUBLEOH7 = 0; # The branches to land on. # Set this on the command line once for each branch with --branch . our @branches; # Convenient shorthand for --branch HEAD and --branch MOZILLA_1_8_BRANCH. # Set these on the command line with --trunk and --moz18. my $TRUNK; my $MOZ18; # The branches to land on if the user doesn't specify a branch. my @DEFAULT_BRANCHES = qw(HEAD MOZILLA_1_8_BRANCH); # The CVS options. Some of these may not make sense in the context # of this script. Use them at your own risk. Note that -f and -r are both # CVS options and CVS commit options (i.e. they can go either before # the command as general CVS options or after the commit command as different # commit-specific options). To avoid ambiguity, you must specify # the CVS options as --cvs-f and --cvs-r. our $CVS_OPTION_allow_root; our $CVS_OPTION_a; our $CVS_OPTION_b; our $CVS_OPTION_T; our $CVS_OPTION_d; our $CVS_OPTION_e; our $CVS_OPTION_f; our $CVS_OPTION_n; our $CVS_OPTION_Q; our $CVS_OPTION_q; our $CVS_OPTION_r; our $CVS_OPTION_s; our $CVS_OPTION_t; our $CVS_OPTION_v; our $CVS_OPTION_w; our $CVS_OPTION_x; our $CVS_OPTION_z; our @CVS_OPTIONS; # The CVS commit options: -l -R -r -F -f and -m. # Some of these may not make sense in the context of this script. # Use them at your own risk. our $CVS_COMMIT_OPTION_l; our $CVS_COMMIT_OPTION_R; our $CVS_COMMIT_OPTION_r; our $CVS_COMMIT_OPTION_F; our $CVS_COMMIT_OPTION_f; our $CVS_COMMIT_OPTION_m; our @CVS_COMMIT_OPTIONS; # Retrieve configuration from a config file, if any. Config files are just # regular Perl files and can override the values of all configuration variables # declared above with "our". my $cfg_file; if (-e ".xcconfig") { $cfg_file = ".xcconfig" } elsif (-e "~/.xcconfig") { $cfg_file = "~/.xcconfig" } if ($cfg_file) { my $return = do $cfg_file; die "couldn't parse $cfg_file: $@" if $@; die "couldn't do $cfg_file: $!" unless defined $return; die "couldn't run $cfg_file" unless $return; } # Parse options from the command line. GetOptions( # Options specific to this script. "verbose" => \$VERBOSE, "trunk" => \$TRUNK, "moz18" => \$MOZ18, "branch=s" => \@branches, "live-dangerously" => \$DOUBLEOH7, # CVS options (those that go between "cvs" and "commit"). "allow-root=s" => \$CVS_OPTION_allow_root, "a" => \$CVS_OPTION_a, "b=s" => \$CVS_OPTION_b, "T=s" => \$CVS_OPTION_T, "d=s" => \$CVS_OPTION_d, "e=s" => \$CVS_OPTION_e, "cvs-f" => \$CVS_OPTION_f, "n" => \$CVS_OPTION_n, "Q" => \$CVS_OPTION_Q, "q" => \$CVS_OPTION_q, "cvs-r" => \$CVS_OPTION_r, "s" => \$CVS_OPTION_s, "t" => \$CVS_OPTION_t, "v|version" => \$CVS_OPTION_v, "w" => \$CVS_OPTION_w, "x" => \$CVS_OPTION_x, "z" => \$CVS_OPTION_z, # CVS commit options (those that go after "commit"). "l" => \$CVS_COMMIT_OPTION_l, "R" => \$CVS_COMMIT_OPTION_R, "r" => \$CVS_COMMIT_OPTION_r, "F=s" => \$CVS_COMMIT_OPTION_F, "f" => \$CVS_COMMIT_OPTION_f, "m=s" => \$CVS_COMMIT_OPTION_m, ); # The rest of the command line should be files or directories to commit. # You can also leave it blank, in which case it'll check the current directory, # just like "cvs commit" does. push(@CVS_OPTIONS, $CVS_OPTION_allow_root ? ("--allow-root", $CVS_OPTION_allow_root) : (), $CVS_OPTION_a ? "-a" : (), $CVS_OPTION_b ? ("-b", $CVS_OPTION_b) : (), $CVS_OPTION_T ? ("-T", $CVS_OPTION_T) : (), $CVS_OPTION_d ? ("-d", $CVS_OPTION_d) : (), $CVS_OPTION_e ? ("-e", $CVS_OPTION_e) : (), $CVS_OPTION_f ? "-f" : (), $CVS_OPTION_n ? "-n" : (), $CVS_OPTION_Q ? "-Q" : (), $CVS_OPTION_q ? "-q" : (), $CVS_OPTION_r ? "-r" : (), $CVS_OPTION_s ? "-s" : (), $CVS_OPTION_t ? "-t" : (), $CVS_OPTION_v ? "-v" : (), $CVS_OPTION_w ? "-w" : (), $CVS_OPTION_x ? "-x" : (), $CVS_OPTION_z ? ("-z", $CVS_OPTION_z) : (), ); push(@CVS_COMMIT_OPTIONS, $CVS_COMMIT_OPTION_l ? "-l" : (), $CVS_COMMIT_OPTION_R ? "-R" : (), $CVS_COMMIT_OPTION_r ? "-r" : (), $CVS_COMMIT_OPTION_F ? ("-F", $CVS_COMMIT_OPTION_F) : (), $CVS_COMMIT_OPTION_f ? "-f" : (), $CVS_COMMIT_OPTION_m ? ("-m", $CVS_COMMIT_OPTION_m) : (), ); ################################################################################ # Initialize # Duplicate the VERBOSE filehandle to STDOUT if we're being verbose; # otherwise point it to /dev/null. my $devnull = File::Spec->devnull(); open(VERBOSE, $VERBOSE ? ">-" : ">$devnull") or warn "Can't output verbose: $!"; ################################################################################ # Get Modified Files and Current Branch my $files = get_modified_files(\@ARGV); if (scalar(keys(%$files)) == 0) { die "*** Didn't find any modified files.\n"; } else { print VERBOSE "*** Modified Files:\n " . join("\n ", sort(keys(%$files))) . "\n"; } my $current_branch = get_current_branch($files); print VERBOSE "*** Working Branch:\n $current_branch\n"; ################################################################################ # Get Branches to Land On # Figure out what branches the user wants to land on. Branches can be specified # via "--branch " or the "--trunk" and "--moz18" shortcuts. If the user # doesn't specify any branches, we land on the trunk and the MOZILLA_1_8_BRANCH. push(@branches, "HEAD") if $TRUNK and !grep($_ eq "HEAD", @branches); push(@branches, "MOZILLA_1_8_BRANCH") if $MOZ18 and !grep($_ eq "MOZILLA_1_8_BRANCH", @branches); push(@branches, @DEFAULT_BRANCHES) if scalar(@branches) == 0; print VERBOSE "*** Committing to Branches:\n " . join("\n ", @branches) . "\n"; ################################################################################ # Check for Problems # Make sure the changes apply cleanly to all branches the user wants # to land them on. my @conflicts; foreach my $branch (@branches) { print VERBOSE "*** Checking for conflicts on $branch... "; foreach my $spec (sort(keys(%$files))) { my ($rv, $output, $errors) = run_cvs("update", [cvs_branch($branch), $spec], 1, 1); if ($rv != 0) { # These are spurious errors that go away once we check in # the removal to the working branch, so we can ignore them. # XXX Can we really? Might they not also occur in other situations # where we shouldn't ignore them? if ($errors =~ m/removed $spec was modified by second party/) { print VERBOSE "(we can safely ignore this conflict)\n"; next; } push(@conflicts, $branch); } } } if (scalar(@conflicts) > 0) { die "Conflicts found on " . join(", ", @conflicts) . ".\n" . "Please resolve them, then try your commit again.\n"; } else { print VERBOSE "No conflicts found; good.\n"; } ################################################################################ # Land on Some Branch # From now on, if we encounter an error, we should try to return the user's tree # to its original state, so override the die handler with a function that tries # to CVS update the tree back to the original working branch. local $SIG{__DIE__} = sub { my ($message) = @_; print $message; print VERBOSE "*** Returning your tree to its original working branch... "; run_cvs("update", [cvs_branch($current_branch), keys(%$files)]); die; }; # We gotta land somewhere once and then merge those changes into other branches. my $land_branch; if (grep($_ eq $current_branch, @branches)) { # The changes are landing on the current branch. Groovy, let's land # there first. It matters for additions and removals, I think. $land_branch = $current_branch; } else { # Just land on the first branch in the list. $land_branch = $branches[0]; print VERBOSE "*** Switching to $land_branch... "; run_cvs("update", [cvs_branch($land_branch), keys(%$files)]); } print VERBOSE "*** Committing to $land_branch... "; my ($rv, $output, $errors) = run_cvs("commit", [@CVS_COMMIT_OPTIONS, keys(%$files)]); ################################################################################ # Extract Commit Info print VERBOSE "*** Extracting commit info.\n"; my @lines = (split/\n/, $output); for ( my $i = 0 ; $i < scalar(@lines); $i++ ) { if ($lines[$i] =~ m/^(?:Checking in|Removing) (.*);$/) { my $spec = $1; print VERBOSE " $spec: "; my $entry = $files->{$spec}; $entry or die " not on the list of files committed!\n"; $i += 2; $lines[$i] =~ m/^(initial|new)\srevision:\s ([\d\.]+|delete)(?:;\s previous\srevision:\s ([\d\.]+))?$/x; if ($1 eq "new") { print VERBOSE "$3 -> $2.\n"; $entry->{new_rev} = $2 eq "delete" ? "" : $2; $entry->{old_rev} = $3; } elsif ($1 eq "initial") { print VERBOSE "new file -> $2.\n"; $entry->{new_rev} = $2; $entry->{old_rev} = ""; } else { die "can't figure out its old and new revisions!\n"; } } } ################################################################################ # Check In to Other Branches foreach my $branch (@branches) { next if $branch eq $land_branch; foreach my $spec (sort(keys(%$files))) { my $entry = $files->{$spec}; if ($entry->{old_rev} && $entry->{new_rev}) { print VERBOSE "*** Merging $spec changes from $entry->{old_rev} " . "to $entry->{new_rev} into $branch... "; run_cvs("update", [cvs_branch($branch), "-j", $entry->{old_rev}, "-j", $entry->{new_rev}, $spec]); } elsif ($entry->{old_rev}) { print VERBOSE "*** Removing $spec on $branch... "; # CVS doesn't tag removed files with a new revision number, # so we merge from the old revision to the branch itself. run_cvs("update", [cvs_branch($branch), "-j", $entry->{old_rev}, "-j", $land_branch, $spec]); } elsif ($entry->{new_rev}) { print VERBOSE "*** Adding $spec on $branch... "; run_cvs("update", [cvs_branch($branch), "-j", $entry->{new_rev}, $spec]); } print VERBOSE "*** Committing $spec on $branch... "; run_cvs("commit", [@CVS_COMMIT_OPTIONS, $spec]); } } print VERBOSE "*** Returning your tree to its original working branch... "; run_cvs("update", [cvs_branch($current_branch), keys(%$files)]); ################################################################################ # Utility Functions # Returns a hash of modified files indexed by file spec. sub get_modified_files { my ($args) = @_; # We figure out which files are modified by running "cvs update" # and grepping for /^(M|A) /. We run the command in dry run mode so we # don't actually update the files in the process. # XXX perhaps we should update them, since we won't be able to commit them # if they aren't up-to-date; on the other hand, CVS makes you update them # manually rather than automatically upon commit, so perhaps there's method # to its madness. print VERBOSE "*** Looking for modified files... "; my ($rv, $output, $errors) = run_cvs("update", $args, 1); # Break the output into lines and filter for lines about changes. my @lines = grep(m/^(M|A|R) /, split(/\n/, $output)); my %files; foreach my $line (@lines) { $line =~ m/^(M|A|R) (.*)/; $files{$2} = get_cvs_file($2); $files{$2}->{change_type} = $1; } return \%files; } # Given a file spec, returns a hash of information about the file extracted # from the CVS/Entries file. sub get_cvs_file { my ($spec) = @_; my ($volume, $directories, $filename) = File::Spec->splitpath($spec); my $cvsdir = $directories ? File::Spec->catdir($directories, "CVS") : "CVS"; my $files = File::Spec->catpath($volume, $cvsdir, "Entries"); open(ENTRIES, "<", $files) or die "Can't read entries file $files for file $spec: $!"; while () { my ($name, $revision, $timestamp, $conflict, $options, $tagdate) = ($_ =~ m|/([^/]*) # filename /([^/]*) # revision /([^/+]*) # timestamp (\+[^/]*)? # (optional) conflict /([^/]*) # options /([^/]*) # tag/date |x); next if $name ne $filename; close ENTRIES; return { name => $name, revision => $revision, conflict => $conflict, options => $options, tagdate => $tagdate }; } die "Couldn't find entry for file $spec in entries file $files."; } # Given a set of files, extracts their current working branch, testing for # multiple branches and date-based tags in the process. sub get_current_branch { my ($files) = @_; my %branches; foreach my $filename (keys %$files) { my $entry = $files->{$filename}; $entry->{tagdate} =~ m/^(T|D)?(.*)/; if ($1 and $1 eq "D") { warn "$filename checked out by date $1\n" } elsif ($2 eq "") { $branches{"HEAD"}++ } else { $branches{$2}++ } if (scalar(keys(%branches)) > 1 && !$DOUBLEOH7) { die("Modified files checked out from multiple branches:\n " . join("\n ", map("$_: $files->{$_}->{tagdate}", sort(keys(%$files)))) . "Sounds scary, so I'm stopping. Want me to continue?\n" . "Run me again with --live-dangerously and tell my authors\n" . "how it went.\n"); } } return (keys(%branches))[0]; } # Runs a CVS command and outputs the results. Runs the command in dry run mode # if dry run is enabled globally ($DRY_RUN) or for this specific function call; # and dies on error by default, but can be set to merely warn on error. # Returns the return value of the CVS command, its output, and its errors. sub run_cvs { my ($cmd, $args, $dry_run, $warn_on_err) = @_; # Let callers override dry run setting, since certain information gathering # routines always run in dry run mode no matter what the global setting is. my ($rv, $output, $errors) = system_capture("cvs", @CVS_OPTIONS, $dry_run && !$CVS_OPTION_n ? "-n" : (), $cmd, @$args); if ($rv != 0) { if (!$warn_on_err) { die "\n$errors\n$ss\n"; } warn "\n$errors\n$ss\n" } else { print VERBOSE "\n$output\n$ss\n"; } return ($rv, $output, $errors); } # Returns the appropriate CVS command line argument for specifying a branch. # Usually this is -r , but if we're dealing with the special HEAD # branch it's -A instead. sub cvs_branch { my ($branch) = @_; return $branch eq "HEAD" ? "-A" : ("-r", $branch); } # Runs a command and captures its output and errors. # Returns the command's exit code, output, and errors. sub system_capture { # XXX This should be using in-memory files, but they require that we close # STDOUT and STDERR before reopening them on the in-memory files, and doing # that on STDERR causes CVS to choke with return value 256. my ($command, @args) = @_; my ($rv, $output, $errors); # Back up the original STDOUT and STDERR so we can restore them later. open(OLDOUT, ">&STDOUT") or die "Can't back up STDOUT to OLDOUT: $!"; open(OLDERR, ">&STDERR") or die "Can't back up STDERR to OLDERR: $!"; use vars qw( *OLDOUT *OLDERR ); # suppress "used only once" warnings # Close and reopen STDOUT and STDERR to in-memory files, which are just # scalars that take output and append it to their value. # XXX Disabled in-memory files in favor of temp files until in-memory issues # can be worked out. #close(STDOUT); #close(STDERR); #open(STDOUT, ">", \$output) or die "Can't open STDOUT to output var: $!"; #open(STDERR, ">", \$errors) or die "Can't open STDERR to errors var: $!"; my $outfile = tempfile(); my $errfile = tempfile(); # Perl 5.6.1 filehandle duplication doesn't support the three-argument form # of open, so we can't just open(STDOUT, ">&", $outfile); instead we have to # create an alias OUTFILE and then do open(STDOUT, ">&OUTFILE"). local *OUTFILE = *$outfile; local *ERRFILE = *$errfile; use vars qw( *OUTFILE *ERRFILE ); # suppress "used only once" warnings open(STDOUT, ">&OUTFILE") or open(STDOUT, ">&OLDOUT") and die "Can't dupe STDOUT to output file: $!"; open(STDERR, ">&ERRFILE") or open(STDOUT, ">&OLDOUT") and open(STDERR, ">&OLDERR") and die "Can't dupe STDERR to errors file: $!"; # Run the command. print VERBOSE "$command " . join(" ", @args) . "\n"; $rv = system($command, @args); # Grab output and errors from the temp files. In a block to localize $/. # XXX None of this would be necessary if in-memory files was working. { local $/ = undef; seek($outfile, 0, 0); seek($errfile, 0, 0); $output = <$outfile>; $errors = <$errfile>; } # Restore original STDOUT and STDERR. close(STDOUT); close(STDERR); open(STDOUT, ">&OLDOUT") or die "Can't restore STDOUT from OLDOUT: $!"; open(STDERR, ">&OLDERR") or die "Can't restore STDERR from OLDERR: $!"; return ($rv, $output, $errors); }