File Coverage

blib/lib/Text/Diff/Parser.pm
Criterion Covered Total %
statement 230 231 99.5
branch 91 106 85.8
condition 50 65 76.9
subroutine 30 30 100.0
pod 7 7 100.0
total 408 439 92.9


line stmt bran cond sub pod time code
1             package Text::Diff::Parser;
2             # $Id: Parser.pm 530 2009-09-09 10:26:49Z fil $
3              
4 5     5   168829 use 5.00404;
  5         23  
  5         218  
5 5     5   35 use strict;
  5         9  
  5         195  
6 5     5   25 use warnings;
  5         34  
  5         198  
7 5     5   26 use vars qw( $VERSION );
  5         10  
  5         427  
8              
9 5     5   26 use Carp;
  5         7  
  5         660  
10 5     5   6598 use IO::File;
  5         96650  
  5         16581  
11              
12             $VERSION = '0.1001';
13             $VERSION = eval $VERSION; # see L
14              
15             ####################################################
16             sub new
17             {
18 15     15 1 35276 my( $package, @args ) = @_;
19              
20 15         99 my $self = bless { changes=>[],
21             source=>'' }, $package;
22              
23 15         80 my $parms;
24 15 100       57 if( 1==@args ) {
25 9 100       34 if( 'HASH' eq ref $args[0] ) {
26 6         14 $parms = $args[0];
27             }
28             else {
29 3         6 my $diff = $args[0];
30 3 100 100     23 if( ref $diff or $diff !~ /\n/ ) {
31 2         7 $parms = { File => $diff };
32             }
33             else {
34 1         5 $parms = { Diff => $diff };
35             }
36             }
37             }
38             else {
39 6         18 $parms = { @args };
40             }
41              
42 15         63 $self->__init( $parms );
43 15         83 return $self;
44             }
45              
46             sub __init
47             {
48 15     15   26 my( $self, $parms ) = @_;
49              
50 15 50       72 $self->{verbose} = 1 if $parms->{Verbose};
51 15         59 $self->{simplify} = $parms->{Simplify};
52 15         36 $self->{strip} = $parms->{Strip};
53 15         32 $self->{trustatat} = 1;
54 15 50       57 $self->{trustatat} = $parms->{TrustAtAt} if exists $parms->{TrustAtAt};
55              
56 15 100       60 if( $parms->{ File } ) {
    100          
57 10         46 $self->parse_file( $parms->{File} );
58             }
59             elsif( $parms->{ Diff } ) {
60 2         8 $self->parse( $parms->{Diff} );
61             }
62 15         41 return $self;
63             }
64              
65             ####################################################
66             sub source
67             {
68 3     3 1 2145 my( $self ) = @_;
69 3         24 return $self->{source};
70             }
71              
72             ####################################################
73             sub changes
74             {
75 34     34 1 19057 my( $self, $file ) = @_;
76 34         73 my $ret = $self->{changes};
77 34 100       108 if( $file ) {
78 4         9 $ret = [];
79 4         7 foreach my $ch ( @{ $self->{changes} } ) {
  4         12  
80 172 100 100     307 next unless $ch->filename1 eq $file or
81             $ch->filename2 eq $file;
82 84         164 push @$ret, $ch;
83             }
84             }
85              
86 34 100       284 return @$ret if wantarray;
87 7         37 return 0+@$ret;
88             }
89              
90              
91             ####################################################
92             sub files
93             {
94 2     2 1 36 my( $self ) = @_;
95 2         34 my %ret;
96 2         7 foreach my $ch ( $self->changes ) {
97 40         70 $ret{$ch->filename1} = $ch->filename2;
98             }
99 2         27 return %ret;
100             }
101              
102              
103             ####################################################
104             sub simplify
105             {
106 9     9 1 30 my( $self ) = @_;
107              
108 9         14 my @keep;
109             my $prev;
110 9         32 foreach my $ch ( $self->changes ) {
111 421 100       1256 if( $ch->type eq '' ) { # skip no-change
112 215         247 undef( $prev );
113 215         318 next;
114             }
115            
116 206 100       484 if( $prev ) {
117 68         138 my $size = $prev->size;
118             ## Combine ADD/REMOVE lines
119 68 100 33     142 if( $prev->type ne $ch->type and # ADD->REMOVE or REMOVE->ADD
      33        
      66        
120             $prev->filename1 eq $ch->filename1 and
121             $prev->filename2 eq $ch->filename2 and
122             $size == $ch->size ) { #close
123              
124 38 100 66     68 if( $prev->type eq 'REMOVE' and
    50 66        
      33        
      33        
125             $prev->line2 == $ch->line2 and
126             ($prev->line1+$size) == $ch->line1 ) {
127 36         77 $prev->{type} = 'MODIFY';
128 36         62 $prev->{lines} = $ch->{lines};
129 36         75 undef( $prev );
130 36         118 next;
131             }
132             elsif( $prev->type eq 'ADD' and
133             ($prev->line2+$size) == $ch->line2 and
134             $prev->line1 == $ch->line1 ) {
135 2         5 $prev->{type} = 'MODIFY';
136 2         3 undef( $prev );
137 2         6 next;
138             }
139             # same size, same file, but not at the same spot
140             }
141             }
142 168         260 push @keep, $ch;
143 168         293 $prev = $ch;
144             }
145 9         52 $self->{changes} = \@keep;
146             }
147              
148              
149             ####################################################
150             sub parse_file
151             {
152 25     25 1 12730 my( $self, $file ) = @_;
153              
154 25         68 local $self->{count1};
155 25         49 local $self->{count2};
156              
157 25         32 my $fh;
158 25 100       68 if( ref $file ) { # assume it's a file handle
159 1         2 $self->{source} = 'user filehandle';
160 1         2 $fh = $file;
161             }
162             else {
163 24         53 $self->{source} = $file;
164 24         227 $fh = IO::File->new;
165 24 50       3813 $fh->open( $file ) or croak "Unable to open $file: $!";
166             }
167              
168 25         2251 $self->{changes}=[];
169 25         329 $self->{state}={ OK=>1 };
170              
171 25         909 while( <$fh> ) {
172 2330         11526 $self->{state}{context} = "line $. of $self->{source}";
173 2330         5158 $self->_parse_line( $_ );
174             }
175 25         56 my $ok = $self->{state}{OK};
176 25         94 delete $self->{state};
177 25 100       95 $self->simplify if $self->{simplify};
178 25         2666 return $ok;
179             }
180              
181              
182             ####################################################
183             sub parse
184             {
185 2     2 1 5 my( $self, $text ) = @_;
186 2         6 $self->{source} = "user string";
187 2         19 $self->{changes}=[];
188 2         9 $self->{state}={ OK=>1 };
189 2         5 local $self->{count1};
190 2         8 local $self->{count2};
191              
192 2         3 my $l=1;
193 2         17 while( $text =~ /(.+?\n)/g ) {
194 38         119 $self->{state}{context} = "line $l of string";
195 38         72 $self->_parse_line( $1 );
196 38         186 $l++;
197             }
198 2         4 my $ok = $self->{state}{OK};
199 2         8 delete $self->{state};
200 2 50       8 $self->simplify if $self->{simplify};
201 2         7 return $ok;
202              
203             }
204              
205             ####################################################
206             sub _parse_line
207             {
208 2368     2368   15709 my( $self, $line ) = @_;
209 2368 50       5951 $self->{verbose} and warn "Parsing $line";
210              
211 2368         3336 my $state = $self->{state};
212              
213 2368 100       22153 if( $state->{unified} ) {
    100          
214 2123         4660 $self->_unified_line( $line );
215 2123 100       10818 return if $state->{unified};
216             }
217             elsif( $state->{standard} ) {
218 24         52 $self->_standard_line( $line );
219 24 100       138 return if $state->{standard};
220             }
221              
222 258         1511 my $file = '(?:-r\d(?:\.\d+)+)|(?:[^-].+)';
223              
224 258 100 100     5875 if( $line =~ /^diff\s+($file)\s+($file)\s*$/ ) {
    100 100        
    100 33        
    50          
225 6         50 my @match = ( $1, $2 );
226 6 50       19 $self->{verbose} and warn "Diff $1 $2";
227 6         22 $state->{filename1} = $self->_filename( $match[0] );
228 6         15 $state->{filename2} = $self->_filename( $match[1] );
229             }
230             elsif( $line =~ /^(\d+)(?:,\d+)?[acd](\d+)(?:,\d+)?$/ ) {
231 8         113 $state->{standard} = 1;
232 8         23 push @{ $self->{changes} }, bless {
  8         115  
233             at1 => $1, line1 => $1,
234             at2 => $2, line2 => $2,
235             filename1 => $state->{filename1},
236             filename2 => $state->{filename2},
237             timestamp1 => '',
238             timestamp2 => ''
239             }, 'Text::Diff::Parser::Change';
240 8 50       64 $self->{verbose} and warn "Standard diff line1=$1 line2=$2";
241             }
242             elsif( $line =~ /^--- (.+?)\t(.+)$/ or
243             $line =~ /^--- ([^\s]+)\s+(.+)$/ or
244             $line =~ /^--- ([^\s]+)$/ ) { # kernel.org style
245 41         113 $self->{count1} = 0;
246 41         67 $self->{count2} = 0;
247 41         221 $state->{unified} = 1;
248 41         113 my $stamp = $2;
249 41         139 my $name = $self->_filename( $1 );
250 41 50       113 $self->{verbose} and warn "Unified diff";
251 41         58 push @{ $self->{changes} }, bless {
  41         381  
252             filename1 => $name,
253             timestamp1 => $stamp,
254             }, 'Text::Diff::Parser::Change';
255             }
256             elsif( $line =~ /^\*\*\* (.+?)\t(.+)$/ or
257             $line =~ /^\*\*\* ([^\s]+)\s+(.+)$/) {
258 0         0 die "Context diff not yet supported at $state->{context}";
259             }
260             }
261              
262             ####################################################
263             sub _filename
264             {
265 94     94   186 my( $self, $file ) = @_;
266 94 100       364 return $file unless $self->{strip};
267 24         39 my $n = $self->{strip};
268 24         213 $file =~ s(^[^/]+/)() while $n--;
269 24         71 return $file;
270             }
271              
272             ####################################################
273             sub _standard_line
274             {
275 24     24   42 my( $self, $line ) = @_;
276              
277 24         94 my %types = ( ' '=>'', '>'=>'ADD', '<'=>'REMOVE' );
278              
279 24         109 my $change = $self->{changes}[-1];
280            
281 24 100       186 if( $line =~ /^([<>])(.+)$/ ) {
282 14         37 my( $mod, $text ) = ( $1, $2 );
283 14         22 $mod = $types{$mod};
284 14         35 $self->_new_line( $mod, $text );
285 14         44 return;
286             }
287 10 100       40 if( $line =~ /^---$/ ) { # pivot
288 4 50       14 $self->{verbose} and warn "Pivot";
289 4         11 return;
290             }
291 6         29 delete $self->{state}{standard}; # let _parse_file deal with it
292             }
293              
294             ####################################################
295             sub _unified_line
296             {
297 2123     2123   21088 my( $self, $line ) = @_;
298            
299 2123         6801 my %types = ( ' '=>'', '+'=>'ADD', '-'=>'REMOVE' );
300              
301 2123         4086 my $change = $self->{changes}[-1];
302 2123 100 100     18667 if( $line =~ /^\+\+\+ (.+?)\t(.+)$/ or
      100        
303             $line =~ /^\+\+\+ ([^\s]+)\s+(.+)$/ or
304             $line =~ /^\+\+\+ ([^\s]+)$/ ) { # kernel.org style
305 41   100     354 $change->{timestamp2} = ($2||'');
306 41         106 $change->{filename2} = $self->_filename( $1 );
307 41         109 $change->{lines} = [];
308 41         123 return;
309             }
310 2082 50       5260 die "Missing +++ line before $line" unless $change->{filename2};
311 2082 100       6736 if( $line =~ /^\@\@ -(\d+)(?:,(\d+))? [+](\d+)(?:,(\d+))? \@\@\s*(.+)?$/ ) {
312 121   100     1437 my @match = ($1, ($2||0), $3, ($4||0), ($5||''));
      100        
      100        
313 121 100       144 if( @{ $change->{lines} } ) {
  121         352  
314 80         187 $change = $self->_new_chunk;
315             }
316 121         281 @{ $change }{ qw( line1 size1 line2 size2 function ) } = @match;
  121         807  
317 121         397 $change->{at1} = $change->{line1};
318 121         217 $change->{at2} = $change->{line2};
319 121         176 $self->{count1} = 0;
320 121         155 $self->{count2} = 0;
321 121         420 return;
322             }
323 1961 50       6056 die "Missing \@\@ line before $line at $self->{state}{context}\n"
324             unless defined $change->{line1};
325              
326             # use Data::Dumper;
327             # die "No size1 in ", Dumper $change unless defined $change->{size1};
328             # die "No size2 in ", Dumper $change unless defined $change->{size2};
329             # warn "$change->{size1} > $self->{count1} $change->{size2} > $self->{count2}";
330              
331 1961         2447 my $done = 1;
332 1961 100 100     12202 if( $self->{trustatat} and ( $change->{size1} > $self->{count1} or
      33        
333             $change->{size2} > $self->{count2} ) ) {
334 1929         2678 $done = 0;
335             }
336              
337 1961 100 100     4914 if( $done and $line =~ /^---/ ) {
338 3         7 $self->{state}{unified} = 0;
339 3         8 return;
340             }
341              
342 1958 100       8993 if( $line =~ /^([-+ ])(.*)?$/) {
343 1930         5061 my( $mod, $text ) = ( $1, $2 );
344 1930         2952 $mod = $types{$mod};
345 1930         4109 $self->_new_line( $mod, $text );
346 1930         7736 return;
347             }
348             # Anything else is the end of the diff, so fall through to the
349             # diff detection bit
350 28         101 $self->{state}{unified} = 0;
351             }
352              
353             sub _new_type
354             {
355 500     500   672 my( $self, $mod ) = @_;
356 500         987 my $change = $self->{changes}[-1];
357              
358 500         599 push @{ $self->{changes} }, bless {
  500         7086  
359             filename1 => $change->{filename1},
360             filename2 => $change->{filename2},
361             line1 => $change->{at1},
362             line2 => $change->{at2},
363             size1 => $change->{size1},
364             size2 => $change->{size2},
365             at1 => $change->{at1},
366             at2 => $change->{at2},
367             function => $change->{function},
368             type => $mod,
369             lines => []
370             }, 'Text::Diff::Parser::Change';
371 500         1605 return $self->{changes}[-1];
372             }
373              
374             sub _new_chunk
375             {
376 80     80   100 my( $self ) = @_;
377 80         141 my $change = $self->{changes}[-1];
378 80         83 push @{ $self->{changes} }, bless {
  80         749  
379             type => '',
380             filename1 => $change->{filename1},
381             filename2 => $change->{filename2},
382             lines => []
383             }, 'Text::Diff::Parser::Change';
384 80         232 return $self->{changes}[-1];
385             }
386              
387             sub _new_line
388             {
389 1944     1944   3471 my( $self, $mod, $text ) = @_;
390              
391 1944 100       4648 $self->{count1}++ if $mod ne 'ADD';
392 1944 100       4097 $self->{count2}++ if $mod ne 'REMOVE';
393              
394 1944 50       4219 $self->{verbose} and warn "_new_line";
395 1944         3063 my $change = $self->{changes}[-1];
396 1944 100       11894 if( defined $change->{type} ) {
397 1895 100       5375 if( $change->{type} ne $mod ) {
398 500 50       1058 $self->{verbose} and warn "_new_type";
399 500         983 $change = $self->_new_type( $mod );
400             }
401             }
402             else {
403 49         1069 $change->{type} = $mod;
404             }
405              
406 1944 100       5858 $change->{at1}++ unless $mod eq 'ADD'; # - or ' ', advance in file1
407 1944 100       4436 $change->{at2}++ unless $mod eq 'REMOVE'; # + or ' ', advance in file2
408 1944         2594 push @{ $change->{lines} }, $text;
  1944         6730  
409             }
410              
411             ######################################################################
412             package Text::Diff::Parser::Change;
413              
414 5     5   61 use strict;
  5         10  
  5         1636  
415              
416 440     440   2304 sub filename1 { $_[0]->{filename1} }
417 405     405   2333 sub filename2 { $_[0]->{filename2} }
418 168     168   1725 sub line1 { $_[0]->{line1} }
419 168     168   1584 sub line2 { $_[0]->{line2} }
420 79     79   742 sub function { $_[0]->{function} }
421 228     228   3167 sub size { 0+@{$_[0]->{lines}} }
  228         2955  
422              
423             sub type
424             {
425 689     689   1563 my( $self ) = @_;
426              
427 689 100 100     6189 return $self->{type} if $self->{type} eq 'ADD' or
      100        
428             $self->{type} eq 'REMOVE' or
429             $self->{type} eq 'MODIFY';
430 246         662 return '';
431             }
432            
433             sub text
434             {
435 2     2   1077 my( $self, $n ) = @_;
436 2 100       8 return @{ $self->{lines} } if 1==@_;
  1         11  
437              
438 1         7 return $self->{lines}[$n];
439             }
440            
441              
442              
443             1;
444             __END__