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