diff options
Diffstat (limited to 'bin/ae2cvs')
-rw-r--r-- | bin/ae2cvs | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/bin/ae2cvs b/bin/ae2cvs new file mode 100644 index 0000000..e7cb22b --- /dev/null +++ b/bin/ae2cvs @@ -0,0 +1,579 @@ +#! /usr/bin/env perl + +$revision = "src/ae2cvs.pl 0.04.D001 2005/08/14 15:13:36 knight"; + +$copyright = "Copyright 2001, 2002, 2003, 2004, 2005 Steven Knight."; + +# +# All rights reserved. This program is free software; you can +# redistribute and/or modify under the same terms as Perl itself. +# + +use strict; +use File::Find; +use File::Spec; +use Pod::Usage (); + +use vars qw( @add_list @args @cleanup @copy_list @libraries + @mkdir_list @remove_list + %seen_dir + $ae_copy $aedir $aedist + $cnum $comment $commit $common $copyright + $cvs_command $cvsmod $cvsroot + $delta $description $exec $help $indent $infile + $proj $pwd $quiet $revision + $summary $usedir $usepath ); + +$aedist = 1; +$cvsroot = undef; +$exec = undef; +$indent = ""; + +sub version { + print "ae2cvs: $revision\n"; + print "$copyright\n"; + exit 0; +} + +{ + use Getopt::Long; + + Getopt::Long::Configure('no_ignore_case'); + + my $ret = GetOptions ( + "aedist" => sub { $aedist = 1 }, + "aegis" => sub { $aedist = 0 }, + "change=i" => \$cnum, + "d=s" => \$cvsroot, + "file=s" => \$infile, + "help|?" => \$help, + "library=s" => \@libraries, + "module=s" => \$cvsmod, + "noexecute" => sub { $exec = 0 }, + "project=s" => \$proj, + "quiet" => \$quiet, + "usedir=s" => \$usedir, + "v|version" => \&version, + "x|execute" => sub { $exec++ if ! defined $exec || $exec != 0 }, + "X|EXECUTE" => sub { $exec = 2 if ! defined $exec || $exec != 0 }, + ); + + Pod::Usage::pod2usage(-verbose => 0) if $help || ! $ret; + + $exec = 0 if ! defined $exec; +} + +$cvs_command = $cvsroot ? "cvs -d $cvsroot -Q" : "cvs -Q"; + +# +# Wrap up the $quiet logic in one place. +# +sub printit { + return if $quiet; + my $string = join('', @_); + $string =~ s/^/$indent/msg if $indent; + print $string; +} + +# +# Wrappers for executing various builtin Perl functions in +# accordance with the -n, -q and -x options. +# +sub execute { + my $cmd = shift; + printit "$cmd\n"; + if (! $exec) { + return 1; + } + ! system($cmd); +} + +sub _copy { + my ($source, $dest) = @_; + printit "cp $source $dest\n"; + if ($exec) { + use File::Copy; + copy($source, $dest); + } +} + +sub _chdir { + my $dir = shift; + printit "cd $dir\n"; + if ($exec) { + chdir($dir) || die "ae2cvs: could not chdir($dir): $!"; + } +} + +sub _mkdir { + my $dir = shift; + printit "mkdir $dir\n"; + if ($exec) { + mkdir($dir); + } +} + +# +# Put some input data through an external filter and capture the output. +# +sub filter { + my ($cmd, $input) = @_; + + use FileHandle; + use IPC::Open2; + + my $pid = open2(*READ, *WRITE, $cmd) || die "Cannot exec '$cmd': $!\n"; + print WRITE $input; + close(WRITE); + my $output = join('', <READ>); + close(READ); + return $output; +} + +# +# Parse a change description, in both 'aegis -l cd" and "aedist" formats. +# +# Returns an array containing the project name, the change number +# (if any), the delta number (if any), the SUMMARY, the DESCRIPTION +# and the lines describing the files in the change. +# +sub parse_change { + my $output = shift; + + my ($p, $c, $d, $c_or_d, $sum, $desc, $filesection, @flines); + + # The project name line comes after NAME in "aegis -l cd" format, + # and PROJECT in "aedist" format. In both cases, the project name + # and the change/delta name are separated a comma. + ($p = $output) =~ s/(?:NAME|PROJECT)\n([^\n]*)\n.*/$1/ms; + ($p, $c_or_d) = (split(/,/, $p)); + + # In "aegis -l cd" format, the project name actually comes after + # the string "Project" and is itself enclosed in double quotes. + $p =~ s/Project "([^"]*)"/$1/; + + # The change or delta string was the right-hand side of the comma. + # "aegis -l cd" format spells it "Change 123." or "Delta 123." while + # "aedist" format spells it "change 123." + if ($c_or_d =~ /\s*[Cc]hange (\d+).*/) { $c = $1 }; + if ($c_or_d =~ /\s*[Dd]elta (\d+).*/) { $d = $1 }; + + # The SUMMARY line is always followed the DESCRIPTION section. + # It seems to always be a single line, but we grab everything in + # between just in case. + ($sum = $output) =~ s/.*\nSUMMARY\n//ms; + $sum =~ s/\nDESCRIPTION\n.*//ms; + + # The DESCRIPTION section is followed ARCHITECTURE in "aegis -l cd" + # format and by CAUSE in "aedist" format. Explicitly under it if the + # string is only "none," which means they didn't supply a description. + ($desc = $output) =~ s/.*\nDESCRIPTION\n//ms; + $desc =~ s/\n(ARCHITECTURE|CAUSE)\n.*//ms; + chomp($desc); + if ($desc eq "none" || $desc eq "none\n") { $desc = undef } + + # The FILES section is followed by HISTORY in "aegis -l cd" format. + # It seems to be the last section in "aedist" format, but stripping + # a non-existent HISTORY section doesn't hurt. + ($filesection = $output) =~ s/.*\nFILES\n//ms; + $filesection =~ s/\nHISTORY\n.*//ms; + + @flines = split(/\n/, $filesection); + + ($p, $c, $d, $sum, $desc, \@flines) +} + +# +# +# +$pwd = Cwd::cwd(); + +# +# Fetch the file list either from our aedist input +# or directly from the project itself. +# +my @filelines; +if ($aedist) { + local ($/); + undef $/; + my $infile_redir = ""; + my $contents; + if (! $infile || $infile eq "-") { + $contents = join('', <STDIN>); + } else { + open(FILE, "<$infile") || die "Cannot open '$infile': $!\n"; + binmode(FILE); + $contents = join('', <FILE>); + close(FILE); + if (! File::Spec->file_name_is_absolute($infile)) { + $infile = File::Spec->catfile($pwd, $infile); + } + $infile_redir = " < $infile"; + } + + my $output = filter("aedist -l -unf", $contents); + my ($p, $c, $d, $s, $desc, $fl) = parse_change($output); + + $proj = $p if ! defined $proj; + $summary = $s; + $description = $desc; + @filelines = @$fl; + + if (! $exec) { + printit qq(MYTMP="/tmp/ae2cvs-ae.\$\$"\n), + qq(mkdir \$MYTMP\n), + qq(cd \$MYTMP\n); + printit q(perl -MMIME::Base64 -e 'undef $/; ($c = <>) =~ s/.*\n\n//ms; print decode_base64($c)'), + $infile_redir, + qq( | zcat), + qq( | cpio -i -d --quiet\n); + $aedir = '$MYTMP'; + push(@cleanup, $aedir); + } else { + $aedir = File::Spec->catfile(File::Spec->tmpdir, "ae2cvs-ae.$$"); + _mkdir($aedir); + push(@cleanup, $aedir); + _chdir($aedir); + + use MIME::Base64; + + $contents =~ s/.*\n\n//ms; + $contents = filter("zcat", decode_base64($contents)); + + open(CPIO, "|cpio -i -d --quiet"); + print CPIO $contents; + close(CPIO); + } + + $ae_copy = sub { + foreach my $dest (@_) { + my $source = File::Spec->catfile($aedir, "src", $dest); + execute(qq(cp $source $dest)); + } + } +} else { + $cnum = $ENV{AEGIS_CHANGE} if ! defined $cnum; + $proj = $ENV{AEGIS_PROJECT} if ! defined $proj; + + $common = "-lib " . join(" -lib ", @libraries) if @libraries; + $common = "$common -proj $proj" if $proj; + + my $output = `aegis -l cd $cnum -unf $common`; + my ($p, $c, $d, $s, $desc, $fl) = parse_change($output); + + $delta = $d; + $summary = $s; + $description = $desc; + @filelines = @$fl; + + if (! $delta) { + print STDERR "ae2cvs: No delta number, exiting.\n"; + exit 1; + } + + $ae_copy = sub { + execute(qq(aegis -cp -ind -delta $delta $common @_)); + } +} + +if (! $usedir) { + $usedir = File::Spec->catfile(File::Spec->tmpdir, "ae2cvs.$$"); + _mkdir($usedir); + push(@cleanup, $usedir); +} + +_chdir($usedir); + +$usepath = $usedir; +if (! File::Spec->file_name_is_absolute($usepath)) { + $usepath = File::Spec->catfile($pwd, $usepath); +} + +if (! -d File::Spec->catfile($usedir, "CVS")) { + $cvsmod = (split(/\./, $proj))[0] if ! defined $cvsmod; + + execute(qq($cvs_command co $cvsmod)); + + _chdir($cvsmod); + + $usepath = File::Spec->catfile($usepath, $cvsmod); +} + +# +# Figure out what we have to do to accomplish everything. +# +foreach (@filelines) { + my @arr = split(/\s+/, $_); + my $type = shift @arr; # source / test + my $act = shift @arr; # modify / create + my $file = pop @arr; + + if ($act eq "create" or $act eq "modify") { + # XXX Do we really only need to do this for + # ($act eq "create") files? + my (undef, $dirs, undef) = File::Spec->splitpath($file); + my $absdir = $usepath; + my $reldir; + my $d; + foreach $d (File::Spec->splitdir($dirs)) { + next if ! $d; + $absdir = File::Spec->catdir($absdir, $d); + $reldir = $reldir ? File::Spec->catdir($reldir, $d) : $d; + if (! -d $absdir && ! $seen_dir{$reldir}) { + $seen_dir{$reldir} = 1; + push(@mkdir_list, $reldir); + } + } + + push(@copy_list, $file); + + if ($act eq "create") { + push(@add_list, $file); + } + } elsif ($act eq "remove") { + push(@remove_list, $file); + } else { + print STDERR "Unsure how to '$act' the '$file' file.\n"; + } +} + +# Now go through and mkdir() the directories, +# adding them to the CVS tree as we do. +if (@mkdir_list) { + if (! $exec) { + printit qq(# The following "mkdir" and "cvs -Q add" calls are not\n), + qq(# necessary for any directories that already exist in the\n), + qq(# CVS tree but which aren't present locally.\n); + } + foreach (@mkdir_list) { + if (! $exec) { + printit qq(if test ! -d $_; then\n); + $indent = " "; + } + _mkdir($_); + execute(qq($cvs_command add $_)); + if (! $exec) { + $indent = ""; + printit qq(fi\n); + } + } + if (! $exec) { + printit qq(# End of directory creation.\n); + } +} + +# Copy in any files in the change, before we try to "cvs add" them. +$ae_copy->(@copy_list) if @copy_list; + +if (@add_list) { + execute(qq($cvs_command add @add_list)); +} + +if (@remove_list) { + execute(qq(rm -f @remove_list)); + execute(qq($cvs_command remove @remove_list)); +} + +# Last, commit the whole bunch. +$comment = $summary; +$comment .= "\n" . $description if $description; +$commit = qq($cvs_command commit -m '$comment' .); +if ($exec == 1) { + printit qq(# Execute the following to commit the changes:\n), + qq(# $commit\n); +} else { + execute($commit); +} + +_chdir($pwd); + +# +# Directory cleanup. +# +sub END { + my $dir; + foreach $dir (@cleanup) { + printit "rm -rf $dir\n"; + if ($exec) { + finddepth(sub { + # print STDERR "unlink($_)\n" if (!-d $_); + # print STDERR "rmdir($_)\n" if (-d $_ && $_ ne "."); + unlink($_) if (!-d $_); + rmdir($_) if (-d $_ && $_ ne "."); + 1; + }, $dir); + rmdir($dir) || print STDERR "Could not remove $dir: $!\n"; + } + } +} + +__END__; + +=head1 NAME + +ae2cvs - convert an Aegis change set to CVS commands + +=head1 SYNOPSIS + +ae2cvs [-aedist|-aegis] [-c change] [-d cvs_root] [-f file] [-l lib] + [-m module] [-n] [-p proj] [-q] [-u dir] [-v] [-x] [-X] + + -aedist use aedist format from input (default) + -aegis query aegis repository directly + -c change change number + -d cvs_root CVS root directory + -f file read aedist from file ('-' == stdin) + -l lib Aegis library directory + -m module CVS module + -n no execute + -p proj project name + -q quiet, don't print commands + -u dir use dir for CVS checkin + -v print version string and exit + -x execute the commands, but don't commit; + two or more -x options commit changes + -X execute the commands and commit changes + +=head1 DESCRIPTION + +The C<ae2cvs> utility can convert an Aegis change into a set of CVS (and +other) commands to make the corresponding change(s) to a carbon-copy CVS +repository. This can be used to keep a front-end CVS repository in sync +with changes made to an Aegis project, either manually or automatically +using the C<integrate_pass_notify_command> attribute of the Aegis +project. + +By default, C<ae2cvs> makes no changes to any software, and only prints +out the necessary commands. These commands can be examined first for +safety, and then fed to any Bourne shell variant (sh, ksh, or bash) to +make the actual CVS changes. + +An option exists to have C<ae2cvs> execute the commands directly. + +=head1 OPTIONS + +The C<ae2cvs> utility supports the following options: + +=over 4 + +=item -aedist + +Reads an aedist change set. +By default, the change set is read from standard input, +or a file specified with the C<-f> option. + +=item -aegis + +Reads the change directly from the Aegis repository +by executing the proper C<aegis> commands. + +=item -c change + +Specify the Aegis change number to be used. +The value of the C<AEGIS_CHANGE> environment variable +is used by default. + +=item -d cvsroot + +Specify the CVS root directory to be used. +This option is passed explicitly to each executed C<cvs> command. +The default behavior is to omit any C<-d> options +and let the executed C<cvs> commands use the +C<CVSROOT> environment variable as they normally would. + +=item -f file + +Reads the aedist change set from the specified C<file>, +or from standard input if C<file> is C<'-'>. + +=item -l lib + +Specifies an Aegis library directory to be searched for global states +files and user state files. + +=item -m module + +Specifies the name of the CVS module to be brought up-to-date. +The default is to use the Aegis project name, +minus any branch numbers; +for example, given an Aegis project name of C<foo-cmd.0.1>, +the default CVS module name is C<foo-cmd>. + +=item -n + +No execute. Commands are printed (including a command for a final +commit of changes), but not executed. This is the default. + +=item -p proj + +Specifies the name of the Aegis project from which this change is taken. +The value of the C<AEGIS_PROJECT> environment variable +is used by default. + +=item -q + +Quiet. Commands are not printed. + +=item -u dir + +Use the already checked-out CVS tree that exists at C<dir> +for the checkins and commits. +The default is to use a separately-created temporary directory. + +=item -v + +Print the version string and exit. + +=item -x + +Execute the commands to bring the CVS repository up to date, +except for the final commit of the changes. Two or more +C<-x> options will cause the change to be committed. + +=item -X + +Execute the commands to bring the CVS repository up to date, +including the final commit of the changes. + +=back + +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item AE2CVS_FLAGS + +Specifies any options to be used to initialize +the C<ae2cvs> utility. +Options on the command line override these values. + +=back + +=head1 AUTHOR + +Steven Knight (knight at baldmt dot com) + +=head1 BUGS + +If errors occur during the execution of the Aegis or CVS commands, and +the -X option is used, a partial change (consisting of those files for +which the command(s) succeeded) will be committed. It would be safer to +generate code to detect the error and print a warning. + +When a file has been deleted in Aegis, the standard whiteout file can +cause a regex failure in this script. It doesn't necessarily happen all +the time, though, so this needs more investigation. + +=head1 TODO + +Add an explicit test for using ae2cvs in the Aegis +integrate_pass_notify_command field to support fully keeping a +repository in sync automatically. + +=head1 COPYRIGHT + +Copyright 2001, 2002, 2003, 2004, 2005 Steven Knight. + +=head1 SEE ALSO + +aegis(1), cvs(1) |