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-2026 H.Merijn Brand. All rights reserved.
4              
5             package VCS::SCCS;
6              
7 4     4   732499 use strict;
  4         9  
  4         178  
8 4     4   19 use warnings;
  4         9  
  4         323  
9              
10 4     4   2329 use POSIX qw(mktime);
  4         38512  
  4         27  
11 4     4   8463 use Carp;
  4         12  
  4         282  
12              
13 4     4   25 use vars qw( $VERSION );
  4         6  
  4         14479  
14             $VERSION = "0.29";
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 232437 my $proto = shift;
22 12 50 33     75 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       472 my $fn = shift or croak ("SCCS needs a valid file name");
27 9 100       401 -e $fn or croak ("$fn does not exist");
28 8 100       342 -f $fn or croak ("$fn is not a file");
29 6 100       197 -s $fn or croak ("$fn is empty");
30 5         67 (my $filename = $fn) =~ s{\b(?:SCCS|sccs)/s\.(?=[^/]+$)}{};
31              
32 5 50       256 open my $fh, "<", $fn or croak ("Cannot open '$fn': $!");
33              
34             # Checksum
35             # ^Ah checksum
36 5 100       650 <$fh> =~ m/^\cAh(\d+)$/ or croak ("SCCS file $fn is supposed to start with a checksum");
37              
38 3         51 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         11 $_ = <$fh>;
61 3         22 while (m{^\cAs (\d+)/(\d+)/(\d+)$}) {
62              
63 73         119 my @delta;
64              
65 73         138 my ($l_ins, $l_del, $l_unc) = map { $_ + 0 } $1, $2, $3;
  219         518  
66              
67 73         123 { local $/ = "\cAe\n";
  73         259  
68 73         328 @delta = split m/\n/, scalar <$fh>;
69             }
70              
71 73         759 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       255 $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         113 my @mr = grep { s/^\cAm\s*// } @delta; # MR number(s)
  217         542  
95 73         156 my @cmnt = grep { s/^\cAc\s*// } @delta; # Comment
  217         505  
96              
97 73   100     216 $sccs{current} ||= [ $rev, $vsn, $v_r, $v_l, $v_b, $v_s ];
98 73         2175 $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       337 exists $sccs{vsn}{$vsn} or $sccs{vsn}{$vsn} = $rev;
123 73         786 $_ = <$fh>;
124             }
125              
126             # Users
127             # ^Au
128             # user1
129             # user2
130             # ...
131             # ^AU
132 3 100       19 if (m{^\cAu}) {
133 2         4 { local $/ = "\cAU\n";
  2         35  
134 2         22 $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         48 while (m/^\cAf \s+ (\S) \s* (.+)?$/x) {
144 5         20 $sccs{flags}{$1} = $2;
145 5         22 $_ = <$fh>;
146             }
147              
148             # Comment
149             # ^At comment
150 3         19 while (s/^\cA[tT]\s*//) {
151 6 100       21 m/\S/ and $sccs{comment} .= $_;
152 6         24 $_ = <$fh>;
153             }
154              
155             # Body
156 3         21 local $/ = undef;
157 3         10944 $sccs{body} = [ split m/\n/, $_ . <$fh> ];
158 3         872 close $fh;
159              
160 3         71 return bless \%sccs, $class;
161             } # new
162              
163             sub file {
164 10     10 1 19 my $self = shift;
165 10         82 return $self->{file};
166             } # file
167              
168             sub checksum {
169 2     2 1 5 my $self = shift;
170 2         13 return $self->{checksum};
171             } # checksum
172              
173             sub users {
174 1     1 1 2 my $self = shift;
175 1         2 return @{$self->{users}};
  1         8  
176             } # users
177              
178             sub flags {
179 2     2 1 4 my $self = shift;
180 2         4 return { %{$self->{flags}} };
  2         20  
181             } # flags
182              
183             sub comment {
184 1     1 1 3 my $self = shift;
185 1         5 return $self->{comment};
186             } # comment
187              
188             sub current {
189 2     2 1 6 my $self = shift;
190 2 50       8 $self->{current} or return;
191 2 100       10 wantarray ? @{$self->{current}} : $self->{current}[0];
  1         12  
192             } # current
193              
194             sub delta {
195 13     13 1 34 my ($self, $rev) = @_;
196 13 50       61 $self->{current} or return;
197 13 100       59 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         4 $rev = $self->{vsn}{$rev};
205             }
206             else {
207 1         9 return;
208             }
209 12         20 return { %{ $self->{delta}{$rev} } };
  12         276  
210             } # delta
211              
212             sub version {
213 9     9 1 450294 my ($self, $rev) = @_;
214 9 100       64 ref $self eq __PACKAGE__ or return $VERSION;
215 6 50       20 $self->{current} or return;
216              
217             # $self->version () returns most recent version
218 6 100       31 $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       12 return $self->{delta}{$rev}{version};
223              
224 1         5 return;
225             } # version
226              
227             sub revision {
228 6     6 1 17 my ($self, $vsn) = @_;
229 6 50       23 $self->{current} or return;
230              
231             # $self->revision () returns most recent revision
232 6 100       35 $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       15 return $self->{vsn}{$vsn};
237              
238 1         5 return;
239             } # revision
240              
241             sub revision_map {
242 1     1 1 2 my $self = shift;
243 1 50       6 $self->{current} or return;
244              
245 70         238 return [ map { [ $_ => $self->{delta}{$_}{version} ] }
246 346         450 sort { $a <=> $b }
247 1         22 keys %{$self->{delta}} ];
  1         59  
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 31 my ($self, $type) = @_;
271              
272 4 100       28 if (ref $type eq "HASH") {
    100          
273 1         3 $self->{tran} = "CUSTOM";
274 1         3 $tran{CUSTOM} = $type;
275             }
276             elsif (exists $tran{uc $type}) {
277 2         9 $self->{tran} = uc $type;
278             }
279             else {
280 1         4 $self->{tran} = undef;
281             }
282             } # set_translate
283              
284             sub _tran {
285 38685     38685   63392 my ($self, $line) = @_;
286 38685 100       118395 my $tt = $self->{tran} or return $line;
287 11 50       26 my $tr = $tran{$tt} or return $line;
288 11         19 my $re = $tr->{re};
289 11         235 $line =~ s{($re)}{$tr->{$1}}g;
290 11         1058 return $line;
291             } # _tran
292              
293             sub translate {
294 32     32 1 102 my ($self, $rev, $line) = @_;
295              
296 32 100       150 my $type = $self->{tran} or return $line;
297 9 50       34 exists $self->{delta}{$rev} or return $line;
298              
299 9         30 (my $def_M = $self->file ()) =~ s{.*/}{};
300              
301             # TODO (or don't): %D%, %H%, %T%, %G%, %F%, %P%, %C%
302 9         18 my %delta = %{$self->delta ($rev)};
  9         26  
303 9         49 my $I = $delta{version};
304 9         16 my $Z = "@(#)";
305 9 50       29 my $M = exists $self->{flags}{"m"} ? $self->{flags}{"m"} : $def_M;
306 9 50       29 my $Q = exists $self->{flags}{"q"} ? $self->{flags}{"q"} : "";
307 9 50       22 my $Y = exists $self->{flags}{"t"} ? $self->{flags}{"t"} : "";
308 9         19 $tran{SCCS}{"%U%"} = $delta{"time"};
309 9         18 $tran{SCCS}{"%E%"} = $delta{"date"};
310 9         35 $tran{SCCS}{"%R%"} = $delta{"release"};
311 9         17 $tran{SCCS}{"%L%"} = $delta{"level"};
312 9         18 $tran{SCCS}{"%B%"} = $delta{"branch"};
313 9         17 $tran{SCCS}{"%S%"} = $delta{"sequence"};
314 9         17 $tran{SCCS}{"%I%"} = $I;
315 9         15 $tran{SCCS}{"%Z%"} = $Z;
316 9         17 $tran{SCCS}{"%M%"} = $M;
317 9         21 $tran{SCCS}{"%W%"} = "$Z$M\t$I";
318 9         24 $tran{SCCS}{"%A%"} = "$Z$Y $M $I$Z";
319 9         15 $tran{SCCS}{"%Q%"} = $Q;
320 9         38 $tran{SCCS}{"%Y%"} = $Y;
321              
322 9 100       44 unless (exists $tran{$type}{re}) {
323 2         5 my $kw = join "|", reverse sort keys %{$tran{$type}};
  2         19  
324 2 50       151 $tran{$type}{re} = $kw ? qr{$kw} : undef;
325             }
326              
327 9         27 return $self->_tran ($line);
328             } # translate
329              
330             sub body {
331 16     16 1 64 my $self = shift;
332              
333 16 50 33     151 $self->{body} && $self->{current} or return;
334 16   66     78 my $r = shift || $self->{current}[0];
335              
336 16 100       68 exists $self->{vsn}{$r} and $r = $self->{vsn}{$r};
337              
338 16         53 my @lvl = ([ 1, "I", 0 ]);
339 16         34 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         29 my $rr = $r;
347 16         48 while ($rr) {
348 301         560 $rseq{$rr} = 1;
349 301         885 $rr = $self->{delta}{$rr}{prev_rev};
350             }
351              
352 16         66 $self->translate ($r, ""); # Initialize translate hash
353              
354 16         25 my $want = 1;
355 16         37 for (@{$self->{body}}) {
  16         48  
356 110245 100       224574 if (m/^\cAE\s+(\d+)$/) {
357 22970         37650 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         41415 foreach my $x (reverse 0 .. $#lvl) {
396 23369 100       51810 $lvl[$x][2] == $e or next;
397 22970         33956 splice @lvl, $x, 1;
398 22970         40025 last;
399             }
400 22970 100       35525 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  59588         105817  
401 22970         36766 next;
402             }
403 87275 100       160951 if (m/^\cAI\s+(\d+)$/) {
404 12442 100       38119 push @lvl, [ $rseq{$1} ? 1 : 0, "I", $1 ];
405 12442 100       18838 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  44492         71351  
406 12442         19782 next;
407             }
408 74833 100       136823 if (m/^\cAD\s+(\d+)$/) {
409 10528 100       36537 push @lvl, [ $rseq{$1} ? 0 : 1, "D", $1 ];
410 10528 100       15978 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  38066         62650  
411 10528         16821 next;
412             }
413 64305 50       112724 if (m/^\cA(.*)/) {
414 0         0 carp "Unsupported SCCS control: ^A$1, line skipped";
415 0         0 next;
416             }
417 64305 100       126108 $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     84 if ($self->{flags}{e} && @body && $body[0] =~ m/^[\x20-\x60]{1,61}$/) {
      66        
422 1         55 my $body = unpack "u" => join "\n" => @body;
423 1 50       12 $body and @body = split m/\n/ => $body;
424             }
425              
426 16 100       8633 return wantarray ? @body : join "\n", @body, "";
427             } # body
428              
429             1;
430              
431             __END__