File Coverage

blib/lib/VCS/SCCS.pm
Criterion Covered Total %
statement 179 181 98.9
branch 86 104 82.6
condition 10 17 58.8
subroutine 20 20 100.0
pod 14 14 100.0
total 309 336 91.9


line stmt bran cond sub pod time code
1             #!/pro/bin/perl
2              
3             # Copyright (c) 2007-2023 H.Merijn Brand. All rights reserved.
4              
5             package VCS::SCCS;
6              
7 3     3   321139 use strict;
  3         48  
  3         83  
8 3     3   19 use warnings;
  3         5  
  3         82  
9              
10 3     3   17 use POSIX qw(mktime);
  3         6  
  3         25  
11 3     3   5046 use Carp;
  3         8  
  3         191  
12              
13 3     3   17 use vars qw( $VERSION );
  3         7  
  3         7902  
14             $VERSION = "0.28";
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 133 my $proto = shift;
22 12 50 33     67 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       407 my $fn = shift or croak ("SCCS needs a valid file name");
27 9 100       261 -e $fn or croak ("$fn does not exist");
28 8 100       269 -f $fn or croak ("$fn is not a file");
29 6 100       160 -s $fn or croak ("$fn is empty");
30 5         58 (my $filename = $fn) =~ s{\b(?:SCCS|sccs)/s\.(?=[^/]+$)}{};
31              
32 5 50       197 open my $fh, "<", $fn or croak ("Cannot open '$fn': $!");
33              
34             # Checksum
35             # ^Ah checksum
36 5 100       413 <$fh> =~ m/^\cAh(\d+)$/ or croak ("SCCS file $fn is supposed to start with a checksum");
37              
38 3         48 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         10 $_ = <$fh>;
61 3         19 while (m{^\cAs (\d+)/(\d+)/(\d+)$}) {
62              
63 73         122 my @delta;
64              
65 73         173 my ($l_ins, $l_del, $l_unc) = map { $_ + 0 } $1, $2, $3;
  219         565  
66              
67 73         111 { local $/ = "\cAe\n";
  73         219  
68 73         354 @delta = split m/\n/, scalar <$fh>;
69             }
70              
71 73         725 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       256 $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         156 my @mr = grep { s/^\cAm\s*// } @delta; # MR number(s)
  217         567  
95 73         119 my @cmnt = grep { s/^\cAc\s*// } @delta; # Comment
  217         498  
96              
97 73   100     211 $sccs{current} ||= [ $rev, $vsn, $v_r, $v_l, $v_b, $v_s ];
98 73         2880 $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       345 exists $sccs{vsn}{$vsn} or $sccs{vsn}{$vsn} = $rev;
123 73         727 $_ = <$fh>;
124             }
125              
126             # Users
127             # ^Au
128             # user1
129             # user2
130             # ...
131             # ^AU
132 3 100       24 if (m{^\cAu}) {
133 2         4 { local $/ = "\cAU\n";
  2         9  
134 2         33 $sccs{users} = [ (<$fh> =~ m{^([A-Za-z].*)$}gm) ];
135             }
136 2         8 $_ = <$fh>;
137             }
138              
139             # Flags
140             # ^Af q Project name
141             # ^Af v ...
142             # ^Af e 1
143 3         19 while (m/^\cAf \s+ (\S) \s* (.+)?$/x) {
144 5         29 $sccs{flags}{$1} = $2;
145 5         20 $_ = <$fh>;
146             }
147              
148             # Comment
149             # ^At comment
150 3         59 while (s/^\cA[tT]\s*//) {
151 6 100       34 m/\S/ and $sccs{comment} .= $_;
152 6         28 $_ = <$fh>;
153             }
154              
155             # Body
156 3         19 local $/ = undef;
157 3         8402 $sccs{body} = [ split m/\n/, $_ . <$fh> ];
158 3         600 close $fh;
159              
160 3         49 return bless \%sccs, $class;
161             } # new
162              
163             sub file {
164 10     10 1 19 my $self = shift;
165 10         62 return $self->{file};
166             } # file
167              
168             sub checksum {
169 2     2 1 5 my $self = shift;
170 2         11 return $self->{checksum};
171             } # checksum
172              
173             sub users {
174 1     1 1 3 my $self = shift;
175 1         2 return @{$self->{users}};
  1         6  
176             } # users
177              
178             sub flags {
179 2     2 1 6 my $self = shift;
180 2         8 return { %{$self->{flags}} };
  2         15  
181             } # flags
182              
183             sub comment {
184 1     1 1 21 my $self = shift;
185 1         8 return $self->{comment};
186             } # comment
187              
188             sub current {
189 2     2 1 5 my $self = shift;
190 2 50       7 $self->{current} or return;
191 2 100       8 wantarray ? @{$self->{current}} : $self->{current}[0];
  1         10  
192             } # current
193              
194             sub delta {
195 13     13 1 22 my ($self, $rev) = @_;
196 13 50       31 $self->{current} or return;
197 13 100       51 if (!defined $rev) {
    100          
    100          
198 1         3 $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         7 return;
208             }
209 12         17 return { %{ $self->{delta}{$rev} } };
  12         234  
210             } # delta
211              
212             sub version {
213 9     9 1 247 my ($self, $rev) = @_;
214 9 100       51 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       28 $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       11 return $self->{delta}{$rev}{version};
223              
224 1         5 return;
225             } # version
226              
227             sub revision {
228 6     6 1 16 my ($self, $vsn) = @_;
229 6 50       18 $self->{current} or return;
230              
231             # $self->revision () returns most recent revision
232 6 100       26 $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       14 return $self->{vsn}{$vsn};
237              
238 1         4 return;
239             } # revision
240              
241             sub revision_map {
242 1     1 1 2 my $self = shift;
243 1 50       4 $self->{current} or return;
244              
245 70         133 return [ map { [ $_ => $self->{delta}{$_}{version} ] }
246 343         387 sort { $a <=> $b }
247 1         3 keys %{$self->{delta}} ];
  1         17  
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 8 my ($self, $type) = @_;
271              
272 4 100       20 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         5 $self->{tran} = uc $type;
278             }
279             else {
280 1         3 $self->{tran} = undef;
281             }
282             } # set_translate
283              
284             sub _tran {
285 38685     38685   55803 my ($self, $line) = @_;
286 38685 100       93988 my $tt = $self->{tran} or return $line;
287 11 50       21 my $tr = $tran{$tt} or return $line;
288 11         15 my $re = $tr->{re};
289 11         193 $line =~ s{($re)}{$tr->{$1}}g;
290 11         78 return $line;
291             } # _tran
292              
293             sub translate {
294 32     32 1 76 my ($self, $rev, $line) = @_;
295              
296 32 100       108 my $type = $self->{tran} or return $line;
297 9 50       23 exists $self->{delta}{$rev} or return $line;
298              
299 9         18 (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         18  
303 9         35 my $I = $delta{version};
304 9         13 my $Z = "@(#)";
305 9 50       20 my $M = exists $self->{flags}{"m"} ? $self->{flags}{"m"} : $def_M;
306 9 50       20 my $Q = exists $self->{flags}{"q"} ? $self->{flags}{"q"} : "";
307 9 50       16 my $Y = exists $self->{flags}{"t"} ? $self->{flags}{"t"} : "";
308 9         16 $tran{SCCS}{"%U%"} = $delta{"time"};
309 9         15 $tran{SCCS}{"%E%"} = $delta{"date"};
310 9         11 $tran{SCCS}{"%R%"} = $delta{"release"};
311 9         11 $tran{SCCS}{"%L%"} = $delta{"level"};
312 9         16 $tran{SCCS}{"%B%"} = $delta{"branch"};
313 9         22 $tran{SCCS}{"%S%"} = $delta{"sequence"};
314 9         15 $tran{SCCS}{"%I%"} = $I;
315 9         14 $tran{SCCS}{"%Z%"} = $Z;
316 9         13 $tran{SCCS}{"%M%"} = $M;
317 9         20 $tran{SCCS}{"%W%"} = "$Z$M\t$I";
318 9         19 $tran{SCCS}{"%A%"} = "$Z$Y $M $I$Z";
319 9         12 $tran{SCCS}{"%Q%"} = $Q;
320 9         14 $tran{SCCS}{"%Y%"} = $Y;
321              
322 9 100       21 unless (exists $tran{$type}{re}) {
323 2         4 my $kw = join "|", reverse sort keys %{$tran{$type}};
  2         15  
324 2 50       92 $tran{$type}{re} = $kw ? qr{$kw} : undef;
325             }
326              
327 9         20 return $self->_tran ($line);
328             } # translate
329              
330             sub body {
331 16     16 1 47 my $self = shift;
332              
333 16 50 33     104 $self->{body} && $self->{current} or return;
334 16   66     60 my $r = shift || $self->{current}[0];
335              
336 16 100       54 exists $self->{vsn}{$r} and $r = $self->{vsn}{$r};
337              
338 16         48 my @lvl = ([ 1, "I", 0 ]);
339 16         29 my @body;
340              
341             # my $v = sub {
342             # join " ", map { sprintf "%s:%02d", $_->[1], $_->[2] } @lvl[1..$#lvl];
343             # }; # v
344              
345             my %rseq;
346 16         27 my $rr = $r;
347 16         36 while ($rr) {
348 301         418 $rseq{$rr} = 1;
349 301         643 $rr = $self->{delta}{$rr}{prev_rev};
350             }
351              
352 16         58 $self->translate ($r, ""); # Initialize translate hash
353              
354 16         24 my $want = 1;
355 16         22 for (@{$self->{body}}) {
  16         40  
356 110245 100       220828 if (m/^\cAE\s+(\d+)$/) {
357 22970         38277 my $e = $1;
358             # print STDERR $v->(), " END $e (@{$lvl[-1]})\n";
359             # SCCS has a seriously ill design so that chunks can overlap
360             # Below example is from actual code
361             # D 9
362             # E 9
363             # I 9
364             # D 10
365             # E 10
366             # I 10
367             # D 53
368             # E 53
369             # I 53
370             # E 53
371             # I 23
372             # D 31
373             # E 31
374             # I 31
375             # D 45
376             # E 45
377             # I 45
378             # E 45
379             # D 53 ---+
380             # E 31 |
381             # E 23 |
382             # E 10 |
383             # E 9 |
384             # D 7 |
385             # E 7 |
386             # I 7 |
387             # E 53 <--+
388             # I 53
389             # E 53
390             # D 53
391             # E 53
392             # I 53
393             # E 53
394             # E 7
395 22970         38287 foreach my $x (reverse 0 .. $#lvl) {
396 23369 100       48087 $lvl[$x][2] == $e or next;
397 22970         30774 splice @lvl, $x, 1;
398 22970         33377 last;
399             }
400 22970 100       33459 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  59588         107941  
401 22970         34801 next;
402             }
403 87275 100       145003 if (m/^\cAI\s+(\d+)$/) {
404 12442 100       34580 push @lvl, [ $rseq{$1} ? 1 : 0, "I", $1 ];
405 12442 100       19369 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  44492         76755  
406 12442         18108 next;
407             }
408 74833 100       128910 if (m/^\cAD\s+(\d+)$/) {
409 10528 100       30874 push @lvl, [ $rseq{$1} ? 0 : 1, "D", $1 ];
410 10528 100       16280 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  38066         64769  
411 10528         16083 next;
412             }
413 64305 50       97173 if (m/^\cA(.*)/) {
414 0         0 carp "Unsupported SCCS control: ^A$1, line skipped";
415 0         0 next;
416             }
417 64305 100       111986 $want and push @body, $self->_tran ($_);
418             # printf STDERR "%2d.%04d/%s: %-29.29s |%s\n", $r, scalar @body, $want, $v->(), $_;
419             }
420              
421 16 50 66     126 if ($self->{flags}{e} && @body && $body[0] =~ m/^[\x20-\x60]{1,61}$/) {
      66        
422 1         17 my $body = unpack "u" => join "\n" => @body;
423 1 50       17 $body and @body = split m/\n/ => $body;
424             }
425              
426 16 100       5906 return wantarray ? @body : join "\n", @body, "";
427             } # body
428              
429             1;
430              
431             __END__