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   30264 use 5.006;
  6         20  
4 6     6   30 use strict;
  6         9  
  6         147  
5 6     6   25 use warnings;
  6         7  
  6         228  
6 6     6   25 use Carp qw/ croak confess /;
  6         16  
  6         389  
7 6     6   34 use Exporter ();
  6         10  
  6         112  
8 6     6   4504 use Algorithm::Diff ();
  6         33375  
  6         495  
9              
10             our $VERSION = '1.44';
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         525  
17 6     6   32 use constant B => 1;
  6         10  
  6         379  
18 6     6   30 use constant OPCODE => 2; # "-", " ", "+"
  6         8  
  6         290  
19 6     6   75 use constant FLAG => 3; # What to display if not OPCODE "!"
  6         9  
  6         16692  
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 25837 my @seqs = ( shift, shift );
30 32   100     133 my $options = shift || {};
31              
32 32         89 for my $i ( 0 .. 1 ) {
33 64         85 my $seq = $seqs[$i];
34 64         95 my $type = ref $seq;
35              
36 64         170 while ( $type eq "CODE" ) {
37 4         9 $seqs[$i] = $seq = $seq->( $options );
38 4         17 $type = ref $seq;
39             }
40              
41 64 100       127 my $AorB = !$i ? "A" : "B";
42              
43 64 100 33     176 if ( $type eq "ARRAY" ) {
    100          
    100          
    50          
44             ## This is most efficient :)
45             $options->{"OFFSET_$AorB"} = 0
46 32 50       176 unless defined $options->{"OFFSET_$AorB"};
47             }
48             elsif ( $type eq "SCALAR" ) {
49 24         88 $seqs[$i] = [split( /^/m, $$seq )];
50             $options->{"OFFSET_$AorB"} = 1
51 24 50       124 unless defined $options->{"OFFSET_$AorB"};
52             }
53             elsif ( ! $type ) {
54             $options->{"OFFSET_$AorB"} = 1
55 4 50       18 unless defined $options->{"OFFSET_$AorB"};
56             $options->{"FILENAME_$AorB"} = $seq
57 4 50       14 unless defined $options->{"FILENAME_$AorB"};
58             $options->{"MTIME_$AorB"} = (stat($seq))[9]
59 4 50       58 unless defined $options->{"MTIME_$AorB"};
60              
61 4         18 local $/ = "\n";
62 4 50       100 open F, "<$seq" or croak "$!: $seq";
63 4         62 $seqs[$i] = [];
64 4         40 close F;
65              
66             }
67             elsif ( $type eq "GLOB" || UNIVERSAL::isa( $seq, "IO::Handle" ) ) {
68             $options->{"OFFSET_$AorB"} = 1
69 4 50       18 unless defined $options->{"OFFSET_$AorB"};
70 4         13 local $/ = "\n";
71 4         78 $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         45 my $output;
80 32         53 my $output_handler = $options->{OUTPUT};
81 32         46 my $type = ref $output_handler ;
82 32 100 66     88 if ( ! defined $output_handler ) {
    100          
    50          
    100          
    50          
83 28         33 $output = "";
84 28     121   111 $output_handler = sub { $output .= shift };
  121         2584  
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   6 $output_handler = sub { push @$out_ref, shift };
  5         11  
96             }
97             elsif ( $type eq "GLOB" || UNIVERSAL::isa $output_handler, "IO::Handle" ) {
98 2         3 my $output_handle = $output_handler;
99 2     10   10 $output_handler = sub { print $output_handle shift };
  10         36  
100             }
101             else {
102 0         0 croak "Unrecognized output type: $type";
103             }
104              
105 32         56 my $style = $options->{STYLE};
106 32 100       84 $style = "Unified" unless defined $options->{STYLE};
107 32 100       128 $style = "Text::Diff::$style" if exists $internal_styles{$style};
108              
109 32 100       305 if ( ! $style->can( "hunk" ) ) {
110 2 50       162 eval "require $style; 1" or die $@;
111             }
112              
113 32 100 66     428 $style = $style->new if ! ref $style && $style->can( "new" );
114              
115 32         56 my $ctx_lines = $options->{CONTEXT};
116 32 100       72 $ctx_lines = 3 unless defined $ctx_lines;
117 32 100       239 $ctx_lines = 0 if $style->isa( "Text::Diff::OldStyle" );
118              
119             my @keygen_args = $options->{KEYGEN_ARGS}
120 32 100       89 ? @{$options->{KEYGEN_ARGS}}
  1         4  
121             : ();
122              
123             ## State vars
124 32         36 my $diffs = 0; ## Number of discards this hunk
125 32         36 my $ctx = 0; ## Number of " " (ctx_lines) ops pushed after last diff.
126 32         31 my @ops; ## ops (" ", +, -) in this hunk
127 32         30 my $hunks = 0; ## Number of hunks
128              
129             my $emit_ops = sub {
130 31 100   31   128 $output_handler->( $style->file_header( @seqs, $options ) )
131             unless $hunks++;
132 31         106 $output_handler->( $style->hunk_header( @seqs, @_, $options ) );
133 31         98 $output_handler->( $style->hunk ( @seqs, @_, $options ) );
134 31         139 $output_handler->( $style->hunk_footer( @seqs, @_, $options ) );
135 32         146 };
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   107 my $dis_a = sub {push @ops, [@_[0,1],"-"]; ++$diffs ; $ctx = 0 };
  30         1122  
  30         39  
  30         64  
144 32     29   91 my $dis_b = sub {push @ops, [@_[0,1],"+"]; ++$diffs ; $ctx = 0 };
  29         146  
  29         34  
  29         50  
145              
146             Algorithm::Diff::traverse_sequences(
147             @seqs,
148             {
149             MATCH => sub {
150 118     118   3403 push @ops, [@_[0,1]," "];
151              
152 118 100 100     314 if ( $diffs && ++$ctx > $ctx_lines * 2 ) {
153 11         34 $emit_ops->( [ splice @ops, 0, $#ops - $ctx_lines ] );
154 11         26 $ctx = $diffs = 0;
155             }
156              
157             ## throw away context lines that aren't needed any more
158 118 100 100     616 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         284 @keygen_args,
165             );
166              
167 32 100       404 if ( $diffs ) {
168 20 50       47 $#ops -= $ctx - $ctx_lines if $ctx > $ctx_lines;
169 20         44 $emit_ops->( \@ops );
170             }
171              
172 32 100       130 $output_handler->( $style->file_footer( @seqs, $options ) ) if $hunks;
173              
174 32 100       513 return defined $output ? $output : $hunks;
175             }
176              
177             sub _header {
178 14     14   20 my ( $h ) = @_;
179 14         39 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     75 return "" unless defined $fn1 && defined $fn2;
190              
191 5 50       208 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   56 my ( $ops, $a_or_b, $format ) = @_;
201              
202 42         48 my $start = $ops->[ 0]->[$a_or_b];
203 42         46 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       126 ++$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         48 my $empty_range = $start == $after;
216 42 100       65 ++$start unless $empty_range;
217              
218             return
219 42 100 100     231 $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   127 my ( $seqs, $op, $a_or_b, $op_prefixes ) = @_;
230              
231 115         120 my $opcode = $op->[OPCODE];
232 115 100       257 return () unless defined $op_prefixes->{$opcode};
233              
234 103 100       147 my $op_sym = defined $op->[FLAG] ? $op->[FLAG] : $opcode;
235 103         105 $op_sym = $op_prefixes->{$op_sym};
236 103 50       153 return () unless defined $op_sym;
237              
238 103 100       218 $a_or_b = $op->[OPCODE] ne "+" ? 0 : 1 unless defined $a_or_b;
    100          
239 103         209 my @line = ( $op_sym, $seqs->[$a_or_b][$op->[$a_or_b]] );
240 103 50       407 unless ( $line[1] =~ /(?:\n|\r\n)$/ ) {
241 0         0 $line[1] .= "\n\\ No newline at end of file\n";
242             }
243 103         386 return @line;
244             }
245              
246             SCOPE: {
247             package Text::Diff::Base;
248              
249             sub new {
250 23     23   34 my $proto = shift;
251 23   33     120 return bless { @_ }, ref $proto || $proto;
252             }
253              
254 9     9   26 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   59 sub hunk_footer { return "" }
261              
262 15     15   26 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   13 shift; ## No instance data
269 12         11 my $options = pop ;
270              
271 12         82 _header(
272             { FILENAME_PREFIX_A => "---", FILENAME_PREFIX_B => "+++", %$options }
273             );
274             }
275              
276             sub Text::Diff::Unified::hunk_header {
277 14     14   15 shift; ## No instance data
278 14         15 pop; ## Ignore options
279 14         17 my $ops = pop;
280              
281 14         35 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   23 shift; ## No instance data
292 14         16 pop; ## Ignore options
293 14         25 my $ops = pop;
294              
295 14         43 my $prefixes = { "+" => "+", " " => " ", "-" => "-" };
296              
297 14         49 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         18  
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   3 shift; ## No instance data
312 4         3 pop; ## Ignore options
313 4         5 my $ops = pop;
314             ## Leave the sequences in @_[0,1]
315              
316 4         5 my $a_range = _range( $ops, A, "" );
317 4         7 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         13 for ( my $start = 0; $start <= $#$ops ; $start = $after ) {
323             ## Scan until next difference
324 17         14 $after = $start + 1;
325 17         19 my $opcode = $ops->[$start]->[OPCODE];
326 17 100       40 next if $opcode eq " ";
327              
328 6         6 my $bang_it;
329 6   100     25 while ( $after <= $#$ops && $ops->[$after]->[OPCODE] ne " " ) {
330 2   33     9 $bang_it ||= $ops->[$after]->[OPCODE] ne $opcode;
331 2         8 ++$after;
332             }
333              
334 6 100       15 if ( $bang_it ) {
335 2         8 for my $i ( $start..($after-1) ) {
336 4         11 $ops->[$i]->[FLAG] = "!";
337             }
338             }
339             }
340              
341 4         14 my $b_prefixes = { "+" => "+ ", " " => " ", "-" => undef, "!" => "! " };
342 4         11 my $a_prefixes = { "+" => undef, " " => " ", "-" => "- ", "!" => "! " };
343              
344 4         12 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   7 my $ops = shift;
356 6         7 my $op = $ops->[0]->[OPCODE];
357 6 100       21 $op = "c" if grep $_->[OPCODE] ne $op, @$ops;
358 6 100       14 $op = "a" if $op eq "+";
359 6 100       10 $op = "d" if $op eq "-";
360 6         8 return $op;
361             }
362              
363             sub Text::Diff::OldStyle::hunk_header {
364 3     3   3 shift; ## No instance data
365 3         4 pop; ## ignore options
366 3         4 my $ops = pop;
367              
368 3         6 my $op = _op $ops;
369              
370 3         6 return join "", _range( $ops, A, "" ), $op, _range( $ops, B, "" ), "\n";
371             }
372              
373             sub Text::Diff::OldStyle::hunk {
374 3     3   4 shift; ## No instance data
375 3         3 pop; ## ignore options
376 3         4 my $ops = pop;
377             ## Leave the sequences in @_[0,1]
378              
379 3         7 my $a_prefixes = { "+" => undef, " " => undef, "-" => "< " };
380 3         7 my $b_prefixes = { "+" => "> ", " " => undef, "-" => undef };
381              
382 3         5 my $op = _op $ops;
383              
384 3 100       8 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__