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