File Coverage

blib/lib/VCS/SnapshotCM/Tools.pm
Criterion Covered Total %
statement 21 259 8.1
branch 0 130 0.0
condition 0 20 0.0
subroutine 7 31 22.5
pod 16 16 100.0
total 44 456 9.6


line stmt bran cond sub pod time code
1             ################################################################################
2             #
3             # $Project: /VCS-SnapshotCM $
4             # $Author: mhx $
5             # $Date: 2005/04/09 13:36:08 +0200 $
6             # $Revision: 9 $
7             # $Snapshot: /VCS-SnapshotCM/0.02 $
8             # $Source: /lib/VCS/SnapshotCM/Tools.pm $
9             #
10             ################################################################################
11             #
12             # Copyright (c) 2004 Marcus Holland-Moritz. All rights reserved.
13             # This program is free software; you can redistribute it and/or modify
14             # it under the same terms as Perl itself.
15             #
16             ################################################################################
17              
18             =head1 NAME
19              
20             VCS::SnapshotCM::Tools - Tools for SnapshotCM Version Control
21              
22             =head1 SYNOPSIS
23              
24             use VCS::SnapshotCM::Tools;
25              
26             $vcs = VCS::SnapshotCM::Tools->new;
27              
28             $vcs->configure(server => 'scmsrv.mydomain');
29              
30             if ($vcs->exists_snapshot(snapshot => '/my-project/Current')) {
31             # ...
32             }
33              
34             # ... and lots more. Use the Source, Luke!
35              
36             =head1 DESCRIPTION
37              
38             VCS::SnapshotCM::Tools is a collection of tools to query information
39             from the SnapshotCM version control system.
40              
41             SnapshotCM is available from L.
42              
43             This module is mainly used to implement the functionality required
44             by the tools L and L. It lacks documentation
45             as well as lots of possible features. The interface may change in
46             backwards-incompatible ways. Use at your own risk.
47              
48             =head1 METHODS
49              
50             =cut
51              
52             package VCS::SnapshotCM::Tools;
53 1     1   8050 use strict;
  1         3  
  1         35  
54 1     1   4 use Carp;
  1         2  
  1         75  
55 1     1   280577 use File::Temp qw( mktemp );
  1         34991  
  1         86  
56 1     1   1035 use IO::File;
  1         1223  
  1         177  
57 1     1   120281 use Time::Local;
  1         1965  
  1         69  
58 1     1   143301 use Data::Dumper;
  1         11922  
  1         125  
59 1     1   12 use vars qw( $VERSION );
  1         1  
  1         4709  
60              
61             $VERSION = do { my @r = '$Snapshot: /VCS-SnapshotCM/0.02 $' =~ /(\d+\.\d+(?:_\d+)?)/; @r ? $r[0] : '9.99' };
62              
63             =head2 C OPTION =E VALUE, ...
64              
65             Create a new VCS::SnapshotCM::Tools object. You may pass the
66             same options as to the L|/"configure"> method.
67              
68             =cut
69              
70             sub new
71             {
72 0     0 1   my $class = shift;
73 0           my $self = bless {
74             debug => 0,
75             server => undef,
76             project => undef,
77             snapshot => undef,
78             }, $class;
79 0           $self->configure(@_);
80 0           $self->_debug(1, "## perl version $] on $^O\n");
81 0           $self->_debug(2, Data::Dumper->Dump([$self], ['self']));
82 0           return $self;
83             }
84              
85             =head2 C OPTION =E VALUE, ...
86              
87             Configures certain properties of a VCS::SnapshotCM::Tools object.
88              
89             =over 2
90              
91             =item C =E 0 | 1
92              
93             Turn debug output on or off.
94              
95             =item C =E I
96              
97             Set a default server hostname.
98              
99             =item C =E I
100              
101             Set a default project name.
102              
103             =back
104              
105             =cut
106              
107             sub configure
108             {
109 0     0 1   my($self, %prop) = @_;
110 0           for my $p (keys %prop) {
111 0 0         if (exists $self->{$p}) {
112 0           $self->{$p} = $prop{$p};
113             }
114             else {
115 0           croak "Unknown property '$p'.";
116             }
117             }
118 0           return $self;
119             }
120              
121             =head2 C
122              
123             Get workspace mapping information for the current directory.
124              
125             =cut
126              
127             sub get_current_mapping
128             {
129 0     0 1   my $self = shift;
130 0           $self->_map_options([], @_);
131 0           my $out = $self->_run("wls -f -M");
132 0           for (@{$out->{stderr}}) {
  0            
133 0 0         /^=/ or last;
134 0 0         if (/^=+\s*Workspace:\s*(.*?)\s*=\s*/) {
135 0           return $self->get_mapping(name => $1);
136             }
137             }
138 0           return undef;
139             }
140              
141             =head2 C OPTION =E VALUE, ...
142              
143             Get workspace mapping information.
144              
145             =cut
146              
147             sub get_mapping
148             {
149 0     0 1   my $self = shift;
150 0           my $arg = $self->_map_options([qw(server dir snapshot name)], @_);
151 0           my $out = $self->_run("wmap list $arg");
152 0           my %tr = (
153             'Workspace Name' => 'name',
154             'Server' => 'server',
155             'Snapshot' => 'snapshot_path',
156             'Mapped Directory' => 'mapped_dir',
157             'Text Format' => 'text_format',
158             'Workspace Type' => 'type',
159             'Working Set' => 'working_set',
160             );
161              
162 0           my @rv;
163 0           for (@{$out->{stdout}}) {
  0            
164 0 0         /^\s*([^:]+):\s*(.*?)\s*$/ or next;
165 0 0         exists $tr{$1} or carp "Unknown wmap property '$1'.\n";
166 0 0 0       push @rv, {} if @rv == 0 or exists $rv[-1]{$tr{$1}};
167 0           $rv[-1]{$tr{$1}} = $2;
168             }
169              
170 0 0         @rv or return;
171              
172 0           for (@rv) {
173 0 0         if (exists $_->{snapshot_path}) {
174 0           ($_->{project}, $_->{snapshot}) = $self->split_snapshot_path($_->{snapshot_path});
175             }
176             }
177              
178 0 0         return wantarray ? @rv : $rv[0];
179             }
180              
181             =head2 C OPTION =E VALUE, ...
182              
183             Try to guess the hostname of the SnapshotCM server.
184              
185             =cut
186              
187             sub guess_server_hostname
188             {
189 0     0 1   my $self = shift;
190 0           my(undef, %opt) = $self->_map_options([qw(*snapshot)], @_);
191 0           my $out = $self->_run("wmap list");
192 0           my %server;
193 0           for (@{$out->{stdout}}) {
  0            
194 0 0         /Server:\s*(.*?)\s*$/ and $server{$1}++;
195             }
196 0           my @servers = keys %server;
197              
198 0 0         unless (@servers) {
199 0           my $out = $self->_run("sslist -P -t1");
200 0           my @servers = @{$out->{stdout}};
  0            
201 0           chomp @servers;
202             }
203              
204 0 0 0       if (@servers > 1 and exists $opt{snapshot}) {
205 0           for (@servers) {
206 0 0         $self->exists_snapshot(server => $_, snapshot => $opt{snapshot})
207             and return $_;
208             }
209             }
210 0 0         return wantarray ? @servers : @servers == 1 ? $servers[0] : undef;
    0          
211             }
212              
213             =head2 C OPTION =E VALUE, ...
214              
215             Poorly named method that guesses local hostname
216             and snapshot properties.
217              
218             =cut
219              
220             sub guess_local
221             {
222 0     0 1   my $self = shift;
223 0           my(undef, %opt) = $self->_map_options([qw(*server[d] *snapshot[m])], @_);
224 0           my %rv;
225              
226 0           my $map = $self->get_current_mapping;
227 0 0         $rv{mapping} = $map if defined $map;
228              
229 0 0         my @servers = exists $opt{server} ? $opt{server}
230             : $self->guess_server_hostname;
231              
232 0           for my $server (@servers) {
233 0           my @ss = ($opt{snapshot});
234 0 0         push @ss, "$map->{project}/$opt{snapshot}" if defined $map;
235              
236 0           for my $snapshot (@ss) {
237 0 0         next unless $snapshot =~ m! ^/ !x;
238 0 0         if ($self->exists_snapshot(server => $server, snapshot => $snapshot)) {
239 0           $rv{server} = $server;
240 0           $rv{path} = $snapshot;
241 0           @rv{qw(project snapshot)} = $self->split_snapshot_path($snapshot);
242 0           return \%rv;
243             }
244             }
245             }
246              
247 0           return undef;
248             }
249              
250             =head2 C OPTION =E VALUE, ...
251              
252             Check if a snapshot exists.
253              
254             =cut
255              
256             sub exists_snapshot
257             {
258 0     0 1   my $self = shift;
259 0           my($arg, %opt) = $self->_map_options([qw(server[md] *snapshot[m])], @_);
260 0           my $snapshot = $self->_expand_snapshot_path($opt{snapshot});
261 0           my $out = $self->_run("sslist $arg -d -H $snapshot");
262 0           for (@{$out->{stdout}}) {
  0            
263 0 0         /^\s*\Q$snapshot\E\s*$/ and return 1;
264             }
265 0           return 0;
266             }
267              
268             =head2 C OPTION =E VALUE, ...
269              
270             Get list of snapshots for a project.
271              
272             =cut
273              
274             sub get_snapshots
275             {
276 0     0 1   my $self = shift;
277 0           my($arg, %opt) = $self->_map_options([qw(server[md] *project[md])], @_);
278 0           my $out = $self->_run("sslist $arg -H -R $opt{project}");
279 0           chomp @{$out->{stdout}};
  0            
280 0           return @{$out->{stdout}};
  0            
281             }
282              
283             =head2 C OPTION =E VALUE, ...
284              
285             Get list of files for a snapshot.
286              
287             =cut
288              
289             sub get_files
290             {
291 0     0 1   my $self = shift;
292 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md])], @_);
293 0           my $out = $self->_run("wls -Rfpv $arg");
294 0           my %f;
295 0           for (@{$out->{stdout}}) {
  0            
296 0           chomp;
297 0 0         if (m! ^ (.*?) (/?) \[(\d+)\] $ !x) {
298 0 0         $f{$1} = { type => ($2 ? 'dir' : 'file'),
299             revision => $3 };
300             }
301             else {
302 0           warn "Cannot parse wls output: $_\n";
303             }
304             }
305 0           return \%f;
306             }
307              
308             =head2 C OPTION =E VALUE, ...
309              
310             Read a certain revision of a file from a snapshot.
311              
312             =cut
313              
314             sub read_file
315             {
316 0     0 1   my $self = shift;
317 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md] rev *file)], @_);
318 0           my $out = $self->_run("wco -p -q $arg $opt{file}");
319 0           return @{$out->{stdout}};
  0            
320             }
321              
322             =head2 C OPTION =E VALUE, ...
323              
324             Get an IO::File reference to a certain revision of a file from a snapshot.
325              
326             =cut
327              
328             sub open_file
329             {
330 0     0 1   my $self = shift;
331 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md] rev *file)], @_);
332 0           $self->_open("wco -p -q $arg $opt{file}");
333             }
334              
335             =head2 C OPTION =E VALUE, ...
336              
337             Read the diff between two revisions of a file.
338              
339             =cut
340              
341             sub read_diff
342             {
343 0     0 1   my $self = shift;
344 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md] rev1=-r{} rev2=-r{} *file)], @_);
345 0           my $out = $self->_run("wdiff $arg $opt{file}");
346 0           return @{$out->{stdout}};
  0            
347             }
348              
349             =head2 C OPTION =E VALUE, ...
350              
351             Get an IO::File reference to the diff between two revisions of a file.
352              
353             =cut
354              
355             sub open_diff
356             {
357 0     0 1   my $self = shift;
358 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md] rev1=-r{} rev2=-r{} *file)], @_);
359 0           $self->_open("wdiff $arg $opt{file}");
360             }
361              
362             =head2 C OPTION =E VALUE, ...
363              
364             Get history information for a file.
365              
366             =cut
367              
368             sub get_history
369             {
370 0     0 1   my $self = shift;
371 0           my($arg, %opt) = $self->_map_options([qw(server[md] snapshot[md] *rev1 *rev2 *file ancestors[b]=-A)], @_);
372 0           my $rev = '';
373 0 0         $rev .= $opt{rev1} if exists $opt{rev1};
374 0 0         $rev .= ":$opt{rev2}" if exists $opt{rev2};
375 0 0         $rev = "-r$rev" if $rev;
376 0 0         my $out = $self->_run("whist -d $rev $arg $opt{file}") or return undef;
377              
378 0           my($info, @rev) = split /\s* ^ -{20,} $ \s*/mx, join('', @{$out->{stdout}});
  0            
379 0 0         defined $info or return undef;
380              
381 0           my %info = $info =~ /^([^:]+):\s*(.*)$/mg;
382              
383             return {
384 0           snapshot => $info{Snapshot},
385             permissions => $info{Permissions},
386             current_rev => $info{'Current revision'},
387             revisions => _get_rev_info(@rev),
388             };
389             }
390              
391             =head2 C PATH
392              
393             Split a snapshot path into project and snapshot.
394              
395             =cut
396              
397             sub split_snapshot_path
398             {
399 0     0 1   my($self, $path) = @_;
400 0 0         exists $self->{_pcache} or $self->rebuild_project_cache;
401 0           for my $p (@{$self->{_pcache}}) {
  0            
402 0 0         if ($path =~ m! ^ \Q$p->[0]\E / (.+) $ !x) {
403 0           return ($p->[0], $1);
404             }
405             }
406 0 0         return ($1, $2) if $path =~ m! ^ (/.*) / ([^/]+) $ !x;
407 0           return ('', $path);
408             }
409              
410             =head2 C
411              
412             Explicitly rebuild the project cache. The project cache is
413             required for splitting snapshot paths correctly.
414              
415             =cut
416              
417             sub rebuild_project_cache
418             {
419 0     0 1   my($self) = @_;
420 0 0         my @servers = defined $self->{server} ? $self->{server}
421             : $self->guess_server_hostname;
422 0           my @projects;
423 0           for my $s (@servers) {
424 0           my $out = $self->_run("sslist -h$s -H");
425 0           my @p = @{$out->{stdout}};
  0            
426 0           chomp @p;
427 0           push @projects, map { [$_ => $s] } @p;
  0            
428             }
429 0           $self->{_pcache} = [sort { length $b->[0] <=> length $a->[0] } @projects];
  0            
430             }
431              
432             sub _map_options
433             {
434 0 0   0     Carp::cluck("Invalid arguments") if @_ % 2;
435              
436 0           my($self, $accept, %opts) = @_;
437              
438 0 0         $self->_debug(1, "## _map_options([".join(", ", map qq{'$_'}, @$accept)."]".
439             (@_>2 ? ", ".join(", ", map qq{'$_'}, @_[2..$#_]) : '').")\n");
440              
441 0           my $caller = (caller(1))[3];
442 0           my %map = (
443             server => '-h{}',
444             dir => '-D{}',
445             rev => '-r{}',
446             snapshot => '-S{}',
447             name => '-N{}',
448             );
449 0           my %default = (
450             server => $self->{server},
451             project => $self->{project},
452             snapshot => $self->{snapshot},
453             );
454             my %process = (
455 0     0     snapshot => sub { $self->_expand_snapshot_path(@_) },
456 0           );
457              
458 0           $self->_debug(2, Data::Dumper->Dump([$self, $accept, \%opts, \%default],
459             [qw(self accept *opts *default)]));
460              
461 0           my %pass;
462             my @arg;
463 0           my $more = 0;
464 0           s/^-// for keys %opts;
465 0           for (@$accept) {
466 0 0         if ($_ eq '*') { $more++; next }
  0            
  0            
467             # (m)andatory (d)efault (b)oolean
468 0 0         my($passthrough, $o) = /^(\*?)(\w+)(?:\[([mdb]+)\])?(?:=(.*))?$/
469             or die "Invalid option spec '$_'";
470 0 0         $map{$o} = $4 if defined $4;
471 0   0       my %mod = map {($_ => 1)} ($3 || '') =~ /./g;
  0            
472 0 0         unless (exists $opts{$o}) {
473 0 0 0       $opts{$o} = $default{$o} if $mod{d} and defined $default{$o};
474 0 0         unless (exists $opts{$o}) {
475 0 0         $mod{m} and croak "Missing option '$o' for '$caller'";
476 0           next;
477             }
478             }
479 0 0         if ($passthrough) {
480 0           $pass{$o} = delete $opts{$o};
481 0           next;
482             }
483 0 0         my $a = $map{$o} or die "Unsupported option '$o'";
484 0           my $in = delete $opts{$o};
485 0 0 0       if (!$mod{b} or $in) {
486 0 0         $in = $process{$o}->($in) if exists $process{$o};
487 0           $a =~ s/\{\}/$in/g;
488 0           push @arg, $a;
489             }
490             }
491 0 0 0       unless ($more || keys(%opts) == 0) {
492 0           my $invalid = join ", ", map { "'$_'" } keys %opts;
  0            
493 0 0         my $s = keys %opts == 1 ? '' : 's';
494 0           croak "Invalid option$s $invalid for '$caller'";
495             }
496 0           my $arg = join ' ', @arg;
497 0 0         return wantarray ? ($arg, %opts, %pass) : $arg;
498             }
499              
500             sub _expand_snapshot_path
501             {
502 0     0     my($self, $path) = @_;
503 0           my($project, $snapshot) = $self->split_snapshot_path($path);
504 0   0       $project ||= $self->{project};
505 0 0         defined $project or Carp::cluck("Project undefined");
506 0 0         return defined $project ? "$project/$snapshot" : $snapshot;
507             }
508              
509             sub _get_rev_info
510             {
511 0     0     my @revisions = @_;
512 0           my %rev;
513            
514 0           for (@revisions) {
515 0 0         m/
516             \A
517             ^ Revision: \s* (\d+) \s* .*? \s* (?: Derivation: \s* (.*?) \s* )? $ \s* # (revision) (derivation)
518             ^ Date: \s* ([^;]+) ; \s* Size: \s* (\d+) \s* bytes \s* $ \s* # (date) (size)
519             ^ Author: \s* (.*?) \s* $ \s* # (author)
520             ^ Snapshot: \s* (.*?) \s* $ \s* # (snapshot)
521             (?: ^ Used \s+ in: \s* (.*? (?: \s* ^\s{8,} .+?)* ) \s* $ )? \s* # (used)
522             (?: ^ Change: \s* (.*?) \s* $ )? \s* # (change)
523             ^ ([\s\S]+) \s* # (comment)
524             \Z
525             /mx or die "Couldn't match revision output";
526              
527 0           my %r = (
528             revision => $1,
529             date => $3,
530             size => $4,
531             author => $5,
532             snapshot => $6,
533             comment => $9,
534             );
535              
536 0 0         defined $2 and $r{derivation} = $2;
537 0 0         defined $7 and $r{used_in} = [ split /\s{8,}/, $7 ];
538 0 0         defined $8 and $r{change} = $8;
539              
540 0 0         my($Y,$M,$D,$h,$m,$s,$zh,$zm) =
541             $r{date} =~ m!(\d+)/(\d+)/(\d+) \s* (\d+):(\d+):(\d+) (?:\s+ [+-](\d{2})(\d{2}))?!x
542             or warn("Cannot parse date '$r{date}'");
543              
544 0           $r{time} = timegm($s, $m, $h, $D, $M-1, $Y) - (($zh * 60) + $zm) * 60;
545              
546 0           $r{comment} =~ s/[\r\n]+$//;
547              
548 0           $rev{$r{revision}} = \%r;
549             }
550              
551 0           return \%rev;
552             }
553              
554             sub _run
555             {
556 0     0     my($self, $cmd) = @_;
557 0           my %rv = (error => 0);
558              
559 0           $self->_debug(1, "## run: $cmd\n");
560              
561 0           my $out = mktemp("soutXXXX");
562 0           my $err = mktemp("serrXXXX");
563 0           my $error;
564              
565 0 0         if (system "$cmd 1>$out 2>$err") {
566 0           $rv{error} = $?;
567             }
568              
569 0 0         if (-f $out) {
570 0           $rv{stdout} = [_slurp($out)];
571 0 0         unlink $out or carp "Couldn't remove temporary file '$out'";
572 0 0         if ($self->{debug} >= 2) {
573 0           $self->_debug(2, "1> $_") for @{$rv{stdout}};
  0            
574             }
575             }
576              
577 0 0         if (-f $err) {
578 0           $rv{stderr} = [_slurp($err)];
579 0 0         unlink $err or carp "Couldn't remove temporary file '$err'";
580 0 0         if ($self->{debug} >= 2) {
581 0           $self->_debug(2, "2> $_") for @{$rv{stderr}};
  0            
582             }
583             }
584              
585 0           return \%rv;
586             }
587              
588             sub _open
589             {
590 0     0     my($self, $cmd) = @_;
591 0           $self->_debug(1, "## open: $cmd\n");
592 0           IO::File->new("$cmd 2>/dev/null |");
593             }
594              
595             sub _debug
596             {
597 0     0     my($self, $level, @args) = @_;
598 0 0         if ($self->{debug} >= $level) {
599 0           my $output = join '', @args;
600 0           $output =~ s/^/[$level] /mg;
601 0           print STDERR $output;
602             }
603             }
604              
605             sub _slurp
606             {
607 0     0     my $file = shift;
608 0 0         my $fh = new IO::File $file or return undef;
609 0 0         return wantarray ? <$fh> : do { local $/; <$fh> };
  0            
  0            
610             }
611              
612             1;
613              
614             =head1 COPYRIGHT
615              
616             Copyright (c) 2004 Marcus Holland-Moritz. All rights reserved.
617             This program is free software; you can redistribute it and/or modify
618             it under the same terms as Perl itself.
619              
620             SnapshotCM is copyright (c) 2000-2003 True Blue Software Company.
621              
622             =head1 SEE ALSO
623              
624             See L, L.
625              
626             =cut
627