#!/usr/bin/perl -w =head1 NAME p4d2p - convert diffs from Perforce to patch format =head1 SYNOPSIS p4d2p [-hiv] [-o comment] [-p command] [files ...] =head2 Options: -h Display this help message -i[suffix] Edit files in place; append suffix to original names -o comment Output the comment instead of the old file version -p command Use the command instead of `p4 print -q` to get files -v Output progress messages =head1 DESCRIPTION Change diffs in Perforce format (from `p4 diff`, `p4 diff2` or `p4 describe`) to a form suitable for input to the GNU `patch` program. Insert headers and diffs, but don't remove anything. Edit each file in place if file names are given; otherwise filter from input to output. =head1 EXAMPLES p4 diff -du ...@ver8_0 | p4d2p -o "-r v80" | patch -p3 p4 diff2 -dc ...@Beta1 ...@Beta2 | p4d2p -o "-r Beta1" | patch -p5 p4 describe -du 123 | p4d2p -o "@122" | patch -p2 =head1 KNOWN BUGS RCS format diffs (from `p4 diff -dn`) are not converted. =head1 SEE ALSO To generate a patch from a change number: L To generate a patch for added files: L Perforce L GNU `patch` L =head1 AUTHOR John Kristian . Thanks to Gurusamy Sarathy for inspiration; but I accept all blame. =cut use Getopt::Std; use POSIX qw(strftime); use Time::Local qw(timegm); use vars qw($opt_h $opt_i $opt_o $opt_p $opt_v); if (!getopts('hi::o:p:v') || $opt_h) { open(STDOUT, '>&STDERR'); exec('perldoc', $0); exit(1); # exec failed } if (not @ARGV) {@ARGV = '-'; undef $^I;} elsif (defined($opt_i)) {$^I = $opt_i; warn "-i$opt_i\n" if $opt_v;} # else $^I comes from Perl's -i option; e.g. perl -i.bak p4d2p ... my $Z = timegm(localtime(0)) / 60; $Z = ($Z >= 0) ? sprintf('+%02d%02d', $Z/60, $Z%60) : sprintf('-%02d%02d',-$Z/60,-$Z%60); my $timeFormat = "%Y-%m-%d %H:%M:%S $Z"; # ISO 8601 my $now = strftime($timeFormat, localtime); my $epoch = "1970-01-01 00:00:00Z"; my ($oldFile, $oldNote, $newFile, $newNote, @movedFiles); while (<>) { # `p4 diff` header if (m<^==== //(.+?)(\#\d+) +- +(.+?) +====( \(.+\))?$>) { ($oldFile, $oldNote, $newFile) = ($1, $2, $3); $oldNote = " " . ($opt_o || $oldNote); my (@stat) = stat($newFile); $newNote = ((@stat && defined($stat[9])) ? strftime($timeFormat, localtime($stat[9])) # time last modified : $now); $newNote = " $newNote"; } # `p4 diff2` header elsif (m<^==== //(.+?)(\#\d+) +\(.+?\) +- +(.+?)(\#\d+) +\(.+?\) +==== +\w+$>) { ($oldFile, $oldNote, $newFile, $newNote) = ($1, $2, $3, $4); $oldNote = " " . ($opt_o || $oldNote); $newNote = " $newNote"; } # `p4 describe` header elsif (m<^==== //(.+?)(\#\d+) .*?====( \(.+\))?$>) { ($newFile, $newNote) = ($1, $2); $oldFile = $newFile; $oldNote = " " . ($opt_o || (($newNote =~ m<\#(\d+)>) && "#" . ($1-1))); # the previous version $newNote = " $newNote"; } # unified diff (the preferred format for `patch`) elsif (defined($oldFile) && m<^\@\@\s.*\s\@\@$>) { warn "emitting diff -u header\n" if $opt_v; print("Index: //$oldFile\n", "--- $oldFile$oldNote\n", "+++ $oldFile$newNote\n"); undef $oldFile; } # context diff elsif (defined($oldFile) && m<^\*+$>) { warn "emitting diff -c header\n" if $opt_v; print("Index: //$oldFile\n", "*** $oldFile$oldNote\n", "--- $oldFile$newNote\n"); undef $oldFile; } # default diff (not recommended for `patch`) elsif (defined($oldFile) && m<^\d+(,\d+)?[acd]\d+>) { warn "emitting diff header (not recommended for `patch`)\n" if $opt_v; print("Index: $oldFile\n", "diff -r //$oldFile $newFile\n"); undef $oldFile; } # `p4 describe` add, branch or delete elsif (m<^\.\.\. (//.+?\#\d+ (add|branch|delete))$>) { push @movedFiles, $1; } } continue { print; # echo all input if (eof) { if (@movedFiles) { my ($verb, @file); for (@movedFiles) { ($newFile, $newNote, $verb) = m; warn $verb if $opt_v; $oldFile = $newFile; if ($verb eq 'delete') { $oldNote = "#" . (($newNote =~ m<\#(\d+)>) && ($1-1)); # the previous version $newNote = $epoch; @file = p4print("//$oldFile$oldNote"); } else { # add or branch $oldNote = $epoch; @file = p4print("//$newFile$newNote"); } if (!$? && @file) { $oldNote = " " . ($opt_o || $oldNote); $newNote = " $newNote"; my $lines = (@file <= 1) ? "" : ("," . @file); print("\nIndex: //$newFile\n", "--- $oldFile$oldNote\n", "+++ $newFile$newNote\n"); if ($verb eq 'delete') { print("\@\@ -1$lines +0,0 \@\@\n"); print(join("-", "", @file)); } else { # add or branch print("\@\@ -0,0 +1$lines \@\@\n"); print(join("+", "", @file)); } print("\n\\ No newline at end of file\n") if ($file[$#file] !~ m<\n$>); } } @movedFiles = (); } undef $oldFile; } } sub p4print { my ($name) = @_; my $cmd = ($opt_p || "p4 print -q")." \"$name\""; # Sadly, executing `p4 print` will consume some input. # Which is one reason not to emit files immediately # upon reading their names from the input stream. my @file = `$cmd`; warn "status $? from $cmd" if ($?); return @file; }