File Coverage

blib/lib/Text/Patch.pm
Criterion Covered Total %
statement 15 148 10.1
branch 0 58 0.0
condition 0 20 0.0
subroutine 5 15 33.3
pod 1 6 16.6
total 21 247 8.5


line stmt bran cond sub pod time code
1             package Text::Patch;
2 1     1   52871 use Exporter;
  1         2  
  1         65  
3             our @ISA = qw( Exporter );
4             our @EXPORT = qw( patch );
5             our $VERSION = '1.8';
6 1     1   5 use strict;
  1         3  
  1         31  
7 1     1   4 use warnings;
  1         5  
  1         33  
8 1     1   4 use Carp;
  1         1  
  1         54  
9              
10 1     1   5 use constant NO_NEWLINE => '\\ No newline at end of file';
  1         1  
  1         2230  
11              
12             sub patch
13             {
14 0     0 1   my $text = shift;
15 0           my $diff = shift;
16 0 0         my %options = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
  0            
17              
18 0           my %handler = ('unified' => \&patch_unified,
19             'context' => \&patch_context,
20             'oldstyle' => \&patch_oldstyle,
21             );
22 0           my $style = $options{STYLE};
23 0 0         croak "required STYLE option is missing" unless $style;
24 0 0         croak "source required" unless defined $text;
25 0 0         croak "diff required" unless defined $diff;
26 0   0       my $code = $handler{lc($style)} || croak "unrecognised STYLE '$style'";
27              
28 0           my @text = split /^/m, $text;
29 0           my @diff = split /^/m, $diff;
30              
31             # analyse source/diff to determine line ending used.
32             # (if source is only 1 line, can't use it to determine line endings)
33 0 0         my $line1 = @text > 1 ? $text[0] : $diff[0];
34 0           my($line1c, $sep) = _chomp($line1);
35 0   0       $sep ||= "\n"; # default to unix line ending
36              
37             # apply patch
38 0           DUMP("got patch", \@diff);
39 0           my $out = $code->(\@text, \@diff, $sep);
40              
41 0           my $lastline = _chomp($diff[-1], $sep);
42 0 0         $out = _chomp($out, $sep) if $lastline eq NO_NEWLINE;
43 0           return $out;
44             }
45              
46             sub patch_unified
47             {
48 0     0 0   my($text, $diff, $sep) = @_;
49 0           my @hunks;
50             my %hunk;
51              
52 0           for( @$diff )
53             {
54             #print STDERR ">>> ... [$_]";
55 0 0         if( /^\@\@\s*-([\d,]+)/ )
56             {
57             #print STDERR ">>> *** HUNK!\n";
58 0           my($pos1, $count1) = split /,/, $1;
59 0           push @hunks, { %hunk };
60 0           %hunk = ();
61 0           $hunk{ FROM } = $pos1 - 1; # diff is 1-based
62             # Modification by Ben L., patches may have @@ -0,0 if the source is empty.
63 0 0         $hunk{ FROM } = 0 if $hunk{ FROM } < 0;
64 0 0         $hunk{ LEN } = defined $count1 ? $count1 : $pos1 == 0 ? 0 : 1;
    0          
65 0           $hunk{ DATA } = [];
66             }
67 0           push @{ $hunk{ DATA } }, $_;
  0            
68             }
69 0           push @hunks, { %hunk }; # push last hunk
70 0           shift @hunks; # first is always empty
71              
72 0           return _patch($text, \@hunks, $sep);
73             }
74              
75             sub patch_oldstyle {
76 0     0 0   my($text, $diff, $sep) = @_;
77 0           my @hunks;
78 0           my $i = 0;
79              
80 0           my $hunk_head = qr/^([\d,]+)([acd])([\d,]+)$/;
81 0           while($i < @$diff) {
82 0           my $l = $diff->[$i];
83 0           my($r1, $type, $r2) = $l =~ $hunk_head;
84 0 0 0       die "Malformed patch at line ".($i + 1)."\n"
      0        
85             unless defined $r1 && $type && defined $r2;
86 0           my($pos1, $count1) = _range($r1);
87 0           my($pos2, $count2) = _range($r2);
88              
89             # parse chunk data
90 0           my @data;
91 0           my $j = $i + 1;
92 0           for(; $j < @$diff; $j++) {
93 0           $l = $diff->[$j];
94 0 0         last if $l =~ $hunk_head;
95 0 0         next if $l =~ /^---/; # separator
96 0           push @data, $l;
97             }
98 0           my $datalen = $j - $i - 1;
99              
100 0 0         if($type eq 'a') { # add
101 0           $count1 = 0; # don't remove any lines
102 0           $pos1++; # add to line after pos1
103             }
104              
105             # convert data to a format _patch() will understand
106 0           for(@data) {
107 0           $_ =~ s/^< /-/;
108 0           $_ =~ s/^> /+/;
109             }
110              
111 0           push @hunks, { FROM => $pos1 - 1,
112             LEN => $count1,
113             DATA => \@data,
114             };
115 0           $i += $datalen + 1;
116             }
117 0           return _patch($text, \@hunks, $sep);
118             }
119              
120             # NB: this works by converting hunks into a kind of unified format
121             sub patch_context {
122 0     0 0   my($text, $diff, $sep) = @_;
123 0           my $i = 0;
124 0           my @hunks;
125              
126             # skip past header
127 0           for(@$diff) {
128 0           $i++;
129 0 0         last if /^\Q***************\E$/; # end header marker
130             }
131              
132             # this sub reads one half of a hunk (from/to part)
133             my $read_part = sub {
134 0     0     my $l = $diff->[$i++];
135 0           TRACE("got line: $l");
136 0 0         die "Malformed patch at line $i\n"
137             unless $l =~ /^(?:\*\*\*|---)\s+([\d,]+)\s+(?:\*\*\*|---)/;
138 0           my($pos, $count) = _range($1);
139 0           my @part;
140 0           while($i < @$diff) {
141 0           my $l = $diff->[$i];
142 0 0         last if $l =~ /^(\*\*\*|---)/;
143 0           push @part, $l;
144 0           $i++;
145             }
146 0           DUMP("got part", \@part);
147 0           return (\@part, $pos, $count);
148 0           };
149              
150 0           while($i < @$diff) {
151             # read the from and to part of this hunk
152 0           my($part1, $pos1, $count1) = $read_part->();
153 0           my($part2, $pos2, $count2) = $read_part->();
154 0           $i++; # skip chunk separator
155              
156             # convert operations to unified style ones
157 0           $_ =~ s/^(.)\s/$1/ for @$part1, @$part2;
158 0           $_ =~ s/^\!/-/ for @$part1; # remove
159 0           $_ =~ s/^\!/+/ for @$part2; # add
160              
161             # merge the parts to create a unified style chunk
162 0           my @data;
163 0           for(;;) {
164 0           my $c1 = $part1->[0];
165 0           my $c2 = $part2->[0];
166 0 0 0       last unless defined $c1 || defined $c2;
167              
168 0 0 0       if(defined $c1 && $c1 =~ /^-/) {
    0 0        
169 0           push @data, shift @$part1; # remove line
170             } elsif(defined $c2 && $c2 =~ /^\+/) {
171 0           push @data, shift @$part2; # add line
172             } else { # context
173 0           my($x1, $x2) = (shift @$part1, shift @$part2);
174 0 0         push @data, defined $x1 ? $x1 : $x2;
175             }
176             }
177 0           push @hunks, { FROM => $pos1 - 1,
178             LEN => $count1,
179             DATA => \@data,
180             };
181 0           DUMP("merged data", \@data);
182             }
183 0           return _patch($text, \@hunks, $sep);
184             }
185              
186             ######################################################################
187             # private
188              
189             # returns (start line, line count)
190             sub _range {
191 0     0     my($range) = @_;
192 0           my($pos1, $pos2) = split /,/, $range;
193 0 0         return ($pos1, defined $pos2 ? $pos2 - $pos1 + 1 : 1);
194             }
195              
196             sub _patch {
197 0     0     my($text, $hunks, $sep) = @_;
198 0           my $hunknum = scalar @$hunks + 1;
199 0 0         die "No hunks found\n" unless @$hunks;
200 0           for my $hunk ( reverse @$hunks )
201             {
202 0           $hunknum--;
203 0           DUMP("hunk", $hunk);
204 0           my @pdata;
205 0           my $num = $hunk->{FROM};
206 0           for( @{ $hunk->{ DATA } } )
  0            
207             {
208 0 0         next unless s/^([ \-\+])//;
209             #print STDERR ">>> ($1) $_";
210 0 0         if($1 ne '+') {
211             # not an addition, check line for match against existing text.
212             # ignore line endings for comparison
213 0           my $orig = _chomp($text->[$num++], $sep); # num 0 based here
214 0           my $expect = _chomp($_, $sep);
215 0           TRACE("checking >>$orig<<");
216 0           TRACE(" against >>$expect<<");
217 0 0         die "Hunk #$hunknum failed at line $num.\n" # actual line number
218             unless $orig eq $expect;
219             }
220 0 0         next if $1 eq '-'; # removals
221 0           push @pdata, $_; # add/replace line
222             }
223 0           splice @$text, $hunk->{ FROM }, $hunk->{ LEN }, @pdata;
224             }
225              
226 0           return join '', @$text;
227             }
228              
229             # chomp $sep from the end of line
230             # if $sep is not given, chomp unix or dos line ending
231             sub _chomp {
232 0     0     my($text, $sep) = @_;
233 0 0         if($sep) {
234 0           $text =~ s/($sep)$//;
235             } else {
236 0           $text =~ s/(\r\n|\n)$//;
237             }
238 0 0         return wantarray ? ($text, $1) : $text;
239             }
240              
241 0     0 0   sub DUMP {}
242 0     0 0   sub TRACE {}
243              
244             #sub DUMP {
245             #use Data::Dumper;
246             #print STDERR Dumper(@_);
247             #}
248             #sub TRACE {
249             #use Data::Dumper;
250             #print STDERR Dumper(@_);
251             #}
252              
253              
254             =pod
255              
256             =head1 NAME
257              
258             Text::Patch - Patches text with given patch
259              
260             =head1 SYNOPSIS
261              
262             use Text::Patch;
263              
264             $output = patch( $source, $diff, STYLE => "Unified" );
265              
266             use Text::Diff;
267              
268             $src = ...
269             $dst = ...
270              
271             $diff = diff( \$src, \$dst, { STYLE => 'Unified' } );
272              
273             $out = patch( $src, $diff, { STYLE => 'Unified' } );
274              
275             print "Patch successful" if $out eq $dst;
276              
277             =head1 DESCRIPTION
278              
279             Text::Patch combines source text with given diff (difference) data.
280             Diff data is produced by Text::Diff module or by the standard diff
281             utility (man diff, see -u option).
282              
283             =over 4
284              
285             =item patch( $source, $diff, options... )
286              
287             First argument is source (original) text. Second is the diff data.
288             Third argument can be either hash reference with options or all the
289             rest arguments will be considered patch options:
290              
291             $output = patch( $source, $diff, STYLE => "Unified", ... );
292              
293             $output = patch( $source, $diff, { STYLE => "Unified", ... } );
294              
295             Options are:
296              
297             STYLE => 'Unified'
298              
299             STYLE can be "Unified", "Context" or "OldStyle".
300              
301             The 'Unified' diff format looks like this:
302              
303             @@ -1,7 +1,6 @@
304             -The Way that can be told of is not the eternal Way;
305             -The name that can be named is not the eternal name.
306             The Nameless is the origin of Heaven and Earth;
307             -The Named is the mother of all things.
308             +The named is the mother of all things.
309             +
310             Therefore let there always be non-being,
311             so we may see their subtlety,
312             And let there always be being,
313             @@ -9,3 +8,6 @@
314             The two are the same,
315             But after they are produced,
316             they have different names.
317             +They both may be called deep and profound.
318             +Deeper and more profound,
319             +The door of all subtleties!
320              
321              
322             =back
323              
324             =head1 TODO
325              
326             Interfaces with files, arrays, etc.
327              
328             =head1 AUTHOR
329              
330             Vladi Belperchinov-Shabanski "Cade"
331              
332            
333              
334             http://cade.datamax.bg
335              
336             =head1 VERSION
337              
338             $Id: Patch.pm,v 1.6 2007/04/07 19:57:41 cade Exp $
339              
340             =cut