summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2022-01-11 20:45:16 -0800
committerRuss Allbery <rra@cpan.org>2022-01-11 20:45:16 -0800
commite7e6c9a95fb4cd68110ecd51a8ea2b5accc17e78 (patch)
tree509eb6dd650ff4664238dfc445abfb4b394b0a00
parent66d9ae6a41b46481882c10474d92b8633a11e279 (diff)
Add docknot release command
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.
-rw-r--r--Build.PL3
-rw-r--r--Changes6
-rw-r--r--README1
-rw-r--r--README.md1
-rwxr-xr-xbin/docknot67
-rw-r--r--cpanfile1
-rw-r--r--docs/docknot.yaml1
-rw-r--r--lib/App/DocKnot/Command.pm6
-rw-r--r--lib/App/DocKnot/Config.pm17
-rw-r--r--lib/App/DocKnot/Dist.pm8
-rw-r--r--lib/App/DocKnot/Release.pm268
-rw-r--r--lib/App/DocKnot/Spin/Versions.pm61
-rw-r--r--lib/App/DocKnot/Util.pm71
-rw-r--r--share/schema/config.yaml6
-rw-r--r--t/data/generate/docknot/output/thread1
-rwxr-xr-xt/release/basic.t116
16 files changed, 599 insertions, 35 deletions
diff --git a/Build.PL b/Build.PL
index a4c8d70..c2258b3 100644
--- a/Build.PL
+++ b/Build.PL
@@ -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',
diff --git a/Changes b/Changes
index adf5cfc..b761adf 100644
--- a/Changes
+++ b/Changes
@@ -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
diff --git a/README b/README
index a5c138f..febcd60 100644
--- a/README
+++ b/README
@@ -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
diff --git a/README.md b/README.md
index 4ab09e4..092cd3e 100644
--- a/README.md
+++ b/README.md
@@ -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>
diff --git a/cpanfile b/cpanfile
index b9c7d8e..2bb9b3e 100644
--- a/cpanfile
+++ b/cpanfile
@@ -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');