summaryrefslogtreecommitdiff
path: root/bin/ae2cvs
diff options
context:
space:
mode:
Diffstat (limited to 'bin/ae2cvs')
-rw-r--r--bin/ae2cvs579
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)