File Coverage

blib/lib/Text/Diff.pm
Criterion Covered Total %
statement 185 192 96.3
branch 90 106 84.9
condition 23 32 71.8
subroutine 35 37 94.5
pod 0 1 0.0
total 333 368 90.4


line stmt bran cond sub pod time code
1             package Text::Diff;
2              
3 6     6   24532 use 5.006;
  6         22  
4 6     6   35 use strict;
  6         13  
  6         139  
5 6     6   35 use warnings;
  6         12  
  6         179  
6 6     6   30 use Carp qw/ croak confess /;
  6         11  
  6         286  
7 6     6   30 use Exporter ();
  6         10  
  6         83  
8 6     6   2255 use Algorithm::Diff ();
  6         25455  
  6         345  
9              
10             our $VERSION = '1.45';
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   45 use constant A => 0; # Array index before match/discard
  6         12  
  6         528  
17 6     6   36 use constant B => 1;
  6         9  
  6         316  
18 6     6   33 use constant OPCODE => 2; # "-", " ", "+"
  6         11  
  6         282  
19 6     6   78 use constant FLAG => 3; # What to display if not OPCODE "!"
  6         51  
  6         12287  
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 60356 my @seqs = ( shift, shift );
30 32   100     104 my $options = shift || {};
31              
32 32         88 for my $i ( 0 .. 1 ) {
33 64         108 my $seq = $seqs[$i];
34 64         102 my $type = ref $seq;
35              
36 64         137 while ( $type eq "CODE" ) {
37 4         8 $seqs[$i] = $seq = $seq->( $options );
38 4         15 $type = ref $seq;
39             }
40              
41 64 100       207 my $AorB = !$i ? "A" : "B";
42              
43 64 100 33     129 if ( $type eq "ARRAY" ) {
    100          
    100          
    50          
44             ## This is most efficient :)
45             $options->{"OFFSET_$AorB"} = 0
46 32 50       137 unless defined $options->{"OFFSET_$AorB"};
47             }
48             elsif ( $type eq "SCALAR" ) {
49 24         61 $seqs[$i] = [split( /^/m, $$seq )];
50             $options->{"OFFSET_$AorB"} = 1
51 24 50       98 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       15 unless defined $options->{"FILENAME_$AorB"};
58             $options->{"MTIME_$AorB"} = (stat($seq))[9]
59 4 50       35 unless defined $options->{"MTIME_$AorB"};
60              
61 4         13 local $/ = "\n";
62 4 50       45 open F, "<$seq" or croak "$!: $seq";
63 4         45 $seqs[$i] = [];
64 4         23 close F;
65              
66             }
67             elsif ( $type eq "GLOB" || UNIVERSAL::isa( $seq, "IO::Handle" ) ) {
68             $options->{"OFFSET_$AorB"} = 1
69 4 50       11 unless defined $options->{"OFFSET_$AorB"};
70 4         11 local $/ = "\n";
71 4         41 $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         50 my $output;
80 32         59 my $output_handler = $options->{OUTPUT};
81 32         58 my $type = ref $output_handler ;
82 32 100 66     91 if ( ! defined $output_handler ) {
    100          
    50          
    100          
    50          
83 28         42 $output = "";
84 28     121   100 $output_handler = sub { $output .= shift };
  121         1697  
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         2 my $out_ref = $output_handler;
95 1     5   3 $output_handler = sub { push @$out_ref, shift };
  5         6  
96             }
97             elsif ( $type eq "GLOB" || UNIVERSAL::isa $output_handler, "IO::Handle" ) {
98 2         6 my $output_handle = $output_handler;
99 2     10   12 $output_handler = sub { print $output_handle shift };
  10         38  
100             }
101             else {
102 0         0 croak "Unrecognized output type: $type";
103             }
104              
105 32         63 my $style = $options->{STYLE};
106 32 100       73 $style = "Unified" unless defined $options->{STYLE};
107 32 100       115 $style = "Text::Diff::$style" if exists $internal_styles{$style};
108              
109 32 100       234 if ( ! $style->can( "hunk" ) ) {
110 2 50       111 eval "require $style; 1" or die $@;
111             }
112              
113 32 100 66     268 $style = $style->new if ! ref $style && $style->can( "new" );
114              
115 32         62 my $ctx_lines = $options->{CONTEXT};
116 32 100       135 $ctx_lines = 3 unless defined $ctx_lines;
117 32 100       176 $ctx_lines = 0 if $style->isa( "Text::Diff::OldStyle" );
118              
119             my @keygen_args = $options->{KEYGEN_ARGS}
120 32 100       82 ? @{$options->{KEYGEN_ARGS}}
  1         4  
121             : ();
122              
123             ## State vars
124 32         65 my $diffs = 0; ## Number of discards this hunk
125 32         41 my $ctx = 0; ## Number of " " (ctx_lines) ops pushed after last diff.
126 32         41 my @ops; ## ops (" ", +, -) in this hunk
127 32         39 my $hunks = 0; ## Number of hunks
128              
129             my $emit_ops = sub {
130 31 100   31   112 $output_handler->( $style->file_header( @seqs, $options ) )
131             unless $hunks++;
132 31         147 $output_handler->( $style->hunk_header( @seqs, @_, $options ) );
133 31         82 $output_handler->( $style->hunk ( @seqs, @_, $options ) );
134 31         108 $output_handler->( $style->hunk_footer( @seqs, @_, $options ) );
135 32         118 };
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   93 my $dis_a = sub {push @ops, [@_[0,1],"-"]; ++$diffs ; $ctx = 0 };
  30         897  
  30         43  
  30         67  
144 32     29   79 my $dis_b = sub {push @ops, [@_[0,1],"+"]; ++$diffs ; $ctx = 0 };
  29         169  
  29         41  
  29         59  
145              
146             Algorithm::Diff::traverse_sequences(
147             @seqs,
148             {
149             MATCH => sub {
150 118     118   4650 push @ops, [@_[0,1]," "];
151              
152 118 100 100     290 if ( $diffs && ++$ctx > $ctx_lines * 2 ) {
153 11         41 $emit_ops->( [ splice @ops, 0, $#ops - $ctx_lines ] );
154 11         30 $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         251 @keygen_args,
165             );
166              
167 32 100       366 if ( $diffs ) {
168 20 50       46 $#ops -= $ctx - $ctx_lines if $ctx > $ctx_lines;
169 20         42 $emit_ops->( \@ops );
170             }
171              
172 32 100       113 $output_handler->( $style->file_footer( @seqs, $options ) ) if $hunks;
173              
174 32 100       406 return defined $output ? $output : $hunks;
175             }
176              
177             sub _header {
178 14     14   39 my ( $h ) = @_;
179 14         41 my ( $p1, $fn1, $t1, $p2, $fn2, $t2 ) = @{$h}{
180 14         23 "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     73 return "" unless defined $fn1 && defined $fn2;
190              
191 5 50       151 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   87 my ( $ops, $a_or_b, $format ) = @_;
201              
202 42         77 my $start = $ops->[ 0]->[$a_or_b];
203 42         64 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       119 ++$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         72 my $empty_range = $start == $after;
216 42 100       71 ++$start unless $empty_range;
217              
218             return
219 42 100 100     259 $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   192 my ( $seqs, $op, $a_or_b, $op_prefixes ) = @_;
230              
231 115         155 my $opcode = $op->[OPCODE];
232 115 100       244 return () unless defined $op_prefixes->{$opcode};
233              
234 103 100       167 my $op_sym = defined $op->[FLAG] ? $op->[FLAG] : $opcode;
235 103         130 $op_sym = $op_prefixes->{$op_sym};
236 103 50       155 return () unless defined $op_sym;
237              
238 103 100       192 $a_or_b = $op->[OPCODE] ne "+" ? 0 : 1 unless defined $a_or_b;
    100          
239 103         195 my @line = ( $op_sym, $seqs->[$a_or_b][$op->[$a_or_b]] );
240 103 50       387 unless ( $line[1] =~ /(?:\n|\r\n)$/ ) {
241 0         0 $line[1] .= "\n\\ No newline at end of file\n";
242             }
243 103         384 return @line;
244             }
245              
246             SCOPE: {
247             package Text::Diff::Base;
248              
249             sub new {
250 23     23   43 my $proto = shift;
251 23   33     97 return bless { @_ }, ref $proto || $proto;
252             }
253              
254 9     9   28 sub file_header { return "" }
255              
256 8     8   20 sub hunk_header { return "" }
257              
258 0     0   0 sub hunk { return "" }
259              
260 29     29   60 sub hunk_footer { return "" }
261              
262 15     15   41 sub file_footer { return "" }
263             }
264              
265             @Text::Diff::Unified::ISA = qw( Text::Diff::Base );
266              
267             sub Text::Diff::Unified::file_header {
268 12     12   35 shift; ## No instance data
269 12         21 my $options = pop ;
270              
271 12         80 _header(
272             { FILENAME_PREFIX_A => "---", FILENAME_PREFIX_B => "+++", %$options }
273             );
274             }
275              
276             sub Text::Diff::Unified::hunk_header {
277 14     14   22 shift; ## No instance data
278 14         20 pop; ## Ignore options
279 14         21 my $ops = pop;
280              
281 14         38 return join( "",
282             "@@ -",
283             _range( $ops, A, "unified" ),
284             " +",
285             _range( $ops, B, "unified" ),
286             " @@\n",
287             );
288             }
289              
290             sub Text::Diff::Unified::hunk {
291 14     14   19 shift; ## No instance data
292 14         20 pop; ## Ignore options
293 14         18 my $ops = pop;
294              
295 14         48 my $prefixes = { "+" => "+", " " => " ", "-" => "-" };
296              
297 14         133 return join "", map _op_to_line( \@_, $_, undef, $prefixes ), @$ops
298             }
299              
300             @Text::Diff::Context::ISA = qw( Text::Diff::Base );
301              
302             sub Text::Diff::Context::file_header {
303 2     2   4 _header { FILENAME_PREFIX_A=>"***", FILENAME_PREFIX_B=>"---", %{$_[-1]} };
  2         12  
304             }
305              
306             sub Text::Diff::Context::hunk_header {
307 4     4   8 return "***************\n";
308             }
309              
310             sub Text::Diff::Context::hunk {
311 4     4   5 shift; ## No instance data
312 4         5 pop; ## Ignore options
313 4         4 my $ops = pop;
314             ## Leave the sequences in @_[0,1]
315              
316 4         7 my $a_range = _range( $ops, A, "" );
317 4         8 my $b_range = _range( $ops, B, "" );
318              
319             ## Sigh. Gotta make sure that differences that aren't adds/deletions
320             ## get prefixed with "!", and that the old opcodes are removed.
321 4         5 my $after;
322 4         11 for ( my $start = 0; $start <= $#$ops ; $start = $after ) {
323             ## Scan until next difference
324 17         21 $after = $start + 1;
325 17         19 my $opcode = $ops->[$start]->[OPCODE];
326 17 100       56 next if $opcode eq " ";
327              
328 6         7 my $bang_it;
329 6   100     16 while ( $after <= $#$ops && $ops->[$after]->[OPCODE] ne " " ) {
330 2   33     9 $bang_it ||= $ops->[$after]->[OPCODE] ne $opcode;
331 2         6 ++$after;
332             }
333              
334 6 100       12 if ( $bang_it ) {
335 2         6 for my $i ( $start..($after-1) ) {
336 4         11 $ops->[$i]->[FLAG] = "!";
337             }
338             }
339             }
340              
341 4         13 my $b_prefixes = { "+" => "+ ", " " => " ", "-" => undef, "!" => "! " };
342 4         7 my $a_prefixes = { "+" => undef, " " => " ", "-" => "- ", "!" => "! " };
343              
344 4         11 return join( "",
345             "*** ", $a_range, " ****\n",
346             map( _op_to_line( \@_, $_, A, $a_prefixes ), @$ops ),
347             "--- ", $b_range, " ----\n",
348             map( _op_to_line( \@_, $_, B, $b_prefixes ), @$ops ),
349             );
350             }
351              
352             @Text::Diff::OldStyle::ISA = qw( Text::Diff::Base );
353              
354             sub _op {
355 6     6   13 my $ops = shift;
356 6         16 my $op = $ops->[0]->[OPCODE];
357 6 100       28 $op = "c" if grep $_->[OPCODE] ne $op, @$ops;
358 6 100       20 $op = "a" if $op eq "+";
359 6 100       16 $op = "d" if $op eq "-";
360 6         18 return $op;
361             }
362              
363             sub Text::Diff::OldStyle::hunk_header {
364 3     3   7 shift; ## No instance data
365 3         7 pop; ## ignore options
366 3         8 my $ops = pop;
367              
368 3         8 my $op = _op $ops;
369              
370 3         9 return join "", _range( $ops, A, "" ), $op, _range( $ops, B, "" ), "\n";
371             }
372              
373             sub Text::Diff::OldStyle::hunk {
374 3     3   7 shift; ## No instance data
375 3         6 pop; ## ignore options
376 3         7 my $ops = pop;
377             ## Leave the sequences in @_[0,1]
378              
379 3         13 my $a_prefixes = { "+" => undef, " " => undef, "-" => "< " };
380 3         12 my $b_prefixes = { "+" => "> ", " " => undef, "-" => undef };
381              
382 3         7 my $op = _op $ops;
383              
384 3 100       13 return join( "",
385             map( _op_to_line( \@_, $_, A, $a_prefixes ), @$ops ),
386             $op eq "c" ? "---\n" : (),
387             map( _op_to_line( \@_, $_, B, $b_prefixes ), @$ops ),
388             );
389             }
390              
391             1;
392              
393             __END__