File Coverage

blib/lib/LCFG/Build/VCS/CVS.pm
Criterion Covered Total %
statement 30 190 15.7
branch 0 90 0.0
condition 0 3 0.0
subroutine 10 22 45.4
pod 7 9 77.7
total 47 314 14.9


line stmt bran cond sub pod time code
1             package LCFG::Build::VCS::CVS; # -*-perl-*-
2 1     1   686 use strict;
  1         1  
  1         24  
3 1     1   3 use warnings;
  1         1  
  1         37  
4              
5             # $Id: CVS.pm.in 31796 2017-01-19 13:31:51Z squinney@INF.ED.AC.UK $
6             # $Source: /var/cvs/dice/LCFG-Build-VCS/lib/LCFG/Build/VCS/CVS.pm.in,v $
7             # $Revision: 31796 $
8             # $HeadURL: https://svn.lcfg.org/svn/source/tags/LCFG-Build-VCS/LCFG_Build_VCS_0_2_3/lib/LCFG/Build/VCS/CVS.pm.in $
9             # $Date: 2017-01-19 13:31:51 +0000 (Thu, 19 Jan 2017) $
10              
11             our $VERSION = '0.2.3';
12              
13 1     1   14 use Cwd ();
  1         1  
  1         11  
14 1     1   3 use File::Find ();
  1         1  
  1         11  
15 1     1   2 use File::Path ();
  1         1  
  1         32  
16 1     1   3 use File::Spec ();
  1         1  
  1         15  
17 1     1   498 use IO::File qw(O_WRONLY O_CREAT O_NONBLOCK O_NOCTTY);
  1         6584  
  1         58  
18 1     1   479 use Try::Tiny;
  1         1709  
  1         44  
19              
20 1     1   535 use Moose;
  1         299353  
  1         8  
21             with 'LCFG::Build::VCS';
22              
23             has '+binpath' => ( default => 'cvs' );
24              
25             has 'root' => (
26             is => 'rw',
27             isa => 'Str',
28             lazy => 1,
29             default => sub { _get_root(@_) },
30             );
31              
32             has '+id' => ( default => 'CVS' );
33              
34             # This should give a speed-up in loading
35              
36 1     1   5062 no Moose;
  1         2  
  1         4  
37             __PACKAGE__->meta->make_immutable;
38              
39             sub auto_detect {
40 0     0 1   my ( $class, $dir ) = @_;
41              
42 0           my $rootfile = File::Spec->catfile( $dir, 'CVS', 'Root' );
43 0 0         my $is_cvs = -f $rootfile ? 1 : 0;
44              
45 0           return $is_cvs;
46             }
47              
48             sub build_cmd {
49 0     0 0   my ( $self, @args ) = @_;
50              
51 0           my @cmd = ( $self->binpath, '-d', $self->root );
52 0 0         if ( $self->quiet ) {
53 0           push @cmd, '-Q';
54             }
55 0           push @cmd, @args;
56              
57 0           return @cmd;
58             }
59              
60             sub _get_root {
61 0     0     my ($self) = @_;
62              
63 0           my $root;
64              
65 0           my $rootfile = File::Spec->catfile( $self->workdir, 'CVS', 'Root' );
66 0 0         if ( -f $rootfile ) {
67 0           eval {
68 0           my $fh = IO::File->new( $rootfile, 'r' );
69 0           chomp( $root = $fh->getline );
70 0           $fh->close;
71             };
72             }
73              
74 0 0         if ( !$root ) {
75 0           $root = $ENV{CVSROOT};
76             }
77              
78 0           return $root;
79             }
80              
81             sub genchangelog {
82 0     0 1   my ($self) = @_;
83              
84 0           my $dir = $self->workdir;
85 0           my $logfile = $self->logfile;
86              
87 0           my $orig_dir = Cwd::abs_path();
88 0 0         chdir $dir or die "gen: Could not access directory, $dir: $!\n";
89              
90 0 0         if ( !-e $logfile ) {
91              
92             # This bit borrowed from File::Touch
93 0 0         sysopen my $fh, $logfile, O_WRONLY | O_CREAT | O_NONBLOCK | O_NOCTTY
94             or die "Cannot create $logfile : $!\n";
95 0 0         $fh->close or die "Cannot close $logfile : $!\n";
96              
97             # Assume it is not already part of the repository
98 0           $self->run_cmd( 'add', $logfile );
99             }
100              
101 0           my $cmd = 'cvs2cl --hide-filenames --accum --file ' . $logfile;
102              
103 0 0         if ( $self->quiet ) {
104 0           $cmd .= ' --global-opts \'-Q\'';
105             }
106              
107             # This requires a full shell to actually work, I think the cvs
108             # command is the root cause.
109              
110 0 0         if ( $self->dryrun ) {
111 0           print "Dry-run: $cmd\n";
112             }
113             else {
114 0           system $cmd;
115 0 0         if ( $? != 0 ) {
116 0           die "Could not run cvs2cl: $!\n";
117             }
118             }
119              
120 0 0         chdir $orig_dir
121             or die "Could not return to original directory, $orig_dir: $!\n";
122              
123 0           return;
124             }
125              
126             sub checkcommitted {
127 0     0 1   my ($self) = @_;
128              
129 0           my $orig_dir = Cwd::abs_path();
130              
131 0           my $dir = $self->workdir;
132 0 0         chdir $dir or die "check: Could not access directory, $dir: $!\n";
133              
134 0           my @status = $self->run_cmd('status');
135              
136 0           my @notcommitted;
137 0           for my $line (@status) {
138 0 0         if ( $line =~ m/^File: (.+?)\s+Status: (.+)$/ ) {
139 0           my ( $file, $status ) = ( $1, $2 );
140 0 0         if ( $status !~ m/Up-to-date/ ) {
141 0           push @notcommitted, $file;
142             }
143             }
144             }
145              
146 0 0         chdir $orig_dir
147             or die "Could not return to original directory, $orig_dir: $!\n";
148              
149 0           my $allcommitted;
150 0 0         if ( scalar @notcommitted > 0 ) {
151 0           $allcommitted = 0;
152             }
153             else {
154 0           $allcommitted = 1;
155             }
156              
157 0 0         if (wantarray) {
158 0           return ( $allcommitted, @notcommitted );
159             }
160             else {
161 0           return $allcommitted;
162             }
163             }
164              
165             sub tagversion {
166 0     0 1   my ( $self, $version ) = @_;
167              
168 0           $self->update_changelog($version);
169              
170 0           my $tag = $self->gen_tag($version);
171              
172 0           my $orig_dir = Cwd::abs_path();
173              
174 0           my $dir = $self->workdir;
175 0 0         chdir $dir or die "tag: Could not access directory, $dir: $!\n";
176              
177 0           eval { $self->run_cmd( 'commit', '-m', "Release: $version" ) };
  0            
178 0 0         if ($@) {
179 0           die "Could not mark release for $dir at $version\n";
180             }
181              
182 0           eval { $self->run_cmd( 'tag', '-F', '-c', $tag ) };
  0            
183 0 0         if ($@) {
184 0           die "Could not tag $dir with $tag\n";
185             }
186              
187 0           eval { $self->run_cmd( 'tag', '-F', '-c', 'latest' ) };
  0            
188 0 0         if ($@) {
189 0           die "Could not tag $dir as latest\n";
190             }
191              
192 0 0         chdir $orig_dir
193             or die "Could not return to original directory, $orig_dir: $!\n";
194              
195 0           return;
196             }
197              
198             sub _process_cvs_entries {
199 0     0     my ( $workdir, $entries ) = @_;
200              
201 0           my $path = $File::Find::name;
202              
203 0 0         if ( -f $path ) {
204 0           my ( $vol, $dirname, $basename ) = File::Spec->splitpath($path);
205 0           my @dirs = File::Spec->splitdir($dirname);
206              
207             # If the last element of the directory list is empty throw it away
208 0 0         if ( length $dirs[-1] == 0 ) {
209 0           pop @dirs;
210             }
211              
212 0 0 0       if ( $dirs[-1] eq 'CVS' && $basename eq 'Entries' ) {
213 0           pop @dirs; # remove the 'CVS' directory
214              
215             # This is a (hopefully) system-independent way of removing
216             # the working directory from the front of the current
217             # directory name to produce relative filenames.
218              
219 0           for ( my $i=0; $i<scalar(@{$workdir}); $i++) {
  0            
220 0 0         if ( $dirs[0] eq $workdir->[$i] ) {
221 0           shift @dirs;
222             }
223             else {
224 0           last;
225             }
226             }
227              
228 0           my $dir = File::Spec->catdir(@dirs);
229              
230 0 0         my $fh = IO::File->new( $path, 'r' )
231             or die "Could not open $path for reading: $!\n";
232 0           while ( defined( my $entry = $fh->getline ) ) {
233              
234 0 0         if ( $entry =~ m{^/ # Line starts with a forward slash
235             ([^/]+) # Stuff which is not a forward slash
236             / # Another forward slash
237             \d # Any digit (avoids deleted files)
238             }x ) {
239 0           push @{$entries}, [ $dir, $1 ];
  0            
240             }
241             }
242 0           $fh->close;
243             }
244             }
245              
246 0           return;
247             }
248              
249             sub export_devel {
250 0     0 1   my ( $self, $version, $builddir ) = @_;
251              
252 0           my $workdir = $self->workdir;
253 0           my $target = join q{-}, $self->module, $version;
254              
255 0           my $exportdir = File::Spec->catdir( $builddir, $target );
256              
257 0 0         if ( !$self->dryrun ) {
258 0           File::Path::rmtree($exportdir);
259 0           eval { File::Path::mkpath($exportdir) };
  0            
260 0 0         if ($@) {
261 0           die "Could not create $exportdir: $@\n";
262             }
263             }
264              
265 0           my @workdir = File::Spec->splitdir($workdir);
266             # If the last element of the directory list is empty throw it away
267 0 0         if ( length $workdir[-1] == 0 ) {
268 0           pop @workdir;
269             }
270              
271 0           my @entries;
272             File::Find::find(
273 0     0     { wanted => sub { _process_cvs_entries( \@workdir, \@entries ) },
274 0           no_chdir => 1,
275             },
276             $workdir
277             );
278              
279 0           for my $entry (@entries) {
280 0           $self->mirror_file( $workdir, $exportdir, @{$entry} );
  0            
281             }
282              
283 0           return $exportdir;
284             }
285              
286             sub export {
287 0     0 1   my ( $self, $version, $builddir ) = @_;
288              
289 0           my $tag = $self->gen_tag($version);
290              
291 0           my $target = join q{-}, $self->module, $version;
292 0           my $exportdir = File::Spec->catdir( $builddir, $target );
293              
294 0 0         if ( !$self->dryrun ) {
295 0 0         if ( !-d $builddir ) {
296 0           eval { File::Path::mkpath($builddir) };
  0            
297 0 0         if ($@) {
298 0           die "Could not create $builddir: $@\n";
299             }
300             }
301              
302 0 0         if ( -d $exportdir ) {
303 0           File::Path::rmtree($exportdir);
304             }
305             }
306              
307 0           my $orig_dir = Cwd::abs_path();
308              
309 0 0         chdir $builddir
310             or die "export: Could not access directory, $builddir: $!\n";
311              
312 0           $self->run_cmd( 'export', '-r', $tag, '-d', $target, $self->module );
313              
314 0 0         chdir $orig_dir
315             or die "Could not return to original directory, $orig_dir: $!\n";
316              
317 0           return $exportdir;
318             }
319              
320             sub checkout_project {
321 0     0 0   my ( $self, $version, $outdir ) = @_;
322              
323 0 0         if ( !defined $outdir ) {
324 0           $outdir = '.';
325             }
326              
327 0           my @args;
328 0 0         if ( defined $version ) {
329 0           my $tag = $self->gen_tag($version);
330              
331 0           @args = ( 'r', $tag );
332             }
333              
334 0           my $orig_dir = Cwd::abs_path();
335              
336 0 0         chdir $outdir or die "tag: Could not access directory, $outdir: $!\n";
337              
338 0           $self->run_cmd( 'checkout', @args, $self->module );
339              
340 0 0         chdir $orig_dir
341             or die "Could not return to original directory, $orig_dir: $!\n";
342              
343 0           return;
344             }
345              
346             sub import_project {
347 0     0 1   my ( $self, $dir, $version, $message ) = @_;
348              
349 0 0         if ( !defined $message ) {
350 0           $message = 'Imported with LCFG build tools';
351             }
352              
353 0           my $vendor_tag = $self->gen_tag();
354 0           my $release_tag = $self->gen_tag($version);
355              
356 0           my $orig_dir = Cwd::abs_path();
357              
358 0 0         chdir $dir or die "tag: Could not access directory, $dir: $!\n";
359              
360 0           $self->run_cmd( 'import',
361             '-I', '!',
362             '-m', $message,
363             $self->module, $vendor_tag, $release_tag );
364              
365 0 0         chdir $orig_dir
366             or die "Could not return to original directory, $orig_dir: $!\n";
367              
368 0           return;
369             }
370              
371             1;
372             __END__
373              
374             =head1 NAME
375              
376             LCFG::Build::VCS::CVS - LCFG build tools for CVS version-control
377              
378             =head1 VERSION
379              
380             This documentation refers to LCFG::Build::VCS::CVS version 0.2.3
381              
382             =head1 SYNOPSIS
383              
384             my $dir = ".";
385              
386             my $spec = LCFG::Build::PkgSpec->new_from_metafile("$dir/lcfg.yml");
387              
388             my $vcs = LCFG::Build::VCS::CVS->new( module => $spec->fullname,
389             workdir => $dir );
390              
391             $vcs->genchangelog();
392              
393             if ( $vcs->checkcommitted() ) {
394             $vcs->tagversion();
395             }
396              
397             =head1 DESCRIPTION
398              
399             This is part of a suite of tools designed to provide a standardised
400             interface to version-control systems so that the LCFG build tools can
401             deal with project version-control in a high-level abstract fashion.
402              
403             This module implements the interface specified by
404             L<LCFG::Build::VCS>. It provides support for LCFG projects which use
405             the CVS version-control system. Facilities are available for
406             procedures such as importing and exporting projects, doing tagged
407             releases, generating the project changelog from the version-control
408             log and checking all changes are committed.
409              
410             More information on the LCFG build tools is available from the website
411             http://www.lcfg.org/doc/buildtools/
412              
413             =head1 ATTRIBUTES
414              
415             =over
416              
417             =item module
418              
419             The name of the software package in this repository. This is required
420             and there is no default value.
421              
422             =item workdir
423              
424             The directory in which the CVS commands should be carried out. This is
425             required and if none is specified then it will default to '.', the
426             current working directory. This must be an absolute path but if you
427             pass in a relative path coercion will automatically occur based on the
428             current working directory.
429              
430             =item binpath
431              
432             The name of the CVS executable, by default this is C<cvs>.
433              
434             =item root
435              
436             This is the CVS root. If not specified the module will attempt to
437             discover the right thing to use the first time you call the
438             accessor. It will look into the CVS/Root file in the working directory
439             for the project or if that fails use the CVSROOT environment variable.
440              
441             =item quiet
442              
443             This is a boolean value which controls the quietness of the CVS
444             commands. By default it is false and commands, such as CVS, will print
445             lots of extra stuff to the screen. If it is set to true the -Q option
446             will be passed to the CVS binary whenever a command is executed. The
447             cvs2cl(1) command used when automatically generating change log files
448             will also honour this option.
449              
450             =item dryrun
451              
452             This is a boolean value which controls whether the commands will
453             actually have a real effect or just print out what would be done. By
454             default it is false.
455              
456             =item logname
457              
458             The name of the logfile to which information should be directed when
459             doing version updates. This is also the name of the logfile to be used
460             if you utilise the automatic changelog generation option. The default
461             file name is 'ChangeLog'.
462              
463             =back
464              
465             =head1 SUBROUTINES/METHODS
466              
467             The following class methods are available:
468              
469             =over
470              
471             =item new
472              
473             Creates a new instance of the class.
474              
475             =item auto_detect($dir)
476              
477             This method returns a boolean value which indicates whether or not the
478             specified directory is part of a checked out working copy of a
479             CVS repository.
480              
481             =back
482              
483             The following instance methods are available:
484              
485             =over
486              
487             =item checkcommitted()
488              
489             Test to see if there are any uncommitted files in the project
490             directory. Note this test does not spot files which have not been
491             added to the version-control system. In scalar context the subroutine
492             returns 1 if all files are committed and 0 (zero) otherwise. In list
493             context the subroutine will return this code along with a list of any
494             files which require committing.
495              
496             =item genchangelog()
497              
498             This method will generate a changelog (the name of which is controlled
499             by the logname attribute) from the log kept within the version-control
500             system. For CVS the cvs2cl(1) command is used.
501              
502             =item tagversion($version)
503              
504             This method is used to tag a set of files for a project at a
505             particular version. It will also update the changelog
506             appropriately. Tags are generated using the I<gen_tag()> method, see
507             below for details.
508              
509             =item gen_tag($version)
510              
511             Tags are generated from the name and version details passed in by
512             replacing any hyphens or dots with underscores and joining the two
513             fields with an underscore. For example, lcfg-foo and 1.0.1 would
514             become lcfg_foo_1_0_1.
515              
516             =item run_cmd(@args)
517              
518             A method used to handle the running of commands for the particular
519             version-control system. This is required for systems like CVS where
520             shell commands have to be executed. Not all modules will need to
521             implement this method as they may well use a proper Perl module API
522             (e.g. subversion).
523              
524             =item export( $version, $builddir )
525              
526             This will export a particular tagged version of the module. You need
527             to specify the target "build" directory into which the exported tree
528             will be put. The exported tree will be named like
529             "modulename-version". For example:
530              
531             my $vcs = LCFG::Build::VCS::CVS->new(module => "lcfg-foo");
532             $vcs->export( "1.2.3", "/tmp" );
533              
534             Would give you an exported tree of code for the lcfg-foo module tagged
535             as lcfg_foo_1_2_3 and it would be put into /tmp/lcfg-foo-1.2.3/
536              
537             Returns the name of the directory into which the tree was exported.
538              
539             =item export_devel( $version, $builddir )
540              
541             This is similar to the export method. It takes the current working
542             tree for a module and exports it directly to another tree based in the
543             specified target "build" directory. This method copies over everything
544             except the special CVS directories. For example:
545              
546             my $vcs = LCFG::Build::VCS::CVS->new(module => "lcfg-foo");
547             $vcs->export_devel( "1.2.3_dev", "/tmp" );
548              
549             Would give you an exported tree of code for the lcfg-foo module
550             directory and it would be put into /tmp/lcfg-foo-1.2.3_dev/
551              
552             Returns the name of the directory into which the tree was exported.
553              
554             =item import_project( $dir, $version, $message )
555              
556             Imports a project source tree into the version-control system. You
557             need to specify the version for the initial tag. Optionally you can
558             specify a message which will be used.
559              
560             =item logfile()
561              
562             This is a convenience method which returns the full path to the
563             logfile based on the workdir and logname attributes.
564              
565             =back
566              
567             =head1 DEPENDENCIES
568              
569             This module is L<Moose> powered and it depends on
570             L<LCFG::Build::VCS>. You will need a working C<cvs> executable
571             somewhere on your system and a CVS repository for this module to be in
572             anyway useful.
573              
574             =head1 SEE ALSO
575              
576             L<LCFG::Build::PkgSpec>, L<LCFG::Build::VCS::None>,
577             L<LCFG::Build::VCS::SVN>, L<LCFG::Build::Tools>
578              
579             =head1 PLATFORMS
580              
581             This is the list of platforms on which we have tested this
582             software. We expect this software to work on any Unix-like platform
583             which is supported by Perl.
584              
585             FedoraCore5, FedoraCore6, ScientificLinux5
586              
587             =head1 BUGS AND LIMITATIONS
588              
589             There are no known bugs in this application. Please report any
590             problems to bugs@lcfg.org, feedback and patches are also always very
591             welcome.
592              
593             =head1 AUTHOR
594              
595             Stephen Quinney <squinney@inf.ed.ac.uk>
596              
597             =head1 LICENSE AND COPYRIGHT
598              
599             Copyright (C) 2008-2013 University of Edinburgh. All rights reserved.
600              
601             This library is free software; you can redistribute it and/or modify
602             it under the terms of the GPL, version 2 or later.
603              
604             =cut