#!/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<http://public.activestate.com/gsar/APC/perl-current/Porting/p4genpatch>
To generate a patch for added files:
L<http://public.activestate.com/gsar/APC/perl-5.6.x/Porting/p4desc>
Perforce L<http://www.perforce.com/perforce/technical.html>
GNU `patch` L<http://www.fsf.org/manual/diffutils-2.8.1/html_node/Multiple-Patches.html>
=head1 AUTHOR
John Kristian <jkristian@docent.com>.
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<//(.+?)(\#\d+) (\w*)$>;
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;
}
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #9 | 3909 | John Kristian |
Added option -i (similar to perl -i). Use the local time zone for output times. |
||
| #8 | 3887 | John Kristian |
Made $timeFormat more human-readable (and more like GNU `diff`). Output '1970-01-01 00:00:00Z' as the $oldNote of an added file or $newNote of a deleted file. |
||
| #7 | 3886 | John Kristian | renamed variables | ||
| #6 | 3885 | John Kristian | improved POD | ||
| #5 | 3884 | John Kristian | output a patch for deleted files, too | ||
| #4 | 3883 | John Kristian | output a patch for added files, too | ||
| #3 | 3882 | John Kristian | improved POD | ||
| #2 | 3881 | John Kristian | Combined the interface (command line syntax) of p4d2p with the implementation of p4diff2patch. | ||
| #1 | 3879 | John Kristian | http://search.cpan.org/src/JHI/perl-5.8.1-RC4/Porting/p4d2p |