Re: Perforce support.

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On 7/12/07, Govind Salinas <govindsalinas@xxxxxxxxx> wrote:
I am hoping to convince my co-workers to start using a Distributed
SCM, hopefully git, and I wanted to see what people had to say about
the Perforce-git interoperability.  To make it more fun we are doing
this on Windows.

There are two scripts. git-p4import.py, is in standard distribution,
and is AFAIK not used very much Windows. The other, git-p4-import.bat
is never tried on anywhere but Windows and is maintained outside
of mainstream Git.

I maintain the second one, git-p4-import.bat. I don't dare to submit
it for distribution with Git: it is very centered on stu^H^H^H^H suboptimal
practices I have at work.

I have been playing around with git for a month or so and have started
writing, what I hope will be, a nice GUI over git that works well on
Windows (Cygwin) and offers some feeling of familiarity to our
Perforce users.  That however is only half the problem.

I trust you already seen git gui...

We need to be able to go back and forth to our main Perforce depot,
and while I understand that git-svn support is very good, I have only
seen limited support of Perforce.

Because Perforce is, in some respects, even worse: it is closed.
It will never get the level of support SVN or even CVS have.

  I was wondering if anyone has been
using git with p4 and how well did it work.  We have very complex and
somewhat large "clients" that do a lot of mapping of directories
(which strikes me as particularly insane) and I was wondering if any
of the tools support that.

I am in such situation. Perforce is so bad in managing it,
that people here had to write a handful of wrappers around
standard commands just to make it limping (and it is falling
apart right now).

I use the git-p4-import.bat to import and export the commits.
It is PITA though. Try it (attached), but don't expect too much.
It is a perl script, geared towards Windows and Activision Perl
(hopefully not too much and it still can work with cygwin's perl).
Some examples (I assume a useful shell, not cmd):

Import a range of P4 changelists, convert p4 path to local path

 $ git-p4-import.bat --p4-path //Very/stupid/perforce \
 --local-path "" --p4-range '@123,456'
Result can be found in the reference named p4/IMPORT.

Export some commits:

 $ git-p4-import.bat --merge my-branch -y --edit=vim
This prepare a change lists, let you edit the changelist description
in vim and submit it. I suggest you always rebase my-branch
before this: p4 linearizes the history, so in case you loose your
original git branch, the _useful_ parenthood information is lost with it.
Better not even have any hopes associated with this.
@rem = 'NT: CMD.EXE vim: syntax=perl noet sw=4
@perl -x -s %0 -- %*
@goto __end_of_file__
@rem ';
#!perl -w
#line 7

local $VERBOSE = 0;
local $DRYRUN = 0;
local $SHOW_DIFFS = 0;
local $AUTO_COMMIT = 0;
local $JUST_COMMIT = 0;
local $GIT_DIR = undef;
local $P4CLIENT = undef;
local $EDIT_COMMIT = 0;
local $FORCE = 0;
local @FULL_IMPORT = 0;
local @DESC = ();
local $SPEC = undef;
local $P4HAVE_FILE = undef;
local %P4USERS = ();
local $FULL_DESC = 1;
local $HEAD_FROM_P4 = 0;
local $P4_EDIT_MERGE = 0;
local $P4_EDIT_CHANGED = 0;
local $P4_EDIT_HEAD = undef;
local $editor = undef;

local $P4_PATH = undef;
local $P4_RANGE = undef;
local $P4_LOCAL_PATH = undef;
local $START_POINT = undef;
local $UPDATE_BRANCH = undef;
local $P4_TAG = undef;

local $P4_SYNC_TO = undef;
# cache for refs/p4import references
local %P4IMPORT_HEADS = ();

sub read_args {
    my $no_opt = 0;
    my $no_EDIT_COMMIT = 0;
    while (my $f = shift) {
	goto _files if $no_opt;
	$no_opt=1, next if $f eq '--';

	$DRYRUN=1, next if $f eq '-n' or $f eq '--dry-run';
	$SHOW_DIFFS=$DRYRUN=1, next if $f eq '--diffs';
	$AUTO_COMMIT=1, next if $f eq '-y' or $f eq '--yes';
	$JUST_COMMIT=1, next if $f =~ /^--(just-)?commit$/;
	$EDIT_COMMIT=1, next if $f eq '-e' or $f eq '--edit';
	$no_EDIT_COMMIT=1, next if $f eq '-ne' or $f eq '--no-edit';
	$FORCE=1, next if $f eq '--force';
	if ($f =~ /^-(e|-edit=)(.*)/) {
	    $editor = $2;
	    $EDIT_COMMIT=1;
	    next;
	}
	$FULL_IMPORT=1, next if $f eq '--full';
	$FULL_DESC++, next if $f eq '--p4-desc';

	$VERBOSE++, next if $f eq '-v' or $f eq '--verbose';
	$P4CLIENT = shift, next if $f eq '--client' and @_;
	$P4CLIENT = $1, next if $f =~ /^--client=(.*)/;
	push(@DESC,'c'.shift), next if $f eq '-C' and @_;
	push(@DESC,"c$1"), next if $f =~ /^--changelist=(.*)/;
	push(@DESC,'f'.shift), next if $f eq '-F' and @_;
	push(@DESC,"f$1"), next if $f =~ /^--file=(.*)/;
	push(@DESC,'4'.shift), next if ($f eq '--ptr' or $f eq '--p4') and @_;
	push(@DESC,"4$2"), next if $f =~ /^--(p4|ptr)=(.*)/;

	$P4_EDIT_CHANGED=1, next if $f eq '--p4-edit-changed';
	if ($f =~ /^--p4-edit-changed=(.*)/) {
	    $P4_EDIT_CHANGED=1;
	    $P4_EDIT_HEAD=$1;
	    next 
	}
	if ($f =~ /^--merge=(.*)/ or
	    ($f eq '--merge' and @_)) {
	    $P4_EDIT_MERGE=1;
	    $P4_EDIT_CHANGED=1;
	    $EDIT_COMMIT=1;
	    $P4_EDIT_HEAD= $f eq '--merge' ? shift: $1;
	    next 
	}
	$P4_TAG=$1, next if $f =~ /^--p4-tag=(.+)/;
	$P4_TAG=shift, next if $f eq '--p4-tag' and @_;
	# $P4_EDIT_MERGE = 'submit' if $f eq '--submit';

	$P4_PATH=$1, next if $f =~ /^--p4-path=(.*)/;
	$P4_PATH=shift, next if $f eq '--p4-path' and @_;
	$P4_RANGE=$1, next if $f =~ /^--p4-range=(.*)/;
	$P4_RANGE=shift, next if $f eq '--p4-range' and @_;
	$P4_LOCAL_PATH=$1, next if $f =~ /^--local-path=(.*)/;
	$P4_LOCAL_PATH=shift, next if $f eq '--local-path' and @_;
	$START_POINT=$1, next if $f =~ /^--start=(.*)/;
	$START_POINT=shift, next if $f eq '--start' and @_;
	$UPDATE_BRANCH=$1, next if $f =~ /^--branch=(.*)/;
	$UPDATE_BRANCH=shift, next if $f eq '--branch' and @_;

	$P4_SYNC_TO=$1, next if $f =~ /--sync=(.*)/;
	$P4_SYNC_TO=shift, next if $f eq '--sync' and @_;

	if ($f eq '--help' or $f eq '-h') {
	    print <<EOF;
$0 [-n|--dry-run] [-y|--yes] [--client <client-name>] [--diffs] \
[-e|--edit] [--just-commit] [--full] [-v|--verbose] [-C <change-number>] \
[-F <filename>] [--ptr|--p4 <p4-path-and/or-revision>] [--p4-desc] \
[--] [<specification>]

Perforce client state importer. Creates a git commit on the current
branch from a state the given p4 client and working directory hold.

<specification> must be given and is expected to be a file which will be
stored on the side branch under the name "spec".
Remote-to-local mapping and the revisions of files are stored in "have",
and the client definition - in "client".

--client client Specify client name (saved in .git/p4/client for the next time)
--full          Perform full import, don't even try to figure out what changed
-y|--yes        Commit automatically (by default only index updated)
--just-commit   To be used after you forgot to run with --yes first time
-n|--dry-run    Do not update the index and do not commit
-e|--edit       Edit commit description before committing
-v|--verbose    Be more verbose. Can be given many times, increases verbosity

--file=file
-F file         Take description for the commit from a file in the
                next parameter

--changelist=change
-C change       Take description for the commit from this p4 change

--p4|--ptr=p4-path-and/or-revision
                Take description for the commit from the p4 change described
                by this p4 path, possibly including revision specification

--p4-path=p4-path
                Import changes directly from Perforce concerning the given
                p4-path. The first change list defines the starting state
                of imported path
--p4-range=revrange
                Restrict the changes to the revrange change lists.
                The first change list is always imported fully,
                all the subsequent - incrementally. If not given -
                all changes on the patch will be imported
--local-path=local-path
                Replace the p4-path given to --p4-path with local-path
                in the imported pathnames
--start=git-ref
                Start commiting at the given reference. If not given,
                the value of reference given to --branch is used,
                otherwise the current HEAD is assumed
--branch=git-branch
                After everything is imported and committed, update the
                reference git-branch with sha1 of the last commit.
                If not given - the last commit of the import is stored
                in the reference /p4/IMPORT

--p4-desc       Increase amount of junk from p4 change description

--diffs         Show files which are different between local filesystem, index,
                and the current HEAD. Does not do anything else

--merge=branch
                Merge with the given sha1 and prepare a p4 submission.
                Current HEAD will be git-merged with branch.
                The working directory must have no local changes

--p4-edit-changed
--p4-edit-changed=branch
                Read the given branch in the index and prepare a p4 submission.
                Current HEAD must fast-forward to sha1. If branch is omited,
                the current HEAD is assumed (useful after push on HEAD).
                The working directory must have no local changes

--sync=ref
                Sync the current client to the state given by ref:head
                file. The file must have been stored by previous import.
                The option considers all references under p4import when
                looking for the "have"-file


The descriptions taken from p4 changes given by -C and --p4 will
be concatenated if the options given multiple times.
"--" can be used to separate options from description files.

EOF
	    exit(0);
	}
	die "$0: unknown option $f\n" if $f =~ /^-/;
    _files:
	warn "$0: spec was already set, $SPEC ignored\n" if defined($SPEC);
	$SPEC = $f;
    }
    $EDIT_COMMIT = 0 if $no_EDIT_COMMIT;
}
read_args(@ARGV);

$editor = $ENV{VISUAL} unless defined($editor);
$editor = $ENV{EDITOR} unless defined($editor);
$editor = 'vim' unless defined($editor);
die "$0: no editor defined\n" if $EDIT_COMMIT and !defined($editor);

if (!defined($GIT_DIR)) {
    $GIT_DIR = git_rev_parse('--git-dir');
    die "$0: GIT_DIR not found\n" if !defined($GIT_DIR) or !-d $GIT_DIR;
}

exit(git_show_diffs() ? 1: 0) if $SHOW_DIFFS;

# the most commands below need GIT_DIR/p4, try to create it
mkdir "$GIT_DIR/p4", 0775;

if (defined($P4_PATH)) {
    $START_POINT = $UPDATE_BRANCH
	if !defined($START_POINT) and defined($UPDATE_BRANCH);
    my $parent = undef;
    $parent = git_rev_parse($START_POINT) if defined($START_POINT);
    $parent = '' if !defined($parent);

    $P4_RANGE = '#head' if !defined($P4_RANGE); # import all changes
    $P4_LOCAL_PATH = $P4_PATH if !defined($P4_LOCAL_PATH);
    $P4_LOCAL_PATH =~ s!^/+!!o;
    s!/+\.\.\.$!!o for ($P4_PATH,$P4_LOCAL_PATH);
    s!/+$!!o for ($P4_PATH,$P4_LOCAL_PATH);
    print "Path conversion:\n'$P4_PATH' ->\n'$P4_LOCAL_PATH'\n" if $VERBOSE > 1;
    import_p4_dir($P4_PATH,$P4_LOCAL_PATH,$P4_RANGE,$parent,$UPDATE_BRANCH);
    exit 0;
}

if (defined($P4_TAG)) {
    my $rc = git_p4_tag_HEAD($P4_TAG);
    exit 127 if $rc & 0xff;
    exit 1 if $rc;
    exit 0;
}

# P4 client was given in command-line. Store it
if ( defined($P4CLIENT) ) {
    mkdir "$GIT_DIR/p4", 0777;
    if ( open(F, '>', "$GIT_DIR/p4/client") ) {
	print F "$P4CLIENT\n";
	close(F);
    } else {
	die "$0: cannot store client name: $!\n"
    }
} else {
    if ( open(F, '<', "$GIT_DIR/p4/client") ) {
	($P4CLIENT) = <F>;
	close(F);
	$P4CLIENT =~ s/^\s*//,$P4CLIENT =~ s/\s*$// if defined($P4CLIENT);
    }
}
die "P4 client not defined\n" if !defined($P4CLIENT) or !length($P4CLIENT);
print "reading client $P4CLIENT\n" if $VERBOSE;
local ($P4ROOT, $p4clnt, $P4HOST);
open(my $fdo, '>', "$GIT_DIR/p4/client.def") or die "p4/client.def: $!\n";
binmode($fdo);
open(my $fdi, '-|', "p4 client -o $P4CLIENT") or die "p4 client: $!\n";
binmode($fdi);
my $last_line_len = 0;
while (<$fdi>) {
    next if /^#/o;
    if ( m/^\s*Root:\s*(\S+)[\\\/]*\s*$/so ) { $P4ROOT = $1 }
    elsif ( m/^\s*Client:\s*(\S+)/o ) { $p4clnt = $1 }
    elsif ( m/^\s*Host:\s*(\S+)/o ) { $P4HOST = $1 }
    ($VERBOSE and print), next if /^(Access|Update):/;
    s/\r?\n$//so;
    my $len = length($_);
    print $fdo "$_\n" if $len or $len != $last_line_len;
    $last_line_len = $len;
}
close($fdi);
close($fdo);

die "Client root not defined\n" unless defined($P4ROOT);
if ( $VERBOSE ) {
    use Cwd;
    print "GIT_DIR: $GIT_DIR\n";
    print "Root: $P4ROOT (cwd: ".cwd()."\n";
    print "Host: $P4HOST\n";
    print "Client: $p4clnt\n" if $p4clnt ne $P4CLIENT;
}

die if defined $P4_SYNC_TO;
#exit(git_sync_to($P4_SYNC_TO)) if defined $P4_SYNC_TO;

if ($P4_EDIT_CHANGED) {
    exit(git_p4_merge($P4_EDIT_HEAD, $P4_EDIT_MERGE, $AUTO_COMMIT));
}

my ($git_head,$git_p4_head,$git_p4_have) = git_p4_init();

if ($JUST_COMMIT) {
    git_p4_commit($git_head, $git_p4_head);
    exit 0;
}

local %gitignore_dirs = ();
$gitignore_dirs{'/'} = read_filter_file("$GIT_DIR/info/exclude");
push(@{$gitignore_dirs{'/'}}, @{read_filter_file('.gitignore')});

my %git_index = ();
$/ = "\0";
my @git_X = ();
print "Reading git file list(git ls-files @git_X --cached -z)...\n" if $VERBOSE;
foreach ( qx{git ls-files @git_X --cached -z} ) {
    chop; # chop \0
    next if m/^\.gitignore$/o;
    next if m/\/\.gitignore$/o;
    next if filtered($_);
    $git_index{$_} = 1;
}

my @git_add = ();
my @git_addx = ();
my @git_del = ();
my @git_upd = ();

print "Reading P4 file list...\n" if $VERBOSE;
local ($Conflicts,$Ignored,$Added,$Deleted,$Updated) = (0,0,0,0,0);
$/ = "\n";
my $in_name = 0;
my @root = split(/[\/\\]+/, $P4ROOT);
my %p4_index = ();
my %p4_a_lc = ();
my %lnames = ();
my %lconflicts = ();
if (opendir(DIR, '.')) {
    $lnames{'.'} = [grep {$_ ne '.' and $_ ne '..'} readdir(DIR)];
    closedir(DIR);
}
open(my $have, "p4 -G -c $P4CLIENT -H $P4HOST -d $P4ROOT have |") or
    die "$0: failed to start p4: $!\n";
binmode($have);
$P4HAVE_FILE = "$GIT_DIR/p4/have";
open(my $storedhave, '>', $P4HAVE_FILE) or die "$P4HAVE_FILE: $!\n";
binmode($storedhave);
my ($cnt,$err,$ent) = (0,0,undef);
while (defined($ent=read_pydict_entry($have))) {
    if (defined($ent->{code}) and defined($ent->{data})) {
	++$err if $ent->{code} eq 'error';
	print STDERR 'p4: '.$ent->{code}.': '.$ent->{data}."\n";
	next;
    }
    next if !defined($ent->{depotFile}) or !defined($ent->{clientFile});
    ++$cnt;
    my $a = $ent->{depotFile};
    $ent->{clientFile} =~ m!^//[^/]+/(.*)!o;
    my $b = $1;
    my @bb = split(/\/+/, $b);
    print $storedhave "$a\0$ent->{clientFile}\0$ent->{haveRev}\0\n";

    if ( $^O eq 'MSWin32' ) {
	# stupid windows, daft activestate, dumb P4
	# This piece below is checking for file name conflicts
	# which happen on windows because of it mangling the names.
	my $blc = lc $b;
	if ( $#bb > 0 ) {
	    my $path = '.';
	    foreach my $n (@bb[0 .. $#bb -1]) {
		my @conflicts =
		    grep {lc $_ eq lc $n and $_ ne $n} @{$lnames{$path}};
		if (@conflicts and !exists($lconflicts{"$path/$n"})) {
		    warn "warning: $a -> $b\n".
			 "warning: conflict between path \"$path/$n\" and ".
			 "local filesystem in \"@conflicts\"\n";
		    $Conflicts++;
		    $lconflicts{"$path/$n"} = 1;
		}
		$path .= "/$n";
		if (!exists($lnames{$path})) {
		    if (opendir(DIR, $path)) {
			$lnames{$path} =
			    [grep {$_ ne '.' and $_ ne '..'} readdir(DIR)];
			closedir(DIR);
			#print "read $path (",scalar(@{$lnames{$path}}),")\n";
		    }
		}
	    }
	}
	if (!exists($p4_a_lc{$blc})) {
	    $p4_a_lc{$blc} = [$a, $b];
	} else {
	    warn("warning: $a -> $b\n".
		 "warning: conflicts with ".
		 $p4_a_lc{$blc}->[0]." -> ".
		 $p4_a_lc{$blc}->[1]."\n");
	    $Conflicts++;
	    next;
	}
    }

    my $i;
    for ($i = 0; $i < $#bb; ++$i) {
	my $bdir = join('/',@bb[0 .. $i]) . '/';
	if ( !exists($gitignore_dirs{$bdir}) ) {
	    $gitignore_dirs{$bdir} = read_filter_file("$bdir.gitignore");
	}
    }
    if (filtered($b)) {
	print " i $b\n" if $VERBOSE > 3;
	$Ignored++;
	next
    }
    $p4_index{$b} = $a;
    if ( exists($git_index{$b}) ) {
	my $needup = 1;
	if (defined($git_p4_have)) {
	    $prev = $git_p4_have->{$a};
	    if (defined($prev)) {
		$prev->[0] =~ m!^//[^/]+/(.*)!o;
		$needup = 0 if ($b eq $1) and ($prev->[1] eq $ent->{haveRev});
		if ($needup and $VERBOSE > 1) {
		    my $reason;
		    $reason = 'local file' if $b ne $1;
		    $reason = 'revision' if $prev->[1] ne $ent->{haveRev};
		    print "$a ($reason changed)\n";
		}
	    }
	}
	if ($needup) {
	    $Updated++;
	    push(@git_upd, $b);
	}
    } else {
	$Added++;
	if ( $b =~ m/\.(bat|cmd|pl|sh|exe|dll)$/io )
	{ push(@git_addx, $b) } else { push(@git_add, $b) }
    }
}
close($storedhave);
close($have);
exit 1 if $err; # the error already reported
die "Nothing in the client $P4CLIENT\n" if !$cnt;

undef %p4_a_lc;

@git_del = grep { !exists($p4_index{$_}) } keys %git_index;
$Deleted = $#git_del + 1;

#foreach (keys %git_index)
#{ push(@git_del, $_) if !exists($p4_index{$_}) }

if ( $DRYRUN ) {
    print($#git_add+$#git_addx+ 2," files to add\n") if $VERBOSE;
    print map {" a $_\n"} @git_add if $VERBOSE > 2;
    print map {" a $_\n"} @git_addx if $VERBOSE > 2;
    print($#git_del+1," files to unreg\n") if $VERBOSE;
    print map {" d $_\n"} @git_del if $VERBOSE > 2;
    print($#git_upd+1," files to update\n") if $VERBOSE;
    print map {" u $_\n"} @git_upd if $VERBOSE > 2;
    print "added: $Added, unregd: $Deleted, updated: $Updated, ignored: $Ignored";
    print ", conflicts: $Conflicts" if $Conflicts;
    print "\n";
} else {
    if (@git_add || @git_addx) {
	print($#git_add+$#git_addx+ 2,
	      " files | git update-index --add -z --stdin\n")
	    if $VERBOSE;
	if (@git_add) {
	    open(GIT, '| git update-index --add --chmod=-x -z --stdin') or
		die "$0 git-update-index(add): $!\n";
	    print GIT map {print " a $_\n" if $VERBOSE > 1; "$_\0"} @git_add;
	    close(GIT);
	}
	if (@git_addx) {
	    open(GIT, '| git update-index --add --chmod=+x -z --stdin') or
		die "$0 git-update-index(add): $!\n";
	    print GIT map {print " a $_\n" if $VERBOSE > 1; "$_\0"} @git_addx;
	    close(GIT);
	}
    }

    if (@git_del) {
	print($#git_del+1," files | git update-index --remove -z --stdin\n")
	    if $VERBOSE;
	open(GIT, '| git update-index --force-remove -z --stdin') or
	    die "$0 git-update-index(del): $!\n";
	print GIT map {print " d $_\n" if $VERBOSE > 1; "$_\0"} @git_del;
	close(GIT);
    }

    if (@git_upd) {
	print($#git_upd+1," files | git update-index -z --stdin\n")
	    if $VERBOSE;
	open(GIT, '| git update-index -z --stdin') or
	    die "$0 git-update-index(upd): $!\n";
	print GIT map {print " u $_\n" if $VERBOSE > 1; "$_\0"} @git_upd;
	close(GIT);
    }

    print "added: $Added, unregd: $Deleted, updated: $Updated, ignored: $Ignored";
    print ", conflicts: $Conflicts" if $Conflicts;
    print "\n";
    git_p4_commit($git_head, $git_p4_head) if $AUTO_COMMIT;
}

exit 0;

sub run_or_exit {
    my $rc = system(@_);
    exit(127) if $rc & 0xff;
    exit(1) if $rc;
    return 0;
}

sub filtered {
    my $name = shift;
    study($name);
    my @path = split(/\/+/o, $name);
    my $dir = '';
    $name = '';

    foreach my $d (@path) {
	$name .= $d;
#	print STDERR "$dir: $name $d\n" if $v;
	foreach my $re (@{$gitignore_dirs{'/'}}) {
	    return 1 if $name =~ m/$re/;
	    return 1 if $d =~ m/$re/;
	}
	if ( length($dir) and exists($gitignore_dirs{$dir}) ) {
	    foreach my $re (@{$gitignore_dirs{$dir}}) {
		return 1 if $name =~ m/$re/;
		return 1 if $d =~ m/$re/;
	    }
	}
	$name .= '/';
	$dir = $name;
    }
#    print STDERR "$name not filtered\n" if $v;
    return 0;
}

sub read_filter_file {
    my @filts = ();
    my $file = shift;
    if ( open(my $if, '<', $file) ) {
	print "added ignore file $file\n" if $VERBOSE;
	$/ = "\n";
	while (my $l = <$if>) {
	    next if $l =~ /^\s*#/o;
	    next if $l =~ /^\s*$/o;
	    $l =~ s/[\r\n]+$//so;
	    $l =~ s/\./\\./go;
	    $l =~ s/\*/.*/go;
	    if ( $l =~ m/\// ) {
		$l = "^$l($|/)";
	    } else {
		$l = "(^|/)$l\$";
	    }
	    print " filter $l\n" if $VERBOSE > 1;
	    push(@filts, qr/$l/);
	}
	close($if);
    }
    return \@filts;
}

sub r_pystr
{
    my $fd = shift;
    my ($len,$str)=('','');
    my ($c,$rd,$b) = (4,0,'');
    while ($c > 0) {
	$rd = sysread($fd,$b,$c);
	warn("failed to read len: $!"), return undef if !defined($rd);
	warn("not enough data for len"), return undef if !$rd;
	$len .= $b;
	$c -= $rd;
    }
    $len = unpack('V',$len);
    while ($len > 0) {
	$rd = sysread($fd,$b,$len);
	warn("failed to read data: $!"), return undef if !defined($rd);
	warn("not enough data"), return undef if !$rd;
	$str .= $b;
	$len -= $rd;
    }
    return $str;
}

sub read_pydict_entry
{
    my $f = shift;
    my ($buf,$rd);
    FIL: while (1) {
	# object type identifier
	$rd = sysread($f, $buf, 1);
	last FIL if $rd == 0;
	warn("p4: object type: $!\n"),last if $rd != 1;
	# '{' is a python marshalled dict
	warn("p4: object type: not {\n"),last if $buf ne '{';
	my $ent = {};
	PAIR: while (1) {
	    my ($b,$key);
	    # key type identifier
	    $rd = sysread($f, $b, 1);
	    warn("p4: key type: $!\n"),last FIL if $rd != 1;
	    if ($b eq 's') { # length-prefixed string
		$key = r_pystr($f);
		warn("p4: key: $!\n"),last FIL if !defined($b);
	    } elsif ($b eq '0') { # NULL-element, end of entry
		last PAIR;
	    } else {
		die("p4: key type: not s (string)\n");
		last FIL;
	    }
	    # value type identifier
	    $rd = sysread($f, $b, 1);
	    warn("p4: $key value type: $!\n"),last FIL if $rd != 1;
	    if ($b eq 's') { # length-prefixed string
		$b = r_pystr($f);
		warn("p4: $key value: $!\n"),last FIL if !defined($b);
		$ent->{$key} = $b;
	    } elsif ($b eq 'i') { # 4-byte integer
		$rd = sysread($f, $b, 4);
		warn("p4: $key value data: $!\n"),last FIL if $rd != 4;
		$ent->{$key} = unpack('V',$b);
	    } else {
		warn("p4: $key value type: not s ($b)\n");
		last FIL;
	    }
	}
	return $ent;
    }
    return undef;
}

sub p4user_to_env {
    my $u = shift;
    $ENV{GIT_AUTHOR_NAME}  = '';
    $ENV{GIT_AUTHOR_EMAIL} = '';
    return if !defined($u);
    if (!exists($P4USERS{$u})) {
	my ($mail,$name) = grep {/^(Email|FullName):/} qx{p4 user -o $u};
	if ($? == 0 and defined($mail) and defined($name)) {
	    s/^\S+:	([^\r\n]*)\r?\n$/$1/so for ($mail,$name);
	    if (length($name) and length($mail)) {
		$P4USERS{$u} = {name=>$name, email=>$mail};
	    }
	}
    }
    if ($P4USERS{$u}) {
	$p4u = $P4USERS{$u};
	$ENV{GIT_AUTHOR_NAME}  = $p4u->{name};
	$ENV{GIT_AUTHOR_EMAIL} = $p4u->{email};
    }
    return 1;
}

sub p4_get_change {
    my ($fd,$p4);
    my $cl = shift;
    if (!open($fd, '>', "$GIT_DIR/p4/files")) {
	warn "p4/files: $!\n";
	return;
    }
    print $fd "-o\n$cl\n";
    close($fd);
    if (!open($p4, "p4 -x $GIT_DIR/p4/files change|")) {
	warn "p4: failed to read p4 change $cl: $!\n";
	return;
    }
    my @change = <$p4>;
    close($p4);
    return @change;
}

sub cl2msg {
    my $cl = shift;
    my($o1,$o2,$i);
    if(!open($o1, '>>', "$GIT_DIR/p4/msg")) {
	warn "p4/msg: $!\n";
	return;
    }
    binmode($o1);
    if(!open($o2, '>>', "$GIT_DIR/p4/p4msg")) {
	warn "p4/p4msg: $!\n";
	close($o1);
	return
    }
    binmode($o2);
    if(!open($i, '-|', "p4 describe -s $cl")){
	warn "p4 describe: $!\n";
	close($o1);
	close($o2);
	return
    }
    binmode($i);
    print $o1 "$cl: " if $FULL_DESC;
    print $o2 "$cl: ";
    my @a;
    my $u = undef;
    while (my $l = <$i>) {
	if ($l =~ /^Change \d+ by (\S+)@[^ ]* on ([^\r\n]*)/so) {
	    $u = $1;
	    $ENV{GIT_AUTHOR_DATE} = $2 if length($2);
	}
	last if $FULL_DESC < 2 and $l =~ /^\s*Affected files \.{3}\s*$/so;
	$l =~ s/\r?\n$//so;
	push @a, $l;
    }
    close($i);
    print $o2 substr($a[2],1),"\n"; # p4 side-branch commit description
    close($o2);
    # import branch commit description
    if ($FULL_DESC > 1) {
	# desc level 2+: keep the Change line
	print $o1 map {"$_\n"} (substr($a[2],1),"\n",@a);
    } else {
	# levels 0 and 1: remove the Change line
	print $o1 map { (length($_) ? substr($_,1):'')."\n" } @a[2..$#a];
    }
    close($o1);
    p4user_to_env($u);
}

# looks for p4import/ commit which points to the given reference
# returns undef if not found
sub git_find_p4info {
    my $branch = shift;
    if (!%P4IMPORT_HEADS) {
	foreach my $l (qx{git show-ref}) {
	    $P4IMPORT_HEADS{$2} = $1
		if $l =~ m!^([0-9a-f]{40}) refs/(p4import/[^\r\n]+)\r?\n!so;
	}
    }
    my ($commit,$parent,$p4commit,$p4parent);
    my $r = git_rev_parse($branch);
    return undef if !defined($r);
    while (my ($k,$p4head) = each %P4IMPORT_HEADS) {
	my $commit;
	do {
	    print "trying $k:$p4head\n" if $VERBOSE >3;
	    ($commit,$p4head) =
		grep { s/^parent ([0-9a-f]{40}).*/$1/s }
		qx{git cat-file commit $p4head};
	    $commit = $p4head = '' if $?;
	    return $p4head if $commit eq $r;
	} while (defined($p4head) && length($p4head));
    }
    warn "$branch is not imported from Perforce\n";
    return undef;
}

sub git_get_p4have {
    my $p4head = shift;
    my $p4have = undef;
    if (defined($p4head) and length($p4head) and
	open(my $f, '-|', "git cat-file blob $p4head:have")) {
	my $old = $/;
	$/ = "\0";
	my $cnt = 0;
	while(1) {
	    my $p4name = <$f>;
	    last if !defined($p4name);
	    $p4name =~ s/^.//so if $cnt; # remove \n
	    my $name = <$f>;
	    my $rev = <$f>;
	    last if !defined($name) or !defined($rev);
	    chop($p4name,$name,$rev);
	    ++$cnt;
	    if (defined($p4have)) {
		$p4have->{$p4name} = [$name,$rev];
	    } else {
		$p4have = {$p4name=>[$name,$rev]};
	    }
	}
	$/ = $old;
	close($f);
	print "loaded $cnt revisions from $p4head\n" if $VERBOSE;
    }
    return $p4have;
}

sub p4_get_have {
    print "reading state of $P4CLIENT\n" if $VERBOSE;
    my ($p4have, $fdi);
    open($fdi, "p4 -G -c $P4CLIENT -H $P4HOST -d $P4ROOT have|") or
	die "p4 have: $!\n";
    binmode($fdi);
    my ($err,$ent) = (0,undef);
    while (defined($ent=read_pydict_entry($fdi))) {
	if (defined($ent->{code}) and defined($ent->{data})) {
	    ++$err if $ent->{code} eq 'error';
	    print STDERR "p4: $ent->{code}: $ent->{data}\n";
	    next;
	}
	next if !defined($ent->{depotFile});
	next if !defined($ent->{clientFile});
	if (defined($p4have)) {
	    $p4have->{$ent->{depotFile}}=[$ent->{clientFile},$ent->{haveRev}];
	} else {
	    $p4have={$$ent->{depotFile}=>[$ent->{clientFile},$ent->{haveRev}]};
	}
    }
    close($fdi);
    return $p4have;
}

sub git_p4_init {
    my ($commit,$parent,$p4commit,$p4parent);
    my $HEAD = git_rev_parse('HEAD');
    $HEAD = '' if !defined($HEAD);
    my $p4head = git_rev_parse("refs/p4import/$P4CLIENT");
    $p4head = '' if !defined($p4head);
    die "No HEAD commit! Refusing to import.\n" if !length($HEAD);
    if (length($p4head)) {
	($commit,$p4parent) =
	    grep { s/^parent (.{40}).*/$1/s }
	    qx{git cat-file commit $p4head};
	$commit = $p4parent = '' if $?;
	$p4parent = '' if !defined($p4parent);
    } else {
	$commit = $p4parent = '';
    }
    while (($commit ne $HEAD) and length($p4parent)) {
	$p4head = $p4parent;
	($commit,$p4parent) =
	    grep { s/^parent (.{40}).*/$1/so }
	    qx{git cat-file commit $p4head};
	$commit = $p4parent = '' if $?;
	$p4parent = '' if !defined($p4parent);
	if ($VERBOSE and ($HEAD eq $commit)) {
	    print "found p4 import commit ";
	    system('git','name-rev',$p4head);
	}
    }
    if ($HEAD ne $commit) {
	$HEAD_FROM_P4 = 0;
	warn "Current HEAD is not from $P4CLIENT, doing full import\n";
    } else {
	$HEAD_FROM_P4 = 1;
    }
    my $p4have = undef;
    if (!$FULL_IMPORT and ($HEAD eq $commit) and length($p4head)) {
	if (open(my $f, '-|', "git cat-file blob $p4head:have")) {
	    my $old = $/;
	    $/ = "\0";
	    my $cnt = 0;
	    while(1) {
		my $p4name = <$f>;
		last if !defined($p4name);
		$p4name =~ s/^.//so if $cnt; # remove \n
		my $name = <$f>;
		my $rev = <$f>;
		last if !defined($name) or !defined($rev);
		chop($p4name,$name,$rev);
		++$cnt;
		if (defined($p4have)) {
		    $p4have->{$p4name} = [$name,$rev];
		} else {
		    $p4have = {$p4name=>[$name,$rev]};
		}
	    }
	    $/ = $old;
	    close($f);
	    print "loaded $cnt revisions from $p4head\n" if $VERBOSE;
	}
    }
    return ($HEAD, $p4head, $p4have);
}

sub get_one_line {
    my ($line) = qx{@_};
    return undef if $?;
    $line = '' if !defined($line);
    $line =~ s/\r?\n//gs;
    return $line;
}

sub git_rev_parse {
    return get_one_line('git', 'rev-parse', shift);
}

sub git_write_tree {
    my $sha1 = get_one_line('git','write-tree');
    return undef if !defined($sha1) or !length($sha1);
    return $sha1;
}

sub git_commit_tree {
    my $sha1 = get_one_line('git','commit-tree',@_);
    return undef if !defined($sha1) or !length($sha1);
    return $sha1;
}

sub git_hash_stdin {
    my $sha1 = get_one_line('git','hash-object','-t','blob','-w','--stdin');
    return undef if !defined($sha1) or !length($sha1);
    return $sha1;
}

sub git_hash_file {
    open(STDIN, '<', $_[0]) or die "$0: git_hash_file $_[0]: $!\n";
    return git_hash_stdin();
}

sub git_update_ref_directly {
    return system('git','update-ref',@_);
}
sub git_update_ref {
    my ($msg,$refname,$refval) = @_;
    if ($refname =~ m!^(ORIG_|FETCH_|MERGE_)?HEAD$!o) {}
    elsif ($refname =~ s!^/+!!o) {}
    elsif ($refname =~ m!^refs/!o) {}
    elsif ($refname =~ m!^(heads|remotes|tags|p4import)/!o) {
	$refname = "refs/$refname"
    } else { $refname = "refs/heads/$refname" }
    print STDERR "Updating $refname with $refval\n" if $VERBOSE > 1;
    return git_update_ref_directly('-m',$msg,$refname,$refval);
}

sub git_p4_commit {
    my ($HEAD, $p4head) = @_;
    my ($commit,$parent,$p4commit,$p4parent);

    my ($fdo,$fdi,$rc);
    $rc = system('git','diff-index','--exit-code','--quiet','--cached','HEAD');
    if ($rc == 0) {
	warn("No changes\n");
	return;
    }

    return if $DRYRUN;

    if (!@DESC && !$EDIT_COMMIT) {
	warn "$0: no commit description given\n";
	return;
    }

    my $p4x = "$GIT_DIR/p4/idx.tmp";
    unlink($p4x);

    $ENV{PAGER} = 'cat';

    if (!defined($SPEC) or !open(STDIN, '<', $SPEC)) {
	if ( $^O eq 'MSWin32' ) {
	    open(STDIN, '<', 'NUL') or die "$SPEC: $!\n";
	} else {
	    open(STDIN, '<', '/dev/null') or die "$SPEC: $!\n";
	}
    }
    my $p4spec = git_hash_stdin();
    die "Failed to store $SPEC in git repo\n" if !defined($p4spec);

    my $p4clnt = git_hash_file("$GIT_DIR/p4/client.def");
    die "Failed to save mappings of $P4CLIENT in git repo" if !defined($p4clnt);

    if (!defined($P4HAVE_FILE)) {
	print "reading state of $P4CLIENT\n" if $VERBOSE;
	$P4HAVE_FILE = "$GIT_DIR/p4/have";
	open($fdo, '>', $P4HAVE_FILE) or die "p4/have: $!\n";
	binmode($fdo);
	open($fdi, "p4 -G -c $P4CLIENT -H $P4HOST -d $P4ROOT have|") or
	    die "p4 have: $!\n";
	binmode($fdi);
	my ($cnt,$err,$ent) = (0,0,undef);
	while (defined($ent=read_pydict_entry($fdi))) {
	    if (defined($ent->{code}) and defined($ent->{data})) {
		++$err if $ent->{code} eq 'error';
		print STDERR 'p4: '.$ent->{code}.': '.$ent->{data}."\n";
		next;
	    }
	    next if !defined($ent->{depotFile});
	    next if !defined($ent->{clientFile});
	    ++$cnt;
	    print $fdo "$ent->{depotFile}\0",
		       "$ent->{clientFile}\0",
		       "$ent->{haveRev}\0\n";
	}
	close($fdi);
	close($fdo);
	exit 1 if $err; # the error already reported
	die "The client $P4CLIENT has nothing\n" if !$cnt;
    }

    my $p4have = git_hash_file($P4HAVE_FILE);
    die "Failed to save state of $P4CLIENT in git repo" if !defined($p4have);

    #
    # Prepare commit messages
    #
    unlink("$GIT_DIR/p4/msg", "$GIT_DIR/p4/p4msg");
    open($fdo, '>', "$GIT_DIR/p4/msg"); close($fdo);
    open($fdo, '>', "$GIT_DIR/p4/p4msg"); close($fdo);

    foreach my $i (@DESC) {
	$i =~ s/^(.)//o;
	if ('c' eq $1) {
	    print "reading changes for $i\n" if $VERBOSE;
	    cl2msg($i);
	} elsif ('f' eq $1) {
	    my($o1,$o2,$i);
	    if (open($o1, '>>', "$GIT_DIR/p4/msg")) {
		if (open($o2, '>>', "$GIT_DIR/p4/p4msg")) {
		    if (open($i, '<', $i)) {
			my $n = 0;
			while(<$i>) {
			    $n++;
			    print $o1 $_;
			    print $o2 $_ if $n == 1;
			}
			close($i);
		    }
		    close($o2);
		}
		close($o1);
	    }
	} elsif ('4' eq $1) {
	    print "reading changes for $i\n" if $VERBOSE;
	    my $change = get_one_line('p4', 'changes', '-m1', $i);
	    if (!defined($change) or $change !~ m/\s+(\d+)\s/) {
		die "$i does not resolve into a change number\n";
	    }
	    cl2msg($1);
	}
    }
    system("$editor $GIT_DIR/p4/msg") if $EDIT_COMMIT;

    # copy mirror-branch commit message into side-branch
    # commit message if no other description were given.
    if (!-s "$GIT_DIR/p4/p4msg") {
	open($fdi, '<', "$GIT_DIR/p4/msg") or die "$GIT_DIR/p4/msg: $!\n";
	sysread($fdi,$buf,-s "$GIT_DIR/p4/msg");
	close($fdi);
	open($fdo, '>>', "$GIT_DIR/p4/p4msg") or die "$GIT_DIR/p4/p4msg: $!\n";
	syswrite($fdo,$buf);
	close($fdo);
    }

    #
    # Store the imported file data
    #

    if ($VERBOSE < 2) {
	if ( $^O eq 'MSWin32' ) { open(STDERR, "NUL") }
	else { open(STDERR, "/dev/null") }
    }

    my $remove_merge_heads = 0;
    my $tree = git_write_tree();
    die "failed to write current tree\n" if !defined($tree);
    open(STDIN, '<', "$GIT_DIR/p4/msg") or die "p4/msg: $!\n";
    if (length($HEAD)) {
	my @mergeparents;
	if (open($fd, '<', "$GIT_DIR/MERGE_HEAD")) {
	    while(<$fd>) {
		s/\r?\n$//gs;
		push(@mergeparents, '-p', $_) if /^[0-9a-f]{40}$/;
	    }
	    close($fd);
	}
	$commit = git_commit_tree($tree, '-p', $HEAD, @mergeparents);
	die "failed to commit the merged tree\n" if !defined($commit);
	$remove_merge_heads = 1 if @mergeparents;
    } else {
	$commit = git_commit_tree($tree);
	die "failed to commit current tree\n" if !defined($commit);
    }
    print "current tree stored in commit $commit\n" if $VERBOSE;

    #
    # Storing import control data
    #
    $ENV{GIT_INDEX_FILE} = $p4x;
    open($fdo, '|-', 'git update-index --add --index-info') or
	die "could not start git update-index\n";
    binmode($fdo);
    print $fdo "100644 $p4spec\tspec\n";
    print $fdo "100644 $p4clnt\tclient\n";
    print $fdo "100644 $p4have\thave\n";
    close($fdo);
    if($?) {
	die "Failed to store $SPEC in p4import index and git repo\n".
	    "Failed to save mappings of $P4CLIENT in p4import index and git repo\n".
	    "Failed to save state of $P4CLIENT in p4import index and git repo\n"
    }
    my $p4tree = git_write_tree();
    die "Failed to store $SPEC (tree) in git repo\n" if $?;

    # Bind import control data to the file data
    open(STDIN, '<', "$GIT_DIR/p4/p4msg") or die "p4/p4msg: $!\n";
    $p4commit = length($p4head) ?
	git_commit_tree($p4tree, '-p', $commit, '-p', $p4head):
	git_commit_tree($p4tree, '-p', $commit);
    die "Failed to store $SPEC (commit) in git repo\n" if $?;

    # Finishing touches: update references
    if (!$DRYRUN) {
	git_update_ref('backup ref of current branch','/p4/backup-HEAD','HEAD');
	git_update_ref('backup ref of p4import',
		       '/p4/backup-p4import',"refs/p4import/$P4CLIENT");
	$rc = git_update_ref('data of p4import','HEAD',$commit);
	die "Failed to update HEAD\n" if $rc;
	unlink("$GIT_DIR/MERGE_HEAD") if $remove_merge_heads;
	$rc = git_update_ref('p4import',"p4import/$P4CLIENT",$p4commit);
	die "Failed to store $SPEC (reference) in git repo\n" if $rc;
    }

    $ENV{GIT_PAGER} = 'cat';
    if ($VERBOSE) {
	print STDOUT (grep {s/\r?\n//gs;s/.*?\s//} qx{git name-rev refs/p4import/$P4CLIENT}), ":\n";
	system('git','log','--max-count=1','--pretty=format:%h %s%n',$p4commit);
    }
    print STDOUT (grep {s/\r?\n//gs;s/.*?\s//} qx{git name-rev HEAD}),":\n";
    system('git','log','--max-count=1','--pretty=format:%h %s%n',$commit);
}

sub import_p4_dir {
    my ($p4path, $local_path, $revrange, $parent, $branch) = @_;
    my ($fd, $ent, $error, $rc) = (undef,undef,0,0);

    print "Running changes\n" if $VERBOSE;

    if (open($fd, '>', "$GIT_DIR/p4/files")) {
	print "looking for changes $p4path/...$revrange\n" if $VERBOSE > 1;
	print $fd "$p4path/...$revrange\n";
	close($fd);
    } else {
	die "$0: p4/files: $!\n"
    }
    open(STDIN, '<', "$GIT_DIR/p4/files") or die "$0: p4/files: $!\n";
    die "$0: changes $p4path: $!\n" if !open($fd, '-|', 'p4 -G -x - changes');
    my %CHANGES = ();
    while (defined($ent = read_pydict_entry($fd))) {
	next if $error;
	if ($ent->{code} eq 'error') {
	    warn "$0: p4: $ent->{data}\n";
	    $error = 1;
	    next;
	}
	print "change $ent->{change}\n" if $VERBOSE > 1;
	$CHANGES{$ent->{change}} = {
	    change =>$ent->{change},
	    desc   =>'', # have to read the desc anyway with describe
	    mtime  =>$ent->{'time'},
	    user   =>$ent->{user},
	    files  =>[],
	};
    }
    close($fd);
    warn("$0: nothing found\n"), exit(0) if !%CHANGES;
    exit 1 if $error;

    print "Running describe for ".scalar(keys %CHANGES)." change lists\n"
	if $VERBOSE;

    if (open($fd, '>', "$GIT_DIR/p4/files")) {
	print $fd map { "$_\n"} sort {$a <=> $b} keys %CHANGES;
	close($fd);
    } else {
	die "$0: p4/files: $!\n"
    }
    open(STDIN, '<', "$GIT_DIR/p4/files") or die "$0: p4/files: $!\n";
    die "$0: describe: $!\n" if !open($fd, '-|', 'p4 -G -x - describe');
    while (defined($ent = read_pydict_entry($fd))) {
	next if $error;
	if ($ent->{code} eq 'error') {
	    warn "$0: p4: $ent->{data}\n";
	    $error = 1;
	    next;
	}
	$CHANGES{$ent->{change}}->{desc} = $ent->{desc};
	$CHANGES{$ent->{change}}->{files} = {};
	for (my $i=0;; ++$i) {
	    my $fn = $ent->{"depotFile$i"};
	    last if !defined($fn);
	    next if $fn !~ m!^$p4path(/|$)!;
	    $CHANGES{$ent->{change}}->{files}->{$fn} = {
		action => $ent->{"action$i"},
	    };
	    # $ent->{"type$i"} : text, binary
	}
    }
    close($fd);
    exit 1 if $error;

    print "Reading file data and creating git history\n" if $VERBOSE;

    # Prepare clean index under the local path
    $ENV{GIT_INDEX_FILE} = "$GIT_DIR/p4/idx.tmp";
    unlink($ENV{GIT_INDEX_FILE});
    if (length($parent)) {
	$rc = system('git', 'read-tree', '-i', '--reset', $parent);
	die "git read-tree $parent\n" if $rc;
	$rc = system('git','update-index','--force-remove','--',$local_path);
	die "git update-index $local_path\n" if $rc;
    }
    # Read file data for each change list
    my $first = undef;
    my $ch;
    foreach my $k (sort {$a <=> $b} keys %CHANGES) {
	$ch = $CHANGES{$k};
	print "$k\n" if $VERBOSE > 1;
	# Read the full tree for the first change number
	if (open($fd, '>', "$GIT_DIR/p4/files")) {
	    if (!defined($first)) {
		print $fd "$p4path/...\@$k\n";
		$first = $k;
	    } else {
		foreach my $f (keys(%{$ch->{files}})) {
		    print $fd "$f\@$k\n";
		}
	    }
	    close($fd);
	} else {
	    die "$0: p4/files: $!\n"
	}
	open(STDIN, '<', "$GIT_DIR/p4/files") or die "$0: p4/files: $!\n";

	print "Reading file data for $k\n" if $VERBOSE > 1;

	die "$0: print: $!\n" if !open($fd, '-|', 'p4 -G -x - print');
	my $tmpfile = "$GIT_DIR/p4/cp.tmp";
	while (defined($ent = read_pydict_entry($fd))) {
	    next if $error;
	    if ($ent->{code} eq 'error') {
		warn "$0: p4: $ent->{data}\n";
		$error = 1;
		next;
	    }
	    if ($ent->{code} eq 'binary') {
		if (!defined($f)) {
		    warn "$0: file data without stat info\n";
		    $error = 1;
		    next;
		}
		$f->{size} += length($ent->{data});
		if (length($ent->{data})) {
		    if (!defined(syswrite($f->{tmpfd}, $ent->{data}))) {
			warn "$f->{depotFile}: $tmpfile: $!\n";
			close($f->{tmpfd});
			$f = undef;
			$error = 1;
			next;
		    }
		} else {
		    # FILE FINISHED IF AN EMPTY BINARY PACKET RECEIVED
		    close($f->{tmpfd});
		    my $fn = $f->{depotFile};
		    # put file data into git repo
		    my $tmpsha1 = git_hash_file($tmpfile);
		    die "Failed to save $fn in git repo\n" if !defined($tmpsha1);
		    unlink($tmpfile);
		    $f->{sha1} = $tmpsha1;

		    print "$k\t$tmpsha1\t$f->{size}\t$fn\n" if $VERBOSE > 2;
		    $ch->{files}->{$fn} = $f;

		    delete $f->{depotFile}; # cleanup
		    delete $f->{tmpfd}; # cleanup
		    $f = undef;
		}
		next;
	    }
	    if ($ent->{code} eq 'stat') {
		die "$0: file $f->{depotFile} truncated\n" if defined($f);
		die "$ent->{depotFile}: the leading path not expected\n"
		    if substr($ent->{depotFile},0,length($p4path)) ne $p4path;
		die "$ent->{depotFile}\@$ent->{change}: the change not expected\n"
		    if $ent->{change} > $k;

		my $prev = $ch->{files}->{$ent->{depotFile}};
		$f = {
		    action => defined($prev->{action}) ? $prev->{action}:'add',
		    change => $ent->{change},
		    depotFile => $ent->{depotFile},
		    mtime => $ent->{'time'},
		    size => 0,
		};
		my $ft;
		open($ft,'>',$tmpfile) or die "$tmpfile: $!\n";
		$f->{tmpfd} = $ft;
	    }
	}
	close($fd);
	exit 1 if $error;
	my $modcnt = 0;
	my @delfiles = ();
	my $fdo = undef;
	open($fdo, '|-', 'git update-index -z --replace --add --index-info') or
	    die "could not start git update-index\n";
	binmode($fdo);
	while (my ($fn,$f) = each %{$ch->{files}}) {
	    if (substr($fn,0,length($p4path)) ne $p4path) {
		die "$fn: the leading path not expected\n";
	    }
	    my $locfile = $fn;
	    substr($locfile,0,length($p4path)) = $local_path;
	    $locfile =~ s!^/+!!o;
	    push(@delfiles, $locfile), next if $f->{action} eq 'delete';
	    print "\t$f->{sha1}\t$locfile\n" if $VERBOSE > 1;
	    my $mode = '100644';
	    $mode = '100755' if $locfile =~ /\.(exe|bat|cmd|pl|dll|so)$/io;
	    print $fdo "$mode $f->{sha1}\t$locfile\0";
	    ++$modcnt;
	}
	close($fdo);
	die "failed to build the trees in $local_path for $k\n" if $?;
	if (@delfiles) {
	    open($fdo, '|-', 'git update-index -z --force-remove --stdin') or
		die "could not start git update-index to remove files\n";
	    binmode($fdo);
	    print $fdo map { "$_\0" } @delfiles;
	    close($fdo);
	    print map { "\tdelete $_\n" } @delfiles if $VERBOSE > 1;
	    die "failed to clean the trees in $local_path for $k\n" if $?;
	    $modcnt += scalar @delfiles;
	}
	my $tree = git_write_tree();
	die "Failed to store tree of $k in git repo\n" if !defined($tree);
	print "\ttree of $k written as $tree\n" if $VERBOSE > 1;
	# prepare commit
	open($fdo,'>',"$GIT_DIR/p4/msg") or die "p4/msg: $!\n";
	print $fdo "$k: $ch->{desc}";
	close($fdo);
	p4user_to_env($ch->{user});
	$ENV{GIT_AUTHOR_DATE} = $ch->{mtime};
	open(STDIN, '<', "$GIT_DIR/p4/msg") or die "p4/msg (commit): $!\n";
	my $commit = length($parent) ?
	    git_commit_tree($tree, '-p', $parent): git_commit_tree($tree);
	die "Failed to commit $k in git repo\n" if !defined($commit);
	print "$k committed as $commit";
	print " ($modcnt modification".($modcnt==1?'':'s').')' if $VERBOSE;
	print "\n";
	$parent = $commit;
    }
    if (!$DRYRUN and length($parent)) {
	git_update_ref("backup ref of $branch", "/p4/backup-$branch",$branch)
	    if defined($branch);
	$branch = '/p4/IMPORT' if !defined($branch);
	my $rc = git_update_ref("p4 import $branch", $branch, $parent);
	die "failed to update $branch with $parent\n" if $rc;
	$branch =~ s!^/+!!;
	print "Branch '$branch' updated with state from $ch->{change}\n";
	$ENV{GIT_PAGER} = 'cat';
	system('git','log','--max-count=1','--pretty=format:%h %s%n',$branch);
    }
}

sub git_show_diffs {
    my $sep = $/;
    $/="\0";
    my ($show, $cnt) = (0, 0);
    if (open(F, '-|', 'git diff-files -r --name-only -z')) {
	while (<F>) {
	    my $c = chop;
	    $_ .= $c if $c ne "\0";
	    print "Changed files:\n" if !$show;
	    print " $_\n";
	    $show = 1;
	    $cnt++;
	}
	close(F);
    }
    if (open(F, '-|', 'git diff-index --cached -r -z HEAD')) {
	$show = 0;
	my ($diff, $info) = (0, 1);
	while (<F>) {
	    my $c = chop;
	    $_ .= $c if $c ne "\0";
	    if ($info) {
		next if !/^:(\d{6}) (\d{6}) ([0-9a-f]{40}) ([0-9a-f]{40}) ./o;
		# show only content changes, p4 does not support exec-bit anyway
		$diff = $3 ne $4;
	    } elsif ($diff) {
		print "Changes between index and HEAD:\n" if !$show;
		print " $_\n";
		$show = 1;
		$cnt++;
	    }
	    $info = !$info;
	}
	close(F);
    }
    $/ = $sep;
    return $cnt;
}

sub git_sync_to {
    my $rc;
    my $branch = shift;
    if (!$FORCE) {
	$rc = system('git', 'diff', '--quiet');
	warn("There are changes in the working directory. Refusing to sync\n")
	    if $rc;
	$rc = system('git', 'diff', '--quiet', '--cached');
	warn("There are changes in the index. Refusing to sync\n") if $rc;
    }
    my $p4head = git_find_p4info($branch);
    print "found p4 import commit $p4head for $branch\n"
	if ($VERBOSE > 1 or $DRYRUN) and defined $p4head;
    return 0 if $DRYRUN and defined $p4head;
    my $p4have = git_get_p4have($p4head);
    die "$p4head has no p4 data\n" if !defined($p4have);
    my ($fd,$fdo);
    die "reading client $P4CLIENT: $!\n"
	if !open($fd, '-|', "p4 -G client -o $P4CLIENT");
    binmode($fd);

    my %cdata = (Client=>'', Description=>'', LineEnd=>'', Options=>'');

    while (defined($ent = read_pydict_entry($fd))) {
	next if $error;
	if ($ent->{code} eq 'error') {
	    warn "p4 client $P4CLIENT: $ent->{data}\n";
	    $error = 1;
	    next;
	}
	foreach my $k (keys %cdata) {
	    $cdata{$k} = $ent->{$k} if defined $ent->{$k};
	}
    }
    close($fd);
    die "client.def: $!\n" if !open($fdo, '>', "$GIT_DIR/p4/client.def");
    binmode($fdo);
    print $fdo "Client: $P4CLIENT\n\n";
    print $fdo "Description:\n";
    $cdata{Description} =~ s/\n/\n\t/gs;
    $cdata{Description} =~ s/\t$//s;
    print $fdo "\t$cdata{Description}\n";
    my $wd = cwd();
    $wd =~ s/\\/\//go;
    print $fdo "Root:\t$wd\n\n";
    print $fdo "LineEnd:\t$cdata{LineEnd}\n\n";
    print $fdo "Options:\t$cdata{Options}\n\n";
    print $fdo "View:\n";
    print $fdo
	map {
	    my $fn = $p4have->{$_}->[0];
	    $fn =~ s!^//[^/]*/!//$P4CLIENT/!o;
	    s/([ \t"'@*#%])/sprintf("%%%02x",ord($1))/geo for ($_,$fn);
	    "\t$_ $fn\n"
	} sort { $a cmp $b }
	keys %{$p4have};
    close($fdo);
    print "loading client $P4CLIENT\n";
    open(STDIN, '<', "$GIT_DIR/p4/client.def") or die "client.def: $!\n";
    run_or_exit('p4', 'client', '-i');
    die "files: $!\n" if !open($fdo, '>', "$GIT_DIR/p4/files");
    print $fdo
	map {
	    my $fn = $_;
	    s/([ \t"'@*#%])/sprintf("%%%02x",ord($1))/geo;
	    "$_#$p4have->{$fn}->[1]\n"
	} sort { $a cmp $b }
	keys %{$p4have};
    close($fdo);
    print "syncing client $P4CLIENT\n";
    open(STDIN, '<', "$GIT_DIR/p4/files") or die "p4/files: $!\n";
    run_or_exit('p4', '-c', $P4CLIENT, '-H', $P4HOST, '-d', $P4ROOT,'-x', '-', 'sync');

    return defined($p4head) ? 0: 1;
}

sub git_p4_tag_HEAD {
    my $cl = shift;
    my @change = p4_get_change($cl);
    return 0x100 if !@change;
    my $d=0;
    my ($first_line) = grep {
	if ($d==1) {
	    if (!/^\s*[\r\n]*$/so) { $d=0; 1 }
	    else { 0 }
	} else {
	    $d=1 if /^Description:/o; 0
	}
    } @change;
    my $fd;
    if (!open($fd, '>', "$GIT_DIR/p4/desc")) {
	warn "p4/desc: $!\n";
	return 0x100;
    }
    unshift(@change, '');
    $first_line =~ s/^\s+//;
    unshift(@change, $first_line);
    print $fd map {s/\r?\n$//so;"$_\n"} @change;
    close($fd);
    my $rc = system('git', 'tag', '-f', '-F', "$GIT_DIR/p4/desc", "p4/$cl");
    print "HEAD tagged p4/$cl\n" if !$rc;
    return $rc;
}

sub git_p4_merge {
    my ($source_head,$do_merge,$auto_submit) = @_;
    my $rc = system('git', 'diff-files', '--quiet');
    exit(127) if $rc & 0xff; # error starting the program
    die "There are changes in $P4ROOT. Clean them up first.\n" if $rc;
    $source_head = 'HEAD' if !defined($source_head);
    my $mergehead = git_rev_parse($source_head);
    exit(1) if !defined($mergehead) or !length($mergehead);
    my $fast_forward = 1;
    # Check if the given reference is a direct descendant of current branch
    if ($source_head ne 'HEAD') {
	my $sha1 = get_one_line('git', 'rev-list', '--max-count=1', "$mergehead..HEAD");
	exit 1 if !defined($sha1);
	if (length($sha1) and defined($sha1) and $sha1 =~ /^[0-9a-f]{40}\b/) {
	    my $msg = "HEAD does not fast-forward to $source_head\n";
	    $do_merge ? warn("Warning: $msg"): die("Fatal: $msg");
	    $fast_forward = 0;
	}
    }
    print "Checking out $source_head ($mergehead) for p4 edit\n" if $VERBOSE;
    my $cnt;
    my @files = ();
    my $sep = $/;
    $/="\0";
    if (open(F, '-|', "git diff-index -R --cached -r -z $mergehead")) {
	my ($diff, $info, $M) = (0, 1, '');
	while (<F>) {
	    my $c = chop;
	    $_ .= $c if $c ne "\0";
	    if ($info) {
		next if !/^:\d{6} \d{6} ([0-9a-f]{40}) ([0-9a-f]{40}) (\w+)/o;
		# use only content changes, p4 does not support exec-bit
		$diff = $1 ne $2;
		$M = $3; # change type marker
	    } elsif ($diff) {
		print "$M $_\n" if $VERBOSE;
		die "File contains characters which p4 cannot support\n"
		    if /[\n@#%*]/s;
		push @files, "$M$_";
		$cnt++;
	    }
	    $info = !$info;
	}
	close(F);
    }
    $/ = $sep;
    if (!$cnt) {
	warn "$0: No content changes found between HEAD and $source_head";
	return 0;
    }
    # Create a new changelist
    my $p4;
    open($p4, "p4 -c $P4CLIENT -H $P4HOST -d $P4ROOT change -o|") or
	die "$0: failed to create changelist\n";
    my @desc = map {s/\r?\n$//so; $_} <$p4>;
    close($p4);

    my $desc_pos = 0;
    foreach (@desc) {
	++$desc_pos;
	last if /^Description:/o;
    }
    my $editfd;
    die "$GIT_DIR/p4/desc.txt: $!\n"
	if !open($editfd, '>', "$GIT_DIR/p4/desc.txt");
    my $range = "..$mergehead";
    # because I have no information about what I am merging with:
    # this is the case when a push modified HEAD.
    $range = "${mergehead}^..$mergehead" if $source_head eq 'HEAD';
    if (open(my $fd, '-|', "git log $range")) {
	while(<$fd>) {
	    # I believe it is not possible to save this information
	    # in Perforce. They are primitive
	    next if /^(commit |Author:|Date:)/;
	    s/\r?\n$//so;
	    next if !length($_); # header/message separator line
	    s/^\s{4}//o;
	    print $editfd "$_\n" if defined($editfd);
	}
	close($fd);
    }
    print $editfd map { /^\s?(.*)/o;"$1\n" } @desc[$desc_pos..$#desc]; 
    close($editfd);

    system("$editor $GIT_DIR/p4/desc.txt") if $EDIT_COMMIT;
    die "$GIT_DIR/p4/desc.txt: $!\n"
	if !open($editfd, '<', "$GIT_DIR/p4/desc.txt");
    my @tmpdesc = <$editfd>;
    die "No change description, not merging.\n"
	if !grep {!m/^(\s|[\r\n])*$/so} @tmpdesc;
    splice(@desc, $desc_pos, $#desc + 1 - $desc_pos,
	   map {s/\r?\n$//so;" $_"} @tmpdesc);
    close($editfd);

    open($p4, '>', "$GIT_DIR/p4/changelist") or
	die "$GIT_DIR/p4/changelist: $!\n";
    print $p4 map {"$_\n"} @desc;
    close($p4);

    return 0 if $DRYRUN;

    open(STDIN, '<', "$GIT_DIR/p4/changelist") or
	die "$GIT_DIR/p4/changelist: $!\n";
    open($p4, "p4 -c $P4CLIENT -H $P4HOST -d $P4ROOT change -i|") or
	die "$0: failed to create changelist\n";
    my ($newchange) = grep {s/^Change (\d+) created\b.*/$1/so} <$p4>;
    close($p4);
    print "Checking out P4 files in changelist $newchange\n" if $VERBOSE;
    sub runp4 {
	run_or_exit('p4','-c',$P4CLIENT,'-H',$P4HOST,'-d',$P4ROOT,@_);
    }
    # open files for edit
    $cnt = 0;
    open($p4, '>', "$GIT_DIR/p4/files") or die "$GIT_DIR/p4/files: $!\n";
    print $p4 "-c\n$newchange\n";
    print $p4 (map {++$cnt; substr($_,1)."\n"} grep {/^M/} @files);
    close($p4);
    runp4('-x',"$GIT_DIR/p4/files", 'edit') if $cnt;
    exit(1) if $?;

    $cnt = 0;
    open($p4, '>', "$GIT_DIR/p4/files") or die "$GIT_DIR/p4/files: $!\n";
    print $p4 "-c\n$newchange\n";
    print $p4 (map {++$cnt; substr($_,1)."\n"} grep {/^A/} @files);
    close($p4);
    runp4('-x',"$GIT_DIR/p4/files", 'add') if $cnt;
    exit(1) if $?;

    $cnt = 0;
    open($p4, '>', "$GIT_DIR/p4/files") or die "$GIT_DIR/p4/files: $!\n";
    print $p4 "-c\n$newchange\n";
    print $p4 (map {++$cnt; substr($_,1)."\n"} grep {/^D/} @files);
    close($p4);
    runp4('-x',"$GIT_DIR/p4/files", 'delete') if $cnt;
    exit(1) if $?;

    # p4 modifies working directory on checkout, stupid thing
    system('git', 'update-index', '--refresh');
    if ($do_merge) {
	run_or_exit('git', 'merge', '--no-commit', $mergehead);
    } else {
	run_or_exit('git', 'read-tree', '-m', '-u', $mergehead);
	print "The state of $source_head($mergehead) is checked out.\n";
    }
    if (!$auto_submit) {
	print "A p4 changelist $newchange is prepared.\n";
	print "To submit:\n\tp4 submit -c $newchange\n";
	print "To commit:\n\tgit-p4-import.bat -y -C SUBMITTED_CHANGE\n"
	    if !$fast_forward;
    } else {
	die "p4/files: $!\n" if !open($p4, '>', "$GIT_DIR/p4/files");
	print $p4 "-c\n$newchange\n";
	close($p4);
	my @res;
	my $subchange = undef;
	@res = qx{p4 -c $P4CLIENT -H $P4HOST -d $P4ROOT -x $GIT_DIR/p4/files submit};
	die "p4: failed to run p4 submit -c $newchange\n" if $?;
	foreach(@res) {
	    print;
	    if (/^\s*Change (\d+) submitted\..*/s) {
		$subchange = $1;
	    }
	    elsif (/^\s*Change (\d+) renamed change (\d+) and submitted.*/s) {
		$subchange = $2 if $1 eq $newchange;
	    }
	}
	die "p4 submit behaved unexpectedly\n".
	    "To tag the current HEAD use \n".
	    "\tgit-p4-import --p4-tag <the new change>\n"
	    if !defined($subchange);
	$subchange =~ s/\r?\n$//so;
	print "Submitted as $subchange\n";
	if ($fast_forward) {
	    $rc = git_p4_tag_HEAD($subchange);
	    exit(127) if $rc & 0xff;
	    exit(1) if $rc;
	    print "Tagged as p4/$subchange\n";
	} else {
	    print "The merged tree is left in $P4ROOT.\n";
	    print "To commit:\n\tgit-p4-import.bat -y -C $subchange\n";
	}
    }
    return 0;
}

__END__
:__end_of_file__


[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux