#!/usr/local/bin/perl # -*-Fundamental-*- # $Id: //guest/richard_geiger/utils/p4cli#4 $ # OK, here's a stab at functions to allow archiving and recreation of # a p4 client state. # # Basically, it's # # p4cli snap > ARCHIVE # # to save client state to the file ARCHIVE; # # p4cli restore < ARCHIVE # # to restore it, or # # p4cli -c NEWCLIENT clone < ARCHIVE # # (in an emptry dir, which will become the root of a new client) to # create a clone of the original. # # Since we run this as part of a larger wrapper environment at # NetApp, I've stubebd in a "standalone_main" function which should # allow it to run in standard p4 environments, but this has not been # heavily tested yet. # require 5.0; # This file leads two lives, since it is used both: 1) as a part of # the NetApp "b4p4" wrapper, and 2) as a standalone, "generic" tool. # The following stuff lets it figure out which guise it's being used # in, and, in the case of the standalone mode, sets up the stuff # needed to work in the absence of the b4p4 environment. # if (! defined($Myname)) { &standalone_main; } else { $NetApp = 1; } sub standalone_main { $Standalone = 1; # We are running standalone/generic # sub dirname { my ($dir) = @_; $dir =~ s%^$%.%; $dir = "$dir/"; if ($dir =~ m%^/[^/]*//*$%) { return "/"; } if ($dir =~ m%^.*[^/]//*[^/][^/]*//*$%) { $dir =~ s%^(.*[^/])//*[^/][^/]*//*$%$1%; { return $dir; } } return "."; } $P4HOST_def = "p4netapp"; $P4PORT_def = "$P4HOST_def:1666"; $SERVERHOME = "/u/p4"; sub nobin { print "$Myname: can't find a p4 binary for this \"$Osname/$Osvers\" host.\n"; exit 1; } if (-d "/u/p4/P4VERS") { # Then assume we're at NetApp, and have the various installed # real p4 binaries here... (Note: I've got "P4VERS" as the # switch link... this is the one that selects the client # version appropriate for use with the server at # public.perforce.com:1666, since it's likely that any work done # on this Standalone mode is for posting there. # ($Osname, $Hostname, $Osvers) = split(/\s+/, `/bin/uname -a`); if ($Osname eq "SunOS") { if ($Osvers =~ /^5\./) { $bin = "solaris"; } elsif ($Osvers =~ /^4\.1\./) { $bin = "sunos"; } else { &nobin; } } elsif ($Osname eq "OSF1" && $Osvers =~ /^V4\./) { $bin = "osf"; } elsif ($Osname eq "Linux") { $bin = "linuxx86"; } elsif ($Osname eq "HP-UX") { $bin = "hpux"; } else { &nobin; } $P4 = "/u/p4/VERS/bin.$bin/p4"; $NetApp = 1; } else { $NetApp = 0; $P4 = "/usr/local/bin/p4"; } use Carp; $| = 1; $Mypath = $0; ($Myname = $Mypath) =~ s%^.*/%%; $Myrealpath = $0; while (-l $Myrealpath) { $Myrealpath = readlink $Myrealpath; if (! defined($Myrealpath)) { print STDERR "$Myname: \"readlink $Myrealpath\" failed: $!.\n"; exit 1; } } $Myname = "${Myname}"; $Myrealdir = &dirname($Myrealpath); $Here = `/bin/pwd`; chop $Here; $Usage = <<USAGE; $Myname: usage: $Myname snap > _archive_ $Myname restore [-f|-n] < _archive_ $Myname -c _clientname_ clone [-f|-n] < _archive_ $Myname norm < _archive_ $Myname help USAGE sub usage { print STDERR "$Usage"; exit 1; } sub help { print <<LIT; $Usage $Myname snap Writes a "client state" archive on the standard output. Intended for use with "p4 cli_clone" - see below. $Myname -c _client_ clone [-f|-n] Reads a snap archive from standard input, and reproduces the state represented by the archive, in a newly-created client "_client_". With the "-f" option, unopened files are flushed (not synced), into the new client. With the "-n" option, unopened files are not synced or flushed into the new client. $Myname restore [-f|-n] In an existing client, reads a snap archive from standard input, and reproduces the state represented by the archive. (See above for a description of -f and -n). $Myname norm Reads a snap archive on the standard input, and rewrites it, in a "normalized" form on the standard output. This exists mainly to allow easy verification that two snapshot archive are equivalent, since normalized snapshots can be diffed (the only differences between two equivalent normalized snapshots will be timestamps in the cpio arhcive header fields, which are easily recognizable). Note: "$Myname restore/clone" do not yet clone state with respect to multiple pending numbered changelists in the client; all files will be opened into the default change list of the new client. LIT close P; exit 1; } while ($#ARGV >= 0) { # General options & switches # if ($ARGV[0] =~ /^-[cpu]$/) { # Handle p4 -c -p & -u as expected... # my $opt = $ARGV[0]; shift @ARGV; if ($#ARGV < 0) { &usage; } if ($opt eq "-c") { $ENV{"P4CLIENT"} = $ARGV[0]; } elsif ($opt eq "-p") { $ENV{"P4PORT"} = $ARGV[0]; } elsif ($opt eq "-u") { $ENV{"P4USER"} = $ARGV[0]; } else { die "impossible \$opt \"$opt\"?!"; } shift @ARGV; next; } elsif ($ARGV[0] =~ /^(snap|restore|clone|norm)$/) { if ($NetApp && ! defined($ENV{"P4PORT"})) { $ENV{"P4PORT"} = "p4netapp:1666"; } $cmd = "cli_$ARGV[0]"; shift @ARGV; exit &$cmd(@ARGV); } elsif ($ARGV[0] eq "help") { &help; } else { &usage; } } &usage; } # It turns out to be a very ugly business, this. This is a good # example of why Perforce needs a better API to the metadatabase, # and, perhaps, if client checkpointing is really desirable, some # special server side support for it. # # Nonetheless, up till now, I have not seen any example of state in # the server, required to recreate a clone client, which cannot be # learned by the standard p4 user commands (along with a little # deductive logic). # # TBD: handle multiple open change lists (add section to dump format!) # TBD: error checks for children exist status (after every &cli_s() or close?) # use FileHandle; use IPC::Open2; # This dumps all client state to an archive file. # sub cli_snap { my(@Args) = @_; if ($#Args >= 0) { &usage; } # One choice would have been to do "normalization" here, i.e., to # throw away all of the client-specific information (client name, # owner, actually Root path, etc.) But why throw away information? # Even thouh it's not needed to reproduce the client state, it # might come in handy someday... # if ($NetApp) { print "=== p4env\n"; print "P4CLIENT=$ENV{'P4CLIENT'}\n"; print "P4PORT=$ENV{'P4PORT'}\n"; } print "=== client\n"; my $Root; if (! open(CLIENT, "$P4 client -o |")) { print STDERR "$Myname: can't open \"$P4 client -o|\": $!.\n"; exit 1; } while (<CLIENT>) { if (/^Access:/) { next; } # prevent archives from identical clients differing print; if (/^Root:\s+(.*)$/) { $Root = $1; } } close CLIENT; if (! chdir $Root) { print STDERR "$Myname: can't chdir \"$Root\": $!.\n"; exit 1; } # TBD: Error check the exits from the rest of these # print "=== opened\n"; system "$P4 opened 2>&1"; print "=== have\n"; system "$P4 have 2>&1"; print "=== resolved\n"; system "$P4 resolved 2>&1"; print "=== resolve -n\n"; system "$P4 resolve -n 2>&1"; print "=== cpio\n"; # Since "p4 opened" reports in depot syntax, and we want to make # the cpio archive of open files using client syntax, we do the # open-where shuffle here... (like "p4 opened_ls" uses) my $wherecmd = "$P4 opened 2>/dev/null | sed -e 's/#.*//' | $P4 -x - where"; my $openedcmd = "$P4 opened 2>/dev/null"; if (! open(O, "$openedcmd |")) { print STDERR "$Myname: open \"$openedcmd\" failed: $!.\n"; exit 1; } if (! open(W, "$wherecmd |")) { print STDERR "$Myname: open \"$wherecmd\" failed: $!.\n"; exit 1; } my $Tmpfile = "/usr/tmp/$Myname.tmp.$$"; if (! open(T, ">$Tmpfile")) { print STDERR "$Myname: open \"/usr/tmp/$Myname.tmp.$$\" failed: $!.\n"; exit 1; } while (<O>) { chop $_; $W_ = <W>; chop $W_; if (/^(.*)#([0-9]+) - ([^ ]+) (.*) \((.*)\)$/) { if ($3 eq "delete") { next; } } # don't try to archive deleted files! $W_ =~ s/^.* //; $W_ =~ s/$Root\///; $_ =~ s/.*#/$W_#/; $_ =~ s/#.*//; print T "$_\n"; } close W; close H; close T; # And now write the cpio archive... # if ($Osname eq "SunOS" && $Osvers =~ /^5\./) { $opts = "H odc"; } else { $opts = "c"; } my $cmd = "cpio -o$opts <$Tmpfile"; if (! open(C, "$cmd |")) { print STDERR "$Myname: open \"$cmd |\" failed: $!.\n"; exit 1; } while (<C>) { print; } close C; unlink $Tmpfile; exit 0; } # Run a command # sub cli_s { my($cmd) = @_; print STDERR "$Myname: % $cmd\n"; return system $cmd; } # assert that we're at the given section header # sub section_check { my($line, $want) = @_; chomp $line; if ($line ne "=== $want") { print STDERR "$Myname: bad archive; expected \"=== $want\", found \"$line\".\n"; exit 1; } } sub cli_restore { my(@Args) = @_; $Restore = 1; # Restore is bacially a variant of clone. We'll use an existing # client, and take advantage of any unopened files on its have list # that are correct for the client we're trying to restore, but # that's about it. # &cli_clone(@Args); } # OK, this is the function that reads an archive (on stdin), and # reproduces the represented client in a new p4 client. # sub cli_clone { my(@Args) = @_; # option switch variables get defaults here... $Flush_unopened = 0; $Nosync = 0; while ($#Args >= 0) { if ($Args[0] eq "-f") { $Flush_unopened = 1; shift @Args; next; } if ($Args[0] eq "-n") { $Nosync = 1; shift @Args; next; } elsif ($Args[0] eq "help") { &help; } &usage; } my $Client; my $Port; my $Root = ""; if (! $Restore) { $Root = `/bin/pwd`; chomp $Root; if (-f "P4ENV") { print STDERR "$Myname: \"P4ENV\" exists; is this directory already a client workspace?\n"; exit 1; } } $Client = $ENV{'P4CLIENT'}; if (! $Restore) { print STDERR "$Myname: verifying that client \"$Client\" doesn't already exist...\n"; } if (! open(CLIENTS, "$P4 clients |")) { print STDERR "$Myname: can't open \"$P4 clients\": $!.\n"; exit 1; } while (<CLIENTS>) { my @w = split(/\s+/, $_); if (@w[1] eq "$Client") { if (! $Restore) { print STDERR "$Myname: A client named \"$Client\" already exists:\n$_"; exit 1; } $Root = @w[4]; } if (! $Restore && @w[4] eq "$Root") { print STDERR "$Myname: A client rooted here already exists:\n$_"; exit 1; } } close CLIENTS; if ($Restore) { if ($Client eq "-" || ! $Root) { print STDERR "$Myname: must restore in an existing client.\n"; exit 1; } if (! chdir $Root) { print STDERR "$Myname: can't chdir to the client root \"$Root\": $!.\n"; exit 1; } # Revert the whole client... &cli_s("$P4 revert ...\n"); } # Make sure we're at the first section header. # while (<>) { if (/^=== /) { last; } } if ($NetApp) { # === p4env # §ion_check($_, "p4env"); # Set up the P4ENV file for this client... # if (! $Restore) { if (! open (P4ENV, ">P4ENV")) { print STDERR "$Myname: Can't create \"P4ENV\": $!.\n"; exit 1; } } while (<>) { if (/^=== /) { last; } elsif (/^P4CLIENT=/) { if (! $Restore) { print P4ENV "P4CLIENT=$Client\n"; $ENV{'P4CLIENT'} = $Client; } } else { if (! $Restore) { print P4ENV $_; } } if (/^P4PORT=(.*)$/) { $Port = $1; if ($Restore && $Port ne $ENV{"P4PORT"}) { print STDERR "$Myname: P4PORT mismatch in the saved and current client:\n". " saved: $Port; current: $ENV{'P4PORT'}.\n"; exit 1; } else { $ENV{'P4PORT'} = $Port; } } } if (! $Restore) { close P4ENV; print "$Myname: wrote P4ENV\n"; } } # === client # §ion_check($_, "client"); # OK, now [re]define the client. # my $oClient, $oRoot; my $view = ""; if (! open(CLIENT, "| $P4 client -i >/usr/tmp/$Myname.tmp.$$ 2>&1")) { print STDERR "$Myname: can't open \"$P4 client -i >/usr/tmp/$Myname.tmp.$$ 2>&1\": $!.\n"; exit 1; } while (<>) { if (/^=== /) { last; } chop; if (/^Client:\s+(.*)$/) { $oClient = $1; print CLIENT "Client:\t$Client\n"; } elsif (/^Root:\s+(.*)$/) { $oRoot = $1; print CLIENT "Root:\t$Root\n"; } elsif (/^Date:/) { next; } elsif (/^View:/) { $Inview = 1; print CLIENT "View:\n"; } elsif ($Inview) { if (/^\t([^\s]+) ([^\s]+)/) { my($d, $c) = ($1, $2); $c =~ s/\/\/$oClient\//\/\/$Client\//; $view .= "\t$d $c\n"; } } else { print CLIENT "$_\n"; } } $stdinsave = $_; print CLIENT $view; close CLIENT; if (! open(CLIENT, "</usr/tmp/$Myname.tmp.$$")) { print STDERR "$Myname: can't open \"/usr/tmp/$Myname.tmp.$$\": $!.\n"; exit 1; } $Ok = 1; while (<CLIENT>) { print; if ($_ !~ /^Client $Client (saved|not changed).\n$/) { $Ok = 0; } } close CLIENT; unlink "/usr/tmp/$Myname.tmp.$$"; if (! $Ok) { exit 1; } # === opened # §ion_check($stdinsave, "opened"); # OK, get the list of opened files... # $S = "\001"; # We use this to store $S-separated tuples in strings while (<>) { if (/^=== /) { last; } chop; if (/^(.*)#([0-9]+) - ([^ ]+) (.*) \((.*)\)/) { $Opened{$1} = "$2$S$3$S$4$S$5"; push(@Opened, "$1"); } } $stdinsave = $_; if ($Restore) { # If we're doing a restore, we need to look at the current have # list of the client, so that we can sync files no longer mapped # in the new mapping to #none. # if (! open(HAVE, "$P4 have |")) { print STDERR "$Myname: can't open \"$P4 have |\": $!.\n"; exit 1; } while (<HAVE>) { chop; if (/^(.*)#([0-9]+) - (.*)$/) { my ($depot_path, $rev, $cli_path) = ($1, $2, $3); $Ohave{$depot_path} = $rev; } } close HAVE; } # === have # §ion_check($stdinsave, "have"); # And now, sync to the rev in the have list for all of the *unopened* files. # (Opened ones will be synced/copied in the sections that follow...) # # TBD: Perhaps, add an option to link from the original tree # (instead of syncing it in from the depot)? # my $op = "sync"; if ($Flush_unopened) { $op = "flush"; } if (! $Nosync) { if (! open(SYNC, "|$P4 -x - $op")) { print STDERR "$Myname: can't open \"$P4 -x - $op\": $!.\n"; exit 1; } } my %Synced; # keyed by client pathname, remember what's been synced yet. while (<>) { if (/^=== /) { last; } chop; if (/^(.*)#([0-9]+) - (.*)$/) { my ($depot_path, $rev, $cli_path) = ($1, $2, $3); $cli_path =~ s/^$oRoot\///; $Have{$cli_path} = $rev; $Map_cli_to_depot{$cli_path} = $depot_path; $Map_depot_to_cli{$depot_path} = $cli_path; if (! $Nosync && ! defined($Opened{$depot_path})) { print SYNC "$depot_path#$rev\n"; $Synced{$cli_path} = 1; } } } if ($Restore) { # Sync out any files that are longer no mapped because the # client mapping changed from the restore... # foreach $depot_path (keys(%Ohave)) { if (! $Nosync && ! defined($Map_depot_to_cli{$depot_path})) { print SYNC "$depot_path#none\n"; } } } if (! $Nosync) { close SYNC; } # From here on out, I contend, the script is identical for either # "clone" or "restore"... # # === resolved # === resolve -n # §ion_check($_, "resolved"); # OK, current thinking is: to get the right rev to initially flush # to for files open for edit which have later been synced to # higher revs, we need to know the union of the resolved and # pending resolve lists for such file. We is multipass, baby. # # We'll store it as a hash "%Res", (keyed by client path) of refs # to hashes that hold per-client file info; current elements of # these hashes are: # # "lowsync" - lowest sync point (rev) for resynced files. # "resolves" - ref to list of "$action$S$depot_path$S$depot_rev"s. # # Note: the action tells us whether it is done or pending, so we # don't have to do anything otherwise to remember which list # (resolved or resolve -n) it's from. # while (<>) { if ($_ eq "=== resolve -n\n") { next; } # go do the next (resolve -n) list if (/^=== /) { last; } chop; if (/^(.*) - (.*) ([^\s#]+)(#.*)$/) { my ($cli_path, $action, $depot_path, $depot_revs) = ($1, $2, $3, $4); my $status; $cli_path =~ s/^$oRoot\///; if (! defined($Res{$cli_path})) { $Res{$cli_path} = {}; ${$Res{$cli_path}}{"lowsync"} = 0; ${$Res{$cli_path}}{"resolves"} = ( ); } # Stash the resolve details... # push(@{${$Res{$cli_path}}{"resolves"}}, "$action$S$depot_path$S$depot_revs"); # And adjust the "lowsync" point if needed... # if ($Map_cli_to_depot{$cli_path} eq $depot_path) { # (An integrate to ourself is a resync...) # my $rev; ($rev) = ($depot_revs =~ m/^#([0-9]+)/); if (${$Res{$cli_path}}{"lowsync"} == 0 || $rev < ${$Res{$cli_path}}{"lowsync"}) { ${$Res{$cli_path}}{"lowsync"} = $rev; } } } } $stdinsave = $_; # Lemmesee... we got da goods. Let's do da dance. # # Every record in the {"resolves"} lists represents a sync or an integrate. # Now, first thing is to sync each file into the client, since resolve # will want a file to work on (silly thing). # # We also do branching here... # if (! open(SYNC, "|$P4 -x - sync")) { print STDERR "$Myname: can't open \"$P4 -x - sync\": $!.\n"; exit 1; } cli_path: foreach $cli_path (keys(%Res)) { my @res = @{${$Res{$cli_path}}{"resolves"}}; foreach $res (@res) { my ($action, $depot_path, $depot_revs) = split(/$S/, $res); if ($action eq "branch from") { &cli_s("$P4 integrate ${depot_path}$depot_revs $cli_path"); $Synced{$cli_path} = 1; next cli_path; } } my $rev; if (${$Res{$cli_path}}{'lowsync'}) { # If we have a "lowsync", then we're gonna sync based on that; $rev = ${$Res{$cli_path}}{'lowsync'} - 1; } else { # We just use our have rev... $rev = $Have{$cli_path}; } print SYNC "$cli_path#$rev\n"; $Synced{$cli_path} = 1; } # Now we do syncs for any open files that weren't synced from being # on the resolves lists... # foreach $open (@Opened) { my($rev, $action, $change, $type) = split(/$S/, $Opened{$open}); if (! defined($Synced{$Map_depot_to_cli{$open}})) { print SYNC "$open#$rev\n"; $Synced{$open} = 1; } } close SYNC; # Ah, and how about deletes? # if (! open(DELETE, "|$P4 -x - delete")) { print STDERR "$Myname: can't open \"$P4 -x - delete\": $!.\n"; exit 1; } foreach $open (@Opened) { my($rev, $action, $change, $type) = split(/$S/, $Opened{$open}); if ($action eq "delete") { print DELETE "$open\n"; } } close DELETE; # Next, a pass to open everything open for add or edit # # Note: I'm not 100% sure that we won't get into server deadlock # trouble with the multiple parallel "p4 -x - edit|add -t $type"s, # below... so be on the lookout! # And, finally, do "p4 edits" for everything that needs it... # foreach $open (@Opened) { my($rev, $action, $change, $type) = split(/$S/, $Opened{$open}); if ($action =~ /^(edit|add)$/) { # (We do one invocation of "p4 -x - edit|add -t $type" for # each type we need...) # if (! defined($EDIT{"${action}_$type"})) { $EDIT{"${action}_$type"} = "${action}_$type"; if (! open($EDIT{"${action}_$type"}, "|$P4 -x - $action -t $type")) { print STDERR "$Myname: can't open \"$P4 -x - $action -t $type\": $!.\n"; exit 1; } } print {$EDIT{"${action}_$type"}} $open."\n"; } } foreach $p4cmd (keys(%EDIT)) { close $EDIT{$p4cmd}; } # Hey, you ain't seen *nuttin* yet! Now we jam through the # resolves list, scheduling 'em all up! # foreach $cli_path (keys(%Res)) { my @res = @{${$Res{$cli_path}}{"resolves"}}; my @resync_points = ( ); foreach $res (@res) { my ($action, $depot_path, $depot_revs) = split(/$S/, $res); # branching has already been done! if ($action eq "branch from") { next; } # now, it must either be an integrate or a resync... # if ($Map_cli_to_depot{$cli_path} eq $depot_path) { # An integrate to ourself is a resync... my $rev; ($rev) = ($depot_revs =~ m/#([0-9]+)$/); # Push the resync point - we must do them in order once # we know what they all are! # push(@resync_points, $rev); } else { # An integrate... my $rev; if ($revs =~ /,/) { $rev = $depot_revs; } else { $rev = "$depot_revs,$depot_revs"; } &cli_s("$P4 integrate -f ${depot_path}$rev $cli_path\n"); } } # Do we have resyncs? # if ($#resync_points >= 0) { # If so, do 'em # foreach $rev (sort { $a <=> $b } @resync_points) { &cli_s("$P4 sync ${cli_path}#$rev"); } } } # OK, I know you might have trouble believing this, but we're ready # to replay the resolves now. # my ($P4r,$P4w) = (FileHandle->new, FileHandle->new); $pid = open2($P4r, $P4w,"$P4 resolve 2>&1"); $Action = "?"; resline: while ($_ = &readline($P4r)) { print STDERR $_; if (/^(.*) - (.*) ([^\s#]+)(#.*)$/) { # OK, here's a merge... # my ($cli_path, $action, $depot_path, $depot_revs) = ($1, $2, $3, $4); my $status; $cli_path =~ s/^$Root\///; # Now, lets find our record for this, to see if we resolve # it (& how) or not. # if (! defined($Res{$cli_path})) { die "assert: missing re record cli_path <$cli_path>?!"; } my @res = @{${$Res{$cli_path}}{"resolves"}}; foreach $res (@res) { my ($r_action, $r_depot_path, $r_depot_revs) = split(/$S/, $res); if ($r_depot_path eq $depot_path && $r_depot_revs eq $depot_revs) { # Ok, this is da one! if ($r_action =~ /^(merging|vs)$/) { $Action = "s"; next resline; } else { if ($r_action eq "ignored") { $Action = "ay"; } elsif ($r_action eq "copy from") { $Action = "at"; } elsif ($r_action eq "merge from") { $Action = "af"; } else { die "assert: unexpected action <$r_action>"; } next resline; } } } die "assert: couldn't find matching re record for <$depot_path/$depot_revs>"; } if (/^Accept\(a\) /) { if (! $Action) { die "assert: \$Action is empty"; } $Action .= "\n"; syswrite($P4w, $Action, length($Action)); print STDERR "$Action"; $Action = ""; $_ = &readline($P4r); print STDERR $_; if (/confirm accept \(y\/n\)\? $/) { syswrite($P4w, "y\n", 2); print STDERR "y\n"; } } } $P4r->close(); $P4w->close(); # === cpio # §ion_check($stdinsave, "cpio"); # OK, it's time to pull in the opened files from the archive... # if (! open(CPIO, "|cpio -icdvu")) { print STDERR "$Myname: can't open \"cpio -icv\": $!.\n"; exit 1; } while (<>) { print CPIO; } close CPIO; $status = $?; if ($status) { print STDERR "$Myname: cpio returned nonzero exit status: $status.\n"; exit 1; } exit 0; } # Routines to handle select/reads from p4 resolve... # Note: these are spcialized to recognize prompts from resolve! # my $Buf = ""; sub retline { my $ret = undef; my $i; $i = index($Buf, "\n"); if ($i >= 0) { $ret = substr($Buf, 0, $i+1); $rem = substr($Buf, $i+1); $Buf = $rem; } elsif ($Buf =~ /(Accept\(a\) .*: | confirm accept \(y\/n\)\? )$/) { $ret = $Buf; $Buf = ""; } return $ret; } sub readline { my($f) = @_; if ($ret = &retline()) { return $ret; } vec($rin, fileno($f), 1) = 1; while(1) { $sel = select($rout=$rin,undef,undef,60); if ($sel < 0) { die "select returned $sel $!"; } elsif ($sel == 0) { die "select for read timed out"; } else { if (vec($rout,fileno($f),1)) { $n_read = sysread($f,$inbuf,1024); if ($n_read == 0) { return undef; } if (! defined($n_read)) { die "sysread error $!"; } } $Buf .= $inbuf; if ($ret = &retline()) { return $ret; } } } } # For debugging only... # sub dumpres { foreach $cli_path (keys(%Res)) { print "### $cli_path lowsync: ${$Res{$cli_path}}{'lowsync'}\n"; foreach $r (@{${Res{$cli_path}}{"resolves"}}) { print " $r\n"; } } } # Normalize a client (mainly used to verify that a cloned client # looks identical to the original). # sub cli_norm { while (<>) { if (/^=== /) { last; } print; } print; while (<>) { if (/^=== /) { last; } elsif (/^P4CLIENT=/) { print "P4CLIENT=\$Client\n"; } else { print $_; } } print; while (<>) { if (/^=== /) { last; } chop; if (/^Client:\s+(.*)$/) { $oClient = $1; print "Client: \$Client\n"; } elsif (/^Root:\s+(.*)$/) { $oRoot = $1; print "Root: \$Root\n"; } elsif (/^Date:/) { print "Date: \$Date\n"; } elsif (/^Owner:/) { print "Owner: \$Owner\n"; } elsif (/^View:/) { $Inview = 1; print "$_\n"; } elsif ($Inview && /^\t([^\s]+) ([^\s]+)/) { my($d, $c) = ($1, $2); $c =~ s/\/\/$oClient\//\/\/\$Client\//; print "\t$d $c\n"; } else { print "$_\n"; } } print; while (<>) { if (/^=== /) { last; } print; } print; while (<>) { if (/^=== /) { last; } chop; if (/^(.*)#([0-9]+) - (.*)$/) { ($path, $rev, $cli_path) = ($1, $2, $3); $cli_path =~ s/^$oRoot/\$Root/; print "$path#$rev - $cli_path\n"; } else { print $_."\n"; } } print; while (<>) { if (/^=== /) { last; } $_ =~ s/^$oRoot/\$Root/; print; } print; while (<>) { if (/^=== /) { last; } $_ =~ s/^$oRoot/\$Root/; print; } print; while (<>) { print; } exit 0; } 1;
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#4 | 1657 | Richard Geiger | fixup the usage message. | ||
#3 | 472 | Richard Geiger | integrate changes made to the NetApp version since the last update | ||
#2 | 128 | Richard Geiger |
Fix a problem handling unresolved integrations with no common ancestor (2-way merge). |
||
#1 | 116 | Richard Geiger | First public distribution. |