diff options
-rw-r--r-- | Build.PL | 3 | ||||
-rw-r--r-- | Changes | 6 | ||||
-rw-r--r-- | README | 1 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rwxr-xr-x | bin/docknot | 67 | ||||
-rw-r--r-- | cpanfile | 1 | ||||
-rw-r--r-- | docs/docknot.yaml | 1 | ||||
-rw-r--r-- | lib/App/DocKnot/Command.pm | 6 | ||||
-rw-r--r-- | lib/App/DocKnot/Config.pm | 17 | ||||
-rw-r--r-- | lib/App/DocKnot/Dist.pm | 8 | ||||
-rw-r--r-- | lib/App/DocKnot/Release.pm | 268 | ||||
-rw-r--r-- | lib/App/DocKnot/Spin/Versions.pm | 61 | ||||
-rw-r--r-- | lib/App/DocKnot/Util.pm | 71 | ||||
-rw-r--r-- | share/schema/config.yaml | 6 | ||||
-rw-r--r-- | t/data/generate/docknot/output/thread | 1 | ||||
-rwxr-xr-x | t/release/basic.t | 116 |
16 files changed, 599 insertions, 35 deletions
@@ -2,7 +2,7 @@ # # Build script for the docknot application. # -# Copyright 2013, 2016, 2018-2021 Russ Allbery <rra@cpan.org> +# Copyright 2013, 2016, 2018-2022 Russ Allbery <rra@cpan.org> # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -78,6 +78,7 @@ my $build = Module::Build->new( 'Path::Tiny' => '0.101', 'Perl6::Slurp' => 0, 'Pod::Thread' => '3.01', + 'Sort::Versions' => 0, 'Template' => 0, 'YAML::XS' => '0.81', perl => '5.024', @@ -2,6 +2,12 @@ 6.01 - Not Released + - Add new docknot release command and corresponding App::DocKnot::Release + module to copy a tarball releaes (normally created by docknot dist) + into a release area, update symlinks, archive any previous releases, + and update the .versions file used by docknot spin. DocKnot now + depends on Sort::Versions. + - Add new docknot update-spin command and corresponding update_spin method in App::DocKnot::Update to update a spin input tree to the latest expectations. Currently, all this does is convert *.rpod @@ -68,6 +68,7 @@ REQUIREMENTS * Path::Tiny 0.101 or later * Perl6::Slurp * Pod::Thread 3.01 or later + * Sort::Versions * Template (part of Template Toolkit) * YAML::XS 0.81 or later @@ -73,6 +73,7 @@ The following additional Perl modules are required to use it: * Path::Tiny 0.101 or later * Perl6::Slurp * Pod::Thread 3.01 or later +* Sort::Versions * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/bin/docknot b/bin/docknot index 3de49a9..b091f4f 100755 --- a/bin/docknot +++ b/bin/docknot @@ -22,7 +22,7 @@ __END__ =for stopwords Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense subcommand -subcommands distdir pgp-key cl2xhtml cvs2xhtml faq2html spin-rss +subcommands distdir pgp-key cl2xhtml cvs2xhtml faq2html spin-rss archivedir =head1 NAME @@ -38,6 +38,8 @@ B<docknot> generate [B<-m> I<metadata>] [B<-w> I<width>] I<template> [I<output>] B<docknot> generate-all [B<-m> I<metadata>] [B<-w> I<width>] +B<docknot> release [B<-a> I<archivedir>] [B<-d> I<distdir>] [B<-m> I<metadata>] + B<docknot> spin [B<-d>] [B<-e> I<pattern> ...] [B<-s> I<url>] I<source> I<output> @@ -79,6 +81,11 @@ Like C<generate>, but generates all of the package documentation for which default output files are configured. This is a quick short-cut to generating all documentation that's shipped with the package. +=item release + +Copy a distribution tarball into a release area, archiving old versions, and +optionally updating configuration for C<spin>. + =item spin Spin a tree of files written in the macro language thread into an HTML web @@ -109,7 +116,9 @@ pointer files. =head1 OPTIONS -Each B<docknot> subcommand takes its own options. +Each B<docknot> subcommand takes its own options. Many also read global +configuration options from DocKnot's configuration. See +L<App::DocKnot::Config/Global Configuration> for more details. =head2 Global Options @@ -137,11 +146,9 @@ or if this option is not set. =item B<-m> I<metadata>, B<--metadata>=I<metadata> -The path to the metadata files for the package whose distribution tarball is -being generated. This should be a directory containing all the package -metadata files required by App::DocKnot. Default: F<docs/docknot.yaml> -relative to the current directory (which is the recommended metadata path for -a project). +The path to the metadata file for the package whose distribution tarball is +being generated. Default: F<docs/docknot.yaml> relative to the current +directory (which is the recommended metadata path for a project). =item B<-p> I<pgp-key>, B<--pgp-key>=I<pgp-key> @@ -159,10 +166,9 @@ in the global DocKnot configuration file. =item B<-m> I<metadata>, B<--metadata>=I<metadata> -The path to the metadata files for the package whose documentation is being -generated. This should be a directory containing all the package metadata -files required by App::DocKnot. Default: F<docs/docknot.yaml> relative to the -current directory (which is the recommended metadata path for a project). +The path to the metadata file for the package whose documentation is being +generated. Default: F<docs/docknot.yaml> relative to the current directory +(which is the recommended metadata path for a project). =item B<-w> I<width>, B<--width>=I<width> @@ -190,10 +196,9 @@ If the template isn't listed above, this argument is required. =item B<-m> I<metadata>, B<--metadata>=I<metadata> -The path to the metadata files for the package whose documentation is being -generated. This should be a directory containing all the package metadata -files required by App::DocKnot. Default: F<docs/docknot.yaml> relative to the -current directory (which is the recommended metadata path for a project). +The path to the metadata file for the package whose documentation is being +generated. Default: F<docs/docknot.yaml> relative to the current directory +(which is the recommended metadata path for a project). =item B<-w> I<width>, B<--width>=I<width> @@ -201,6 +206,35 @@ Column width at which the generated output is wrapped. Default: 74. =back +=head2 release + +=over 4 + +=item B<-a> I<archivedir>, B<--archivedir>=I<archivedir> + +The release area into which to put the distribution tarball. The current +distribution will be put in a subdirectory named after the +C<distribution.section> key in the package configuration. Older versions will +be moved to the F<ARCHIVE> subdirectory of I<archivedir>. Default: The +C<archivedir> option in the global DocKnot configuration file. This option is +required if there is no configuration file or if this option is not set. + +=item B<-d> I<distdir>, B<--distdir>=I<distdir> + +The directory from which to get the new distribution tarball, normally +generated by C<dist>. The latest version in this directory will be used. +Default: The C<destdir> option in the global DocKnot configuration file. This +option is required if there is no configuration file or if this option is not +set. + +=item B<-m> I<metadata>, B<--metadata>=I<metadata> + +The path to the metadata file for the package whose distribution tarball is +being generated. Default: F<docs/docknot.yaml> relative to the current +directory (which is the recommended metadata path for a project). + +=back + =head2 spin =over 4 @@ -214,7 +248,8 @@ I<output> tree that do not have a corresponding file in the I<source> tree. =item B<-e> I<pattern>, B<--exclude>=I<pattern> Exclude files matching the given regular expression I<pattern> from being -converted. This flag may be used multiple times. +converted. The pattern is matched only against the file name, not its full +path. This flag may be given multiple times. =item B<-s> I<url>, B<--style-url>=I<url> @@ -16,6 +16,7 @@ requires 'Path::Iterator::Rule'; requires 'Path::Tiny', '0.101'; requires 'Perl6::Slurp'; requires 'Pod::Thread', '3.01'; +requires 'Sort::Versions'; requires 'Template'; requires 'YAML::XS', '0.81'; diff --git a/docs/docknot.yaml b/docs/docknot.yaml index 044acd6..daef76b 100644 --- a/docs/docknot.yaml +++ b/docs/docknot.yaml @@ -145,6 +145,7 @@ requirements: | * Path::Tiny 0.101 or later * Perl6::Slurp * Pod::Thread 3.01 or later + * Sort::Versions * Template (part of Template Toolkit) * YAML::XS 0.81 or later diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm index 48c7240..0068b34 100644 --- a/lib/App/DocKnot/Command.pm +++ b/lib/App/DocKnot/Command.pm @@ -78,6 +78,12 @@ our %COMMANDS = ( options => ['metadata|m=s', 'width|w=i'], maximum => 0, }, + release => { + method => 'release', + module => 'App::DocKnot::Release', + options => ['archivedir|a=s', 'distdir|d=s', 'metadata|m=s'], + maximum => 0, + }, spin => { method => 'spin', module => 'App::DocKnot::Spin', diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm index 5efbc48..036cfc7 100644 --- a/lib/App/DocKnot/Config.pm +++ b/lib/App/DocKnot/Config.pm @@ -107,7 +107,7 @@ __END__ =for stopwords Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG Kwalify -distdir +distdir archivedir =head1 NAME @@ -170,6 +170,16 @@ default) may contain the following keys: =over 4 +=item archivedir + +Specifies the directory into which distribution tarballs are placed by the +C<docknot release> command. The current distribution will be put in a +subdirectory named after the C<distribution.section> key in the package +configuration. Older versions will be moved to the F<ARCHIVE> subdirectory of +I<archivedir>. + +If this is not specified, the B<-a> option to C<docknot release> is mandatory. + =item distdir Specifies the directory into which to build and store distribution tarballs @@ -177,7 +187,8 @@ Specifies the directory into which to build and store distribution tarballs as working directories while the distribution is being built, and the final tarballs are stored in this directory. -If this is not specified, the B<-d> option to C<docknot dist> is mandatory. +If this is not specified, the B<-d> options to C<docknot dist> and C<docknot +release> is mandatory. =item pgp_key @@ -228,7 +239,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2013-2021 Russ Allbery <rra@cpan.org> +Copyright 2013-2022 Russ Allbery <rra@cpan.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot/Dist.pm b/lib/App/DocKnot/Dist.pm index 50d3b9b..e78329d 100644 --- a/lib/App/DocKnot/Dist.pm +++ b/lib/App/DocKnot/Dist.pm @@ -262,9 +262,9 @@ sub _sign_tarballs { # Create a new App::DocKnot::Dist object, which will be used for subsequent # calls. # -# $args - Anonymous hash of arguments with the following keys: +# $args_ref - Anonymous hash of arguments with the following keys: # distdir - Path to the directory for distribution tarball -# metadata - Path to the directory containing package metadata +# metadata - Path to the package metadata # perl - Path to Perl to use (default: search the user's PATH) # # Returns: Newly created object @@ -520,7 +520,7 @@ Default: The binary named C<gpg> on the user's PATH. =item metadata -The path to the directory containing metadata for a package. Default: +The path to the metadata for the package on which to operate. Default: F<docs/docknot.yaml> relative to the current directory. =item perl @@ -598,7 +598,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 2019-2021 Russ Allbery <rra@cpan.org> +Copyright 2019-2022 Russ Allbery <rra@cpan.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/App/DocKnot/Release.pm b/lib/App/DocKnot/Release.pm new file mode 100644 index 0000000..f2cef26 --- /dev/null +++ b/lib/App/DocKnot/Release.pm @@ -0,0 +1,268 @@ +# Release a distribution tarball for a package. +# +# This is the implementation of the docknot release command, which copies a +# release tarball (normally generated by docknot dist) into a publication +# area, archives old versions, and updates the .versions database for spin. +# +# SPDX-License-Identifier: MIT + +############################################################################## +# Modules and declarations +############################################################################## + +package App::DocKnot::Release 6.00; + +use 5.024; +use autodie; +use warnings; + +use App::DocKnot::Config; +use App::DocKnot::Spin::Versions; +use App::DocKnot::Util qw(latest_tarball); +use Carp qw(croak); +use Path::Tiny qw(path); + +############################################################################## +# Public interface +############################################################################## + +# Create a new App::DocKnot::Release object, which will be used for subsequent +# calls. +# +# $args_ref - Anonymous hash of arguments with the following keys: +# archivedir - Path to the archive directory +# distdir - Path to where docknot dist puts distribution tarballs +# metadata - Path to the package metadata +# +# Returns: Newly created object +# Throws: Text exceptions on invalid package metadata +# Text exceptions on invalid global configuration +# Text exceptions on invalid distdir argument +sub new { + my ($class, $args_ref) = @_; + + # Create the config reader. + my %config_args; + if ($args_ref->{metadata}) { + $config_args{metadata} = $args_ref->{metadata}; + } + my $config_reader = App::DocKnot::Config->new(\%config_args); + + # Load the global and package configuration. + my $global_config_ref = $config_reader->global_config(); + my $config_ref = $config_reader->config(); + + # Ensure we were given a valid archivedir and distdir arguments if they + # were not set in the global configuration. + my $archivedir = $args_ref->{archivedir} + // $global_config_ref->{archivedir}; + if (!defined($archivedir)) { + croak('archivedir path not given'); + } elsif (!-d $archivedir) { + croak( + "archivedir path $archivedir does not exist or is not a directory", + ); + } + my $distdir = $args_ref->{distdir} // $global_config_ref->{distdir}; + if (!defined($distdir)) { + croak('distdir path not given'); + } elsif (!-d $distdir) { + croak("distdir path $distdir does not exist or is not a directory"); + } + + # Build an App::DocKnot::Spin::Versions object if configured with a path + # to a versions database. + my $versions; + if ($global_config_ref->{versions}) { + my $versions_path = path($global_config_ref->{versions}); + $versions = App::DocKnot::Spin::Versions->new($versions_path); + } + + # Create and return the object. + #<<< + my $self = { + archivedir => path($archivedir), + distdir => path($distdir), + package => $config_ref->{name}, + section => $config_ref->{distribution}{section}, + tarname => $config_ref->{distribution}{tarname}, + version_name => $config_ref->{distribution}{version}, + versions => $versions, + }; + #>>> + bless($self, $class); + return $self; +} + +# Release a new version and update .versions if so configured. +# +# Throws: Text exception on any failures +sub release { + my ($self) = @_; + my $tarball_ref = latest_tarball($self->{distdir}, $self->{tarname}); + if (!defined($tarball_ref)) { + croak("no release of $self->{tarname} found in $self->{distdir}"); + } + + # Archive old versions. This is only done if the current version in the + # archive directory is different than the version we're about to release. + # If it is not, we overwrite the version in the archive directory, since + # we assume we're replacing a release. + my $current_path = $self->{archivedir}->child($self->{section}); + my $current_ref = latest_tarball($current_path, $self->{tarname}); + if (defined($current_ref)) { + if ($current_ref->{version} ne $tarball_ref->{version}) { + my $old_root = $self->{archivedir}->child('ARCHIVE'); + my $old_path = $old_root->child($self->{tarname}); + $old_path->mkpath(); + for my $file ($current_ref->{files}->@*) { + $current_path->child($file)->move($old_path->child($file)); + } + } + } + + # Copy the new version into place and update the symlinks. + $current_path->mkpath(); + for my $file ($tarball_ref->{files}->@*) { + $self->{distdir}->child($file)->copy($current_path->child($file)); + my $generic_name = $file; + $generic_name =~ s{ \A (\Q$self->{tarname}\E) - [\d.]+ [.] }{$1.}xms; + my $generic_path = $current_path->child($generic_name); + $generic_path->remove(); + symlink($file, $generic_path); + } + + # Update the .versions file. + if ($self->{versions}) { + my $name = $self->{version_name}; + my $version = $tarball_ref->{version}; + my $date = $tarball_ref->{date}; + $self->{versions}->update_version($name, $version, $date); + } + return; +} + +############################################################################## +# Module return value and documentation +############################################################################## + +1; +__END__ + +=for stopwords +Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense archivedir distdir + +=head1 Name + +App::DocKnot::Release - Release a distribution tarball + +=head1 SYNOPSIS + + use App::DocKnot::Release; + my $docknot = App::DocKnot::Release->new(); + $docknot->release(); + +=head1 REQUIREMENTS + +Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, +Git::Repository, Path::Tiny, and YAML::XS, all of which are available from +CPAN. + +=head1 DESCRIPTION + +This component of DocKnot releases a distribution tarball (normally created by +C<docknot dist> or App::DocKnot::Dist), maintains a software distribution +directory, and updates a version and release date database. + +=head1 CLASS METHODS + +=over 4 + +=item new(ARGS) + +Create a new App::DocKnot::Release object. This should be used for all +subsequent actions. ARGS should be a hash reference with one or more of the +following keys: + +=over 4 + +=item archivedir + +The release area into which to put the distribution tarball. The current +distribution will be put in a subdirectory named after the +C<distribution.section> key in the package configuration. Older versions will +be moved to the F<ARCHIVE> subdirectory of I<archivedir>. Required if not set +in the global configuration file. + +=item distdir + +The directory from which to get the new distribution tarball, normally +generated by C<docknot dist>. The latest version in this directory will be +used. Required if not set in the global configuration file. + +=item metadata + +The path to the metadata for the package on which to operate. Default: +F<docs/docknot.yaml> relative to the current directory. + +=back + +=back + +=head1 INSTANCE METHODS + +=over 4 + +=item release() + +Copy the distribution tarball (in multiple formats, with PGP signatures) into +a release area, updates symlink pointing to the latest version, and move any +old release to an archive area. + +If C<versions> is set in the global configuration file, updates the +F<.versions> file found at that path with the new release version and release +date. See L<App::DocKnot::Spin::Versions> for more information about +F<.versions> files. + +=back + +=head1 AUTHOR + +Russ Allbery <rra@cpan.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2022 Russ Allbery <rra@cpan.org> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=head1 SEE ALSO + +L<docknot(1)>, L<App::DocKnot::Config>, L<App::DocKnot::Dist>, +L<App::DocKnot::Spin::Versions> + +This module is part of the App-DocKnot distribution. The current version of +DocKnot is available from CPAN, or directly from its web site at +L<https://www.eyrie.org/~eagle/software/docknot/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm index c0e04f4..d417951 100644 --- a/lib/App/DocKnot/Spin/Versions.pm +++ b/lib/App/DocKnot/Spin/Versions.pm @@ -64,6 +64,8 @@ sub _datetime_to_seconds { # Text exception on file parsing errors sub _read_data { my ($self) = @_; + $self->{depends} = {}; + $self->{versions} = {}; my $timestamp; my $lineno = 0; @@ -119,13 +121,7 @@ sub new { my ($class, $path) = @_; # Create an empty object. - #<<< - my $self = { - depends => {}, - path => path($path), - versions => {}, - }; - #>>> + my $self = { path => path($path) }; bless($self, $class); # Parse the file into the newly-created object. @@ -157,6 +153,50 @@ sub release_date { return defined($version) ? $version->[1] : undef; } +# Update the version and release date for a package. Add the change to Git if +# the .versions file is at the top of a Git repository. +# +# $package - Name of the package +# $version - New version +# $timestamp - New release date as seconds since epoch +# +# Throws: Text exception on failure +sub update_version { + my ($self, $package, $version, $timestamp) = @_; + my $date = strftime('%Y-%m-%d', localtime($timestamp)); + my $time = strftime('%H:%M:%S', localtime($timestamp)); + + # Edits the line for the package to replace the version and release date. + my $edit = sub { + my $line = $_; + my ($product, $old_version, $old_date, $old_time) + = split(q{ }, $line); + return if $product ne $package; + + # We're going to replace the old version with the new one, but we need + # to space-pad one or the other if they're not the same length. + my $version_string = $version; + while (length($old_version) > length($version_string)) { + $version_string .= q{ }; + } + while (length($old_version) < length($version_string)) { + $old_version .= q{ }; + } + + # Make the replacement. + $line =~ s{ \Q$old_version\E }{$version_string}xms; + $line =~ s{ \Q$old_date\E }{$date}xms; + $line =~ s{ \Q$old_time\E }{$time}xms; + $_ = $line; + }; + + # Apply that change to our versions file, and then re-read the contents to + # update the internal data structure. + $self->{path}->edit_utf8($edit); + $self->_read_data(); + return; +} + # Return the latest version for a given package. # # $package - Name of the package @@ -274,6 +314,13 @@ PATH, or 0 if no releases affect that file. Return the release date of the latest release of PACKAGE (in UTC), or C<undef> if there is no release information for PACKAGE. +=item update_version(PACKAGE, VERSION, TIMESTAMP) + +Given a new VERSION and TIMESTAMP (in seconds since epoch) for a release of +PACKAGE, update the release information in the F<.versions> file for that +package accordingly. If the F<.versions> file is at the root of a Git +repository, this change will be staged with C<git add>. + =item version(PACKAGE) Return the version of the latest release of PACKAGE, or C<undef> if there is diff --git a/lib/App/DocKnot/Util.pm b/lib/App/DocKnot/Util.pm index 5999b2b..e97be4c 100644 --- a/lib/App/DocKnot/Util.pm +++ b/lib/App/DocKnot/Util.pm @@ -18,8 +18,9 @@ use warnings; use Carp qw(croak); use Exporter qw(import); use List::SomeUtils qw(all); +use Sort::Versions qw(versioncmp); -our @EXPORT_OK = qw(is_newer print_checked print_fh); +our @EXPORT_OK = qw(is_newer latest_tarball print_checked print_fh); ############################################################################## # Public interface @@ -39,6 +40,47 @@ sub is_newer { return all { $file_mtime >= $_ } @others_mtimes; } +# Find the files for a given package with the latest version and return them +# along with some associated metadata. +# +# $path - Path::Tiny path to directory +# $tarname - Name of the tarball before the version component +# +# Returns: Anonymous hash with the following keys: +# version - Latest version found +# date - Date (in seconds since epoch) of oldest file +# files - Array of files for that version +# or undef if no matching files were found +# Throws: Text exception on any error +sub latest_tarball { + my ($path, $tarname) = @_; + + # Collect the list of matching files and extract their version numbers. + return if !$path->is_dir(); + my $regex = qr{ \A \Q$tarname\E - ([\d.]+) [.] }xms; + my @files = map { $_->basename() } $path->children($regex); + my @versions = map { m{ $regex }xms ? [$1, $_] : () } @files; + return if !@versions; + + # Find the latest version and filter the list of files down to only that + # version. + @versions = reverse(sort { versioncmp($a->[0], $b->[0]) } @versions); + my $latest = $versions[0][0]; + @files = map { $_->[1] } grep { $_->[0] eq $latest } @versions; + + # Find the timestamps of those files. + my @times = sort(map { $path->child($_)->stat()->[9] } @files); + + # Return the results. + #<<< + return { + version => $latest, + date => $times[0], + files => \@files, + }; + #<<< +} + # print with error checking. autodie unfortunately can't help us because # print can't be prototyped and hence can't be overridden. # @@ -95,7 +137,8 @@ App::DocKnot::Util - Shared utility functions for other DocKnot modules =head1 REQUIREMENTS -Perl 5.24 or later and the List::SomeUtils module, available from CPAN. +Perl 5.24 or later and the modules List::SomeUtils and Sort::Versions, +available from CPAN. =head1 DESCRIPTION @@ -114,6 +157,28 @@ or equal to the last modified times of all SOURCE files, and otherwise returns a false value. Used primarily to determine if a given output file is up-to-date with respect to its source files. +=item latest_tarball(PATH, NAME) + +Returns data including a file list for the latest tarballs (by version number) +for a given software package NAME in the directory PATH. Versions are compared +using Sort::Versions. The return valid is a hash with the following keys: + +=over 4 + +=item date + +The timestamp of the oldest file for that version, in seconds since epoch. + +=item files + +The list of files found for that version. + +=item version + +The version number extracted from this set of files. + +=back + =item print_checked(ARG[, ARG ...]) The same as print (without a file handle argument), except that it throws a @@ -137,7 +202,7 @@ Russ Allbery <rra@cpan.org> =head1 COPYRIGHT AND LICENSE -Copyright 1999-2011, 2013, 2021 Russ Allbery <rra@cpan.org> +Copyright 1999-2011, 2013, 2021-2022 Russ Allbery <rra@cpan.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/share/schema/config.yaml b/share/schema/config.yaml index d49312c..d74e2cb 100644 --- a/share/schema/config.yaml +++ b/share/schema/config.yaml @@ -1,14 +1,18 @@ # Kwalify schema for DocKnot configuration file. # -# Copyright 2021 Russ Allbery <rra@cpan.org> +# Copyright 2021-2022 Russ Allbery <rra@cpan.org> # # SPDX-License-Identifier: MIT type: map mapping: + archivedir: + type: text distdir: type: text pandoc: type: text pgp_key: type: text + versions: + type: text diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread index 2b4371a..30d83e7 100644 --- a/t/data/generate/docknot/output/thread +++ b/t/data/generate/docknot/output/thread @@ -121,6 +121,7 @@ The following additional Perl modules are required to use it: \bullet(packed)[Path::Tiny 0.101 or later] \bullet(packed)[Perl6::Slurp] \bullet(packed)[Pod::Thread 3.01 or later] +\bullet(packed)[Sort::Versions] \bullet(packed)[Template (part of Template Toolkit)] \bullet(packed)[YAML::XS 0.81 or later] diff --git a/t/release/basic.t b/t/release/basic.t new file mode 100755 index 0000000..4379136 --- /dev/null +++ b/t/release/basic.t @@ -0,0 +1,116 @@ +#!/usr/bin/perl +# +# Tests for the App::DocKnot::Release module API. +# +# Copyright 2022 Russ Allbery <rra@cpan.org> +# +# SPDX-License-Identifier: MIT + +use 5.024; +use autodie; +use warnings; + +use lib 't/lib'; + +use Git::Repository (); +use Path::Tiny qw(path); + +use Test::More tests => 30; + +# Isolate from the environment. +local $ENV{XDG_CONFIG_HOME} = '/nonexistent'; +local $ENV{XDG_CONFIG_DIRS} = '/nonexistent'; + +# Load the module. +require_ok('App::DocKnot::Release'); + +# Construct a working area. +my $tempdir = Path::Tiny->tempdir(); +my $archive_path = $tempdir->child('archive'); +$archive_path->mkpath(); +my $old_path = $archive_path->child('ARCHIVE'); +my $dist_path = $tempdir->child('dist'); +$dist_path->mkpath(); + +# Make a release when there are no existing files. +my @extensions = qw(tar.gz tar.gz.asc tar.xz tar.xz.asc); +for my $ext (@extensions) { + $dist_path->child('Empty-1.9.' . $ext)->touch(); +} +my $metadata = path('t', 'data', 'dist', 'package', 'docs', 'docknot.yaml'); +my %options = ( + archivedir => $archive_path, + distdir => $dist_path, + metadata => $metadata, +); +my $release = App::DocKnot::Release->new(\%options); +$release->release(); + +# Check that the files were copied correctly and the symlinks were created. +for my $ext (@extensions) { + my $file = 'Empty-1.9.' . $ext; + ok($archive_path->child('devel', $file)->is_file(), "Copied $file"); + my $link = 'Empty.' . $ext; + is(readlink($archive_path->child('devel', $link)), $file, "Linked $link"); +} + +# Build a Git repository and a .versions file. +my $spin_path = $tempdir->child('spin'); +$spin_path->mkpath(); +my $versions_path = $spin_path->child('.versions'); +$versions_path->spew_utf8( + "empty 1.9 2022-01-01 16:00:00 software/empty/index.th\n", +); +Git::Repository->run('init', { cwd => "$spin_path", quiet => 1 }); +my $repo = Git::Repository->new(work_tree => "$spin_path"); +$repo->run(config => '--add', 'user.name', 'Test'); +$repo->run(config => '--add', 'user.email', 'test@example.com'); +$repo->run(add => '-A', q{.}); +$repo->run(commit => '-q', '-m', 'Initial commit'); + +# Construct a configuration file. +my $config_path = $tempdir->child('docknot', 'config.yaml'); +$config_path->parent()->mkpath(); +my @config = ( + "archivedir: $archive_path", + "distdir: $dist_path", + "versions: $versions_path", +); +$config_path->spew_utf8(join("\n", @config), "\n"); +local $ENV{XDG_CONFIG_HOME} = "$tempdir"; + +# Make another release, now relying on the global configuration. Add some +# other files to distdir to ensure they're ignored. +for my $ext (@extensions) { + $dist_path->child('Empty-1.10.' . $ext)->touch(); + $dist_path->child('foo-1.0.' . $ext)->touch(); +} +$release = App::DocKnot::Release->new({ metadata => $metadata }); +$release->release(); + +# Check that the files were copied correctly, the symlinks were created, and +# the old files were moved. Check that the old files were copied to the +# archive directory. +for my $ext (@extensions) { + my $file = 'Empty-1.10.' . $ext; + ok($archive_path->child('devel', $file)->is_file(), "Copied $file"); + my $old = 'Empty-1.9.' . $ext; + ok(!$archive_path->child('devel', $old)->is_file(), "Removed $old"); + ok( + $archive_path->child('ARCHIVE', 'Empty', $old)->is_file(), + "Archived $old", + ); + my $link = 'Empty.' . $ext; + is(readlink($archive_path->child('devel', $link)), $file, "Updated $link"); +} + +# Check that the version file was updated. +my @versions = split(q{ }, $versions_path->slurp_utf8()); +is($versions[0], 'empty', '.versions line'); +is($versions[1], '1.10', '...version updated'); +isnt(join(q{ }, @versions[2, 3]), '2022-01-01 16:00:00', '...date updated'); +is($versions[4], 'software/empty/index.th', '...dependency unchanged'); + +# Check that the change was staged. +my $status = $repo->run('status', '-s'); +is($status, ' M .versions', '.versions change was staged'); |