#!/usr/local/bin/perl # -*-Fundamental-*- # $Id: //guest/richard_geiger/utils/p4cli#3 $ # 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 = < _archive_ $Myname restore < _archive_ $Myname -c _clientname_ clone [-f|-n] < _archive_ $Myname norm < _archive_ $Myname help USAGE sub usage { print STDERR "$Usage"; exit 1; } sub help { print <= 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 () { 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 () { chop $_; $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 () { 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 () { 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, ") { 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 () { 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;