#!/usr/local/bin/perl

use Benchmark;
use ClearCase::Argv 1.00;
use ClearCase::SyncTree 0.23;
use File::Basename;
use File::Find;
use File::Path;
use File::Spec 0.82;
use Getopt::Long;

use constant MSWIN => $^O =~ /MSWin32|Windows_NT/i;

require 5.005 if MSWIN;

my $prog = basename($0, qw(.pl));
my $rc = 0;

sub usage {
    my $msg = shift;
    my $retcode = (defined($msg) && !$msg) ? 0 : 2;
    if ($retcode) {
	select STDERR;
	print "$prog: Error: $msg\n\n" if $msg;
    }
    print <<EOF;
Usage: $prog [flags] -sbase <dir> -dbase <vob-dir> [pname...]
Flags:
   -help		Print this message and exit
   -sbase <dir>		The source base directory
   -dbase <vob-dir>	The destination base directory
   -flist <file>	A file containing a list of src files, or "-" for stdin
   -map			Interpret \@ARGV as a hash mapping src => dest
   -follow		Follow symlinks when traversing <pname> directories
   -force		Continue despite errors
   -ci			Check in changes (default is to leave co'ed)
   -cr			Check in so as to preserve CR's (slower)
   -ctime		Checked in files get current time (no -ptime)
   -rmname		Remove files from dest area that aren't in src
   -label <lbtype>	Apply the specified label when done
   -c <comment>		Use specified comment for checkins
   -no			Exit after showing a preview
   -yes			Perform all work without prompts
   -nprotect		Turn off "cleartool protect -chmod" phase
   -reuse		Attempt to reuse existing elements of same name
   -summary 		Print a summary of cleartool activities at end
   -verbose <n>		Set to 0 for least verbosity, 2 for most (default=1)
   -Narrow [!]<re>	Limit files found to those which match /re/
   -Version		Print the current $prog version and quit
   -/dbg=1		Verbose mode: show cleartool cmds as they run
Notes:
    All flags may be abbreviated to their shortest unique name.
    Run "perldoc synctree" for detailed documentation and examples.
Examples:
    $prog -sbase /tmp/newcode -dbase /vobs_tps/foo /tmp/newcode
    $prog -sb /tmp/newcode -db /vobs_tps/foo -N '\.java\$' /tmp/newcode
    $prog -sb /tmp/newcode -db /vobs_tps/foo -N '!\.old\$' /tmp/newcode
EOF
    exit $retcode;
}

my(%opt, %xfer);

{
    my($only, $skip);
    sub wanted {
	my $path = File::Spec->rel2abs($File::Find::name);
	$path =~ s%\\%/%g if MSWIN;
	if (! -d && defined $opt{Narrow}) {
	    $only ||= join('|', grep !/^!/, @{$opt{Narrow}});
	    return if $only && $path !~ /$only/;
	    $skip = join('|', map {(m/^!(.*)/)[0]} grep /^!/, @{$opt{Narrow}});
	    return if $skip && $path =~ /$skip/;
	}
	if (-f $_ || -l $_) {
	    $xfer{$path} = $path;
	} elsif (-d _) {
	    if ($_ eq 'lost+found') {
		$File::Find::prune = 1;
		return;
	    }
	} elsif (! -e _) {
	    die "$prog: Error: no such file or directory: $path\n";
	} else {
	    die "$prog: Error: unsupported file type: $path\n";
	}
    }
}

ClearCase::Argv->attropts;
ClearCase::Argv->inpathnorm(0);

local $Getopt::Long::ignorecase = 0;  # global override for dumb default
# A little hack to allow use of flag abbreviations without ambiguity
# warnings, e.g. parse -c <cmnt> independently of -ci etc.
{
    local $Getopt::Long::autoabbrev = 0;
    local $Getopt::Long::passthrough = 1;
    GetOptions(\%opt, qw(comment|c=s nprotect reuse)) || exit 1;
}
GetOptions(\%opt, qw(sbase=s dbase=s flist=s lbtype|label|mklabel=s map
		     Narrow=s@
		     Add Modify
		     ci cr ctime follow force rmname
		     help no ok quiet yes
		     summary verbose=i Version
)) || exit 1;
usage() if $opt{help};
if ($opt{Version}) {
    print ClearCase::SyncTree->version, "\n";
    exit 0;
}
usage("-sbase is a required flag") if !$opt{sbase};
usage("-dbase is a required flag") if !$opt{dbase};

# Implement the -summary functionality.
if ($opt{summary}) {
    my $start = new Benchmark;
    ClearCase::Argv->summary;	# start keeping stats
    END {
	if ($start && $opt{summary}) {
	    # print out the stats we kept
	    print STDERR ClearCase::Argv->summary;
	    # show timing data
	    my $timing = timestr(timediff(new Benchmark, $start));
	    print "Elapsed time: $timing\n";
	}
    }
}

if (!exists($opt{verbose}) || $opt{verbose} == 1) {
    # do nothing
} elsif ($opt{verbose} == 0) {
    ClearCase::Argv->quiet(1);
} else {
    ClearCase::Argv->dbglevel(1);
}

# Create the object we'll be working with.
my $sync = ClearCase::SyncTree->new;

# The dest base must be normalized right away so we can work with it.
# This means conversion to an absolute, view-extended, pathname.
$opt{dbase} = $sync->dstbase($opt{dbase});

# Normalize src dir path too.
die "$prog: Error: no such directory $opt{sbase}\n" unless -d $opt{sbase};
$opt{sbase} = File::Spec->rel2abs($opt{sbase});
$opt{sbase} =~ s%\\%/%g if MSWIN;

# Suppress blathering on stdout from cleartool if asked.
ClearCase::Argv->quiet(1) if $opt{quiet};

if ($opt{flist}) {
    usage("-flist and -map are mutually exclusive") if $opt{map};
    open(FLIST, $opt{flist}) || die "$prog: Error: $opt{flist}: $!";
    while(<FLIST>) {
	chomp;
	s/^\s+//;
	s/\s+$//;
	next if ! $_ || /^#/;
	my($from, $to) = split /\s*=>\s*/;
	$from = File::Spec->canonpath(join('/', $opt{sbase}, $from))
				if ! File::Spec->file_name_is_absolute($from);
	$to = File::Spec->canonpath(join('/', $opt{dbase}, $to))
			     if $to && ! File::Spec->file_name_is_absolute($to);
	next if -d $from;
	die "$prog: Error: $from: No such file or directory\n" unless -e $from;
	if (MSWIN) {
	    for ($from, $to) { s%\\%/%g }
	}
	$xfer{$from} = $to || $from;
    }
    close(FLIST);
}

if ($opt{map}) {
    while (@ARGV) {
	my($from, $to) = split /\s*=[=>]\s*/, shift;
	$to ||= shift;
	die "$prog: Error: odd number of files specified with -map\n" if !$to;
	next if -d $from;
	$xfer{$from} = $to;
    }
} else {
    push(@ARGV, $opt{sbase}) if !@ARGV && !%xfer;
    # Convert warnings from within find() into fatal errors.
    local $SIG{__WARN__} = sub { die "$prog: Error: @_" };
    my %find_cfg;
    $find_cfg{wanted} = \&wanted;
    $find_cfg{follow_fast}++ if $opt{follow};
    for my $pname (@ARGV) {
	find(\%find_cfg, $pname);
    }
}

#########################################################################
# At this point we've parsed the cmd line, derived the file list, etc.
# and are set to do some real work. First, set some options in the object.
#########################################################################

# Allow the protect default to be overridden.
$sync->protect(0) if $opt{nprotect};
# Attempt element reconstitution.
$sync->reuse(1) if $opt{reuse};
# Turn off the default exception handler if -force.
$sync->err_handler(\$rc) if $opt{force};
# Specify the comment to attach to any changes.
if (!$opt{comment}) {
    ($opt{comment} = "By:$0") =~ s%\\%/%g;
}
$sync->comment($opt{comment});
# Suppress -ptime flag on checkins if requested.
$sync->ctime(1) if $opt{ctime};
# Tell it where the files are coming from. We already said where they're
# going (->dstbase) above.
$sync->srcbase($opt{sbase});
# Supply the list of required files.
$sync->srcmap(%xfer);

#########################################################################
# Now, the object knows what it needs to do. Begin the 'action' methods
# which tell it to start doing things.
#########################################################################

# Compare src and dest lists and figure out what to do.
$sync->analyze($opt{rmname});
# If -no, give a preview and exit. Ask for OK to proceed unless -yes.
if (!$opt{yes} || $opt{no}) {
    my $changes = $sync->preview($opt{rmname});
    exit 0 unless $changes;
    exit 0 if $opt{no};
    my $msg = "Continue with these $changes file changes?";
    $msg = qq("$msg") if MSWIN;
    exit 0 if system(qw(clearprompt proceed -pro), $msg);
}
# Create new elements in the target area.
$sync->add unless $opt{Add};
# Update existing files which differ between src and dest.
$sync->modify unless $opt{Modify};
# Remove any files from dest that aren't in src, if requested.
$sync->subtract if $opt{rmname};
# Optionally label the above work, including any still-checked-out files.
$sync->label($opt{lbtype}) if $opt{lbtype};
## Workaround for a CC problem - xml files may have binary data.
## But in CC 4.1 there's a new "xml" eltype that *should* fix it.
## $sync->eltypemap('\.xml$' => 'compressed_file');
# Prompt the user before checkin if -yes not in use.
if (!$opt{yes} && !$opt{ci}) {
    my $b = MSWIN ? 'Cancel' : 'Abort';
    my $msg = "Check in all changes? (No to leave checked out, $b to unco all)";
    $msg = qq("$msg") if MSWIN;
    my $resp = system(qw(clearprompt yes_no -pro), $msg) >> 8;
    $sync->fail if $resp > 1;
    $opt{ci} = !$resp;
}

# Get rid of the exception handler before starting the checkin
# process, as once a checkin succeeds there's no going back.
# Instead, count subsequent errors in a scalar.
$sync->err_handler(\$rc);
# Now check in the changes: one at a time if -cr, otherwise
# all at once.
$sync->no_cr unless $opt{cr};
$sync->checkin if $opt{ci} || $opt{cr} || $opt{yes};

exit $rc;

__END__

=head1 NAME

synctree - Normalize a tree of files with a tree of ClearCase elements

=head1 SYNOPSIS

Run this script with the C<-help> option for usage details. Here are
some additional sample usages with explanations:

  synctree -ci -sbase /tmp/newcode -dbase /vobs_tps/xxx /tmp/newcode/xxx

[Take all files located under /tmp/newcode/xxx, remove the leading
"/tmp/newcode", from each of their pathnames, and place the remaining
relative paths under "/vobs_tps/xxx". Check in when done.]

  synctree -cr -sbase /vobs/hpux/bin -dbase /vobs_rel/hpux/bin

[Sync all files under "/vobs_rel/hpux/bin" with those in
"/vobs/hpux/bin", making sure to preserve their CR's.]

  synctree -sb /A/B -db /X/Y -map /A/B/foo /X/Y/bar /A/B/here /X/Y/there

Take 'foo' from directory /A/B and check it in as 'bar' in /X/Y.
Similarly for 'here' and 'there'.

=head1 DESCRIPTION

Brings a VOB area into alignment with a specified set of files from a
source area. This is analogous in some ways to clearexport_* and
clearimport but those cannot work incrementally; they do an
all-or-nothing import. Synctree is useful if you have a ClearCase tree
that must be kept in sync with a CVS tree during a transition period,
or for overlaying releases of third-party products upon previous ones,
or exporting deliverable files from a nightly build to a release VOB
while preserving CR's, or similar.

The default operation is to mkelem all files which exist in
I<E<lt>srcE<gt>> but not in I<E<lt>destE<gt>>, modify any files which
exist in both but differ, but B<not> to remove files which are present
in I<E<lt>destE<gt>> and not in I<E<lt>srcE<gt>>.  The I<-rmname> flag
will cause this removal to happen as well.

This script must run in a view context; the branching behavior of any
checkouts it performs will be governed by the view's config spec.
Also, the directory named by the I<-dbase> flag must exist and lie
under a mounted VOB tag.

The list of source files to operate on may be provided with the
I<-flist> option or it may come from C<@ARGV>. Any directories
encountered on C<@ARGV> will be traversed recursively. If no
source-file-list is provided, the directory specified with I<-sbase> is
used as the default.

File paths may be given as relative or absolute; all filenames are
turned into absolute paths, then the path given with the I<-sbase>
parameter is removed and replaced with that of I<-dbase> to produce the
destination pathname.

Symbolic links are supported, even on Windows.  Note that the text of
the link is transported I<verbatim> from source area to dest area; thus
relative symlinks may no longer resolve in the destination.

Consider using the I<-n> flag the first time you use this on a valued
VOB, even though nothing irreversible is done (e.g.  no I<rmelem>,
I<rmbranch>, I<rmver>, I<rmtype>, etc.).  And by the same token use
I<-ci> and I<-yes> with care.

=head1 FILE MAPPING

Synctree has lots of support for remapping filenames. The options can
be pretty confusing and thus deserve special treatment here.

All filename mapping is enabled with the B<-map> flag.  Without
I<-map>, a list of files provided on the command line is interpreted as
a set of I<from> files; their I<to> paths are derived via
I<s/^sbase/dbase/> and thus the file basenames do not change. However,
in the presence of I<-map> the @ARGV is instead interpreted as a hash
alternating B<from> and B<to> names.  Thus

  synctree -sbase /etc -dbase /vobs_etc /etc/passwd /etc/group

would make two files under /vobs_etc called passwd and group, whereas

  synctree -sbase /etc -dbase /vobs_etc -map /etc/passwd /vobs_etc/foo

would create one file (/vobs_etc/foo) which is a copy of /etc/passwd.
Alternatively the mapping may be specified with a literal B<=E<gt>>:

  synctree -sb /etc -db /vobs_etc -map '/etc/passwd => /vobs_etc/foo' ...

but note that this must be quoted against shell expansion. The
I<=E<gt>> style is also allowed in files specified via B<-flist>,
thus:

  synctree -sb /etc -db /vobs_etc -map -flist - << EOF
  /etc/passwd => /vobs_etc/foo
  /etc/group  => /vobs_etc/bar
  EOF

=head1 COMPARISONS

Synctree is comparable to I<citree> and I<clearfsimport>. It is similar
to citree but runs on both Windows and UNIX. It has the following
advantages over clearfsimport:

=over 4

=item *

Synctree works with all ClearCase versions whereas clearfsimport is
first supported in CC 4.2.

=item *

Synctree handles C<MVFS->MVFS> transfers while preserving CR's whereas
clearfsimport treats the source as flat files.

=item *

Synctree has support for mapping filenames in transit and a I<-Narrow>
option for limiting the set of files to transfer.

=item *

Synctree is built on an API (B<ClearCase::SyncTree>) which aids custom
tool development in Perl whereas clearfsimport is a command-line
interface only.

=item *

Synctree has support for I<element retention>. I.e. if an element is
added in one pass and removed (rmnamed) in a subsequent pass, and if a
third pass would make another element of the same name, synctree can
optionally (I<-reuse>) make a link to the existing file instead of
making a new element that may be considered an "evil twin".

=back

However, unless one of the above applies the supported, integrated
solution (B<clearfsimport>) is generally preferable. And of course all
of these features I<may> eventually be supported by clearfsimport.

=head1 BUGS

=over 4

=item *

Subtraction of symlinks is currently unimplemented (it's just a little
corner case I haven't gotten to).

=item *

SyncTree does not transport empty directories, and added/removed
directories aren't shown explicitly in the list of operations to be
performed. This is a structural artifact/flaw.

=item *

I have not tested SyncTree in snapshot views and would not expect it to
work out of the box, though I did try to code for the possibility.

=back

=head1 DEBUGGING

The special flag I<-/dbg=1> will cause all underlying cleartool
commands to be printed as they are run (this is actually a feature of
the Argv module on which I<synctree> is built).

=head1 AUTHOR

David Boyce <dsb@boyski.com>

=head1 COPYRIGHT

Copyright (c) 2000,2001 David Boyce. All rights reserved.  This Perl
program is free software; you may redistribute and/or modify it under
the same terms as Perl itself.

=head1 STATUS

This is currently ALPHA code and thus I reserve the right to change the
UI incompatibly. At some point I'll bump the version suitably and
remove this warning, which will constitute an (almost) ironclad promise
to leave the interface alone.

=head1 PORTING

The guts of this program are in the ClearCase::SyncTree module, which
is known to work on Solaris 2.6-7 and Windows NT 4.0SP3-5, and with
perl 5.004_04 and 5.6. The I<synctree> wrapper program per se has had
only rudimentary testing on Windows but appears to work fine there.

=head1 SEE ALSO

perl(1), "perldoc ClearCase::SyncTree"

=cut
