#!/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). |