File Coverage

blib/lib/VCS/Lite/Delta.pm
Criterion Covered Total %
statement 210 219 95.8
branch 102 122 83.6
condition 33 42 78.5
subroutine 10 11 90.9
pod 5 5 100.0
total 360 399 90.2


line stmt bran cond sub pod time code
1             package VCS::Lite::Delta;
2              
3 9     9   71 use strict;
  9         17  
  9         361  
4 9     9   55 use warnings;
  9         14  
  9         653  
5             our $VERSION = '0.11';
6              
7             #----------------------------------------------------------------------------
8              
9             =head1 NAME
10              
11             VCS::Lite::Delta - VCS::Lite differences
12              
13             =head1 SYNOPSIS
14              
15             use VCS::Lite;
16              
17             # diff
18              
19             my $lit = VCS::Lite->new('/home/me/foo1.txt');
20             my $lit2 = VCS::Lite->new('/home/me/foo2.txt');
21             my $difftxt = $lit->delta($lit2)->diff;
22             print OUTFILE $difftxt;
23              
24             # patch
25              
26             my $delt = VCS::Lite::Delta->new('/home/me/patch.diff');
27             my $lit3 = $lit->patch($delt);
28             print OUTFILE $lit3->text;
29              
30             =head1 DESCRIPTION
31              
32             This module provides a Delta class for the differencing functionality of
33             VCS::Lite
34              
35             =cut
36              
37             #----------------------------------------------------------------------------
38              
39             #############################################################################
40             #Library Modules #
41             #############################################################################
42              
43 9     9   55 use Carp;
  9         18  
  9         760  
44              
45             #----------------------------------------------------------------------------
46              
47             # Error handling, use package vars to control it for now.
48 9     9   49 use vars qw($error_action $error_msg $error_line);
  9         17  
  9         36047  
49              
50             #----------------------------------------------------------------------------
51              
52             #############################################################################
53             #Interface Methods #
54             #############################################################################
55              
56             sub new {
57 13     13 1 2893 my $class = shift;
58 13         30 my $src = shift;
59              
60             # DWIM logic, based on $src parameter.
61              
62             # Case 0: string. Use $id as file name, becomes case 2
63 13 100       51 if ( !ref $src ) {
64 6 50       399 open my $fh, $src or croak("failed to open '$src': $!");
65 6         17 $src = $fh; # becomes case 2 below
66             }
67 13         34 my $atyp = ref $src;
68              
69             # Case 1: $src is arrayref
70 13 100       958 return bless {
71             id1 => $_[0],
72             id2 => $_[1],
73             sep => $_[2],
74             diff => [@$src]
75             },
76             $class
77             if $atyp eq 'ARRAY';
78              
79 6         1253 my $sep = shift;
80 6         11 my %proto;
81              
82             # Decode $sep as needed
83              
84 6 100       22 if (ref($sep) eq 'HASH') {
85 2         8 %proto = %$sep;
86 2         6 $sep = $proto{in};
87 2         3 delete $proto{in};
88             }
89              
90 6   33     36 $sep ||= $/;
91 6 50       46 local $/ = $sep if $sep;
92 6   50     19 $sep ||= '';
93 6         22 my @diff;
94              
95             # Case 2: $src is globref (file handle) - slurp file
96 6 50       17 if ( $atyp eq 'GLOB' ) {
    0          
97 6         294 @diff = <$src>;
98             }
99              
100             # Case 3: $src is scalar ref (string)
101             elsif ( $atyp eq 'SCALAR' ) {
102 0         0 @diff = split /(?=$sep)/, $$src;
103             }
104              
105             # Case otherwise is an error.
106             else {
107 0         0 croak "Invalid argument to VCS::Lite::Delta::new";
108             }
109              
110             # If we have reached this point, we have been passed something in a
111             # text/diff format. It could be diff or udiff format.
112              
113 6         20 my ( $id1, $id2 ) = @_;
114 6         11 my @out;
115              
116 6 100       27 if ( $diff[0] =~ /^---/ ) { # udiff format
117 3         9 my $state = 'inputdef';
118 3         7 my ( $a_line, $a_count, @a_hunk, $b_line, $b_count, @b_hunk );
119 3         13 for my $lin ( 0 .. $#diff ) {
120 92         154 local $_ = $diff[$lin];
121 92 100       197 chomp if $proto{chomp};
122             # inputdef = --- and +++ to identify the files being diffed
123              
124 92 100       1468 if ( $state eq 'inputdef' ) {
125 9 100       38 $id1 = $1 if /^--- # ---
126             \s
127             (\S+)/x; # file => $1
128 9 100       36 $id2 = $1 if /^\+{3} # +++
129             \s
130             (\S+)/x; # file => $1
131 9 100       29 $state = 'patch' if /^\@\@/;
132             }
133              
134             # patch expects @@ -a,b +c,d @@
135              
136 92 100       234 if ( $state eq 'patch' ) {
137 11 100       64 next unless /^\@\@
138             \s+
139             -
140             (\d+) # line of file 1 => $1
141             ,
142             (\d+) # count of file 1 => $2
143             \s*
144             \+
145             (\d+) # line of file 2 => $3
146             ,
147             (\d+) # count of file 2 => $4
148             \s*
149             \@\@/x;
150 10         27 $a_line = $1 - 1;
151 10         21 $a_count = $2;
152 10         19 $b_line = $3 - 1;
153 10         17 $b_count = $4;
154 10         12 $state = 'detail';
155 10         20 next;
156             }
157              
158             # detail expects [-+ ]line of text
159              
160 81 100       167 if ( $state eq 'detail' ) {
161 75         136 my $ind = substr $_, 0, 1, '';
162 75 50       226 _error( $lin, 'Bad diff' ), return undef
163             unless $ind =~ /[ +\-i\\]/;
164              
165 75 100       988 next if $ind eq '\\';
166              
167             #[- ]line, add to @a_hunk
168 74 100       1299 if ( $ind ne '+' ) {
169 64         84 my $lead = '-';
170 64 100 100     384 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
171 1         3 $lead .= '/';
172 1         17 s/$sep$//s;
173             }
174 64         205 push @a_hunk, [ $lead, $a_line++, $_ ];
175 64         79 $a_count--;
176 64 50       158 _error( $lin, 'Too large diff' ), return undef
177             if $a_count < 0;
178             }
179              
180             #[+ ]line, add to @b_hunk
181 74 100       133 if ( $ind ne '-' ) {
182 56         68 my $lead = '+';
183 56 100 100     229 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
184 1         3 $lead .= '/';
185 1         10 s/$sep$//s;
186             }
187 56         154 push @b_hunk, [ $lead, $b_line++, $_ ];
188 56         57 $b_count--;
189 56 50       112 _error( $lin, 'Too large diff' ), return undef
190             if $b_count < 0;
191             }
192              
193             # are we there yet, daddy?
194 74 100 66     207 if ( !$a_count and !$b_count ) {
195 10         29 push @out, [ @a_hunk, @b_hunk ];
196 10         22 @a_hunk = @b_hunk = ();
197 10         27 $state = 'patch';
198             }
199             }
200             } # next line of patch
201 3         157 return bless {
202             id1 => $id1,
203             id2 => $id2,
204             sep => $sep,
205             diff => \@out,
206             %proto
207             }, $class;
208             }
209              
210             # not a udiff mode patch, assume straight diff mode
211              
212 3         6 my $state = 'patch';
213 3         6 my ( $a_line, $a_count, @a_hunk, $b_line, $b_count, @b_hunk );
214 3         11 for my $lin ( 0 .. $#diff ) {
215 46         226 local $_ = $diff[$lin];
216 46 100       863 chomp if $proto{chomp};
217              
218             # patch expects ww,xx[acd]yy,zz style
219              
220 46 100       84 if ( $state eq 'patch' ) {
221 11 100       150 next unless /^(\d+) # start line of file 1 => $1
222             (?:,(\d+))? # end line of file 1 => $2
223             ([acd]) # Add, change, delete => $3
224             (\d+) # start line of file 2 => $4
225             (?:,(\d+))? # end line of file 2 => $5
226             /x;
227 10         24 $a_line = $1 - 1;
228 10 100       28 $a_count = $2 ? ( $2 - $a_line ) : 1;
229 10         18 $b_line = $4 - 1;
230 10 100       22 $b_count = $5 ? ( $5 - $b_line ) : 1;
231 10 100       23 $a_count = 0 if $3 eq 'a';
232 10 100       382 $b_count = 0 if $3 eq 'd';
233 10         12 $state = 'detail';
234 10         16 next;
235             }
236              
237             # detail expects < lines --- > lines
238              
239 35 50       63 if ( $state eq 'detail' ) {
240 35 100       74 next if /^---/; # ignore separator
241 29         52 my $ind = substr $_, 0, 2, '';
242 29 50       92 _error( $lin, 'Bad diff' ), return undef
243             unless $ind =~ /[<>\\] /;
244              
245             # < line goes to @a_hunk
246 29 100       50 if ( $ind eq '< ' ) {
247 18         20 my $lead = '-';
248 18 100 66     84 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
249 1         3 $lead .= '/';
250 1         29 s/$sep$//s;
251             }
252 18         46 push @a_hunk, [ $lead, $a_line++, $_ ];
253 18         29 $a_count--;
254 18 50       39 _error( $lin, 'Too large diff' ), return undef
255             if $a_count < 0;
256             }
257              
258             # > line goes to @b_hunk
259 29 100       59 if ( $ind eq '> ' ) {
260 10         12 my $lead = '+';
261 10 100 100     44 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
262 1         4 $lead .= '/';
263 1         13 s/$sep$//s;
264             }
265 10         28 push @b_hunk, [ $lead, $b_line++, $_ ];
266 10         10 $b_count--;
267 10 50       22 _error( $lin, 'Too large diff' ), return undef
268             if $b_count < 0;
269             }
270              
271             # are we there yet, daddy?
272 29 100 66     90 if ( !$a_count and !$b_count ) {
273 10         20 push @out, [ @a_hunk, @b_hunk ];
274 10         20 @a_hunk = @b_hunk = ();
275 10         25 $state = 'patch';
276             }
277             }
278             }
279 3         90 return bless {
280             id1 => $id1,
281             id2 => $id2,
282             sep => $sep,
283             diff => \@out,
284             %proto
285             }, $class;
286             }
287              
288             sub _error {
289 0     0   0 ( $error_line, my $msg ) = @_;
290              
291 0         0 $error_msg = "Line $error_line: $msg";
292              
293 0 0       0 goto &$error_action if ref($error_action) eq 'CODE';
294 0 0       0 confess $error_msg if $error_action eq 'raise';
295              
296 0 0       0 print STDERR $error_msg, "\n" unless $error_action eq 'silent';
297             }
298              
299             sub _diff_hunk {
300              
301 17     17   26 my $sep = shift;
302 17         21 my $r_line_offset = shift;
303              
304 17         19 my @ins;
305 17         27 my ( $ins_firstline, $ins_lastline ) = ( 0, 0 );
306 17         17 my @del;
307 17         23 my ( $del_firstline, $del_lastline ) = ( 0, 0 );
308 17         20 my $op;
309 17         20 my $shortins = '';
310 17         18 my $shortdel = '';
311            
312             # construct @ins and @del from hunk
313              
314 17         35 for (@_) {
315 47         82 my ( $typ, $lno, $txt ) = @$_;
316 47         84 my $short = substr($typ, 1, 1, '');
317 47         51 $lno++;
318 47 100       82 if ( $typ eq '+' ) {
319 21         37 push @ins, $txt;
320 21   66     68 $ins_firstline ||= $lno;
321 21         21 $ins_lastline = $lno;
322 21 100       69 $shortins = "\n\\ No newline at end of file\n" if $short;
323             }
324             else {
325 26         40 push @del, $txt;
326 26   66     71 $del_firstline ||= $lno;
327 26         31 $del_lastline = $lno;
328 26 100       1466 $shortdel = "\n\\ No newline at end of file\n" if $short;
329             }
330             }
331              
332             # Work out whether we are a, c or d
333              
334 17 100       57 if ( !@del ) {
    100          
335 3         19 $op = 'a';
336 3         11 $del_firstline = $ins_firstline - $$r_line_offset - 1;
337             }
338             elsif ( !@ins ) {
339 2         3 $op = 'd';
340 2         5 $ins_firstline = $del_firstline + $$r_line_offset - 1;
341             }
342             else {
343 12         19 $op = 'c';
344             }
345              
346 17         34 $$r_line_offset += @ins - @del;
347              
348 17   66     38 $ins_lastline ||= $ins_firstline;
349 17   100     33 $del_lastline ||= $del_firstline;
350              
351             # Make the header line
352              
353 17         52 my $outstr =
354             "$del_firstline,$del_lastline$op$ins_firstline,$ins_lastline\n";
355 17         196 $outstr =~ s/(^|\D)(\d+),\2(?=\D|$)/$1$2/g;
356              
357             # < deletions
358 17         36 for (@del) {
359 26         64 $outstr .= '< ' . $_ . $sep;
360             }
361 17         27 $outstr .= $shortdel;
362            
363             # ---
364 17 100 100     83 $outstr .= "---\n" if @ins && @del;
365              
366             # > insertions
367 17         28 for (@ins) {
368 21         81 $outstr .= '> ' . $_ . $sep;
369             }
370 17         25 $outstr .= $shortins;
371              
372 17         96 $outstr;
373             }
374              
375             sub diff {
376 4     4 1 3904 my $self = shift;
377 4   100     52 my $sep = shift || $self->{sep} || '';
378              
379 4         7 my $off = 0;
380              
381 4         8 join '', map { _diff_hunk( $sep, \$off, @$_ ) } @{ $self->{diff} };
  17         50  
  4         10  
382             }
383              
384             sub udiff {
385 6     6 1 10614 my $self = shift;
386 6   100     84 my $sep = shift || $self->{sep} || '';
387              
388 6         13 my ( $in, $out, $diff ) = @{$self}{qw/id1 id2 diff/};
  6         76  
389              
390             # Header with file names
391              
392 6         39 my @out = ( "--- $in \n", "+++ $out \n" );
393              
394 6         14 my $offset = 0;
395              
396 6         20 for (@$diff) {
397 22         49 my @t1 = grep { $_->[0] =~ /^\-/ } @$_;
  248         927  
398 22         40 my @t2 = grep { $_->[0] =~ /^\+/ } @$_;
  248         1327  
399              
400 22         41 my $short1 = '';
401 134         245 $short1 = "\n\\ No newline at end of file\n"
402 22 100       38 if grep { $_->[0] eq '-/' } @t1;
403 22         31 my $short2 = '';
404 114         233 $short2 = "\n\\ No newline at end of file\n"
405 22 100       32 if grep { $_->[0] eq '+/' } @t2;
406            
407             # Work out base line numbers in both files
408              
409 22 100       68 my $base1 = @t1 ? $t1[0][1] : $t2[0][1] - $offset;
410 22 100       50 my $base2 = @t2 ? $t2[0][1] : $t1[0][1] + $offset;
411 22         34 $base1++;
412 22         26 $base2++; # Our lines were 0 based
413 22         188 $offset += @t2 - @t1;
414 22         70 my $count1 = @t1;
415 22         28 my $count2 = @t2;
416              
417             # Header line
418 22         80 push @out, "@@ -$base1,$count1 +$base2,$count2 @@\n";
419              
420             # Use Algorithm::Diff::sdiff to munge out any lines in common inside
421             # the hunk
422 22         41 my @txt1 = map { $_->[2] } @t1;
  134         249  
423 22         46 my @txt2 = map { $_->[2] } @t2;
  114         192  
424              
425 22         97 my @ad = Algorithm::Diff::sdiff( \@txt1, \@txt2 );
426 22         4905 my @defer;
427              
428             # for each subhunk, we want all the file1 lines first, then all the file2 lines
429              
430 22         51 for (@ad) {
431 139         234 my ( $ind, $txt1, $txt2 ) = @$_;
432              
433             # we want to flush out the + lines when we run off the end of a 'c' section
434              
435 139 100       872 ( push @out, @defer ), @defer = () unless $ind eq 'c';
436              
437             # unchanged lines, just wack 'em out
438 139 100       508 ( push @out, ' ' . $txt1 . $sep ), next if $ind eq 'u';
439              
440             # output original line (- line)
441 47 100       207 push @out, '-' . $txt1 . $sep unless $ind eq '+';
442              
443             # defer changed + lines
444 47 100       136 push @defer, '+' . $txt2 . $sep unless $ind eq '-';
445             }
446 22         48 push @out, $short1;
447            
448             # and flush at the end
449 22         154 push @out, @defer, $short2;
450             }
451 6 50       153 wantarray ? @out : join '', @out;
452             }
453              
454             sub id {
455 1     1 1 1826 my $self = shift;
456              
457 1 50       11 if (@_) {
458 0         0 $self->{id1} = shift;
459 0         0 $self->{id2} = shift;
460             }
461              
462 1         3 @{$self}{qw/id1 id2/};
  1         16  
463             }
464              
465             sub hunks {
466 7     7 1 2136 my $self = shift;
467              
468 7         11 @{ $self->{diff} };
  7         42  
469             }
470              
471             1;
472              
473             __END__