File Coverage

blib/lib/LCFG/Build/VCS.pm
Criterion Covered Total %
statement 38 171 22.2
branch 0 66 0.0
condition 0 28 0.0
subroutine 13 22 59.0
pod 6 9 66.6
total 57 296 19.2


line stmt bran cond sub pod time code
1             package LCFG::Build::VCS; # -*-perl-*-
2 1     1   787 use strict;
  1         2  
  1         32  
3 1     1   5 use warnings;
  1         2  
  1         27  
4              
5 1     1   26 use v5.10;
  1         3  
6              
7             # $Id: VCS.pm.in 35396 2019-01-17 12:01:51Z squinney@INF.ED.AC.UK $
8             # $Source: /var/cvs/dice/LCFG-Build-VCS/lib/LCFG/Build/VCS.pm.in,v $
9             # $Revision: 35396 $
10             # $HeadURL: https://svn.lcfg.org/svn/source/tags/LCFG-Build-VCS/LCFG_Build_VCS_0_3_8/lib/LCFG/Build/VCS.pm.in $
11             # $Date: 2019-01-17 12:01:51 +0000 (Thu, 17 Jan 2019) $
12              
13             our $VERSION = '0.3.8';
14              
15 1     1   958 use DateTime ();
  1         487005  
  1         79  
16 1     1   566 use File::Copy ();
  1         2393  
  1         27  
17 1     1   7 use File::Path ();
  1         2  
  1         15  
18 1     1   5 use File::Spec ();
  1         1  
  1         15  
19 1     1   723 use File::Temp ();
  1         9294  
  1         29  
20 1     1   7 use IO::File ();
  1         3  
  1         20  
21 1     1   978 use IPC::Run qw(run);
  1         26451  
  1         59  
22 1     1   535 use Template v2.14 ();
  1         19258  
  1         27  
23              
24 1     1   474 use Moose::Role;
  1         5346  
  1         8  
25 1     1   5949 use Moose::Util::TypeConstraints;
  1         2  
  1         6  
26              
27             subtype 'AbsPath'
28             => as 'Str'
29             => where { File::Spec->file_name_is_absolute($_) }
30             => message { 'Directory must be an absolute path.' };
31              
32             # coerce the input string (which is possibly a relative path) into an
33             # absolute path which does not have a trailing /
34              
35             coerce 'AbsPath'
36             => from 'Str'
37             => via { my $path = File::Spec->file_name_is_absolute($_) ? $_ : File::Spec->rel2abs($_); $path =~ s{/$}{}; $path };
38              
39             requires qw/checkcommitted genchangelog tagversion export export_devel import_project checkout_project/;
40              
41             has 'id' => (
42             is => 'ro',
43             isa => 'Str',
44             required => 1,
45             );
46              
47             has 'module' => (
48             is => 'rw',
49             isa => 'Str',
50             required => 1,
51             );
52              
53             has 'workdir' => (
54             is => 'rw',
55             isa => 'AbsPath',
56             required => 1,
57             coerce => 1,
58             default => q{.},
59             );
60              
61             has 'binpath' => (
62             is => 'rw',
63             isa => 'Str',
64             required => 0,
65             );
66              
67             has 'quiet' => (
68             is => 'rw',
69             isa => 'Bool',
70             default => 0,
71             required => 0,
72             );
73              
74             has 'dryrun' => (
75             is => 'rw',
76             isa => 'Bool',
77             default => 0,
78             required => 0,
79             );
80              
81             has 'logname' => (
82             is => 'rw',
83             isa => 'Str',
84             default => 'ChangeLog',
85             required => 1,
86             );
87              
88             sub build_cmd {
89 0     0 0   my ( $self, @args ) = @_;
90              
91 0           my @cmd = ( $self->binpath, @args );
92              
93 0           return @cmd;
94             }
95              
96             sub run_cmd {
97 0     0 1   my ( $self, @args ) = @_;
98              
99 0           my @cmd = $self->build_cmd(@args);
100              
101 0           my @out;
102 0 0         if ( $self->dryrun ) {
103 0           my $cmd = join q( ), @cmd;
104 0           print "Dry-run: $cmd\n";
105             }
106             else {
107 0           my ( $in, $out, $err );
108              
109 0           my $success = run \@cmd, \$in, \$out, \$err;
110 0 0         if ( !$success ) {
111 0           die "Error whilst running @cmd: $err\n";
112             }
113 0 0         if ($err) {
114 0           warn "$err\n";
115             }
116              
117 0           @out = split /[\r\n]+/, $out;
118             }
119              
120 0           return @out;
121             }
122              
123             sub logfile {
124 0     0 1   my ($self) = @_;
125              
126 0           return File::Spec->catfile( $self->workdir, $self->logname );
127             }
128              
129             sub gen_tag {
130 0     0 1   my ( $self, $version ) = @_;
131              
132             # Build a tag from the name and version (if specified) and then
133             # replace any period or hyphen characters.
134             #
135             # name: lcfg-foo, version: 1.0.1, gives: lcfg_foo_1_0_1
136              
137 0           my $tag;
138 0 0         if ( !defined $version ) {
139 0           $tag = $self->module;
140             }
141             else {
142              
143 0 0         if ( $version eq 'latest' ) {
144 0           $tag = 'latest';
145             }
146             else {
147 0           $tag = join q{_}, $self->module, $version;
148             }
149              
150             }
151              
152 0           $tag =~ s/\./_/g;
153 0           $tag =~ s/\-/_/g;
154              
155 0           return $tag;
156             }
157              
158             sub update_changelog {
159 0     0 1   my ( $self, $version, $options ) = @_;
160 0   0       $options //= {};
161              
162 0           $options->{dryrun} = $self->dryrun;
163 0           $options->{id} = $self->id;
164 0           $options->{version} = $version;
165 0   0       $options->{style} ||= 'default';
166              
167 0           my $dir = $self->workdir;
168              
169 0           my ( $logfile, $needs_add );
170 0 0         if ( $options->{style} eq 'debian' ) {
171 0           $logfile = File::Spec->catfile( $dir, 'debian', 'changelog' );
172 0           $needs_add = !-e $logfile;
173              
174 0 0         if ( !$options->{pkgname} ) {
175              
176             # Cook up something sensible which looks like a Debian package
177             # name
178              
179 0           $options->{pkgname} = lc $self->module;
180              
181             # underscores are not permitted, helpfully replace with dashes
182 0           $options->{pkgname} =~ s/_/-/g;
183              
184             # For safety remove any other invalid characters
185 0           $options->{pkgname} =~ s/[^a-z0-9-]//;
186             }
187              
188 0           update_debian_changelog( $logfile, $options );
189             } else {
190 0           $logfile = $self->logfile;
191 0           $needs_add = !-e $logfile;
192              
193 0           update_lcfg_changelog( $logfile, $options );
194             }
195              
196 0 0         if ( !$self->dryrun ) {
197 0 0         if ($needs_add) {
198 0           $self->run_cmd( 'add', $logfile );
199             }
200             }
201              
202 0           return;
203             }
204              
205             sub update_lcfg_changelog {
206 0     0 0   my ( $logfile, $options ) = @_;
207 0   0       $options //= {};
208              
209 0   0       $options->{date} ||= DateTime->now->ymd;
210              
211 0           my $dir = (File::Spec->splitpath($logfile))[1];
212              
213 0           my $tmplog = File::Temp->new(
214             TEMPLATE => 'lcfgXXXXXX',
215             UNLINK => 1,
216             DIR => $dir,
217             );
218              
219 0           my $tmpname = $tmplog->filename;
220              
221 0           $tmplog->print(<<"EOT");
222             $options->{date} $options->{id}: new release
223              
224             \t* Release: $options->{version}
225              
226             EOT
227              
228 0 0         if ( -f $logfile ) {
229 0 0         my $log = IO::File->new( $logfile, 'r' )
230             or die "Could not open $logfile: $!\n";
231              
232 0           while ( defined( my $line = <$log> ) ) {
233 0           $tmplog->print($line);
234             }
235              
236 0           $log->close;
237             }
238              
239             $tmplog->close
240 0 0         or die "Could not close temporary file, $tmpname: $!\n";
241              
242 0 0         if ( !$options->{dryrun} ) {
243 0 0         rename $tmpname, $logfile
244             or die "Could not rename $tmpname as $logfile: $!\n";
245             }
246              
247 0           return;
248             }
249              
250             # These update_*_changelog subroutines are also used externally from
251             # places which do not have access to the VCS object so they are not
252             # class methods.
253              
254             sub update_debian_changelog {
255 0     0 0   my ( $logfile, $options ) = @_;
256 0   0       $options //= {};
257              
258 0   0       $options->{urgency} ||= 'low';
259 0   0       $options->{distribution} ||= 'unstable';
260 0   0       $options->{release} //= 1;
261 0   0       $options->{message} ||= 'New upstream release';
262              
263             # RFC822 date
264 0           $options->{date} = DateTime->now->strftime('%a, %d %b %Y %H:%M:%S %z');
265              
266 0 0         if ( !$options->{email} ) {
267 0           my $user_name = (getpwuid($<))[0];
268              
269 0   0       my $email_addr = $ENV{DEBEMAIL} || $ENV{EMAIL};
270              
271 0 0         if ( !$email_addr ) {
272 0           require Net::Domain;
273              
274 0           my $domain = Net::Domain::hostdomain();
275            
276 0           $email_addr = join '@', $user_name, $domain;
277             }
278              
279             # trim any leading or trailing whitespace
280 0           $email_addr =~ s/^\s+//; $email_addr =~ s/\s+$//;
  0            
281              
282 0 0         if ( $email_addr !~ m/<.+>/ ) {
283 0   0       my $email_name = $ENV{DEBFULLNAME} || $ENV{NAME} || $user_name;
284 0           $email_name =~ s/^\s+//; $email_name =~ s/\s+$//;
  0            
285              
286 0           $email_addr = "$email_name <$email_addr>";
287             }
288              
289 0           $options->{email} = $email_addr;
290             }
291              
292 0           my ( $dir, $basename ) = (File::Spec->splitpath($logfile))[1,2];
293              
294 0           my $tmplog = File::Temp->new(
295             TEMPLATE => 'lcfgXXXXXX',
296             UNLINK => 1,
297             DIR => $dir,
298             );
299 0           my $tmpname = $tmplog->filename;
300              
301 0 0         my $tt = Template->new(
302             {
303             INCLUDE_PATH => $dir,
304             }
305             ) or die $Template::ERROR . "\n";
306              
307 0           my $template = q{
308             [%- FOREACH entry IN entries -%]
309             [% entry.item('pkgname') %] ([% entry.item('version') %]-[% entry.item('release') %]) [% entry.item('distribution') %]; urgency=[% entry.item('urgency') %]
310              
311             * [% entry.item('message') %]
312              
313             -- [% entry.item('email') %] [% entry.item('date') %]
314              
315             [% END -%]
316             [% IF current_logfile %][% INSERT $current_logfile %][% END -%]
317             };
318              
319 0           my %args = (
320             entries => [$options],
321             );
322              
323 0 0         if ( -e $logfile ) {
324 0           $args{current_logfile} = $basename;
325             }
326              
327 0 0         $tt->process( \$template, \%args, $tmplog )
328             or die $tt->error() . "\n";
329              
330 0 0         $tmplog->close
331             or die "Could not close temporary file, $tmpname: $!\n";
332              
333 0 0         if ( !$options->{dryrun} ) {
334 0 0         rename $tmpname, $logfile
335             or die "Could not rename $tmpname as $logfile: $!\n";
336             }
337              
338 0           return;
339              
340             }
341              
342             sub mirror_file {
343 0     0 1   my ( $self, $workdir, $exportdir, $dirname, $fname ) = @_;
344              
345 0           my $from_dir = File::Spec->catdir( $workdir, $dirname );
346 0           my $to_dir = File::Spec->catdir( $exportdir, $dirname );
347              
348 0 0 0       if ( !$self->dryrun && !-d $to_dir ) {
349 0           eval { File::Path::mkpath($to_dir) };
  0            
350 0 0         if ($@) {
351 0           die "Could not create $to_dir: $@\n";
352             }
353              
354 0           my ($dev, $ino, $mode, $nlink, $uid,
355             $gid, $rdev, $size, $atime, $mtime,
356             $ctime, $blksize, $blocks
357             ) = stat $from_dir;
358              
359 0 0         chmod $mode, $to_dir or die "chmod on $to_dir failed: $!\n";
360              
361             # We don't care about atime/mtime for directories
362             }
363              
364 0           my $from = File::Spec->catfile( $workdir, $dirname, $fname );
365 0           my $to = File::Spec->catfile( $exportdir, $dirname, $fname );
366              
367 0           my ($dev, $ino, $mode, $nlink, $uid,
368             $gid, $rdev, $size, $atime, $mtime,
369             $ctime, $blksize, $blocks
370             ) = stat $from;
371              
372 0 0         if ( $self->dryrun ) {
373 0           print "Dry-run: $from -> $to\n";
374             }
375             else {
376 0 0         File::Copy::syscopy( $from, $to )
377             or die "Copy $from to $to failed: $!\n";
378              
379 0 0         chmod $mode, $to or die "chmod on $to to ($mode) failed: $!\n";
380 0 0         utime $atime, $mtime, $to or die "utime on $to to ($atime, $mtime) failed: $!\n";
381              
382             }
383              
384 0           return;
385             }
386              
387             sub store_version {
388 0     0 1   my ( $self, $version ) = @_;
389              
390 0           warn "Updating build ID file\n";
391              
392 0           my $dir = $self->workdir;
393 0           my $version_file = 'lcfg-build-id.txt';
394              
395 0           my $tmpfh = File::Temp->new(
396             UNLINK => 1,
397             DIR => $dir,
398             SUFFIX => '.tmp',
399             );
400              
401 0           my $tmpname = $tmpfh->filename;
402              
403 0           $tmpfh->say($version);
404              
405 0 0         $tmpfh->close
406             or die "Could not close temporary file, $tmpname: $!\n";
407              
408 0 0         if ( !$self->dryrun ) {
409 0 0         rename $tmpname, $version_file
410             or die "Could not rename $tmpname as $version_file: $!\n";
411             }
412              
413 0           return;
414             }
415              
416             1;
417             __END__
418              
419             =head1 NAME
420              
421             LCFG::Build::VCS - LCFG version-control infrastructure
422              
423             =head1 VERSION
424              
425             This documentation refers to LCFG::Build::VCS version 0.3.8
426              
427             =head1 SYNOPSIS
428              
429             my $vcs = LCFG::Build::VCS::CVS->new();
430              
431             $vcs->genchangelog();
432              
433             if ( $vcs->checkcommitted() ) {
434             $vcs->tagversion();
435             }
436              
437             =head1 DESCRIPTION
438              
439             This is a suite of tools designed to provide a standardised interface
440             to version-control systems so that the LCFG build tools can deal with
441             project version-control in a high-level abstract fashion. Typically
442             they provide support for procedures such as importing and exporting
443             projects, doing tagged releases, generating the project changelog from
444             the version-control log and checking all changes are committed.
445              
446             This is an interface, you should not attempt to create objects
447             directly using this module. You will need to implement a sub-class,
448             for example L<LCFG::Build::VCS::CVS>. This interface requires certain
449             attributes and methods be specified within any implementing sub-class,
450             see below for details. For complete details you should read the
451             documentation associated with the specific sub-class.
452              
453             More information on the LCFG build tools is available from the website
454             http://www.lcfg.org/doc/buildtools/
455              
456             =head1 ATTRIBUTES
457              
458             =over
459              
460             =item module
461              
462             The name of the software package in this repository. This is required
463             and there is no default value.
464              
465             =item workdir
466              
467             The directory in which the version-control system commands should be
468             carried out. This is required and if none is specified then it will
469             default to '.', the current working directory. This must be an
470             absolute path but if you pass in a relative path coercion will
471             automatically occur based on the current working directory.
472              
473             =item binpath
474              
475             The path to the version-control tool. This is required and it is
476             expected that any module which implements this interface will set a
477             suitable default command name.
478              
479             =item quiet
480              
481             This is a boolean value which controls the quietness of the
482             version-control system commands. By default it is false and commands,
483             such as CVS, will print lots of extra stuff to the screen.
484              
485             =item dryrun
486              
487             This is a boolean value which controls whether the commands will
488             actually have a real effect or just print out what would be done. By
489             default it is false.
490              
491             =item logname
492              
493             The name of the logfile to which information should be directed when
494             doing version updates. This is also the name of the logfile to be used
495             if you utilise the automatic changelog generation option. The default
496             file name is 'ChangeLog'.
497              
498             =back
499              
500             =head1 SUBROUTINES/METHODS
501              
502             This module provides a few fully-implemented methods which are likely
503             to be useful for all sub-classes which implement the interface.
504              
505             =over
506              
507             =item gen_tag($version)
508              
509             Generate a tag based on the package name and the specified
510             version. Tags are generated from the module name attribute and the
511             version information passed in by replacing any hyphens or dots with
512             underscores and joining the two fields with an underscore. For
513             example, lcfg-foo and 1.0.1 would become lcfg_foo_1_0_1. If no version
514             is specified then just the module name will be used.
515              
516             =item update_changelog($version)
517              
518             This will add a standard-format release tag entry to the top of the
519             change log file.
520              
521             =item mirror_file( $sourcedir, $targetdir, $reldir, $basename )
522              
523             This will copy a file from the source directory to the target
524             directory. The relative path of the file (within the source directory)
525             must be split into the relative directory path and filename. Effort is
526             made to preserve the mode and, in the case of files, atime and
527             mtime. This is used by various modules in the export_devel() method to
528             mirror the project directory into a build directory.
529              
530             =item logfile()
531              
532             This is a convenience method which returns the full path to the
533             logfile based on the workdir and logname attributes.
534              
535             =back
536              
537             As well as the methods above, any class which implements this
538             interface MUST provide methods for:
539              
540             =over
541              
542             =item checkcommitted()
543              
544             Test to see if there are any uncommitted files in the project
545             directory. Note this test does not spot files which have not been
546             added to the version-control system. In scalar context the subroutine
547             returns 1 if all files are committed and 0 (zero) otherwise. In list
548             context the subroutine will return this code along with a list of any
549             files which require committing.
550              
551             =item genchangelog($version)
552              
553             This method will generate a changelog (the name of which is controlled
554             by the logname attribute) from the log kept within the version-control
555             system.
556              
557             =item store_version($version)
558              
559             This method can be used to store the version string (e.g. C<1.2.3>)
560             into a file named F<lcfg-build-id.txt> in the top-level directory for
561             the project. This is useful if you need to have eacy access to the
562             version string in build scripts.
563              
564             =item tagversion($version)
565              
566             This method is used to tag a set of files for a project at a
567             particular version. It will also update the changelog
568             appropriately. The tag name is generated using the I<gen_tag()>
569             method, see below for full details.
570              
571             =item run_cmd(@args)
572              
573             A method used to handle the running of commands for the particular
574             version-control system. This is required for systems like CVS where
575             shell commands have to be executed. Not all modules will need to
576             implement this method as they may well use a proper Perl module API
577             (e.g. subversion).
578              
579             =item export( $version, $dir )
580              
581             Exports the source code for the project tagged at the specified
582             release. The second argument specifies the directory into which the
583             exported project directory will be placed.
584              
585             =item export_devel( $version, $dir )
586              
587             Exports the current development version of the source code for the
588             project (i.e. your working copy). The second argument specifies the
589             directory into which the exported project directory will be placed.
590              
591             =item import_project( $dir, $version, $message )
592              
593             Imports a project source tree into the version-control system.
594              
595             =item checkout_project( $version, $dir )
596              
597             Does a check-out from the version-control system of the project tagged
598             at the specified version. Unlike the export() method this checked-out
599             copy will include the files necessary for the version-control system
600             (e.g. CVS or .svn directories).
601              
602             =back
603              
604             =head1 DEPENDENCIES
605              
606             This module is L<Moose> powered. It also requires L<DateTime> and L<IPC::Run>.
607              
608             =head1 SEE ALSO
609              
610             L<LCFG::Build::PkgSpec>, L<LCFG::Build::VCS::CVS>, L<LCFG::Build::VCS::None>, L<LCFG::Build::Tools>
611              
612             =head1 PLATFORMS
613              
614             This is the list of platforms on which we have tested this
615             software. We expect this software to work on any Unix-like platform
616             which is supported by Perl.
617              
618             FedoraCore5, FedoraCore6, ScientificLinux5
619              
620             =head1 BUGS AND LIMITATIONS
621              
622             There are no known bugs in this application. Please report any
623             problems to bugs@lcfg.org, feedback and patches are also always very
624             welcome.
625              
626             =head1 AUTHOR
627              
628             Stephen Quinney <squinney@inf.ed.ac.uk>
629              
630             =head1 LICENSE AND COPYRIGHT
631              
632             Copyright (C) 2008-2019 University of Edinburgh. All rights reserved.
633              
634             This library is free software; you can redistribute it and/or modify
635             it under the terms of the GPL, version 2 or later.
636              
637             =cut