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   316097 use strict;
  3         35  
  3         86  
8 3     3   15 use warnings;
  3         5  
  3         81  
9              
10 3     3   14 use POSIX qw(mktime);
  3         5  
  3         27  
11 3     3   5084 use Carp;
  3         11  
  3         158  
12              
13 3     3   15 use vars qw( $VERSION );
  3         5  
  3         7530  
14             $VERSION = "0.26";
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     66 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       428 my $fn = shift or croak ("SCCS needs a valid file name");
27 9 100       258 -e $fn or croak ("$fn does not exist");
28 8 100       293 -f $fn or croak ("$fn is not a file");
29 6 100       167 -s $fn or croak ("$fn is empty");
30 5         43 (my $filename = $fn) =~ s{\b(?:SCCS|sccs)/s\.(?=[^/]+$)}{};
31              
32 5 50       186 open my $fh, "<", $fn or croak ("Cannot open '$fn': $!");
33              
34             # Checksum
35             # ^Ah checksum
36 5 100       340 <$fh> =~ m/^\cAh(\d+)$/ or croak ("SCCS file $fn is supposed to start with a checksum");
37              
38 3         41 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         21 while (m{^\cAs (\d+)/(\d+)/(\d+)$}) {
62              
63 73         117 my @delta;
64              
65 73         164 my ($l_ins, $l_del, $l_unc) = map { $_ + 0 } $1, $2, $3;
  219         550  
66              
67 73         119 { local $/ = "\cAe\n";
  73         229  
68 73         346 @delta = split m/\n/, scalar <$fh>;
69             }
70              
71 73         728 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       253 $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         114 my @mr = grep { s/^\cAm\s*// } @delta; # MR number(s)
  217         538  
95 73         115 my @cmnt = grep { s/^\cAc\s*// } @delta; # Comment
  217         527  
96              
97 73   100     190 $sccs{current} ||= [ $rev, $vsn, $v_r, $v_l, $v_b, $v_s ];
98 73         2776 $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       489 exists $sccs{vsn}{$vsn} or $sccs{vsn}{$vsn} = $rev;
123 73         781 $_ = <$fh>;
124             }
125              
126             # Users
127             # ^Au
128             # user1
129             # user2
130             # ...
131             # ^AU
132 3 100       16 if (m{^\cAu}) {
133 2         4 { local $/ = "\cAU\n";
  2         8  
134 2         20 $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         20 while (m/^\cAf \s+ (\S) \s* (.+)?$/x) {
144 5         20 $sccs{flags}{$1} = $2;
145 5         32 $_ = <$fh>;
146             }
147              
148             # Comment
149             # ^At comment
150 3         16 while (s/^\cA[tT]\s*//) {
151 6 100       31 m/\S/ and $sccs{comment} .= $_;
152 6         28 $_ = <$fh>;
153             }
154              
155             # Body
156 3         12 local $/ = undef;
157 3         8245 $sccs{body} = [ split m/\n/, $_ . <$fh> ];
158 3         613 close $fh;
159              
160 3         58 return bless \%sccs, $class;
161             } # new
162              
163             sub file {
164 10     10 1 15 my $self = shift;
165 10         70 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         8  
176             } # users
177              
178             sub flags {
179 2     2 1 5 my $self = shift;
180 2         5 return { %{$self->{flags}} };
  2         17  
181             } # flags
182              
183             sub comment {
184 1     1 1 12 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       8 $self->{current} or return;
191 2 100       10 wantarray ? @{$self->{current}} : $self->{current}[0];
  1         11  
192             } # current
193              
194             sub delta {
195 13     13 1 25 my ($self, $rev) = @_;
196 13 50       36 $self->{current} or return;
197 13 100       49 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         6 return;
208             }
209 12         21 return { %{ $self->{delta}{$rev} } };
  12         192  
210             } # delta
211              
212             sub version {
213 9     9 1 251 my ($self, $rev) = @_;
214 9 100       51 ref $self eq __PACKAGE__ or return $VERSION;
215 6 50       15 $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       12 return $self->{delta}{$rev}{version};
223              
224 1         5 return;
225             } # version
226              
227             sub revision {
228 6     6 1 15 my ($self, $vsn) = @_;
229 6 50       19 $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       17 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         205 return [ map { [ $_ => $self->{delta}{$_}{version} ] }
246 338         377 sort { $a <=> $b }
247 1         3 keys %{$self->{delta}} ];
  1         35  
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         3 $self->{tran} = "CUSTOM";
274 1         2 $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   57387 my ($self, $line) = @_;
286 38685 100       90730 my $tt = $self->{tran} or return $line;
287 11 50       24 my $tr = $tran{$tt} or return $line;
288 11         17 my $re = $tr->{re};
289 11         160 $line =~ s{($re)}{$tr->{$1}}g;
290 11         77 return $line;
291             } # _tran
292              
293             sub translate {
294 32     32 1 77 my ($self, $rev, $line) = @_;
295              
296 32 100       114 my $type = $self->{tran} or return $line;
297 9 50       23 exists $self->{delta}{$rev} or return $line;
298              
299 9         22 (my $def_M = $self->file ()) =~ s{.*/}{};
300              
301             # TODO (or don't): %D%, %H%, %T%, %G%, %F%, %P%, %C%
302 9         17 my %delta = %{$self->delta ($rev)};
  9         20  
303 9         41 my $I = $delta{version};
304 9         14 my $Z = "@(#)";
305 9 50       19 my $M = exists $self->{flags}{"m"} ? $self->{flags}{"m"} : $def_M;
306 9 50       19 my $Q = exists $self->{flags}{"q"} ? $self->{flags}{"q"} : "";
307 9 50       17 my $Y = exists $self->{flags}{"t"} ? $self->{flags}{"t"} : "";
308 9         17 $tran{SCCS}{"%U%"} = $delta{"time"};
309 9         18 $tran{SCCS}{"%E%"} = $delta{"date"};
310 9         11 $tran{SCCS}{"%R%"} = $delta{"release"};
311 9         15 $tran{SCCS}{"%L%"} = $delta{"level"};
312 9         12 $tran{SCCS}{"%B%"} = $delta{"branch"};
313 9         13 $tran{SCCS}{"%S%"} = $delta{"sequence"};
314 9         13 $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         20 $tran{SCCS}{"%A%"} = "$Z$Y $M $I$Z";
319 9         12 $tran{SCCS}{"%Q%"} = $Q;
320 9         12 $tran{SCCS}{"%Y%"} = $Y;
321              
322 9 100       23 unless (exists $tran{$type}{re}) {
323 2         3 my $kw = join "|", reverse sort keys %{$tran{$type}};
  2         15  
324 2 50       99 $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 40 my $self = shift;
332              
333 16 50 33     106 $self->{body} && $self->{current} or return;
334 16   66     63 my $r = shift || $self->{current}[0];
335              
336 16 100       56 exists $self->{vsn}{$r} and $r = $self->{vsn}{$r};
337              
338 16         47 my @lvl = ([ 1, "I", 0 ]);
339 16         24 my @body;
340              
341             # my $v = sub {
342             # join " ", map { sprintf "%s:%02d", $_->[1], $_->[2] } @lvl[1..$#lvl];
343             # }; # v
344              
345 16         57 $self->translate ($r, ""); # Initialize translate hash
346              
347 16         23 my $want = 1;
348 16         25 for (@{$self->{body}}) {
  16         40  
349 110245 100       211919 if (m/^\cAE\s+(\d+)$/) {
350 22970         36056 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         36219 foreach my $x (reverse 0 .. $#lvl) {
389 23369 100       46463 $lvl[$x][2] == $e or next;
390 22970         30583 splice @lvl, $x, 1;
391 22970         34168 last;
392             }
393 22970 100       31984 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  59588         106221  
394 22970         33525 next;
395             }
396 87275 100       146396 if (m/^\cAI\s+(\d+)$/) {
397 12442 100       36088 push @lvl, [ $r >= $1 ? 1 : 0, "I", $1 ];
398 12442 100       18715 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  44492         75801  
399 12442         17968 next;
400             }
401 74833 100       121504 if (m/^\cAD\s+(\d+)$/) {
402 10528 100       31285 push @lvl, [ $r >= $1 ? 0 : 1, "D", $1 ];
403 10528 100       15985 $want = (grep { $_->[0] == 0 } @lvl) ? 0 : 1;
  38066         64944  
404 10528         15217 next;
405             }
406 64305 50       99462 if (m/^\cA(.*)/) {
407 0         0 carp "Unsupported SCCS control: ^A$1, line skipped";
408 0         0 next;
409             }
410 64305 100       112956 $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     72 if ($self->{flags}{e} && @body && $body[0] =~ m/^[\x20-\x60]{1,61}$/) {
      66        
415 1         10 my $body = unpack "u" => join "\n" => @body;
416 1 50       9 $body and @body = split m/\n/ => $body;
417             }
418              
419 16 100       6081 return wantarray ? @body : join "\n", @body, "";
420             } # body
421              
422             1;
423              
424             __END__