#!/usr/bin/perl -w
#$Id: c4 709 2005-05-03 21:32:07Z wsnyder $
######################################################################
#
# Copyright 2002-2005 by Wilson Snyder.  This program is free software;
# you can redistribute it and/or modify it under the terms of either the GNU
# General Public License or the Perl Artistic License.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the Perl Artistic License
# along with this module; see the file COPYING.  If not, see
# www.cpan.org
#                                                                           
######################################################################

require 5.006_001;
use lib "blib/lib";
use lib "blib/arch";

use Getopt::Long;
use IO::File;
use Pod::Usage;
use Cwd qw(getcwd chdir);
use File::Find;
use File::Spec;
use File::Spec::Functions;
use Carp;
use strict;

use P4::C4;
use P4::C4::Path qw(fileNoLinks);

use vars qw ($Debug);

#======================================================================

$ENV{P4CONFIG} ||= '.p4config';

#======================================================================
# main

autoflush STDOUT 1;
autoflush STDERR 1;

$Debug = 0;
our $Opt_Cmd;
our @Opt_CmdParams = ();

our $P4Opt = new P4::Getopt;
@ARGV = $P4Opt->parameter(@ARGV);
Getopt::Long::config ("pass_through", "no_auto_abbrev");
if (! GetOptions (
		  "help"	=> \&usage,
		  "debug"	=> \&debug,
		  "<>"		=> \&parameter,
		  )) {
    usage();
}

if (!defined $Opt_Cmd) {
    usage();
}

# Run relative to absolute cwd, so the client spec doesn't have links.
# This is because perforce checks we match the client spec, but doesn't consider
# symlinks to the same place to be good enough.
chdir(fileNoLinks(getcwd()));

# Set user, allowing a default
if ($ENV{C4USER_DEFAULT} && !$ENV{P4USER}) {
    my $p4 = new P4::Client;
    $P4Opt->setClientOpt($p4);
    $p4->SetUser($ENV{C4USER_DEFAULT});
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    if (!$p4->isUser($ENV{P4USER}||$ENV{USER})) {
	$ENV{P4USER} = $ENV{C4USER_DEFAULT};
    }
}

# Convert common CVS command names to what perforce wants
$Opt_Cmd = "submit" if $Opt_Cmd eq "ci";
$Opt_Cmd = "submit" if $Opt_Cmd eq "commit";
$Opt_Cmd = "delete" if $Opt_Cmd eq "rm";

# Parse the command, and see if there are any file specs
# Replace the file specs with absolute filenames, if possible.
# Furthermore, chdir to the first one in an attempt to find the
# right client file path.
print "Cmd: ",join(' ',$Opt_Cmd),"\n" if $Debug;
print "  Pos: ",join(' ',@Opt_CmdParams),"\n" if $Debug;
my @cmdParsed = $P4Opt->parseCmd($Opt_Cmd, @Opt_CmdParams);
my @cmdOut;
my $didCwd;
my $pwd = getcwd();
for (my $i=0; $i<=$#cmdParsed; $i++) {
    my $arg   = $Opt_CmdParams[$i];
    my $parse = $cmdParsed[$i];
    print "  Arg $arg -> $parse\n" if $Debug;
    if ($parse =~ /^file/
	&& !P4::C4::Path::isDepotFilename($arg)) {
	$arg = catfile($arg,"...") if -d $arg;
	my $abs = fileNoLinks($arg);
	push @cmdOut, $abs;
	print "   abs-> $abs\n" if $Debug;
	# Can we cwd there?
	if (!$didCwd && $abs) {
	    my @dirlist = File::Spec->splitdir($abs);
	    while ($#dirlist > 0) {
		my $fn = catdir(@dirlist);
		if (-d $fn) {
		    $didCwd = $fn;
		    print "   SetCWD $fn\n" if $Debug;
		    last;
		}
		pop @dirlist;
	    }
	}
    } else {
	push @cmdOut, $arg;
    }
}
chdir($didCwd) if $didCwd;   # Do last, else second file in list will be messed up
@Opt_CmdParams = @cmdOut;

# Execute comand
if ($Opt_Cmd eq "change-max") {
    cmd_change_max();
} elsif ($Opt_Cmd eq "client-create") {
    cmd_client_create();
} elsif ($Opt_Cmd eq "client-delete") {
    cmd_client_delete();
} elsif ($Opt_Cmd eq "get") {
    die "%Error: Use 'c4 update' instead of 'c4 get'.\n";
} elsif ($Opt_Cmd eq "update") {
    if (is_c4_managed()) {
	cmd_update();
    } else {
	die "%Error: Client is not under c4 management.\n";
    }
} elsif ($Opt_Cmd eq "unknown") {
    cmd_unknown();
} elsif ($Opt_Cmd eq "submit") {
    my %opts = P4::Getopt->hashCmd($Opt_Cmd, @Opt_CmdParams);
    if (!$opts{-p4} && is_c4_managed()) {
	cmd_submit();
    } else {
	@Opt_CmdParams = P4::Getopt->stripOneArg('-p4',@Opt_CmdParams);
    	cmd_p4();
    }
} elsif ($Opt_Cmd eq "sync") {
    my %opts = P4::Getopt->hashCmd($Opt_Cmd, @Opt_CmdParams);
    if (!$opts{-p4} && is_c4_managed()) {
	die "%Error: Client is under c4 management, use 'c4 update' instead of 'c4 sync'.\n";
	# Or, use -p4 switch
    } else {
	@Opt_CmdParams = P4::Getopt->stripOneArg('-p4',@Opt_CmdParams);
    	cmd_p4();
    }
} elsif ($Opt_Cmd eq "help-summary") {
    print "All commands:\n";
    cmd_help_summary();
} elsif ($Opt_Cmd eq "help") {
    if ($#Opt_CmdParams < 0) {
	print "C4 Wrapper commands:\n";
	print "\tc4      --help\n";
	print "\tc4      help-summary\n";
	print "\tc4      change-max\n";
	print "\tc4      client-create -t <template> <client>\n";
	print "\tc4      client-delete -d <client>\n";
	print "\tc4 [-n] update [files...]\n";
	print "\nP4 Help:\n";
    }
    cmd_p4();
} else {
    cmd_p4();
}

#----------------------------------------------------------------------

sub usage {
    print '$Id: c4 709 2005-05-03 21:32:07Z wsnyder $ ', "\n";
    pod2usage(-verbose=>2, -exitval => 2);
    exit (1);
}

sub debug {
    $P4::C4::Debug = 1;
    $Debug = 1;
}

sub parameter {
    my $param = shift;
    if ($param =~ /^-/) {
	if (!defined $Opt_Cmd) {
	    die "$0: %Error: Invalid global option: $param\n";
	} else {
	    push @Opt_CmdParams, $param;
	}
    } else {
	if (!defined $Opt_Cmd) {
	    $Opt_Cmd = $param;
	} else {
	    push @Opt_CmdParams, $param;
	}
    }
}
 
#######################################################################
#######################################################################
#######################################################################
# Commands invoked by the user

sub is_c4_managed {
    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    return $p4->clientC4Managed();
}

sub cmd_p4 {
    # Call c4 using all the default parameters from the command line
    my @cmd = ("p4");
    push @cmd, $P4Opt->get_parameters;
    push @cmd, $Opt_Cmd;
    push @cmd, "-n" if ($P4Opt->noop);	# convert 'c4 -n update' to 'c4 update -n'
    push @cmd, @Opt_CmdParams;

    # Run
    print "\t",join(' ',@cmd),"\n" if $Debug;
    local $! = undef;
    system @cmd;
    my $status = $?; my $msgx = $!;
    if (!$Debug) {
	($status == 0) or exit($status);  # Don't print a message, p4 did
    }
    ($status == 0) or croak "%Error: Command Failed $status $msgx, stopped";
}

sub cmd_client_create {
    print "cmd_client_create ",join(' ',@Opt_CmdParams),"\n"  if $Debug;

    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    $p4->createClient('-c4', '-rmdir', @Opt_CmdParams);
}

sub cmd_client_delete {
    print "cmd_client_delete ",join(' ',@Opt_CmdParams),"\n"  if $Debug;

    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    $p4->clientDelete(@Opt_CmdParams);
}

sub cmd_update {
    print "cmd_update ",join(' ',@Opt_CmdParams),"\n"  if $Debug;

    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    $p4->update(@Opt_CmdParams);
}

sub cmd_submit {
    print "cmd_submit ",join(' ',@Opt_CmdParams),"\n"  if $Debug;

    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    $p4->submitCheckC4(@Opt_CmdParams);
    @Opt_CmdParams = P4::Getopt->stripOneArg('-f',@Opt_CmdParams);
    cmd_p4();
}

sub cmd_unknown {
    print "cmd_unknown ",join(' ',@Opt_CmdParams),"\n"  if $Debug;

    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    $p4->unknown(@Opt_CmdParams);
}

sub cmd_change_max {
    my $p4 = new P4::C4(opt=>$P4Opt);
    $p4->Init() or die "$0: %Error: Failed to connect to Perforce Server\n";
    my $max=$p4->changeMax(@Opt_CmdParams);
    if ($max) {
	print $max,"\n";
    } else {
	die "%Error: Change number not determined.\n";  # Want bad exit status
    }
}

sub cmd_help_summary {
    my $self = shift;
    my $longest = 1;
    foreach my $cmd ($P4Opt->commands_sorted) {
	$longest = length($cmd) if length($cmd)>$longest;
    }
    foreach my $cmd ($P4Opt->commands_sorted) {
	my $args = $P4Opt->command_arg_text($cmd);
	printf("    %-${longest}s  %s\n",$cmd,$args);
    }
}

#######################################################################
__END__

=pod

=head1 NAME

c4 - CVSish wrapper for perforce p4 program

=head1 SYNOPSIS

    c4        help

    c4        client-create -t <template> <client>
    c4        client-delete -d -f <client>
    c4 [-n]   update
    c4        unknown

    c4 <any p4 command>
       i.e.:  c4 add <file>
              c4 delete <file>
              c4 diff <file>    		     

=head1 DESCRIPTION

C4 allows a default user, see the ENVIRONMENT section.

When passed a filename on any c4 command, c4 makes the filename absolute,
with all symlinks reduced, and chdir's if possible to the first such
filename.  If the argument is a directory, a /... is appended.  This makes
it a lot more likely that perforce commands will work when issued from
outside a client area.  (For example: c4 add /my/area/boo/blaz.)  For this
to work best, your p4 ClientRoot should point to the area that is
physically on the disk, with all symlinks resolved.

When a client is created in a special way, C4 makes L<p4> seem more like
L<cvs> by having all files writable by default.  C4 adds several commands
(update/client_create) to support this, see the COMMANDS section.

=head1 COMMANDS

Any command not listed here is passed directly to perforce.

=over 4

=item change-max [files...]

Return the maximum change number present in the current client or passed
list of files.

=item ci

Alias for C<c4 submit>.

=item commit

Alias for C<c4 submit>.

=item client-create [-t template] <client>

Create a client specification, making sure it gets named, and setting the
clobber, allwrite, and rmdir attributes as required for c4 update.  Also,
create a .p4config file with the name of the client in it.

=item client-delete -d <client>

Delete a client specification in a way which leaves the database as clean
as possible.  Consists of the following steps:

=over 4

C<p4 revert ...> to make sure no files are left open.

C<p4 client> to edit the spec to remove any View: lines.

C<p4 sync> to remove the views that were deleted from the file system.

C<p4 client -d> to remove the client spec.

C<rm .p4config> to cleanup the local directory.

=back

=item rm [-c changelist] file...

Alias for C<c4 delete>.

=item submit [-p4] [-f] [normal p4 args]

This performs a p4 submit.  

Under c4 style management, it prints an error if the area is not up to date
before submitting.  -f overrides this check.  Use -f carefully as edited
files may not yet be opened for editing as they would be if the update was
done.

Under traditional management, or with the -p4 flag, calls the normal p4
submit command.

=item sync [-p4] [normal p4 args]

This performs a p4 sync.

Under c4 style management, C<c4 update> should be used instead.  Under
traditional management, or with the -p4 flag, calls the normal C<p4 sync>
command.

=item update [-n] [-a] [-pi] [-rl] [file...]

Update is similar to cvs update.  With -n, only the actions to be taken are
shown, it does not actually change anything.  On an update without a -n, c4
performs the following steps:

=over 4

C<p4 edit> any files that have been changed from the version checked out
from the depot.

With the -a option, C<p4 add> any files that are unknown to perforce (?
becomes an A.)

With the -rl option, C<p4 delete> any files that are missing in client (l
becomes an R.)

C<p4 revert> any files that were C<p4 edit>ed, but now match the version
in the depot.

C<p4 sync> to get recent changes.

Look for any unknown files not in a .cvsignore file.

=back

A "c4 update -a -rl" will thus issue appropriate adds and deletes to make
the depot match the current client view.

Update also prints a summary of each file that is modified in the client
area, and with the -pi option, ignored (i) files are listed as well.
Lowercase letters are used for actions that were not caused by the user,
and are different from cvs letters.  Here are the letters, the state
causing that message, and what may happen on an update.

    A    (A)dded in client, not yet in depot, stays Add.
    M    (M)odified in client, different from depot, stays Modified.
    R    (R)emoved in client, still in depot, stays Removed.
    U    (U)pdate required, unmodified in client, new version from depot.
    a    Never in client, (a)dded in depot, will appear.
    d    Same in client, (d)eleted in depot, will disappear.
    l    (L)ost in client, exists in depot, will appear.
    m    (m)odified in client, but now matches depot, will be reverted.
    i    (i)gnored and not in depot, and in .cvsignore or global ignores.
    ?    Unknown - Not in depot, not added, not in .cvsignore, no action.

Unknown files (?) should be ignored with a .cvsignore file as described in
the FILES section.

=item unknown [-a] [file...]

Prints files that are not known to perforce, and do not have a .cvsignore
line.  This is similar to the '?' lines printed by "c4 update -n", however
"c4 unknown" may be used with standard perforce clients.

With -a, mark all unknown files to be added to the depot (same as -a option
in c4 update.)

=back

=head1 ARGUMENTS

=over 4

=item --help

Displays this message and program version and exits.

=back

=head1 ENVIRONMENT

=over 4

=item C4USER_DEFAULT

If specified, when a command is issued and the USER does not have a P4
license, then rather then create a new P4 user, the user in C4USER_DEFAULT
will be used.  This allows infrequent read-only users to share the same
license.

=item CVSIGNORE

If specified, adds space seperated list of ignores to global ignore list.
See .cvsignore below.

=back

=head1 FILES

=over 4

=item .c4cache

This is a temporary file created and maintained by c4 in the top level of
your client area.  You may safely delete it, however the next c4 operation
may be significantly slower.

=item .cvsignore

Specifies files that are in the same directory as the .cvsignore that should
be ignored; there will be no ?'s printed when a 'c4 update' is executed.

Ignore files are mostly compatible with CVS.  The list of global ignores is
initialized with:

    *~      #*      .#*     ,*      _$*     *$
    *.old   *.bak   *.BAK   *.orig  *.rej   .del-*
    *.a     *.olb   *.o     *.obj   *.so    *.exe
    *.Z     *.elc   *.ln
    .c4cache     .p4config
    .make.state  .nse_depinfo  .dependency-info
    tags         TAGS
    core

Patterns in the home directory file ~/.cvsignore, or the CVSIGNORE
environment variable are appended to this list.

Each directory may have a local '.cvsignore' file.  The patterns found in
local `.cvsignore' are only valid for the directory that contains them, not
for any sub-directories.  

In any of the places listed above, a single exclamation mark (`!')  clears
the ignore list.  This can be used if you want to store any file which
normally is ignored.

The wildcards * and ? are honored, no other wildcards are currently
supported.

As with CVS, comments are NOT supported!

=back

=head1 DISTRIBUTION

The latest version is available from CPAN and from L<http://www.veripool.com/>.

Copyright 2002-2005 by Wilson Snyder.  This package is free software; you
can redistribute it and/or modify it under the terms of either the GNU
Lesser General Public License or the Perl Artistic License.

=head1 AUTHORS

Wilson Snyder <wsnyder@wsnyder.org>

=head1 SEE ALSO

L<p4>,
L<c4_job_edit>,
L<P4::C4>

=cut

######################################################################
### Local Variables:
### compile-command: "./c4 "
### End:
