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   34 use strict;
  9         13  
  9         275  
4 9     9   35 use warnings;
  9         14  
  9         394  
5             our $VERSION = '0.12';
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   41 use Carp;
  9         12  
  9         496  
44              
45             #----------------------------------------------------------------------------
46              
47             # Error handling, use package vars to control it for now.
48 9     9   43 use vars qw($error_action $error_msg $error_line);
  9         11  
  9         17576  
49              
50             #----------------------------------------------------------------------------
51              
52             #############################################################################
53             #Interface Methods #
54             #############################################################################
55              
56             sub new {
57 13     13 1 1073 my $class = shift;
58 13         15 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       37 if ( !ref $src ) {
64 6 50       151 open my $fh, $src or croak("failed to open '$src': $!");
65 6         15 $src = $fh; # becomes case 2 below
66             }
67 13         25 my $atyp = ref $src;
68              
69             # Case 1: $src is arrayref
70 13 100       495 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         345 my $sep = shift;
80 6         6 my %proto;
81              
82             # Decode $sep as needed
83              
84 6 100       13 if (ref($sep) eq 'HASH') {
85 2         5 %proto = %$sep;
86 2         3 $sep = $proto{in};
87 2         3 delete $proto{in};
88             }
89              
90 6   33     24 $sep ||= $/;
91 6 50       23 local $/ = $sep if $sep;
92 6   50     9 $sep ||= '';
93 6         15 my @diff;
94              
95             # Case 2: $src is globref (file handle) - slurp file
96 6 50       12 if ( $atyp eq 'GLOB' ) {
    0          
97 6         127 @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         14 my ( $id1, $id2 ) = @_;
114 6         4 my @out;
115              
116 6 100       27 if ( $diff[0] =~ /^---/ ) { # udiff format
117 3         6 my $state = 'inputdef';
118 3         2 my ( $a_line, $a_count, @a_hunk, $b_line, $b_count, @b_hunk );
119 3         8 for my $lin ( 0 .. $#diff ) {
120 92         108 local $_ = $diff[$lin];
121 92 100       127 chomp if $proto{chomp};
122             # inputdef = --- and +++ to identify the files being diffed
123              
124 92 100       115 if ( $state eq 'inputdef' ) {
125 9 100       24 $id1 = $1 if /^--- # ---
126             \s
127             (\S+)/x; # file => $1
128 9 100       20 $id2 = $1 if /^\+{3} # +++
129             \s
130             (\S+)/x; # file => $1
131 9 100       19 $state = 'patch' if /^\@\@/;
132             }
133              
134             # patch expects @@ -a,b +c,d @@
135              
136 92 100       105 if ( $state eq 'patch' ) {
137 11 100       43 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         19 $a_line = $1 - 1;
151 10         11 $a_count = $2;
152 10         12 $b_line = $3 - 1;
153 10         9 $b_count = $4;
154 10         8 $state = 'detail';
155 10         14 next;
156             }
157              
158             # detail expects [-+ ]line of text
159              
160 81 100       103 if ( $state eq 'detail' ) {
161 75         91 my $ind = substr $_, 0, 1, '';
162 75 50       158 _error( $lin, 'Bad diff' ), return undef
163             unless $ind =~ /[ +\-i\\]/;
164              
165 75 100       103 next if $ind eq '\\';
166              
167             #[- ]line, add to @a_hunk
168 74 100       100 if ( $ind ne '+' ) {
169 64         42 my $lead = '-';
170 64 100 100     204 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
171 1         6 $lead .= '/';
172 1         15 s/$sep$//s;
173             }
174 64         113 push @a_hunk, [ $lead, $a_line++, $_ ];
175 64         59 $a_count--;
176 64 50       92 _error( $lin, 'Too large diff' ), return undef
177             if $a_count < 0;
178             }
179              
180             #[+ ]line, add to @b_hunk
181 74 100       97 if ( $ind ne '-' ) {
182 56         41 my $lead = '+';
183 56 100 100     176 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
184 1         2 $lead .= '/';
185 1         13 s/$sep$//s;
186             }
187 56         86 push @b_hunk, [ $lead, $b_line++, $_ ];
188 56         48 $b_count--;
189 56 50       72 _error( $lin, 'Too large diff' ), return undef
190             if $b_count < 0;
191             }
192              
193             # are we there yet, daddy?
194 74 100 66     131 if ( !$a_count and !$b_count ) {
195 10         17 push @out, [ @a_hunk, @b_hunk ];
196 10         16 @a_hunk = @b_hunk = ();
197 10         17 $state = 'patch';
198             }
199             }
200             } # next line of patch
201 3         62 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         2 my $state = 'patch';
213 3         3 my ( $a_line, $a_count, @a_hunk, $b_line, $b_count, @b_hunk );
214 3         8 for my $lin ( 0 .. $#diff ) {
215 46         57 local $_ = $diff[$lin];
216 46 100       69 chomp if $proto{chomp};
217              
218             # patch expects ww,xx[acd]yy,zz style
219              
220 46 100       64 if ( $state eq 'patch' ) {
221 11 100       46 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         19 $a_line = $1 - 1;
228 10 100       17 $a_count = $2 ? ( $2 - $a_line ) : 1;
229 10         9 $b_line = $4 - 1;
230 10 100       15 $b_count = $5 ? ( $5 - $b_line ) : 1;
231 10 100       18 $a_count = 0 if $3 eq 'a';
232 10 100       17 $b_count = 0 if $3 eq 'd';
233 10         8 $state = 'detail';
234 10         13 next;
235             }
236              
237             # detail expects < lines --- > lines
238              
239 35 50       50 if ( $state eq 'detail' ) {
240 35 100       54 next if /^---/; # ignore separator
241 29         38 my $ind = substr $_, 0, 2, '';
242 29 50       71 _error( $lin, 'Bad diff' ), return undef
243             unless $ind =~ /[<>\\] /;
244              
245             # < line goes to @a_hunk
246 29 100       38 if ( $ind eq '< ' ) {
247 18         13 my $lead = '-';
248 18 100 66     62 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
249 1         3 $lead .= '/';
250 1         20 s/$sep$//s;
251             }
252 18         31 push @a_hunk, [ $lead, $a_line++, $_ ];
253 18         15 $a_count--;
254 18 50       23 _error( $lin, 'Too large diff' ), return undef
255             if $a_count < 0;
256             }
257              
258             # > line goes to @b_hunk
259 29 100       40 if ( $ind eq '> ' ) {
260 10         8 my $lead = '+';
261 10 100 100     36 if (($lin < $#diff) && $diff[$lin+1] =~ /^\\/) {
262 1         2 $lead .= '/';
263 1         14 s/$sep$//s;
264             }
265 10         17 push @b_hunk, [ $lead, $b_line++, $_ ];
266 10         7 $b_count--;
267 10 50       17 _error( $lin, 'Too large diff' ), return undef
268             if $b_count < 0;
269             }
270              
271             # are we there yet, daddy?
272 29 100 66     66 if ( !$a_count and !$b_count ) {
273 10         15 push @out, [ @a_hunk, @b_hunk ];
274 10         13 @a_hunk = @b_hunk = ();
275 10         14 $state = 'patch';
276             }
277             }
278             }
279 3         102 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   15 my $sep = shift;
302 17         14 my $r_line_offset = shift;
303              
304 17         17 my @ins;
305 17         23 my ( $ins_firstline, $ins_lastline ) = ( 0, 0 );
306 17         12 my @del;
307 17         17 my ( $del_firstline, $del_lastline ) = ( 0, 0 );
308 17         17 my $op;
309 17         14 my $shortins = '';
310 17         14 my $shortdel = '';
311            
312             # construct @ins and @del from hunk
313              
314 17         27 for (@_) {
315 47         52 my ( $typ, $lno, $txt ) = @$_;
316 47         59 my $short = substr($typ, 1, 1, '');
317 47         28 $lno++;
318 47 100       62 if ( $typ eq '+' ) {
319 21         25 push @ins, $txt;
320 21   66     48 $ins_firstline ||= $lno;
321 21         22 $ins_lastline = $lno;
322 21 100       40 $shortins = "\n\\ No newline at end of file\n" if $short;
323             }
324             else {
325 26         29 push @del, $txt;
326 26   66     54 $del_firstline ||= $lno;
327 26         22 $del_lastline = $lno;
328 26 100       53 $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       38 if ( !@del ) {
    100          
335 3         5 $op = 'a';
336 3         5 $del_firstline = $ins_firstline - $$r_line_offset - 1;
337             }
338             elsif ( !@ins ) {
339 2         3 $op = 'd';
340 2         3 $ins_firstline = $del_firstline + $$r_line_offset - 1;
341             }
342             else {
343 12         13 $op = 'c';
344             }
345              
346 17         25 $$r_line_offset += @ins - @del;
347              
348 17   66     30 $ins_lastline ||= $ins_firstline;
349 17   100     32 $del_lastline ||= $del_firstline;
350              
351             # Make the header line
352              
353 17         37 my $outstr =
354             "$del_firstline,$del_lastline$op$ins_firstline,$ins_lastline\n";
355 17         169 $outstr =~ s/(^|\D)(\d+),\2(?=\D|$)/$1$2/g;
356              
357             # < deletions
358 17         28 for (@del) {
359 26         43 $outstr .= '< ' . $_ . $sep;
360             }
361 17         15 $outstr .= $shortdel;
362            
363             # ---
364 17 100 100     78 $outstr .= "---\n" if @ins && @del;
365              
366             # > insertions
367 17         20 for (@ins) {
368 21         39 $outstr .= '> ' . $_ . $sep;
369             }
370 17         20 $outstr .= $shortins;
371              
372 17         66 $outstr;
373             }
374              
375             sub diff {
376 4     4 1 2328 my $self = shift;
377 4   100     36 my $sep = shift || $self->{sep} || '';
378              
379 4         7 my $off = 0;
380              
381 4         9 join '', map { _diff_hunk( $sep, \$off, @$_ ) } @{ $self->{diff} };
  17         45  
  4         10  
382             }
383              
384             sub udiff {
385 6     6 1 2296 my $self = shift;
386 6   100     46 my $sep = shift || $self->{sep} || '';
387              
388 6         7 my ( $in, $out, $diff ) = @{$self}{qw/id1 id2 diff/};
  6         40  
389              
390             # Header with file names
391              
392 6         24 my @out = ( "--- $in \n", "+++ $out \n" );
393              
394 6         7 my $offset = 0;
395              
396 6         12 for (@$diff) {
397 22         36 my @t1 = grep { $_->[0] =~ /^\-/ } @$_;
  248         499  
398 22         25 my @t2 = grep { $_->[0] =~ /^\+/ } @$_;
  248         393  
399              
400 22         24 my $short1 = '';
401 134         181 $short1 = "\n\\ No newline at end of file\n"
402 22 100       25 if grep { $_->[0] eq '-/' } @t1;
403 22         22 my $short2 = '';
404 114         156 $short2 = "\n\\ No newline at end of file\n"
405 22 100       20 if grep { $_->[0] eq '+/' } @t2;
406            
407             # Work out base line numbers in both files
408              
409 22 100       45 my $base1 = @t1 ? $t1[0][1] : $t2[0][1] - $offset;
410 22 100       33 my $base2 = @t2 ? $t2[0][1] : $t1[0][1] + $offset;
411 22         19 $base1++;
412 22         15 $base2++; # Our lines were 0 based
413 22         30 $offset += @t2 - @t1;
414 22         21 my $count1 = @t1;
415 22         20 my $count2 = @t2;
416              
417             # Header line
418 22         55 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         26 my @txt1 = map { $_->[2] } @t1;
  134         172  
423 22         31 my @txt2 = map { $_->[2] } @t2;
  114         131  
424              
425 22         65 my @ad = Algorithm::Diff::sdiff( \@txt1, \@txt2 );
426 22         2931 my @defer;
427              
428             # for each subhunk, we want all the file1 lines first, then all the file2 lines
429              
430 22         38 for (@ad) {
431 139         144 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       249 ( push @out, @defer ), @defer = () unless $ind eq 'c';
436              
437             # unchanged lines, just wack 'em out
438 139 100       284 ( push @out, ' ' . $txt1 . $sep ), next if $ind eq 'u';
439              
440             # output original line (- line)
441 47 100       87 push @out, '-' . $txt1 . $sep unless $ind eq '+';
442              
443             # defer changed + lines
444 47 100       94 push @defer, '+' . $txt2 . $sep unless $ind eq '-';
445             }
446 22         28 push @out, $short1;
447            
448             # and flush at the end
449 22         92 push @out, @defer, $short2;
450             }
451 6 50       92 wantarray ? @out : join '', @out;
452             }
453              
454             sub id {
455 1     1 1 383 my $self = shift;
456              
457 1 50       8 if (@_) {
458 0         0 $self->{id1} = shift;
459 0         0 $self->{id2} = shift;
460             }
461              
462 1         2 @{$self}{qw/id1 id2/};
  1         8  
463             }
464              
465             sub hunks {
466 7     7 1 406 my $self = shift;
467              
468 7         7 @{ $self->{diff} };
  7         22  
469             }
470              
471             1;
472              
473             __END__