Refactor the XMl processing of various entity types to make the code
[scons.git] / bin / ae2cvs
1 #! /usr/bin/env perl
2
3 $revision = "src/ae2cvs.pl 0.04.D001 2005/08/14 15:13:36 knight";
4
5 $copyright = "Copyright 2001, 2002, 2003, 2004, 2005 Steven Knight.";
6
7 #
8 # All rights reserved.  This program is free software; you can
9 # redistribute and/or modify under the same terms as Perl itself.
10 #
11
12 use strict;
13 use File::Find;
14 use File::Spec;
15 use Pod::Usage ();
16
17 use vars qw( @add_list @args @cleanup @copy_list @libraries
18              @mkdir_list @remove_list
19              %seen_dir
20              $ae_copy $aedir $aedist
21              $cnum $comment $commit $common $copyright
22              $cvs_command $cvsmod $cvsroot
23              $delta $description $exec $help $indent $infile
24              $proj $pwd $quiet $revision
25              $summary $usedir $usepath );
26
27 $aedist = 1;
28 $cvsroot = undef;
29 $exec = undef;
30 $indent = "";
31
32 sub version {
33         print "ae2cvs: $revision\n";
34         print "$copyright\n";
35         exit 0;
36 }
37
38 {
39     use Getopt::Long;
40
41     Getopt::Long::Configure('no_ignore_case');
42
43     my $ret = GetOptions (
44         "aedist" => sub { $aedist = 1 },
45         "aegis" => sub { $aedist = 0 },
46         "change=i" => \$cnum,
47         "d=s" => \$cvsroot,
48         "file=s" => \$infile,
49         "help|?" => \$help,
50         "library=s" => \@libraries,
51         "module=s" => \$cvsmod,
52         "noexecute" => sub { $exec = 0 },
53         "project=s" => \$proj,
54         "quiet" => \$quiet,
55         "usedir=s" => \$usedir,
56         "v|version" => \&version,
57         "x|execute" => sub { $exec++ if ! defined $exec || $exec != 0 },
58         "X|EXECUTE" => sub { $exec = 2 if ! defined $exec || $exec != 0 },
59     );
60
61     Pod::Usage::pod2usage(-verbose => 0) if $help || ! $ret;
62
63     $exec = 0 if ! defined $exec;
64 }
65
66 $cvs_command = $cvsroot ? "cvs -d $cvsroot -Q" : "cvs -Q";
67
68 #
69 # Wrap up the $quiet logic in one place.
70 #
71 sub printit {
72     return if $quiet;
73     my $string = join('', @_);
74     $string =~ s/^/$indent/msg if $indent;
75     print $string;
76 }
77
78 #
79 # Wrappers for executing various builtin Perl functions in
80 # accordance with the -n, -q and -x options.
81 #
82 sub execute {
83     my $cmd = shift;
84     printit "$cmd\n";
85     if (! $exec) {
86         return 1;
87     }
88     ! system($cmd);
89 }
90
91 sub _copy {
92     my ($source, $dest) = @_;
93     printit "cp $source $dest\n";
94     if ($exec) {
95         use File::Copy;
96         copy($source, $dest);
97     }
98 }
99
100 sub _chdir {
101     my $dir = shift;
102     printit "cd $dir\n";
103     if ($exec) {
104         chdir($dir) || die "ae2cvs:  could not chdir($dir): $!";
105     }
106 }
107
108 sub _mkdir {
109     my $dir = shift;
110     printit "mkdir $dir\n";
111     if ($exec) {
112         mkdir($dir);
113     }
114 }
115
116 #
117 # Put some input data through an external filter and capture the output.
118 #
119 sub filter {
120     my ($cmd, $input) = @_;
121
122     use FileHandle;
123     use IPC::Open2;
124
125     my $pid = open2(*READ, *WRITE, $cmd) || die "Cannot exec '$cmd':  $!\n";
126     print WRITE $input;
127     close(WRITE);
128     my $output = join('', <READ>);
129     close(READ);
130     return $output;
131 }
132
133 #
134 # Parse a change description, in both 'aegis -l cd" and "aedist" formats.
135 #
136 # Returns an array containing the project name, the change number
137 # (if any), the delta number (if any), the SUMMARY, the DESCRIPTION
138 # and the lines describing the files in the change.
139 #
140 sub parse_change {
141     my $output = shift;
142
143     my ($p, $c, $d, $c_or_d, $sum, $desc, $filesection, @flines);
144
145     # The project name line comes after NAME in "aegis -l cd" format,
146     # and PROJECT in "aedist" format.  In both cases, the project name
147     # and the change/delta name are separated a comma.
148     ($p = $output) =~ s/(?:NAME|PROJECT)\n([^\n]*)\n.*/$1/ms;
149     ($p, $c_or_d) = (split(/,/, $p));
150
151     # In "aegis -l cd" format, the project name actually comes after
152     # the string "Project" and is itself enclosed in double quotes.
153     $p =~ s/Project "([^"]*)"/$1/;
154
155     # The change or delta string was the right-hand side of the comma.
156     # "aegis -l cd" format spells it "Change 123." or "Delta 123." while
157     # "aedist" format spells it "change 123."
158     if ($c_or_d =~ /\s*[Cc]hange (\d+).*/) { $c = $1 };
159     if ($c_or_d =~ /\s*[Dd]elta (\d+).*/) { $d = $1 };
160
161     # The SUMMARY line is always followed the DESCRIPTION section.
162     # It seems to always be a single line, but we grab everything in
163     # between just in case.
164     ($sum = $output) =~ s/.*\nSUMMARY\n//ms;
165     $sum =~ s/\nDESCRIPTION\n.*//ms;
166
167     # The DESCRIPTION section is followed ARCHITECTURE in "aegis -l cd"
168     # format and by CAUSE in "aedist" format.  Explicitly under it if the
169     # string is only "none," which means they didn't supply a description.
170     ($desc = $output) =~ s/.*\nDESCRIPTION\n//ms;
171     $desc =~ s/\n(ARCHITECTURE|CAUSE)\n.*//ms;
172     chomp($desc);
173     if ($desc eq "none" || $desc eq "none\n") { $desc = undef }
174
175     # The FILES section is followed by HISTORY in "aegis -l cd" format.
176     # It seems to be the last section in "aedist" format, but stripping
177     # a non-existent HISTORY section doesn't hurt.
178     ($filesection = $output) =~ s/.*\nFILES\n//ms;
179     $filesection =~ s/\nHISTORY\n.*//ms;
180
181     @flines = split(/\n/, $filesection);
182
183     ($p, $c, $d, $sum, $desc, \@flines)
184 }
185
186 #
187 #
188 #
189 $pwd = Cwd::cwd();
190
191 #
192 # Fetch the file list either from our aedist input
193 # or directly from the project itself.
194 #
195 my @filelines;
196 if ($aedist) {
197     local ($/);
198     undef $/;
199     my $infile_redir = "";
200     my $contents;
201     if (! $infile || $infile eq "-") {
202         $contents = join('', <STDIN>);
203     } else {
204         open(FILE, "<$infile") || die "Cannot open '$infile': $!\n";
205         binmode(FILE);
206         $contents = join('', <FILE>);
207         close(FILE);
208         if (! File::Spec->file_name_is_absolute($infile)) {
209             $infile = File::Spec->catfile($pwd, $infile);
210         }
211         $infile_redir = " < $infile";
212     }
213
214     my $output = filter("aedist -l -unf", $contents);
215     my ($p, $c, $d, $s, $desc, $fl) = parse_change($output);
216
217     $proj = $p if ! defined $proj;
218     $summary = $s;
219     $description = $desc;
220     @filelines = @$fl;
221
222     if (! $exec) {
223         printit qq(MYTMP="/tmp/ae2cvs-ae.\$\$"\n),
224                 qq(mkdir \$MYTMP\n),
225                 qq(cd \$MYTMP\n);
226         printit q(perl -MMIME::Base64 -e 'undef $/; ($c = <>) =~ s/.*\n\n//ms; print decode_base64($c)'),
227                 $infile_redir,
228                 qq( | zcat),
229                 qq( | cpio -i -d --quiet\n);
230         $aedir = '$MYTMP';
231         push(@cleanup, $aedir);
232     } else {
233         $aedir = File::Spec->catfile(File::Spec->tmpdir, "ae2cvs-ae.$$");
234         _mkdir($aedir);
235         push(@cleanup, $aedir);
236         _chdir($aedir);
237
238         use MIME::Base64;
239
240         $contents =~ s/.*\n\n//ms;
241         $contents = filter("zcat", decode_base64($contents));
242
243         open(CPIO, "|cpio -i -d --quiet");
244         print CPIO $contents;
245         close(CPIO);
246     }
247
248     $ae_copy = sub {
249         foreach my $dest (@_) {
250             my $source = File::Spec->catfile($aedir, "src", $dest);
251             execute(qq(cp $source $dest));
252         }
253     }
254 } else {
255     $cnum = $ENV{AEGIS_CHANGE} if ! defined $cnum;
256     $proj = $ENV{AEGIS_PROJECT} if ! defined $proj;
257
258     $common = "-lib " . join(" -lib ", @libraries) if @libraries;
259     $common = "$common -proj $proj" if $proj;
260
261     my $output = `aegis -l cd $cnum -unf $common`;
262     my ($p, $c, $d, $s, $desc, $fl) = parse_change($output);
263
264     $delta = $d;
265     $summary = $s;
266     $description = $desc;
267     @filelines = @$fl;
268
269     if (! $delta) {
270         print STDERR "ae2cvs:  No delta number, exiting.\n";
271         exit 1;
272     }
273
274     $ae_copy = sub {
275         execute(qq(aegis -cp -ind -delta $delta $common @_));
276     }
277 }
278
279 if (! $usedir) {
280     $usedir = File::Spec->catfile(File::Spec->tmpdir, "ae2cvs.$$");
281     _mkdir($usedir);
282     push(@cleanup, $usedir);
283 }
284
285 _chdir($usedir);
286
287 $usepath = $usedir;
288 if (! File::Spec->file_name_is_absolute($usepath)) {
289     $usepath = File::Spec->catfile($pwd, $usepath);
290 }
291
292 if (! -d File::Spec->catfile($usedir, "CVS")) {
293     $cvsmod = (split(/\./, $proj))[0] if ! defined $cvsmod;
294
295     execute(qq($cvs_command co $cvsmod));
296
297     _chdir($cvsmod);
298
299     $usepath = File::Spec->catfile($usepath, $cvsmod);
300 }
301
302 #
303 # Figure out what we have to do to accomplish everything.
304 #
305 foreach (@filelines) {
306     my @arr = split(/\s+/, $_);
307     my $type = shift @arr;      # source / test
308     my $act = shift @arr;       # modify / create
309     my $file = pop @arr;
310
311     if ($act eq "create" or $act eq "modify") {
312         # XXX Do we really only need to do this for
313         #     ($act eq "create") files?
314         my (undef, $dirs, undef) = File::Spec->splitpath($file);
315         my $absdir = $usepath;
316         my $reldir;
317         my $d;
318         foreach $d (File::Spec->splitdir($dirs)) {
319             next if ! $d;
320             $absdir = File::Spec->catdir($absdir, $d);
321             $reldir = $reldir ? File::Spec->catdir($reldir, $d) : $d;
322             if (! -d $absdir && ! $seen_dir{$reldir}) {
323                 $seen_dir{$reldir} = 1;
324                 push(@mkdir_list, $reldir);
325             }
326         }
327
328         push(@copy_list, $file);
329
330         if ($act eq "create") {
331             push(@add_list, $file);
332         }
333     } elsif ($act eq "remove") {
334         push(@remove_list, $file);
335     } else {
336         print STDERR "Unsure how to '$act' the '$file' file.\n";
337     }
338 }
339
340 # Now go through and mkdir() the directories,
341 # adding them to the CVS tree as we do.
342 if (@mkdir_list) {
343     if (! $exec) {
344         printit qq(# The following "mkdir" and "cvs -Q add" calls are not\n),
345                 qq(# necessary for any directories that already exist in the\n),
346                 qq(# CVS tree but which aren't present locally.\n);
347     }
348     foreach (@mkdir_list) {
349         if (! $exec) {
350             printit qq(if test ! -d $_; then\n);
351             $indent = "  ";
352         }
353         _mkdir($_);
354         execute(qq($cvs_command add $_));
355         if (! $exec) {
356             $indent = "";
357             printit qq(fi\n);
358         }
359     }
360     if (! $exec) {
361         printit qq(# End of directory creation.\n);
362     }
363 }
364
365 # Copy in any files in the change, before we try to "cvs add" them.
366 $ae_copy->(@copy_list) if @copy_list;
367
368 if (@add_list) {
369     execute(qq($cvs_command add @add_list));
370 }
371
372 if (@remove_list) {
373     execute(qq(rm -f @remove_list));
374     execute(qq($cvs_command remove @remove_list));
375 }
376
377 # Last, commit the whole bunch.
378 $comment = $summary;
379 $comment .= "\n" . $description if $description;
380 $commit = qq($cvs_command commit -m '$comment' .);
381 if ($exec == 1) {
382     printit qq(# Execute the following to commit the changes:\n),
383             qq(# $commit\n);
384 } else {
385     execute($commit);
386 }
387
388 _chdir($pwd);
389
390 #
391 # Directory cleanup.
392 #
393 sub END {
394     my $dir;
395     foreach $dir (@cleanup) {
396         printit "rm -rf $dir\n";
397         if ($exec) {
398             finddepth(sub {
399                 # print STDERR "unlink($_)\n" if (!-d $_);
400                 # print STDERR "rmdir($_)\n" if (-d $_ && $_ ne ".");
401                 unlink($_) if (!-d $_);
402                 rmdir($_) if (-d $_ && $_ ne ".");
403                 1;
404             }, $dir);
405             rmdir($dir) || print STDERR "Could not remove $dir:  $!\n";
406         }
407     }
408 }
409
410 __END__;
411
412 =head1 NAME
413
414 ae2cvs - convert an Aegis change set to CVS commands
415
416 =head1 SYNOPSIS
417
418 ae2cvs [-aedist|-aegis] [-c change] [-d cvs_root] [-f file] [-l lib]
419         [-m module] [-n] [-p proj] [-q] [-u dir] [-v] [-x] [-X]
420
421         -aedist         use aedist format from input (default)
422         -aegis          query aegis repository directly
423         -c change       change number
424         -d cvs_root     CVS root directory
425         -f file         read aedist from file ('-' == stdin)
426         -l lib          Aegis library directory
427         -m module       CVS module
428         -n              no execute
429         -p proj         project name
430         -q              quiet, don't print commands
431         -u dir          use dir for CVS checkin
432         -v              print version string and exit
433         -x              execute the commands, but don't commit;
434                         two or more -x options commit changes
435         -X              execute the commands and commit changes
436
437 =head1 DESCRIPTION
438
439 The C<ae2cvs> utility can convert an Aegis change into a set of CVS (and
440 other) commands to make the corresponding change(s) to a carbon-copy CVS
441 repository.  This can be used to keep a front-end CVS repository in sync
442 with changes made to an Aegis project, either manually or automatically
443 using the C<integrate_pass_notify_command> attribute of the Aegis
444 project.
445
446 By default, C<ae2cvs> makes no changes to any software, and only prints
447 out the necessary commands.  These commands can be examined first for
448 safety, and then fed to any Bourne shell variant (sh, ksh, or bash) to
449 make the actual CVS changes.
450
451 An option exists to have C<ae2cvs> execute the commands directly.
452
453 =head1 OPTIONS
454
455 The C<ae2cvs> utility supports the following options:
456
457 =over 4
458
459 =item -aedist
460
461 Reads an aedist change set.
462 By default, the change set is read from standard input,
463 or a file specified with the C<-f> option.
464
465 =item -aegis
466
467 Reads the change directly from the Aegis repository
468 by executing the proper C<aegis> commands.
469
470 =item -c change
471
472 Specify the Aegis change number to be used.
473 The value of the C<AEGIS_CHANGE> environment variable
474 is used by default.
475
476 =item -d cvsroot
477
478 Specify the CVS root directory to be used.
479 This option is passed explicitly to each executed C<cvs> command.
480 The default behavior is to omit any C<-d> options
481 and let the executed C<cvs> commands use the
482 C<CVSROOT> environment variable as they normally would.
483
484 =item -f file
485
486 Reads the aedist change set from the specified C<file>,
487 or from standard input if C<file> is C<'-'>.
488
489 =item -l lib
490
491 Specifies an Aegis library directory to be searched for global states
492 files and user state files.
493
494 =item -m module
495
496 Specifies the name of the CVS module to be brought up-to-date.
497 The default is to use the Aegis project name,
498 minus any branch numbers;
499 for example, given an Aegis project name of C<foo-cmd.0.1>,
500 the default CVS module name is C<foo-cmd>.
501
502 =item -n
503
504 No execute.  Commands are printed (including a command for a final
505 commit of changes), but not executed.  This is the default.
506
507 =item -p proj
508
509 Specifies the name of the Aegis project from which this change is taken.
510 The value of the C<AEGIS_PROJECT> environment variable
511 is used by default.
512
513 =item -q
514
515 Quiet.  Commands are not printed.
516
517 =item -u dir
518
519 Use the already checked-out CVS tree that exists at C<dir>
520 for the checkins and commits.
521 The default is to use a separately-created temporary directory.
522
523 =item -v
524
525 Print the version string and exit.
526
527 =item -x
528
529 Execute the commands to bring the CVS repository up to date,
530 except for the final commit of the changes.  Two or more
531 C<-x> options will cause the change to be committed.
532
533 =item -X
534
535 Execute the commands to bring the CVS repository up to date,
536 including the final commit of the changes.
537
538 =back
539
540 =head1 ENVIRONMENT VARIABLES
541
542 =over 4
543
544 =item AE2CVS_FLAGS
545
546 Specifies any options to be used to initialize
547 the C<ae2cvs> utility.
548 Options on the command line override these values.
549
550 =back
551
552 =head1 AUTHOR
553
554 Steven Knight (knight at baldmt dot com)
555
556 =head1 BUGS
557
558 If errors occur during the execution of the Aegis or CVS commands, and
559 the -X option is used, a partial change (consisting of those files for
560 which the command(s) succeeded) will be committed.  It would be safer to
561 generate code to detect the error and print a warning.
562
563 When a file has been deleted in Aegis, the standard whiteout file can
564 cause a regex failure in this script.  It doesn't necessarily happen all
565 the time, though, so this needs more investigation.
566
567 =head1 TODO
568
569 Add an explicit test for using ae2cvs in the Aegis
570 integrate_pass_notify_command field to support fully keeping a
571 repository in sync automatically.
572
573 =head1 COPYRIGHT
574
575 Copyright 2001, 2002, 2003, 2004, 2005 Steven Knight.
576
577 =head1 SEE ALSO
578
579 aegis(1), cvs(1)