pjs/tools/cross-commit

505 строки
19 KiB
Perl
Executable File

#!/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 <name>.
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 <file> -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 <name>" 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 (<ENTRIES>) {
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 <branch name>, 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);
}