File Coverage

blib/lib/VCS/Lite.pm
Criterion Covered Total %
statement 173 195 88.7
branch 69 96 71.8
condition 43 56 76.7
subroutine 22 24 91.6
pod 9 9 100.0
total 316 380 83.1


line stmt bran cond sub pod time code
1             package VCS::Lite;
2              
3 9     9   150873 use strict;
  9         25  
  9         342  
4 9     9   41 use warnings;
  9         14  
  9         435  
5             our $VERSION = '0.12';
6              
7             #----------------------------------------------------------------------------
8              
9             =head1 NAME
10              
11             VCS::Lite - Minimal version control system
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             # merge
31              
32             my $lit4 = $lit->merge($lit->delta($lit2),$lit->delta($lit3));
33             print OUTFILE $lit4->text;
34              
35             =head1 DESCRIPTION
36              
37             This module provides the functions normally associated with a version
38             control system, but without needing or implementing a version control
39             system. Applications include wikis, document management systems and
40             configuration management.
41              
42             It makes use of the module Algorithm::Diff. It provides the facility
43             for basic diffing, patching and merging.
44              
45             =cut
46              
47             #----------------------------------------------------------------------------
48              
49             #############################################################################
50             #Library Modules #
51             #############################################################################
52              
53 9     9   49 use Carp;
  9         18  
  9         746  
54 9     9   5447 use Algorithm::Diff qw(traverse_sequences);
  9         37122  
  9         531  
55 9     9   3497 use VCS::Lite::Delta;
  9         87  
  9         17242  
56              
57             #----------------------------------------------------------------------------
58              
59             #############################################################################
60             #Interface Methods #
61             #############################################################################
62              
63             sub new {
64 37     37 1 4775 my ($class,$id,$sep,$src,@args) = @_;
65              
66 37         65 my %proto = ();
67              
68             # Decode $sep as needed
69              
70 37 100       105 if (ref($sep) eq 'HASH') {
71 10         33 %proto = %$sep;
72 10         15 $sep = $proto{in};
73 10         20 delete $proto{in};
74             }
75            
76             # DWIM logic, based on $src parameter.
77              
78             # Case 0: $src missing. Use $id as file name, becomes case 3
79 37 100 33     698 open $src,$id or croak("failed to open '$id': $!") unless $src;
80            
81 37         75 my $atyp = ref $src;
82 37   66     138 $sep ||= $/;
83 37 50       155 local $/ = $sep if $sep;
84 37   50     230 $proto{out} ||= $\ || '';
      66        
85 37         53 my $out_sep = $proto{out};
86 37         33 my @contents;
87              
88             # Case 1: $src is string
89 37 50       134 if (!$atyp) {
    100          
    50          
    0          
90 0         0 @contents = split /(?=$sep)/,$src;
91             }
92             # Case 2: $src is arrayref
93             elsif ($atyp eq 'ARRAY') {
94 17         195 @contents = @$src;
95             }
96             # Case 3: $src is globref (file handle)
97             elsif ($atyp eq 'GLOB') {
98 20         785 @contents = <$src>;
99             }
100             # Case 4: $src is coderef - callback
101             elsif ($atyp eq 'CODE') {
102 0         0 while (my $item=&$src(@args)) {
103 0         0 push @contents,$item;
104             }
105             }
106             # Case otherwise is an error.
107             else {
108 0         0 croak "Invalid argument";
109             }
110            
111 37 100 66     426 $proto{last_line_short} = 1
112             if @contents && ($contents[-1] !~ /$sep$/);
113            
114 37 100       85 if ($proto{chomp}) {
115 6         510 s/$sep$//s for @contents;
116 6   66     21 $proto{out} ||= $sep;
117             }
118            
119 37         948 bless { id => $id,
120             contents => \@contents,
121             separator => $sep,
122             %proto },$class;
123             }
124              
125             sub original {
126 2     2 1 4 my $self = shift;
127              
128 2         4 my $pkg = ref $self;
129              
130 2 50       9 exists($self->{original}) ?
131             bless ({ id => $self->id,
132             contents => $self->{original},
133             separator => $self->{separator},
134             out => $self->{out},
135             chomp => $self->{chomp},
136             }, $pkg ) :
137             $self;
138             }
139              
140             sub apply {
141 3     3 1 11 my ($self,$other,%par) = @_;
142              
143 3         4 my $pkg = ref $self;
144 3         5 my $base = $par{base};
145 3   100     12 $base ||= 'contents';
146 3 50       12 $base = $pkg->new( $self->id,
147             $self->{separator},
148             $self->{$base})
149             unless ref $base;
150 3 100       390 my $cbase = exists($other->{original}) ? $other->original : $base;
151 3         10 my $mrg = $cbase->merge($base,$other);
152 3         21 my $mrg2 = $base->merge($self,$mrg);
153 3   66     26 $self->{original} ||= $self->{contents};
154 3         10 $self->{contents} = [$mrg2->text];
155             }
156            
157             sub text {
158 40     40 1 1668 my ($self,$sep) = @_;
159            
160 40   100     201 $sep ||= $self->{out} || '';
      66        
161              
162 40 100       58 wantarray ? @{$self->{contents}} : join $sep,@{$self->{contents}};
  27         590  
  13         213  
163             }
164              
165             sub id {
166 26     26 1 456 my $self = shift;
167              
168 26 50       152 @_ ? ($self->{id} = shift) : $self->{id};
169             }
170              
171             sub delta {
172 9     9 1 1084 my $lite1 = shift;
173 9         14 my $lite2 = shift;
174 9         17 my %par = @_;
175            
176 9         36 my @wl1 = $lite1->_window($par{window});
177 9         67 my @wl2 = $lite2->_window($par{window});
178 29 100       3526 my @d = map { [map { [$_->[0] . ($_->[2]{short} ? '/' : ''),
  175         477  
179             $_->[1], $_->[2]{line} ] } @$_ ] }
180 1627     1627   25734 Algorithm::Diff::diff(\@wl1,\@wl2,sub { $_[0]{window}; })
181 9 100       113 or return undef;
182              
183 7         73 VCS::Lite::Delta->new(\@d,$lite1->id,$lite2->id,$lite1->{out});
184             }
185              
186             sub _window {
187 18     18   22 my $self = shift;
188              
189 18   100     473 my $win = shift || 0;
190 18 50       47 my ($win_from,$win_to) = ref($win) ? (-$win->[0],$win->[1]) :
191             (-$win,$win);
192 18         20 my @wintxt;
193 18         14 my $max = $#{$self->{contents}};
  18         37  
194 18         49 for (0..$max) {
195 1599         1147 my $win_lb = $_ + $win_from;
196 1599 100       1998 $win_lb = 0 if $win_lb < 0;
197 1599         1126 my $win_ub = $_ + $win_to;
198 1599 100       1984 $win_ub = $max if $win_ub > $max;
199 1599         4439 push @wintxt, join $self->{out},
200 1599 100 100     1514 (@{$self->{contents}}[$win_lb .. $win_ub],
201             (($win_ub < $max) || !$self->{last_line_short}) ?
202             '' : ());
203             }
204              
205 18 100 100     143 map { {line => $self->{contents}[$_],
  1599         4251  
206             window => $wintxt[$_],
207             ( $self->{last_line_short} && ($_ == $max)) ? ( short => 1 ) : (),
208             } }
209             (0..$max);
210             }
211              
212             sub diff {
213 1     1 1 9 my $self = shift;
214              
215 1         5 $self->delta(@_)->diff;
216             }
217              
218             sub patch {
219 6     6 1 956 my $self = shift;
220 6         6 my $patch = shift;
221 6 50       16 $patch = VCS::Lite::Delta->new($patch,@_)
222             unless ref $patch eq 'VCS::Lite::Delta';
223 6         7 my @out = @{$self->{contents}};
  6         59  
224 6         11 my $id = $self->id;
225 6         6 my $pkg = ref $self;
226 6         16 my @pat = $patch->hunks;
227              
228 6         9 for (@pat) {
229 20         21 for (@$_) {
230 148         120 my ($ind,$lin,$txt) = @$_;
231 148 100       257 next unless $ind =~ /^-/;
232 82 50       118 _error($lin,'Patch failed'),return undef
233             if $out[$lin] ne $txt;
234             }
235             }
236              
237 6         7 my $line_offset = 0;
238 6         6 my $lls = 0;
239            
240 6         8 for (@pat) {
241 20         20 my @txt1 = grep {$_->[0] =~ /^\-/} @$_;
  148         206  
242 20         19 my @txt2 = grep {$_->[0] =~ /^\+/} @$_;
  148         173  
243 20 100       33 my $base_line = @txt2 ? $txt2[0][1] : $txt1[0][1] + $line_offset;
244 20         23 splice @out,$base_line,scalar(@txt1),map {$_->[2]} @txt2;
  66         84  
245 20         27 $line_offset += @txt2 - @txt1;
246 20         20 $lls += grep {$_->[0] eq '+/'} @txt2;
  66         87  
247             }
248              
249 6         48 $pkg->new($id,{
250             in => $self->{separator},
251             chomp => $self->{chomp},
252             out => $self->{out},
253             last_line_short => $lls,
254             },\@out);
255             }
256              
257             # Equality of two array references (contents)
258            
259             sub _equal {
260 0     0   0 my ($a,$b) = @_;
261            
262 0 0       0 return 0 if @$a != @$b;
263            
264 0         0 foreach (0..$#$a) {
265 0 0       0 return 0 if $a->[$_] ne $b->[$_];
266             }
267            
268 0         0 1;
269             }
270              
271             sub merge {
272 8     8 1 41 my ($self,$d1,$d2) = @_;
273 8         11 my $pkg = ref $self;
274              
275 8         20 my $orig = [$self->text];
276 8         39 my $chg1 = [$d1->text];
277 8         37 my $chg2 = [$d2->text];
278 8         517 my $out_title = $d1->{id} . '|' . $d2->{id};
279 8         10 my %ins1;
280 8         10 my $del1 = '';
281              
282             traverse_sequences( $self->{contents}, $chg1, {
283 784     784   21164 MATCH => sub { $del1 .= ' ' },
284 24     24   112 DISCARD_A => sub { $del1 .= '-' },
285 12     12   44 DISCARD_B => sub { push @{$ins1{$_[0]}},$chg1->[$_[1]] },
  12         50  
286 8         115 } );
287              
288 8         86 my %ins2;
289 8         10 my $del2 = '';
290              
291             traverse_sequences( $self->{contents}, $chg2, {
292 749     749   16837 MATCH => sub { $del2 .= ' ' },
293 59     59   247 DISCARD_A => sub { $del2 .= '-' },
294 66     66   28133 DISCARD_B => sub { push @{$ins2{$_[0]}},$chg2->[$_[1]] },
  66         175  
295 8         83 } );
296              
297             # First pass conflict detection: deletion on file 1 and insertion on file 2
298              
299 8         134 $del1 =~ s(\-+){
300 9         21 my $stlin = length $`;
301 9         15 my $numdel = length $&;
302              
303 9 100       29 my @confl = map {exists $ins2{$_} ? ($_) : ()}
  15         40  
304             ($stlin+1..$stlin+$numdel-1);
305 9 100       64 @confl ? '*' x $numdel : $&;
306             }eg;
307              
308             # Now the other way round: deletion on file 2 and insertion on file 1
309              
310 8         54 $del2 =~ s(\-+){
311 38         57 my $stlin = length $`;
312 38         40 my $numdel = length $&;
313              
314 38 50       68 my @confl = map {exists $ins1{$_} ? ($_) : ()}
  21         50  
315             ($stlin+1..$stlin+$numdel-1);
316 38 50       149 @confl ? '*' x $numdel : $&;
317             }eg;
318              
319             # Conflict type 1 is insert of 2 into deleted 1, Conflict type 2 is insert of 1 into deleted 2
320             # @defer is used to hold the 'other half' alternative for the conflict
321              
322 8         16 my $conflict = 0;
323 8         7 my $conflict_type = 0;
324 8         10 my @defer;
325              
326             my @out;
327              
328 8         9 for (0..@{$self->{contents}}) {
  8         29  
329              
330             # Get details pertaining to current @f0 input line
331 816         929 my $line = $self->{contents}[$_];
332 816         802 my $d1 = substr $del1,$_,1;
333 816 100       1225 my $ins1 = $ins1{$_} if exists $ins1{$_};
334 816         673 my $d2 = substr $del2,$_,1;
335 816 100       1160 my $ins2 = $ins2{$_} if exists $ins2{$_};
336              
337             # Insert/insert conflict. This is not a conflict if both inserts are identical.
338              
339 816 50 66     2217 if ($ins1 && $ins2 && !&_equal($ins1,$ins2)) {
    100 33        
340 0         0 push @out, ('*'x20)."Start of conflict ".(++$conflict).
341             " Insert to Primary, Insert to Secondary ".('*'x60)."\n";
342              
343 0         0 push @out, @$ins1, ('*'x100)."\n", @$ins2;
344 0         0 push @out, ('*'x20)."End of conflict ".$conflict.('*'x80)."\n";
345             } elsif (!$conflict_type) { #Insert/Delete conflict
346              
347             # Normal insertion - may be from $ins1 or $ins2. Apply the inser and junk both $ins1 and $ins2
348              
349 809   100     1786 $ins1 ||= $ins2;
350              
351 809 100       1118 push @out, @$ins1 if defined $ins1;
352              
353 809         651 undef $ins1;
354 809         583 undef $ins2;
355             }
356              
357             # Detect start of conflict 1 and 2
358              
359 816 100 100     2389 if (!$conflict_type && $d1 eq '*') {
360 2         12 push @out, ('*'x20)."Start of conflict ".(++$conflict).
361             " Delete from Primary, Insert to Secondary ".('*'x60)."\n";
362              
363 2         3 $conflict_type = 1;
364             }
365              
366 816 50 66     2307 if (!$conflict_type && $d2 eq '*') {
367 0         0 push @out, ('*'x20)."Start of conflict ".(++$conflict).
368             " Delete from Secondary, Insert to Primary ".('*'x60)."\n";
369              
370 0         0 $conflict_type = 2;
371             }
372              
373             # Handle case where we are in an Insert/Delete conflict block already
374              
375 816 100       1179 if ($conflict_type == 1) {
376 9 100       22 if ($d1 eq '*') {
377              
378             # Deletion block continues...
379 7 100       17 push @defer,(@$ins2) if $ins2;
380 7 50       15 push @defer,$line if !$d2;
381             } else {
382              
383             # handle end of block, dump out @defer and clear it
384              
385 2         6 push @out, ('*'x100)."\n",@defer;
386 2         7 undef @defer;
387 2         7 push @out, ('*'x20)."End of conflict ".$conflict.('*'x80)."\n";
388 2         3 $conflict_type = 0;
389             }
390             }
391              
392 816 50       1025 if ($conflict_type == 2) {
393 0 0       0 if ($d2 eq '*') {
394              
395             # Deletion block continues...
396 0 0       0 push @defer,(@$ins1) if $ins1;
397 0 0       0 push @defer,$line if !$d1;
398             } else {
399              
400             # handle end of block, dump out @defer and clear it
401              
402 0         0 push @out, ('*'x100),"\n", @defer;
403 0         0 undef @defer;
404 0         0 push @out, ('*'x20)."End of conflict ".$conflict.('*'x80)."\n";
405 0         0 $conflict_type = 0;
406             }
407             }
408 816 100       1102 last unless defined $line; # for end of file, don't want to push undef
409 808 100 100     3401 push @out, $line unless ($d1 eq '-' || $d2 eq '-') && !$conflict_type;
      100        
410             }
411 8         62 $pkg->new($out_title, undef, \@out);
412             }
413              
414 0     0     sub _error {};
415              
416             1;
417              
418             __END__