File Coverage

blib/lib/VCS/SCCS.pm
Criterion Covered Total %
statement 175 177 98.8
branch 86 104 82.6
condition 10 17 58.8
subroutine 20 20 100.0
pod 14 14 100.0
total 305 332 91.8


line stmt bran cond sub pod time code
1             #!/pro/bin/perl
2              
3             # Copyright (c) 2007-2020 H.Merijn Brand. All rights reserved.
4              
5             package VCS::SCCS;
6              
7 3     3   324876 use strict;
  3         35  
  3         86  
8 3     3   15 use warnings;
  3         6  
  3         80  
9              
10 3     3   15 use POSIX qw(mktime);
  3         5  
  3         23  
11 3     3   5306 use Carp;
  3         7  
  3         214  
12              
13 3     3   20 use vars qw( $VERSION );
  3         5  
  3         7798  
14             $VERSION = "0.27";
15              
16             ### ###########################################################################
17              
18             # We can safely use \d instead of [0-9] for this ancient format
19              
20             sub new {
21 12     12 1 145 my $proto = shift;
22 12 50 33     68 my $class = ref ($proto) || $proto or return;
23              
24             # We can safely rule out "0" as a valid filename, ans 99.9999% of
25             # SCCS source files start with s.
26 12 100       402 my $fn = shift or croak ("SCCS needs a valid file name");
27 9 100       279 -e $fn or croak ("$fn does not exist");
28 8 100       281 -f $fn or croak ("$fn is not a file");
29 6 100       172 -s $fn or croak ("$fn is empty");
30 5         50 (my $filename = $fn) =~ s{\b(?:SCCS|sccs)/s\.(?=[^/]+$)}{};
31              
32 5 50       190 open my $fh, "<", $fn or croak ("Cannot open '$fn': $!");
33              
34             # Checksum
35             # ^Ah checksum
36 5 100       339 <$fh> =~ m/^\cAh(\d+)$/ or croak ("SCCS file $fn is supposed to start with a checksum");
37              
38 3         44 my %sccs = (
39             file => $filename,
40              
41             checksum => $1,
42             delta => {},
43             users => [],
44             flags => {},
45             comment => "",
46             body => undef,
47              
48             current => undef,
49             vsn => {}, # version to revision map
50              
51             tran => undef,
52             );
53              
54             # Delta's At least one! ^A[ixg] ignored
55             # ^As inserted/deleted/unchanged
56             # ^Ad D version date time user v_new v_old
57             # ^Am MR
58             # ^Ac comment
59             # ^Ae
60 3         9 $_ = <$fh>;
61 3         26 while (m{^\cAs (\d+)/(\d+)/(\d+)$}) {
62              
63 73         130 my @delta;
64              
65 73         173 my ($l_ins, $l_del, $l_unc) = map { $_ + 0 } $1, $2, $3;
  219         559  
66              
67 73         128 { local $/ = "\cAe\n";
  73         236  
68 73         376 @delta = split m/\n/, scalar <$fh>;
69             }
70              
71 73         755 my ($type, $vsn, $v_r, $v_l, $v_b, $v_s,
72             $date, $y, $m, $d, $time, $H, $M, $S,
73             $user, $rev, $prv) =
74             (shift (@delta) =~ m{
75             \cAd # Delta
76             \s+ ([DR]) # Type Delta/Remove?
77             \s+ ((\d+)\.(\d+)
78             (?:\.(\d+)(?:\.(\d+))?)?) # Vsn %R%.%L%[.%B%[.%S%]]
79             \s+ ((\d\d)/(\d\d)/(\d\d)) # Date %E%
80             \s+ ((\d\d):(\d\d):(\d\d)) # Time %U%
81             \s+ (\S+) # User
82             \s+ (\d+) # current rev
83             \s+ (\d+) # new rev
84             \s*$
85             }x);
86 73 100       258 $y += $y < 70 ? 2000 : 1900; # SCCS is not Y2k safe!
87              
88             # Type R rev's are removed/overridden deltas:
89             # D 4.21 22 21
90             # D 4.20 21 19
91             # R 4.20 20 19
92             # D 4.19 19 18
93              
94 73         126 my @mr = grep { s/^\cAm\s*// } @delta; # MR number(s)
  217         567  
95 73         131 my @cmnt = grep { s/^\cAc\s*// } @delta; # Comment
  217         524  
96              
97 73   100     215 $sccs{current} ||= [ $rev, $vsn, $v_r, $v_l, $v_b, $v_s ];
98 73         3087 $sccs{delta}{$rev} = {
99             lines_ins => $l_ins,
100             lines_del => $l_del,
101             lines_unc => $l_unc,
102              
103             type => $type,
104              
105             version => $vsn, # %I%
106             release => $v_r, # %R%
107             level => $v_l, # %L%
108             branch => $v_b, # %B%
109             sequence => $v_s, # %S%
110              
111             date => $date, # %E%
112             time => $time, # %U%
113             stamp => mktime ($S, $M, $H, $d, $m - 1, $y - 1900, -1, -1, -1),
114              
115             committer => $user,
116              
117             mr => join (", ", @mr),
118             comment => join ("\n", @cmnt),
119              
120             prev_rev => $prv,
121             };
122 73 50       351 exists $sccs{vsn}{$vsn} or $sccs{vsn}{$vsn} = $rev;
123 73         739 $_ = <$fh>;
124             }
125              
126             # Users
127             # ^Au
128             # user1
129             # user2
130             # ...
131             # ^AU
132 3 100       30 if (m{^\cAu}) {
133 2         5 { local $/ = "\cAU\n";
  2         10  
134 2         19 $sccs{users} = [ (<$fh> =~ m{^([A-Za-z].*)$}gm) ];
135             }
136 2         6 $_ = <$fh>;
137             }
138              
139             # Flags
140             # ^Af q Project name
141             # ^Af v ...
142             # ^Af e 1
143 3         24 while (m/^\cAf \s+ (\S) \s* (.+)?$/x) {
144 5         17 $sccs{flags}{$1} = $2;
145 5         31 $_ = <$fh>;
146             }
147              
148             # Comment
149             # ^At comment
150 3         20 while (s/^\cA[tT]\s*//) {
151 6 100       30 m/\S/ and $sccs{comment} .= $_;
152 6         28 $_ = <$fh>;
153             }
154              
155             # Body
156 3         12 local $/ = undef;
157 3         8656 $sccs{body} = [ split m/\n/, $_ . <$fh> ];
158 3         581 close $fh;
159              
160 3         51 return bless \%sccs, $class;
161             } # new
162              
163             sub file {
164 10     10 1 17 my $self = shift;
165 10         67 return $self->{file};
166             } # file
167              
168             sub checksum {
169 2     2 1 6 my $self = shift;
170 2         8 return $self->{checksum};
171             } # checksum
172              
173             sub users {
174 1     1 1 3 my $self = shift;
175 1         2 return @{$self->{users}};
  1         7  
176             } # users
177              
178             sub flags {
179 2     2 1 6 my $self = shift;
180 2         4 return { %{$self->{flags}} };
  2         19  
181             } # flags
182              
183             sub comment {
184 1     1 1 29 my $self = shift;
185 1         7 return $self->{comment};
186             } # comment
187              
188             sub current {
189 2     2 1 5 my $self = shift;
190 2 50       22 $self->{current} or return;
191 2 100       9 wantarray ? @{$self->{current}} : $self->{current}[0];
  1         12  
192             } # current
193              
194             sub delta {
195 13     13 1 27 my ($self, $rev) = @_;
196 13 50       33 $self->{current} or return;
197 13 100       49 if (!defined $rev) {
    100          
    100          
198 1         4 $rev = $self->{current}[0];
199             }
200             elsif (exists $self->{delta}{$rev}) {
201             #$rev = $rev;
202             }
203             elsif (exists $self->{vsn}{$rev}) {
204 1         3 $rev = $self->{vsn}{$rev};
205             }
206             else {
207 1         9 return;
208             }
209 12         17 return { %{ $self->{delta}{$rev} } };
  12         194  
210             } # delta
211              
212             sub version {
213 9     9 1 256 my ($self, $rev) = @_;
214 9 100       56 ref $self eq __PACKAGE__ or return $VERSION;
215 6 50       17 $self->{current} or return;
216              
217             # $self->version () returns most recent version
218 6 100       25 $rev or return $self->{current}[1];
219              
220             # $self->revision (12) returns version for that revision
221             exists $self->{delta}{$rev} and
222 2 100       10 return $self->{delta}{$rev}{version};
223              
224 1         8 return;
225             } # version
226              
227             sub revision {
228 6     6 1 14 my ($self, $vsn) = @_;
229 6 50       18 $self->{current} or return;
230              
231             # $self->revision () returns most recent revision
232 6 100       28 $vsn or return $self->{current}[0];
233              
234             # $self->revision (12) returns version for that revision
235             exists $self->{vsn}{$vsn} and
236 2 100       13 return $self->{vsn}{$vsn};
237              
238 1         6 return;
239             } # revision
240              
241             sub revision_map {
242 1     1 1 3 my $self = shift;
243 1 50       4 $self->{current} or return;
244              
245 70         164 return [ map { [ $_ => $self->{delta}{$_}{version} ] }
246 334         376 sort { $a <=> $b }
247 1         3 keys %{$self->{delta}} ];
  1         22  
248             } # revision
249              
250             my %tran = (
251             SCCS => { # Documentation only
252             },
253             RCS => {
254             # "%W%[ \t]*%G%" => '$""Id""$',
255             # "%W%[ \t]*%E%" => '$""Id""$',
256             # "%W%" => '$""Id""$',
257             # "%Z%%M%[ \t]*%I%[ \t]*%G%" => '$""SunId""$',
258             # "%Z%%M%[ \t]*%I%[ \t]*%E%" => '$""SunId""$',
259             # "%M%[ \t]*%I%[ \t]*%G%" => '$""Id""$',
260             # "%M%[ \t]*%I%[ \t]*%E%" => '$""Id""$',
261             # "%M%" => '$""RCSfile""$',
262             # "%I%" => '$""Revision""$',
263             # "%G%" => '$""Date""$',
264             # "%E%" => '$""Date""$',
265             # "%U%" => '',
266             },
267             );
268              
269             sub set_translate {
270 4     4 1 11 my ($self, $type) = @_;
271              
272 4 100       18 if (ref $type eq "HASH") {
    100          
273 1         2 $self->{tran} = "CUSTOM";
274 1         3 $tran{CUSTOM} = $type;
275             }
276             elsif (exists $tran{uc $type}) {
277 2         7 $self->{tran} = uc $type;
278             }
279             else {
280 1         4 $self->{tran} = undef;
281             }
282             } # set_translate
283              
284             sub _tran {
285 38685     38685   57553 my ($self, $line) = @_;
286 38685 100       92669 my $tt = $self->{tran} or return $line;
287 11 50       22 my $tr = $tran{$tt} or return $line;
288 11         16 my $re = $tr->{re};
289 11         161 $line =~ s{($re)}{$tr->{$1}}g;
290 11         81 return $line;
291             } # _tran
292              
293             sub translate {
294 32     32 1 78 my ($self, $rev, $line) = @_;
295              
296 32 100       118 my $type = $self->{tran} or return $line;
297 9 50       25 exists $self->{delta}{$rev} or return $line;
298              
299 9         20 (my $def_M = $self->file ()) =~ s{.*/}{};
300              
301             # TODO (or don't): %D%, %H%, %T%, %G%, %F%, %P%, %C%
302 9         19 my %delta = %{$self->delta ($rev)};
  9         20  
303 9         38 my $I = $delta{version};
304 9         14 my $Z = "@(#)";
305 9 50       22 my $M = exists $self->{flags}{"m"} ? $self->{flags}{"m"} : $def_M;
306 9 50       21 my $Q = exists $self->{flags}{"q"} ? $self->{flags}{"q"} : "";
307 9 50       18 my $Y = exists $self->{flags}{"t"} ? $self->{flags}{"t"} : "";
308 9         17 $tran{SCCS}{"%U%"} = $delta{"time"};
309 9         15 $tran{SCCS}{"%E%"} = $delta{"date"};
310 9         16 $tran{SCCS}{"%R%"} = $delta{"release"};
311 9         13 $tran{SCCS}{"%L%"} = $delta{"level"};
312 9         12 $tran{SCCS}{"%B%"} = $delta{"branch"};
313 9         14 $tran{SCCS}{"%S%"} = $delta{"sequence"};
314 9         14 $tran{SCCS}{"%I%"} = $I;
315 9         13 $tran{SCCS}{"%Z%"} = $Z;
316 9         16 $tran{SCCS}{"%M%"} = $M;
317 9         20 $tran{SCCS}{"%W%"} = "$Z$M\t$I";
318 9         17 $tran{SCCS}{"%A%"} = "$Z$Y $M $I$Z";
319 9         13 $tran{SCCS}{"%Q%"} = $Q;
320 9         13 $tran{SCCS}{"%Y%"} = $Y;
321              
322 9 100       22 unless (exists $tran{$type}{re}) {
323 2         5 my $kw = join "|", reverse sort keys %{$tran{$type}};
  2         16  
324 2 50       110 $tran{$type}{re} = $kw ? qr{$kw} : undef;
325             }
326              
327 9         23 return $self->_tran ($line);
328             } # translate
329              
330             sub body {
331 16     16 1 46 my $self = shift;
332              
333 16 50 33     112 $self->{body} && $self->{current} or return;
334 16   66     65 my $r = shift || $self->{current}[0];
335              
336 16 100       63 exists $self->{vsn}{$r} and $r = $self->{vsn}{$r};
337              
338 16         58 my @lvl = ([ 1, "I", 0 ]);
339 16         22 my @body;
340              
341             # my $v = sub {
342             # join " ", map { sprintf "%s:%02d", $_->[1], $_->[2] } @lvl[1..$#lvl];
343             # }; # v
344              
345 16         63 $self->translate ($r, ""); # Initialize translate hash
346              
347 16         23 my $want = 1;
348 16         25 for (@{$self->{body}}) {
  16         43  
349 110245 100       218525 if (m/^\cAE\s+(\d+)$/) {
350 22970         40614 my $e = $1;
351             # print STDERR $v->(), " END $e (@{$lvl[-1]})\n";
352             # SCCS has a seriously ill design so that chunks can overlap
353             # Below example is from actual code
354             # D 9
355             # E 9
356             # I 9
357             # D 10
358             # E 10
359             # I 10
360             # D 53
361             # E 53
362             # I 53
363             # E 53
364             # I 23
365             # D 31
366             # E 31
367             # I 31
368             # D 45
369             # E 45
370             # I 45
371             # E 45
372             # D 53 ---+
373             # E 31 |
374             # E 23 |
375             # E 10 |
376             # E 9 |
377             # D 7 |
378             # E 7 |
379             # I 7 |
380             # E 53 <--+
381             # I 53
382             # E 53
383             # D 53
384             # E 53
385             # I 53
386             # E 53
387             # E 7
388 22970         37458 foreach my $x (reverse 0 .. $#lvl) {
389 23369 100       47865 $lvl[$x][2] == $e or next;
390 22970         30141 splice @lvl, $x, 1;
391 22970         35083 last;
392             }
393 22970 100       33380 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  59588         108832  
394 22970         33550 next;
395             }
396 87275 100       153065 if (m/^\cAI\s+(\d+)$/) {
397 12442 100       38362 push @lvl, [ $r >= $1 ? 1 : 0, "I", $1 ];
398 12442 100       19625 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  44492         77323  
399 12442         18634 next;
400             }
401 74833 100       127917 if (m/^\cAD\s+(\d+)$/) {
402 10528 100       32742 push @lvl, [ $r >= $1 ? 0 : 1, "D", $1 ];
403 10528 100       16837 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  38066         65962  
404 10528         15777 next;
405             }
406 64305 50       105045 if (m/^\cA(.*)/) {
407 0         0 carp "Unsupported SCCS control: ^A$1, line skipped";
408 0         0 next;
409             }
410 64305 100       115216 $want and push @body, $self->_tran ($_);
411             # printf STDERR "%2d.%04d/%s: %-29.29s |%s\n", $r, scalar @body, $want, $v->(), $_;
412             }
413              
414 16 50 66     94 if ($self->{flags}{e} && @body && $body[0] =~ m/^[\x20-\x60]{1,61}$/) {
      66        
415 1         13 my $body = unpack "u" => join "\n" => @body;
416 1 50       24 $body and @body = split m/\n/ => $body;
417             }
418              
419 16 100       6758 return wantarray ? @body : join "\n", @body, "";
420             } # body
421              
422             1;
423              
424             __END__