#! /usr/bin/perl ### ## This script is an example implementation of the process of undoing ## a merge, as defined in the "Can you undo a merge?" section on page ## 80 of the "Practical Perforce" O'Reilly book (ISBN 0-596-10185-6). ## ## The script is unsupported and has not had rigorous testing done on it. ## You should try it out on test data and understand what it does before ## relying on it in practice. ## ## One limitation of note is that once you've launched an interactive ## merge tool, there's no check done to see if any of the files changed. ## So, if you're in the merge tool and want to quit the whole process, ## just closing the merge tool will bring control back to this script, ## and it will continue along. If this case is important to you, adding ## an extra user-input prompt after the merge tool has completed would ## probably be sufficient. ### use File::Copy; use Cwd 'abs_path'; # turn off output buffering so we always get prompts $| = 1; # extract the name of the executable file $_ = abs_path($0); /([0-9a-zA-Z\.\-\ ]+)$/; $name = $1; if(@ARGV != 5) { print "Script to help automate the process of undoing a merge.\n\n"; print @ARGV . " arguments received: \"@ARGV\"\n"; print "USAGE: $name P4PORT P4USER P4CLIENT \"merge tool\" local_file\n"; print "EXAMPLE: $name p4:1666 Abe Abe-r07.2 \"p4v -merge\" /r07.2/readme.txt\n"; print "On Windows, the merge tool's name is \"p4merge\", on unix it's \"p4v -merge\".\n\n"; print "Custom tool arguments: \$p \$u \$c \"merge program\" \%f\n"; print "Other custom tool options: \"Add to applicable context menus\"\n"; print " \"Run tool in terminal window\".\n"; print "\n"; exit; } $P4PORT = $ARGV[0]; $P4USER = $ARGV[1]; $P4CLIENT = $ARGV[2]; $P4MERGE = $ARGV[3]; $wsfile = $ARGV[4]; $P4 = "p4 -p $P4PORT -u $P4USER -c $P4CLIENT "; ### ## run "p4 resolved" on the file in order to get the "base" and "theirs" revision data. ## ### =comment Example "resolved" output to parse. [] p4 -ztag resolved -o file ... path /work/u/4/zilch/file ... toFile //ws4/zilch/file ... fromFile //zilch/file ... startToRev #none ... endToRev #none ... startFromRev #15 ... endFromRev #16 ... how merge from Another example, this time with two resolves. [] p4 -ztag resolved robots.txt ... path /work/u/4/zilch/robots.txt ... toFile //ws4/zilch/robots.txt ... fromFile //zilch/robots.txt ... startToRev #none ... endToRev #none ... startFromRev #1 ... endFromRev #2 ... how ignored ... path /work/u/4/zilch/robots.txt ... toFile //ws4/zilch/robots.txt ... fromFile //zilch/robots.txt ... startToRev #none ... endToRev #none ... startFromRev #2 ... endFromRev #3 ... how copy from For multiple resolves, we loop over the blocks, starting at the most recent. =cut $resolved = "$P4 -ztag resolved -o \"$wsfile\" 2>&1"; print "$resolved\n"; @ztagResolved = `$resolved`; if(@ztagResolved[0] =~ / - no file\(s\) resolved\./) { print "Line number " . __LINE__ . "\n"; print "Error; \"$resolved\" said there was nothing to do.\n"; print "Did you run the script on the correct file? The raw error is:\n"; print "@ztagResolved\n"; exit 1; } # look at the first record and make sure it looks right. unexpected errors # should show up here. if(@ztagResolved[0] !~ /^\.\.\. path /) { print "Line number " . __LINE__ . "\n"; print "Unexpected error: \n@ztagResolved\n"; exit 1; } @tags; %ztag; foreach (@ztagResolved) { #%ztag; @split = split(/\s+/); if(/^\.\.\. path /) { if ($wsfile ne "@split[2 .. @split-1]") { print "Line number " . __LINE__ . "\n"; print "File argument not the same: \"$wsfile\", \"@split[2 .. @split-1]\"\n"; print "Expected argument as an absolute path in local syntax.\n"; exit 1; } } #print "\@split = @split\n"; # when we hit a previously defined key, we assume we're onto a second resolve # block. if( defined $ztag{"@split[1]"} ) { # the already-defined key should always be 'path'. fail if not. if(@split[1] ne "path") { print "Line number " . __LINE__ . "\n"; die "ztag parse error! redefinition (\"@split\").\n"; } push @tags, {%ztag}; %ztag = (); } $ztag{"@split[1]"} = "@split[2 .. @split-1]"; } push @tags, {%ztag}; %ztag = (); print "\@tags = " . @tags . "\n"; @origFiles; foreach(reverse @tags) { # print @tags . "\n"; # print "\n" . __LINE__ . "\n"; while ( my ($dname, $dpath) = each(%$_) ) { print "\t$dname => $dpath\n"; } &undo($_); } print "\nRemove the backup(s) of your original file(s)?\n"; foreach (@origFiles) { print " $_\n"; } while(1) { print "(yes/no)? "; $_ = ; chomp; $_ = lc $_; if($_ eq "yes") { print "\n"; foreach(@origFiles){ print "Removing \"$_\".\n"; unlink($_); } last; } if($_ eq "no" ) { print "\nNot removing original copies.\n"; last; } print "\nUnknown option \"$_\". Enter either \"yes\" or \"no\".\n\n"; } print "Done.\n"; exit 0; sub undo { my $ztag = shift; # while ( my ($a, $b) = each(%$ztag) ) { print "\t$a => $b\n"; } ### ## set up the file names to work with ### # make a backup of the workspace file as "filename.orig.startFromRev". # quit if it already exists. $origFile = $wsfile . ".orig.". $$ztag{"startFromRev"}; if(-e $origFile) { die "Backup file \"$origFile\" exists! Quitting.\n"; } copy("$wsfile", "$origFile"); push @origFiles, $origFile; # file gets created on merge, below. make sure it doesn't already exist. $tmpFile = $wsfile . ".tmp"; if(-e $tmpfile) { print "Line number " . __LINE__ . "\n"; die "Tmp file \"$tmpFile\" exists! Quitting.\n"; } $base = $wsfile . ".base"; if(-e $base) { print "Line number " . __LINE__ . "\n"; die "\"Base\" file \"$base\" already exists! Quitting.\n"; } if(!defined $$ztag{"startFromRev"}) { print "Line number " . __LINE__ . "\n"; die "\"p4 resolved\" ztag parse error, can't find \"startFromRev\"!\n"; } $sRev = $$ztag{"startFromRev"}; $theirs = $wsfile . ".theirs"; if(-e $theirs) { print "Line number " . __LINE__ . "\n"; die "\"Theirs\" file \"$theirs\" already exists! Quitting.\n"; } if(!defined $$ztag{"endFromRev"}) { print "Line number " . __LINE__ . "\n"; die "ztag parse error, can't find endFromRev!\n"; } $eRev = $$ztag{"endFromRev"}; ### ## print out the "base" and "theirs" files ### $pBase = "$P4 print -o \"$base\" \"$wsfile" . $sRev . "\" 2>&1"; $oTheirs = "$P4 print -o \"$theirs\" \"$wsfile" . $eRev . "\" 2>&1"; # expect "p4 print" output like this: # //depot/robots.txt#1 - add change 1115 (text) print "$pBase\n"; $_ = `$pBase`; if(!/^\/\/.*\#\d+ - .* \d+ \(.*\)/) { print "Line number " . __LINE__ . "\n";die "\"$pBase\" failed!\n"; } print; print "$oTheirs\n"; $_ = `$oTheirs`; if(!/^\/\/.*\#\d+ - .* \d+ \(.*\)/) { print "Line number " . __LINE__ . "\n";die "\"$pTheirs\" failed!\n"; } print; =comment # this block is useful in case you want the script to be less interactive. it'll # always launch the merge tool when it's an 'edit', otherwise it'll run merge3. if($$ztag{"how"} !~ /edit/) { $m = "$P4 merge3 -r " . " \"$theirs\" \"$base\" \"$wsfile\" > \"$tmpFile\" 2>&1"; print "$m\n"; print `$m`; } else { $gMerge = $P4MERGE . " "; $m = "$gMerge" . " \"$base\" \"$theirs\" \"$wsfile\" \"$tmpFile\" 2>&1"; print "$m\n"; print `$m`; } =cut $gMerge = $P4MERGE; $aMerge = "p4 merge3 -r "; print "\nWhat do you want to do? Resolve type is: " . $$ztag{"how"} . "\n"; $choice; while(1) { $default; print "(g)ui, or (a)utomatic? "; if($$ztag{"how"} =~ "edit") { print "(default: g) "; $default = "g"; } else { print "(default: a) "; $default = "a"; } $_ = ; chomp; # print "got \"$_\"\n"; if($_ eq "") { $_ = $default; } if(/g|a/i) { $choice = lc $_; last; } } # user has control over saved file. if($choice eq "g") { $m = $gMerge . " \"$base\" \"$theirs\" \"$wsfile\" \"$tmpFile\" 2>&1"; print "$m\n"; print `$m`; } elsif($choice eq "a") { $m = $aMerge . " \"$theirs\" \"$base\" \"$wsfile\" > \"$tmpFile\" 2>&1"; print "$m\n"; print `$m`; } # add a prompt here if you want to be able to bail out after merge ### ## finish up the undo process and clean up temp files ### $revert = "$P4 revert \"$wsfile\" 2>&1"; print "$revert\n" . `$revert`; $sync = "$P4 sync \"$wsfile" . "$sRev\" 2>&1"; print "$sync\n" . `$sync`; $edit = "$P4 edit \"$wsfile\" 2>&1"; print "$edit\n" . `$edit`; print "removing \"$wsfile\".\n"; unlink("$wsfile"); print "moving \"$tmpFile\" into \"$wsfile\".\n"; move("$tmpFile", "$wsfile"); unlink("$theirs"); unlink("$base"); print "\n\n"; # leaving the orig copy for the user to manually remove. } # end of 'undo' function print "EOF; exiting.\n"; exit; # eof