File Coverage

blib/lib/Text/Diff.pm
Criterion Covered Total %
statement 183 189 96.8
branch 89 104 85.5
condition 23 32 71.8
subroutine 35 37 94.5
pod 0 1 0.0
total 330 363 90.9


line stmt bran cond sub pod time code
1             package Text::Diff;
2              
3 6     6   32976 use 5.006;
  6         21  
4 6     6   31 use strict;
  6         7  
  6         145  
5 6     6   25 use warnings;
  6         9  
  6         233  
6 6     6   24 use Carp qw/ croak confess /;
  6         13  
  6         380  
7 6     6   32 use Exporter ();
  6         8  
  6         98  
8 6     6   4487 use Algorithm::Diff ();
  6         31526  
  6         360  
9              
10             our $VERSION = '1.43';
11             our @ISA = qw/ Exporter /;
12             our @EXPORT = qw/ diff /;
13              
14             ## Hunks are made of ops. An op is the starting index for each
15             ## sequence and the opcode:
16 6     6   48 use constant A => 0; # Array index before match/discard
  6         10  
  6         521  
17 6     6   28 use constant B => 1;
  6         11  
  6         279  
18 6     6   26 use constant OPCODE => 2; # "-", " ", "+"
  6         9  
  6         248  
19 6     6   81 use constant FLAG => 3; # What to display if not OPCODE "!"
  6         9  
  6         13643  
20              
21             my %internal_styles = (
22             Unified => undef,
23             Context => undef,
24             OldStyle => undef,
25             Table => undef, ## "internal", but in another module
26             );
27              
28             sub diff {
29 32     32 0 20889 my @seqs = ( shift, shift );
30 32   100     105 my $options = shift || {};
31              
32 32         78 for my $i ( 0 .. 1 ) {
33 64         65 my $seq = $seqs[$i];
34 64         80 my $type = ref $seq;
35              
36 64         132 while ( $type eq "CODE" ) {
37 4         6 $seqs[$i] = $seq = $seq->( $options );
38 4         13 $type = ref $seq;
39             }
40              
41 64 100       109 my $AorB = !$i ? "A" : "B";
42              
43 64 100 33     116 if ( $type eq "ARRAY" ) {
    100          
    100          
    50          
44             ## This is most efficient :)
45             $options->{"OFFSET_$AorB"} = 0
46 32 50       145 unless defined $options->{"OFFSET_$AorB"};
47             }
48             elsif ( $type eq "SCALAR" ) {
49 24         56 $seqs[$i] = [split( /^/m, $$seq )];
50             $options->{"OFFSET_$AorB"} = 1
51 24 50       89 unless defined $options->{"OFFSET_$AorB"};
52             }
53             elsif ( ! $type ) {
54             $options->{"OFFSET_$AorB"} = 1
55 4 50       14 unless defined $options->{"OFFSET_$AorB"};
56             $options->{"FILENAME_$AorB"} = $seq
57 4 50       11 unless defined $options->{"FILENAME_$AorB"};
58             $options->{"MTIME_$AorB"} = (stat($seq))[9]
59 4 50       41 unless defined $options->{"MTIME_$AorB"};
60              
61 4         12 local $/ = "\n";
62 4 50       56 open F, "<$seq" or croak "$!: $seq";
63 4         42 $seqs[$i] = [];
64 4         26 close F;
65              
66             }
67             elsif ( $type eq "GLOB" || UNIVERSAL::isa( $seq, "IO::Handle" ) ) {
68             $options->{"OFFSET_$AorB"} = 1
69 4 50       16 unless defined $options->{"OFFSET_$AorB"};
70 4         12 local $/ = "\n";
71 4         65 $seqs[$i] = [<$seq>];
72             }
73             else {
74 0         0 confess "Can't handle input of type ", ref;
75             }
76             }
77              
78             ## Config vars
79 32         39 my $output;
80 32         52 my $output_handler = $options->{OUTPUT};
81 32         36 my $type = ref $output_handler ;
82 32 100 66     79 if ( ! defined $output_handler ) {
    100          
    50          
    100          
    50          
83 28         33 $output = "";
84 28     121   82 $output_handler = sub { $output .= shift };
  121         1548  
85             }
86             elsif ( $type eq "CODE" ) {
87             ## No problems, mate.
88             }
89             elsif ( $type eq "SCALAR" ) {
90 0         0 my $out_ref = $output_handler;
91 0     0   0 $output_handler = sub { $$out_ref .= shift };
  0         0  
92             }
93             elsif ( $type eq "ARRAY" ) {
94 1         3 my $out_ref = $output_handler;
95 1     5   5 $output_handler = sub { push @$out_ref, shift };
  5         10  
96             }
97             elsif ( $type eq "GLOB" || UNIVERSAL::isa $output_handler, "IO::Handle" ) {
98 2         2 my $output_handle = $output_handler;
99 2     10   9 $output_handler = sub { print $output_handle shift };
  10         35  
100             }
101             else {
102 0         0 croak "Unrecognized output type: $type";
103             }
104              
105 32         45 my $style = $options->{STYLE};
106 32 100       67 $style = "Unified" unless defined $options->{STYLE};
107 32 100       100 $style = "Text::Diff::$style" if exists $internal_styles{$style};
108              
109 32 100       269 if ( ! $style->can( "hunk" ) ) {
110 2 50       116 eval "require $style; 1" or die $@;
111             }
112              
113 32 100 66     346 $style = $style->new if ! ref $style && $style->can( "new" );
114              
115 32         38 my $ctx_lines = $options->{CONTEXT};
116 32 100       62 $ctx_lines = 3 unless defined $ctx_lines;
117 32 100       174 $ctx_lines = 0 if $style->isa( "Text::Diff::OldStyle" );
118              
119             my @keygen_args = $options->{KEYGEN_ARGS}
120 32 100       71 ? @{$options->{KEYGEN_ARGS}}
  1         36  
121             : ();
122              
123             ## State vars
124 32         31 my $diffs = 0; ## Number of discards this hunk
125 32         30 my $ctx = 0; ## Number of " " (ctx_lines) ops pushed after last diff.
126 32         22 my @ops; ## ops (" ", +, -) in this hunk
127 32         28 my $hunks = 0; ## Number of hunks
128              
129             my $emit_ops = sub {
130 31 100   31   111 $output_handler->( $style->file_header( @seqs, $options ) )
131             unless $hunks++;
132 31         97 $output_handler->( $style->hunk_header( @seqs, @_, $options ) );
133 31         81 $output_handler->( $style->hunk ( @seqs, @_, $options ) );
134 31         104 $output_handler->( $style->hunk_footer( @seqs, @_, $options ) );
135 32         105 };
136              
137             ## We keep 2*ctx_lines so that if a diff occurs
138             ## at 2*ctx_lines we continue to grow the hunk instead
139             ## of emitting diffs and context as we go. We
140             ## need to know the total length of both of the two
141             ## subsequences so the line count can be printed in the
142             ## header.
143 32     30   73 my $dis_a = sub {push @ops, [@_[0,1],"-"]; ++$diffs ; $ctx = 0 };
  30         636  
  30         33  
  30         47  
144 32     29   67 my $dis_b = sub {push @ops, [@_[0,1],"+"]; ++$diffs ; $ctx = 0 };
  29         116  
  29         25  
  29         42  
145              
146             Algorithm::Diff::traverse_sequences(
147             @seqs,
148             {
149             MATCH => sub {
150 118     118   2775 push @ops, [@_[0,1]," "];
151              
152 118 100 100     253 if ( $diffs && ++$ctx > $ctx_lines * 2 ) {
153 11         25 $emit_ops->( [ splice @ops, 0, $#ops - $ctx_lines ] );
154 11         19 $ctx = $diffs = 0;
155             }
156              
157             ## throw away context lines that aren't needed any more
158 118 100 100     474 shift @ops if ! $diffs && @ops > $ctx_lines;
159             },
160             DISCARD_A => $dis_a,
161             DISCARD_B => $dis_b,
162             },
163             $options->{KEYGEN}, # pass in user arguments for key gen function
164 32         242 @keygen_args,
165             );
166              
167 32 100       302 if ( $diffs ) {
168 20 50       36 $#ops -= $ctx - $ctx_lines if $ctx > $ctx_lines;
169 20         41 $emit_ops->( \@ops );
170             }
171              
172 32 100       102 $output_handler->( $style->file_footer( @seqs, $options ) ) if $hunks;
173              
174 32 100       420 return defined $output ? $output : $hunks;
175             }
176              
177             sub _header {
178 14     14   17 my ( $h ) = @_;
179 14         35 my ( $p1, $fn1, $t1, $p2, $fn2, $t2 ) = @{$h}{
180 14         17 "FILENAME_PREFIX_A",
181             "FILENAME_A",
182             "MTIME_A",
183             "FILENAME_PREFIX_B",
184             "FILENAME_B",
185             "MTIME_B"
186             };
187              
188             ## remember to change Text::Diff::Table if this logic is tweaked.
189 14 100 66     64 return "" unless defined $fn1 && defined $fn2;
190              
191 5 50       217 return join( "",
    50          
192             $p1, " ", $fn1, defined $t1 ? "\t" . localtime $t1 : (), "\n",
193             $p2, " ", $fn2, defined $t2 ? "\t" . localtime $t2 : (), "\n",
194             );
195             }
196              
197             ## _range encapsulates the building of, well, ranges. Turns out there are
198             ## a few nuances.
199             sub _range {
200 42     42   46 my ( $ops, $a_or_b, $format ) = @_;
201              
202 42         45 my $start = $ops->[ 0]->[$a_or_b];
203 42         35 my $after = $ops->[-1]->[$a_or_b];
204              
205             ## The sequence indexes in the lines are from *before* the OPCODE is
206             ## executed, so we bump the last index up unless the OP indicates
207             ## it didn't change.
208 42 100       107 ++$after
    100          
209             unless $ops->[-1]->[OPCODE] eq ( $a_or_b == A ? "+" : "-" );
210              
211             ## convert from 0..n index to 1..(n+1) line number. The unless modifier
212             ## handles diffs with no context, where only one file is affected. In this
213             ## case $start == $after indicates an empty range, and the $start must
214             ## not be incremented.
215 42         47 my $empty_range = $start == $after;
216 42 100       59 ++$start unless $empty_range;
217              
218             return
219 42 100 100     196 $start == $after
    100          
    100          
220             ? $format eq "unified" && $empty_range
221             ? "$start,0"
222             : $start
223             : $format eq "unified"
224             ? "$start,".($after-$start+1)
225             : "$start,$after";
226             }
227              
228             sub _op_to_line {
229 115     115   103 my ( $seqs, $op, $a_or_b, $op_prefixes ) = @_;
230              
231 115         101 my $opcode = $op->[OPCODE];
232 115 100       216 return () unless defined $op_prefixes->{$opcode};
233              
234 103 100       139 my $op_sym = defined $op->[FLAG] ? $op->[FLAG] : $opcode;
235 103         89 $op_sym = $op_prefixes->{$op_sym};
236 103 50       138 return () unless defined $op_sym;
237              
238 103 100       185 $a_or_b = $op->[OPCODE] ne "+" ? 0 : 1 unless defined $a_or_b;
    100          
239 103         320 return ( $op_sym, $seqs->[$a_or_b][$op->[$a_or_b]] );
240             }
241              
242             SCOPE: {
243             package Text::Diff::Base;
244              
245             sub new {
246 23     23   29 my $proto = shift;
247 23   33     103 return bless { @_ }, ref $proto || $proto;
248             }
249              
250 9     9   17 sub file_header { return "" }
251              
252 8     8   17 sub hunk_header { return "" }
253              
254 0     0   0 sub hunk { return "" }
255              
256 29     29   45 sub hunk_footer { return "" }
257              
258 15     15   26 sub file_footer { return "" }
259             }
260              
261             @Text::Diff::Unified::ISA = qw( Text::Diff::Base );
262              
263             sub Text::Diff::Unified::file_header {
264 12     12   13 shift; ## No instance data
265 12         11 my $options = pop ;
266              
267 12         76 _header(
268             { FILENAME_PREFIX_A => "---", FILENAME_PREFIX_B => "+++", %$options }
269             );
270             }
271              
272             sub Text::Diff::Unified::hunk_header {
273 14     14   16 shift; ## No instance data
274 14         10 pop; ## Ignore options
275 14         17 my $ops = pop;
276              
277 14         26 return join( "",
278             "@@ -",
279             _range( $ops, A, "unified" ),
280             " +",
281             _range( $ops, B, "unified" ),
282             " @@\n",
283             );
284             }
285              
286             sub Text::Diff::Unified::hunk {
287 14     14   10 shift; ## No instance data
288 14         13 pop; ## Ignore options
289 14         18 my $ops = pop;
290              
291 14         37 my $prefixes = { "+" => "+", " " => " ", "-" => "-" };
292              
293 14         49 return join "", map _op_to_line( \@_, $_, undef, $prefixes ), @$ops
294             }
295              
296             @Text::Diff::Context::ISA = qw( Text::Diff::Base );
297              
298             sub Text::Diff::Context::file_header {
299 2     2   3 _header { FILENAME_PREFIX_A=>"***", FILENAME_PREFIX_B=>"---", %{$_[-1]} };
  2         17  
300             }
301              
302             sub Text::Diff::Context::hunk_header {
303 4     4   7 return "***************\n";
304             }
305              
306             sub Text::Diff::Context::hunk {
307 4     4   3 shift; ## No instance data
308 4         3 pop; ## Ignore options
309 4         5 my $ops = pop;
310             ## Leave the sequences in @_[0,1]
311              
312 4         5 my $a_range = _range( $ops, A, "" );
313 4         8 my $b_range = _range( $ops, B, "" );
314              
315             ## Sigh. Gotta make sure that differences that aren't adds/deletions
316             ## get prefixed with "!", and that the old opcodes are removed.
317 4         4 my $after;
318 4         10 for ( my $start = 0; $start <= $#$ops ; $start = $after ) {
319             ## Scan until next difference
320 17         17 $after = $start + 1;
321 17         19 my $opcode = $ops->[$start]->[OPCODE];
322 17 100       44 next if $opcode eq " ";
323              
324 6         5 my $bang_it;
325 6   100     22 while ( $after <= $#$ops && $ops->[$after]->[OPCODE] ne " " ) {
326 2   33     9 $bang_it ||= $ops->[$after]->[OPCODE] ne $opcode;
327 2         6 ++$after;
328             }
329              
330 6 100       17 if ( $bang_it ) {
331 2         5 for my $i ( $start..($after-1) ) {
332 4         11 $ops->[$i]->[FLAG] = "!";
333             }
334             }
335             }
336              
337 4         10 my $b_prefixes = { "+" => "+ ", " " => " ", "-" => undef, "!" => "! " };
338 4         10 my $a_prefixes = { "+" => undef, " " => " ", "-" => "- ", "!" => "! " };
339              
340 4         10 return join( "",
341             "*** ", $a_range, " ****\n",
342             map( _op_to_line( \@_, $_, A, $a_prefixes ), @$ops ),
343             "--- ", $b_range, " ----\n",
344             map( _op_to_line( \@_, $_, B, $b_prefixes ), @$ops ),
345             );
346             }
347              
348             @Text::Diff::OldStyle::ISA = qw( Text::Diff::Base );
349              
350             sub _op {
351 6     6   6 my $ops = shift;
352 6         8 my $op = $ops->[0]->[OPCODE];
353 6 100       22 $op = "c" if grep $_->[OPCODE] ne $op, @$ops;
354 6 100       11 $op = "a" if $op eq "+";
355 6 100       13 $op = "d" if $op eq "-";
356 6         7 return $op;
357             }
358              
359             sub Text::Diff::OldStyle::hunk_header {
360 3     3   2 shift; ## No instance data
361 3         3 pop; ## ignore options
362 3         4 my $ops = pop;
363              
364 3         4 my $op = _op $ops;
365              
366 3         7 return join "", _range( $ops, A, "" ), $op, _range( $ops, B, "" ), "\n";
367             }
368              
369             sub Text::Diff::OldStyle::hunk {
370 3     3   3 shift; ## No instance data
371 3         3 pop; ## ignore options
372 3         3 my $ops = pop;
373             ## Leave the sequences in @_[0,1]
374              
375 3         8 my $a_prefixes = { "+" => undef, " " => undef, "-" => "< " };
376 3         5 my $b_prefixes = { "+" => "> ", " " => undef, "-" => undef };
377              
378 3         5 my $op = _op $ops;
379              
380 3 100       8 return join( "",
381             map( _op_to_line( \@_, $_, A, $a_prefixes ), @$ops ),
382             $op eq "c" ? "---\n" : (),
383             map( _op_to_line( \@_, $_, B, $b_prefixes ), @$ops ),
384             );
385             }
386              
387             1;
388              
389             __END__