#!/usr/local/bin/perl # -*-Fundamental-*- ## Notice: this script is not presently considered "done" for public ## distribution; if you do find this, and use it, bear that in mind! ## - rmg 12/28/2001 ## TBD ## ## - handle indempotentcy when a patch with an edit for F ## is done after a patch adding F, when there's no submit in between? ## ## - force type target to match source, or only when it's in the change? ## ## # perl_template - please see the comment at the end! #eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' # & eval 'exec perl -S $0 $argv:q' # if 0; # THE PRECEEDING STUFF EXECS perl via $PATH use Carp; use strict; $| = 1; my $Myname; ($Myname = $0) =~ s%^.*/%%; my $Usage = <<LIT; $Myname: usage: $Myname [<-cdHpPu p4 opts>] -out <change_num> <branch_path> | $Myname [<-cdHpPu p4 opts>] [-clean] [-open|-submit] [-change default|new|<change_num>] [ <target_root> ] -or- "p4patch -p4editor <descfile> <changespec>" (for internal use only, i.e., when I call myself!) LIT sub usage { print STDERR $Usage; exit 1; } sub help { print STDERR <<LIT; $Usage $Myname operates in one of two modes, "out" or "in". In either mode, it will accept any of the standard p4 -c, -d, -H, -p, -P and -u options, to specify the context for accessing a Perforce server. "Out" mode is selected by supplying the "-out" option flag; otherwise, the mode defaults to "in". $Myname can be used in a pipeline, with the standard output of an "out" mode instance being piped to the standard intput of an "in" mode instance. The "out" mode produces a "patchfile", containing enough information to encapsulate a single changeset in its entirety. The content consists (essentially) of the output from "p4 describe -du <change_num>", followed by a cpio archive containing the complete content of any files in the change that were opened for branch, integrate, or add. The "<branch_path>" parameter, which must be in Perforce depot syntax, gives a path prefix representing the "branch" portion of the pathnames in the change; this will be stripped from the pathnames in the "p4 describe" output (along with the #<n> revision numbers). The "in" mode reads a "patchfile" (from the standard input), and applies the changes it specifies to a "target" local directory tree, which may also be a Perforce client workspace. In "in" mode, if the "-open" or "-submit" option is used, then the target assumed to be a Perforce client workspace, and the appropriate Perforce commands (required to effect the changes in Perforce) are executed. With "-open", the files are opened (as required) and the patches are applied, but no "p4 submit" is done. With "-submit", the "p4 submit" is executed after the patchfile has been applied. Further, with "in" mode to a Perforce client workspace, the "-change" option can be used to specify the changelist to be used. By default, (or with "-change default") the deafult changelist will be used; "-change new" will cause a new number changelist to be created used; and "-change <number>" will caused the named numbered changelist to be used. The "<target_root>" parameter specifies, in either local or Perforce client syntax, the path to the top of the directory tree corresponding to the <branch_path> in the patchfile. If neither of "-open" nor "-submit" are explcitly given, but the <target_root> is in Perforce syntax, then "-open" is assumed. At this time, only simple, "single-line" mappings are supported, i.e., a single <branch_path> with -out, and a single <target_root> For example: <To Be Written> LIT exit 1; } # option switch variables get defaults here... my $P4CLIENT; my $PWD; my $P4HOST; my $P4PORT; my $P4PASSWD; my $P4USER; my $P4editor = 0; my $Patchmode = "in"; my $Change = "default"; my $P4mode; my $Clean = 0; my @Args; my $Args; while ($#ARGV >= 0) { if ($ARGV[0] eq "-open") { $P4mode = "open"; shift; next; } elsif ($ARGV[0] eq "-submit") { $P4mode = "submit"; shift; next; } elsif ($ARGV[0] eq "-out") { $Patchmode = "out"; shift; next; } elsif ($ARGV[0] eq "-clean") { $Clean = 1; shift; next; } elsif ($ARGV[0] eq "-p4editor") { $P4editor = 1; shift; next; } elsif ($ARGV[0] eq "-change") { shift; if ($ARGV[0] < 0) { &usage; }; $Change = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-c") { shift; if ($ARGV[0] < 0) { &usage; }; $P4CLIENT = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-d") { shift; if ($ARGV[0] < 0) { &usage; }; $PWD = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-H") { shift; if ($ARGV[0] < 0) { &usage; }; $P4HOST = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-p") { shift; if ($ARGV[0] < 0) { &usage; }; $P4PORT = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-P") { shift; if ($ARGV[0] < 0) { &usage; }; $P4PASSWD = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-u") { shift; if ($ARGV[0] < 0) { &usage; }; $P4USER = $ARGV[0]; shift; next; } elsif ($ARGV[0] eq "-help") { &help; } elsif ($ARGV[0] =~ /^-/) { &usage; } if ($Args ne "") { $Args .= " "; } push(@Args, $ARGV[0]); shift; } if ($P4editor) { # So we can do a # P4EDIT="$Myname -p4edit <descfile>" p4 submit" # and be the editor, which just inserts (or appends) the # desciption text supplied in $descfile. # if ($#Args != 1) { &fail("wrong number of args for \"-p4edit\" mode!"); } my ($descfile, $change) = @Args; if (! open(DESC, "<$descfile")) { &fail("can't open \"<$descfile\": $!."); } if (! open(OCHG, "<$change")) { &fail("can't open \"<$change\": $!."); } if (! open(NCHG, ">$change.new")) { &fail("can't open \">$change.new\": $!."); } my $mode = "finddesc"; my $desc_added = 0; while (<OCHG>) { if (/^Description:/) { print NCHG; $mode = "indesc"; } elsif (/^Files:/) { my $fileshdr = $_; while (<DESC>) { print NCHG "\t$_"; } close DESC; unlink $descfile; $desc_added = 1; $mode = "infiles"; print NCHG $fileshdr; } elsif ($mode eq "indesc") { # we copy through any existing Description lines... if (/^\s<enter description here>$/) { next; } # (well, almost any!) print NCHG; } else # anything else gets copied through unchanged... { print NCHG; } } close OCHG; if (! $desc_added) { while (<DESC>) { print NCHG "\t$_"; } close DESC; unlink $descfile; $desc_added = 1; } close NCHG; if (! rename("$change.new", $change)) { &fail("can't rename(\"$change.new\", \"$change\"): $!."); } exit 0; } my $P4 = "p4"; if ($P4CLIENT) { $P4 .= " -c $P4CLIENT"; } if ($PWD) { $P4 .= " -d $PWD"; } if ($P4HOST) { $P4 .= " -H $P4HOST"; } if ($P4PORT) { $P4 .= " -p $P4PORT"; } if ($P4PASSWD) { $P4 .= " -P $P4PASSWD"; } if ($P4USER) { $P4 .= " -u $P4USER"; } ################################################################################ # # "out" mode. # # This is relatively straightforward, since it's basically just # packaging up information from inside a nice safe Perforce depot - # there's no dependence on some sort of client state (as there in for # the "in" mode). # if ($Patchmode eq "out") { &out; } sub out # overcome indentation { if ($#Args != 1) { &usage; } my $change_num = shift @Args; my $branch_path = shift @Args; if ($branch_path !~ /^\/\//) { print STDERR "$Myname: <branch_path> must be in depot notation.\n"; &usage; } if (! open(INFO, "$P4 info |")) { &fail("open(\"$P4 info |\"): $!."); } my $Server; while (<INFO>) { if (/^Server address: (.*)$/) { $Server = $1; } } close INFO; $branch_path =~ s/\/$//; if (! open(DESC, "$P4 describe -du $change_num |")) { &fail("open(\"$P4 describe -du $change_num |\"): $!."); } my @addfiles; my %skipped; my $skipping = 0; my $fatality = 0; print "$Server: "; while (<DESC>) { if (/^\.\.\. (.*) (\w+)$/) { my ($depot_path, $action) = ($1, $2); my $path = $depot_path; if ($path !~ /^$branch_path\//) { print STDERR "$Myname: revision <$depot_path> not in <$branch_path> (skipped)\n"; $skipped{$depot_path} = 1; next; } $path =~ s/^$branch_path\///; $path =~ s/#\d+$//; print "... $path $action\n"; # Remember added or branched (or imported, which is # "branched" form a remote depot) files, so we remember to # add them to the new-file archive... # if ($action =~ /^(branch|add|import)$/) { push(@addfiles, $depot_path); } } elsif (/^==== (.*) \(([a-z\+\/]+)\) ====$/) { my ($depot_path, $type) = ($1, $2); if ($skipped{$depot_path}) { $skipping = 1; next; } $skipping = 0; $depot_path =~ s/^$branch_path\///; $depot_path =~ s/#\d+$//; print "==== $depot_path ($type) ====\n"; } else { if (! $skipping) { print; } } } close DESC; if ($fatality) { exit 1; } if ($#addfiles >= 0) { # We need to tack on the cpio archive... # # So: we need to make a temp tree to hold the files. # # Note: yep, if we catch a sig and die unexpectedly, we can # strand temp files. Ain't we a stinker? (yep, a we need that # elusive Perl cpio module!) # print STDERR "### cpio\n"; my $tmpdir = "/usr/tmp/$Myname.$$.dir"; my $tmpfiles = "/usr/tmp/$Myname.$$.files"; if (! mkdir $tmpdir) { &fail("couldn't make temp dir \"$tmpdir\": $!."); } if (! open(FILES, ">$tmpfiles")) { &fail("open(FILES, \">$tmpfiles\"): $!."); } foreach my $depot_path (@addfiles) { my $path = $depot_path; $path =~ s/^$branch_path\///; $path =~ s/#\d+$//; my $tmppath = "$tmpdir/$path"; &insdir(&dirname($tmppath), 0755); if (system("$P4 print -q $depot_path > $tmppath")) { system "/bin/rm -rf $tmpfiles"; &fail("\"$P4 print -q $depot_path > $tmppath\" failed."); } print FILES "$path\n"; } close FILES; print "\n#### cpio\n"; # OK, now we have the temp tree built; run a cpio to construct the cpio archive. # if (! open(CPIO, "cd $tmpdir && /bin/cpio -c -o < $tmpfiles |")) { system "/bin/rm -rf $tmpfiles"; &fail("open(CPIO, \"cd $tmpdir && /bin/cpio -c -o < $tmpfiles |\"): $!."); } while (<CPIO>) { print; } close CPIO; my $status = $?; system "/bin/rm -rf $tmpdir $tmpfiles"; if ($status) { &fail("\"cd $tmpdir && /bin/cpio -c -o < $tmpfiles\" returned <$status>."); } } exit 0; } ################################################################################ # # "in" mode. # # This is somewhat trickier than "out" mode, since it depends on the # state of the client tree (or Perforce workspace). E.g., if the # patch contains a file deletion, and the file doesn't exist in the # target, should this be a fatal error, a warning, or silently # ignored? # For this first cut, we'll go with a warning-where-possible # preference, on the principal that the operation may be more like a # merge than a fixed patch. Fatality will be reserved for obvious # failures of primitive operations. But one size may not fit all, # so in later versions we might want to have a "strict" mode, or # even think about adding support for making the whole thing more # explicitly "mergy". # if ($#Args > 0) { &usage; } my $target_root = "."; if ($#Args == 0) { $target_root = shift @Args; } $target_root =~ s/\/$//; # just in case. # If $target_root is in Perforce syntax, map it into a local # filesystem path. # if ($target_root =~ /^\/\/([^\/]+)(.*)/) { my ($client_name, $client_path) = ($1, $2); $client_path =~ s/^\///; if (! $P4mode) { $P4mode = "open"; } my $root; my $valid = 0; foreach (split(/\n/, `$P4 client -o $client_name`)) { if (/^Root:\s+(.*)/) { $root = $1; } if (/^Update:\s/) { $valid = 1; } } if (! $valid) { &fail("no such client workspace \"$client_name\"."); } $target_root = "$root"; if ($client_path) { $target_root .= "/$client_path"; } } if (! chdir($target_root)) { &fail("chdir(\"$target_root\"): $!"); } # To avoid a confused Perforce: # my $pwd = `/bin/pwd`; chomp $pwd; $ENV{"PWD"} = $pwd; # Parse the description, up to the start of the diffs. # my $Change_head; my $Description; my %Files; while (<STDIN>) { if (/: Change \d+ /) { $Change_head = $_; <>; last; } } if (! $Change_head) { &fail("couldn't recognize a change description."); } while (<STDIN>) { if (/^\t(.*)/) { $Description .= "$1\n"; } else { last; } } $Description .= "\n=== $Change_head\n"; my $affected = 0; while (<STDIN>) { if (/^Affected files .../) { $affected = 1; <>; last; } } if (! $affected) { &fail("couldn't find \"Affected files\" section."); } while (<STDIN>) { if (/^\.\.\. (.*) (\w+)$/) { my ($path, $action) = ($1, $2); $path =~ s/#\d+$//; $Files{$path} = $action; } else { last; } } # Ok, here with $Change_head, $Description and %Files, but we haven't # modified anything yet. A great time for some error checking and # prep, before we actually do anything that would change the state of # the target. # # Someday, maybe; if we error check here, and suumarize possible # problems, the user can chose wther to proceed, before changing any # statein the target, which may be difficult to revert... # #foreach my $path (sort(keys(%Files))) # { # } # OK, do we need a new changelist (or to append the description # to an existing one)? # if ($P4mode && $Change ne "default") { my $descfile = &mkdescfile($Description); $ENV{"P4EDITOR"} = "$Myname -p4editor $descfile"; if ($Change eq "new") { $Change = ""; } my($status, @output) = &s("$P4 change $Change"); if ($status != 0) { &fail("$P4 change returned exit status <$status>"); } if (! $Change) { foreach (@output) { if (/Change (\d+) created./) { $Change = $1; } } if (! $Change) { &fail("couldn't find new change number"); } } } # OK, here we know Change is valid. # Here we go. Iterate through the %Files list, doing whichever # p4 or local filesystem actions are implied by the values therein. # For now, let's just implement the "natural" path, and think about # the dizzying special-case error handling another day... the user will see # any errors, and _they_ can decide what to do before submitting. # my (@edits, @adds, @deletes); foreach my $path (sort(keys(%Files))) { my $action = $Files{$path}; if ($action eq "delete") { if ($P4mode) { push(@deletes, $path); } else { &s("/bin/rm $path"); } } elsif ($action =~ /^(branch|add|import)$/) { if ($P4mode) { push(@adds, $path); } # (if we're not in $P4mode, then cpio will just overlay whatever's # there, if anything. } elsif ($action =~ /^(edit|integrate)$/) { # For these, the changes will be in the form of a "p4 diff -du" diff, # to be processed by "patch". # if ($P4mode) { push(@edits, $path); } # now ready to patch } } # Hey, doesn't this pup need some error checking?! TBD # sub dop4 { my ($op, $Change, @paths) = @_; if (! open(P4, "|$P4 -x - $op -c $Change")) { &fail("open(P4, \"|$P4 -x - $op -c $Change\": $!."); } foreach my $p (@paths) { print P4 $p."\n"; } close P4; } if ($P4mode) { print STDERR "### p4 deletes\n"; &dop4("delete", $Change, @deletes); print STDERR "### p4 edits\n"; &dop4("edit", $Change, @edits); } ## patching phase # print STDERR "### patching\n"; my $have_cpio = 0; if ($Clean) { &s("/usr/bin/find . ". "\\( -name \\*.rej -o -name \\*.orig \\) -exec /bin/rm -f {} \\;"); } while (<STDIN>) { if (/^#### cpio$/) { $have_cpio = 1; last; } if (/^==== (.*) \(([a-z\+\/]+)\) ====$/) { my ($path, $type) = ($1, $2); <STDIN>; my $patchpath = "$path.patch"; # ? &insdir(&dirname($patchpath), 0755); # but shouldn't it already exist? if (! open(PATCH, ">$patchpath")) { &fail("open(\">$patchpath\"): $!"); } print PATCH <<EOM; --- $path~ Wed Dec 19 10:28:05 2001 +++ $path Wed Dec 19 10:29:08 2001 EOM while (<STDIN>) { if (/^$/) { last; } # There's always an empty line after the diffs, so we # can't miss "#### cpio" here - right? print PATCH; } close PATCH; &s("/usr/bin/patch -p0 < $patchpath"); unlink $patchpath; if ($P4mode && $type =~ /([^\/]+)\/([^\/]+)$/) { &s("$P4 reopen -t $2 $path"); } } } if ($have_cpio) { print STDERR "### cpio/p4 adds\n"; if (! open(CPIO, "|/bin/cpio -c -u -d -i")) { &fail("open(CPIO, \"|/bin/cpio -d -c -i\": $!."); } while (<STDIN>) { print CPIO; } close CPIO; my $status = $?; if ($status) { &fail("cpio exitted with status: $status."); } if ($P4mode) { &dop4("add", $Change, @adds); } } if ($P4mode eq "submit") { # We're gonna go all the way, yehaw! # First, stash the description: my $descfile = &mkdescfile($Description); # (The P4EDITOR invocation will only happen here when $Change is "default") # $ENV{"P4EDITOR"} = "$Myname -p4editor $descfile"; &s("$P4 submit -c $Change"); } exit 0; #--------- Utility functions sub mkdescfile { my ($Description) = @_; my $descfile = "/usr/tmp/$Myname.$$.desc"; if (! open(DESC, ">$descfile")) { &fail("can't open \"<$descfile\": $!."); } print DESC $Description; close DESC; return $descfile; } sub fail { my ($m) = @_; print STDERR "$Myname: $m\n"; exit 1; } sub dirname { my ($dir) = @_; $dir =~ s%^$%.%; $dir = "$dir/"; if ($dir =~ m%^/[^/]*//*$%) { return "/"; } if ($dir =~ m%^.*[^/]//*[^/][^/]*//*$%) { $dir =~ s%^(.*[^/])//*[^/][^/]*//*$%$1%; { return $dir; } } return "."; } sub mkd { my($dir, $mode) = @_; #printf STDERR "$Myname: mkdir %s %04o\n", $dir, $mode; mkdir($dir, $mode) || &fail("can't mkdir \"$dir\": $!."); } # insure that the directory(s) required to store path "$dir" exist. # if $dir" or any require parent in the $dir pathname do not exist, # created them with the specified mode. # sub insdir { my($dir, $insmode) = @_; if (! -e $dir) { &insdir(&dirname($dir)); &mkd($dir, 0755); return; } # So, it already exists, is it a dir? if (! -d $dir) { &fail("existing \"$dir\" is not a directory."); } if (! $insmode) { return; } # Last thing to insure is the mode... my(@stat) = stat($dir) || &fail("can't stat \"$dir\": $!."); if (($stat[2] & 0777) == $insmode) { return; } chmod $insmode, $dir || &fail("can't chmod \"$dir\": $!."); } # Execute a system command, (which may be a $P4 command). # Returns ($status, @output), where # $status is, for perforce commands that exit with 0, the count of the # number of "error: " lines in the output; for all other commands, # it is simply the exit status. # @output is the lines of the compbined stderr and stdout # (p4 -s format, for p4 commands) # sub s { my ($cmd) = @_; my $p4 = 0; if ($cmd =~ /^$P4 /) { $cmd =~ s/^$P4 /$P4 -s /; $p4 = 1; } print STDERR "$Myname: > $cmd\n"; my @output = split(/\n/, `$cmd 2>&1`); my $status = $?; my $p4_errors = 0; foreach (@output) { my $line; if ($p4) { my $tag; ($tag, $line) = ($_ =~ /^([a-z\d]+): (.*)/); if ($tag eq "error") { $p4_errors++; } } else { $line = $_; } print STDERR "$Myname: : $line\n"; } if ($status == 0 && $p4) { $status = $p4_errors; } return ($status, @output); }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#9 | 2526 | Richard Geiger | make p4patch -out silent in the non-error case. | ||
#8 | 2463 | Richard Geiger | handle -change <n> correctly! | ||
#7 | 2462 | Richard Geiger |
clean defaults off Use full explicit path /usr/bin/find |
||
#6 | 1616 | Richard Geiger | Add -clean (partial) | ||
#5 | 1197 | Richard Geiger |
Add -change for controlling what change is used. Allows recombining changes, and a more complete preview before submitting. |
||
#4 | 1195 | Richard Geiger |
Move the note about the original change number (etc) to the bottom of the new description, so "p4 changes" doesn't see it (instead of the more meaningful description!) |
||
#3 | 1194 | Richard Geiger |
p4patch now sets ENV{"PWD"} after chdir, soas not to addle Perforce's brains. |
||
#2 | 1193 | Richard Geiger | Add notice about not being ready for prime time at the top. | ||
#1 | 1192 | Richard Geiger | Add first rev of "p4patch" (last rev from chinacat depot). |