summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2022-01-15 17:13:04 -0800
committerRuss Allbery <rra@cpan.org>2022-01-15 17:13:04 -0800
commit93c237ea28bca715913206ad8281762fb8c4426f (patch)
tree0f01c01d58246deedb607112a092e12f8d0de564
parent43c3505a30836ad5fd15d048e07143392464bfc1 (diff)
parentca30d64285e120b991f994808100ff866d7bdf7a (diff)
Update upstream source from tag 'upstream/6.01'
Update to upstream version '6.01' with Debian dir 878f458f477ea3beb96e10fac4dd203394554c41
-rw-r--r--Build.PL8
-rw-r--r--Changes27
-rw-r--r--LICENSE6
-rw-r--r--MANIFEST9
-rw-r--r--META.json38
-rw-r--r--META.yml37
-rw-r--r--README12
-rw-r--r--README.md10
-rw-r--r--TODO5
-rwxr-xr-xbin/docknot88
-rw-r--r--cpanfile6
-rw-r--r--docs/docknot.yaml10
-rw-r--r--lib/App/DocKnot.pm19
-rw-r--r--lib/App/DocKnot/Command.pm20
-rw-r--r--lib/App/DocKnot/Config.pm30
-rw-r--r--lib/App/DocKnot/Dist.pm10
-rw-r--r--lib/App/DocKnot/Generate.pm7
-rw-r--r--lib/App/DocKnot/Release.pm268
-rw-r--r--lib/App/DocKnot/Spin.pm262
-rw-r--r--lib/App/DocKnot/Spin/Pointer.pm62
-rw-r--r--lib/App/DocKnot/Spin/RSS.pm137
-rw-r--r--lib/App/DocKnot/Spin/Sitemap.pm6
-rw-r--r--lib/App/DocKnot/Spin/Thread.pm167
-rw-r--r--lib/App/DocKnot/Spin/Versions.pm90
-rw-r--r--lib/App/DocKnot/Update.pm162
-rw-r--r--lib/App/DocKnot/Util.pm77
-rw-r--r--share/schema/config.yaml6
-rwxr-xr-xt/cli/errors.t10
-rwxr-xr-xt/cli/spin.t7
-rw-r--r--t/data/generate/docknot/output/thread8
-rw-r--r--t/data/perltidyrc5
-rw-r--r--t/data/spin/input/journal/.rss4
-rw-r--r--t/data/spin/update/input/module.rpod1
-rw-r--r--t/data/spin/update/input/readme.rpod2
-rw-r--r--t/data/spin/update/input/script.rpod3
-rw-r--r--t/data/spin/update/output/module.spin2
-rw-r--r--t/data/spin/update/output/readme.spin6
-rw-r--r--t/data/spin/update/output/script.spin3
-rwxr-xr-xt/dist/basic.t7
-rw-r--r--t/lib/Test/DocKnot/Spin.pm6
-rw-r--r--t/lib/Test/RRA.pm2
-rw-r--r--t/lib/Test/RRA/Config.pm2
-rw-r--r--t/lib/Test/RRA/ModuleVersion.pm12
-rwxr-xr-xt/release/basic.t116
-rwxr-xr-xt/spin/errors.t2
-rwxr-xr-xt/spin/sitemap.t2
-rwxr-xr-xt/spin/tree.t26
-rwxr-xr-xt/spin/versions.t6
-rwxr-xr-xt/update/spin.t65
49 files changed, 1389 insertions, 487 deletions
diff --git a/Build.PL b/Build.PL
index 1304468..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"),
@@ -74,9 +74,11 @@ my $build = Module::Build->new(
'JSON::MaybeXS' => 0,
'Kwalify' => 0,
'List::SomeUtils' => '0.07',
- 'Path::Tiny' => 0,
+ 'Path::Iterator::Rule' => 0,
+ 'Path::Tiny' => '0.101',
'Perl6::Slurp' => 0,
- 'Pod::Thread' => '3.00',
+ 'Pod::Thread' => '3.01',
+ 'Sort::Versions' => 0,
'Template' => 0,
'YAML::XS' => '0.81',
perl => '5.024',
diff --git a/Changes b/Changes
index cf11728..eebda3f 100644
--- a/Changes
+++ b/Changes
@@ -1,6 +1,31 @@
Revision history for DocKnot
-6.00 - Not Released
+6.01 - 2022-01-15
+
+ - 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
+ pointer files to *.spin pointer files.
+
+ - docknot spin now uses Path::Iterator::Rule and Path::Tiny to construct
+ its paths, which eliminates the need to change the working directory
+ while processing input files.
+
+ - Fix spurious requirement for a package metadata file when running
+ docknot spin.
+
+ - Don't overwrite output files from docknot generate or generate-all
+ if the generation fails.
+
+ - Require Pod::Thread 3.01 or later.
+
+6.00 - 2021-12-25
- Add a new *.spin input file for docknot spin that points to an external
file with instructions for how to convert it to HTML. Via this
diff --git a/LICENSE b/LICENSE
index 5aa6925..f738b34 100644
--- a/LICENSE
+++ b/LICENSE
@@ -9,8 +9,8 @@ Comment: This file documents the copyright statements and licenses for
Files: *
Copyright: 1993-1994, 1996-1998, 2000, 2002-2014
The Board of Trustees of the Leland Stanford Junior University
- 2000-2004, 2006-2021 Russ Allbery <eagle@eyrie.org>
- 1999-2011, 2013-2021 Russ Allbery <rra@cpan.org>
+ 2000-2004, 2006-2022 Russ Allbery <eagle@eyrie.org>
+ 1999-2011, 2013-2022 Russ Allbery <rra@cpan.org>
License: Expat
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
@@ -34,7 +34,7 @@ License: Expat
Files: t/data/perltidyrc
Copyright: 2012-2013
The Board of Trustees of the Leland Stanford Junior University
- 2021 Russ Allbery <eagle@eyrie.org>
+ 2021-2022 Russ Allbery <eagle@eyrie.org>
License: all-permissive
Copying and distribution of this file, with or without modification, are
permitted in any medium without royalty provided the copyright notice and
diff --git a/MANIFEST b/MANIFEST
index db727c5..466bdb9 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -11,6 +11,7 @@ lib/App/DocKnot/Command.pm
lib/App/DocKnot/Config.pm
lib/App/DocKnot/Dist.pm
lib/App/DocKnot/Generate.pm
+lib/App/DocKnot/Release.pm
lib/App/DocKnot/Spin.pm
lib/App/DocKnot/Spin/Pointer.pm
lib/App/DocKnot/Spin/RSS.pm
@@ -139,6 +140,12 @@ t/data/spin/sitemap/complex
t/data/spin/sitemap/complex.html
t/data/spin/sitemap/duplicate
t/data/spin/sitemap/invalid
+t/data/spin/update/input/module.rpod
+t/data/spin/update/input/readme.rpod
+t/data/spin/update/input/script.rpod
+t/data/spin/update/output/module.spin
+t/data/spin/update/output/readme.spin
+t/data/spin/update/output/script.spin
t/data/spin/versions/continuation
t/data/spin/versions/invalid-continuation
t/data/spin/versions/invalid-date
@@ -235,6 +242,7 @@ t/lib/Test/RRA.pm
t/lib/Test/RRA/Config.pm
t/lib/Test/RRA/ModuleVersion.pm
t/metadata/licenses.t
+t/release/basic.t
t/spin/errors.t
t/spin/file.t
t/spin/markdown.t
@@ -249,6 +257,7 @@ t/style/module-version.t
t/style/obsolete-strings.t
t/style/strict.t
t/update/basic.t
+t/update/spin.t
TODO
META.yml
META.json
diff --git a/META.json b/META.json
index 8089af5..897c313 100644
--- a/META.json
+++ b/META.json
@@ -33,9 +33,11 @@
"JSON::MaybeXS" : "0",
"Kwalify" : "0",
"List::SomeUtils" : "0.07",
- "Path::Tiny" : "0",
+ "Path::Iterator::Rule" : "0",
+ "Path::Tiny" : "0.101",
"Perl6::Slurp" : "0",
- "Pod::Thread" : "3.00",
+ "Pod::Thread" : "3.01",
+ "Sort::Versions" : "0",
"Template" : "0",
"YAML::XS" : "0.81",
"perl" : "5.024"
@@ -51,55 +53,59 @@
"provides" : {
"App::DocKnot" : {
"file" : "lib/App/DocKnot.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Command" : {
"file" : "lib/App/DocKnot/Command.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Config" : {
"file" : "lib/App/DocKnot/Config.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Dist" : {
"file" : "lib/App/DocKnot/Dist.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Generate" : {
"file" : "lib/App/DocKnot/Generate.pm",
- "version" : "6.00"
+ "version" : "6.01"
+ },
+ "App::DocKnot::Release" : {
+ "file" : "lib/App/DocKnot/Release.pm",
+ "version" : "6.01"
},
"App::DocKnot::Spin" : {
"file" : "lib/App/DocKnot/Spin.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Spin::Pointer" : {
"file" : "lib/App/DocKnot/Spin/Pointer.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Spin::RSS" : {
"file" : "lib/App/DocKnot/Spin/RSS.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Spin::Sitemap" : {
"file" : "lib/App/DocKnot/Spin/Sitemap.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Spin::Thread" : {
"file" : "lib/App/DocKnot/Spin/Thread.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Spin::Versions" : {
"file" : "lib/App/DocKnot/Spin/Versions.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Update" : {
"file" : "lib/App/DocKnot/Update.pm",
- "version" : "6.00"
+ "version" : "6.01"
},
"App::DocKnot::Util" : {
"file" : "lib/App/DocKnot/Util.pm",
- "version" : "6.00"
+ "version" : "6.01"
}
},
"release_status" : "stable",
@@ -117,6 +123,6 @@
"web" : "https://github.com/rra/docknot"
}
},
- "version" : "6.00",
+ "version" : "6.01",
"x_serialization_backend" : "JSON::PP version 4.04"
}
diff --git a/META.yml b/META.yml
index 49223c5..7f5a5c6 100644
--- a/META.yml
+++ b/META.yml
@@ -17,43 +17,46 @@ name: App-DocKnot
provides:
App::DocKnot:
file: lib/App/DocKnot.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Command:
file: lib/App/DocKnot/Command.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Config:
file: lib/App/DocKnot/Config.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Dist:
file: lib/App/DocKnot/Dist.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Generate:
file: lib/App/DocKnot/Generate.pm
- version: '6.00'
+ version: '6.01'
+ App::DocKnot::Release:
+ file: lib/App/DocKnot/Release.pm
+ version: '6.01'
App::DocKnot::Spin:
file: lib/App/DocKnot/Spin.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Spin::Pointer:
file: lib/App/DocKnot/Spin/Pointer.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Spin::RSS:
file: lib/App/DocKnot/Spin/RSS.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Spin::Sitemap:
file: lib/App/DocKnot/Spin/Sitemap.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Spin::Thread:
file: lib/App/DocKnot/Spin/Thread.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Spin::Versions:
file: lib/App/DocKnot/Spin/Versions.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Update:
file: lib/App/DocKnot/Update.pm
- version: '6.00'
+ version: '6.01'
App::DocKnot::Util:
file: lib/App/DocKnot/Util.pm
- version: '6.00'
+ version: '6.01'
requires:
Date::Parse: '0'
File::BaseDir: '0'
@@ -67,9 +70,11 @@ requires:
JSON::MaybeXS: '0'
Kwalify: '0'
List::SomeUtils: '0.07'
- Path::Tiny: '0'
+ Path::Iterator::Rule: '0'
+ Path::Tiny: '0.101'
Perl6::Slurp: '0'
- Pod::Thread: '3.00'
+ Pod::Thread: '3.01'
+ Sort::Versions: '0'
Template: '0'
YAML::XS: '0.81'
perl: '5.024'
@@ -78,5 +83,5 @@ resources:
homepage: https://www.eyrie.org/~eagle/software/docknot
license: http://www.opensource.org/licenses/mit-license.php
repository: https://github.com/rra/docknot.git
-version: '6.00'
+version: '6.01'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/README b/README
index cf25bbe..e0df429 100644
--- a/README
+++ b/README
@@ -1,8 +1,8 @@
- DocKnot 6.00
+ DocKnot 6.01
(Static web site and documentation generator)
Maintained by Russ Allbery <rra@cpan.org>
- Copyright 1999-2021 Russ Allbery <rra@cpan.org>. This software is
+ Copyright 1999-2022 Russ Allbery <rra@cpan.org>. This software is
distributed under a BSD-style license. Please see the section LICENSE
below for more information.
@@ -64,9 +64,11 @@ REQUIREMENTS
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
- * Path::Tiny
+ * Path::Iterator::Rule
+ * Path::Tiny 0.101 or later
* Perl6::Slurp
- * Pod::Thread 3.00 or later
+ * Pod::Thread 3.01 or later
+ * Sort::Versions
* Template (part of Template Toolkit)
* YAML::XS 0.81 or later
@@ -158,7 +160,7 @@ LICENSE
The DocKnot package as a whole is covered by the following copyright
statement and license:
- Copyright 1999-2021 Russ Allbery <rra@cpan.org>
+ Copyright 1999-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
diff --git a/README.md b/README.md
index f430882..f5db663 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ version](https://img.shields.io/cpan/v/App-DocKnot)](https://metacpan.org/releas
[![Debian
package](https://img.shields.io/debian/v/docknot/unstable)](https://tracker.debian.org/pkg/docknot)
-Copyright 1999-2021 Russ Allbery <rra@cpan.org>. This software is
+Copyright 1999-2022 Russ Allbery <rra@cpan.org>. This software is
distributed under a BSD-style license. Please see the section
[License](#license) below for more information.
@@ -69,9 +69,11 @@ The following additional Perl modules are required to use it:
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
-* Path::Tiny
+* Path::Iterator::Rule
+* Path::Tiny 0.101 or later
* Perl6::Slurp
-* Pod::Thread 3.00 or later
+* Pod::Thread 3.01 or later
+* Sort::Versions
* Template (part of Template Toolkit)
* YAML::XS 0.81 or later
@@ -159,7 +161,7 @@ requests are gratefully reviewed and normally accepted.
The DocKnot package as a whole is covered by the following copyright
statement and license:
-> Copyright 1999-2021
+> Copyright 1999-2022
> Russ Allbery <rra@cpan.org>
>
> Permission is hereby granted, free of charge, to any person obtaining a
diff --git a/TODO b/TODO
index 26f6e77..4307de5 100644
--- a/TODO
+++ b/TODO
@@ -21,9 +21,6 @@ Metadata required for web pages:
Metadata required for release:
-* Path to Git repository
-* Path to publication directory (from URL?)
-* GPG key IDs for package signing
* License overrides for build-license
* Default license for build-license
@@ -40,10 +37,8 @@ DocKnot itself:
* Pull the version number of a package from a single source
* Pull the synopsis for a Perl module from the Perl build system
* Document the metadata format
-* Incorporate spin into this package (and rewrite as Perl modules)
* Incorporate cvs2xhtml into this package
* Incorporate faq2html into this package (and rewrite as a Perl module)
-* Incorporate release into this package
* Incorporate build-license into this package (and rewrite as a Perl module)
* Automatically run build-license during the release process
* Add metadata files and overrides to build-license
diff --git a/bin/docknot b/bin/docknot
index c9e3f12..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>
@@ -47,6 +49,8 @@ B<docknot> spin-thread [B<-f>] [B<-s> I<url>] [I<source> [I<output>]]
B<docknot> update [B<-m> I<metadata>] [B<-o> I<output>]
+B<docknot> update-spin [I<path>]
+
=head1 DESCRIPTION
B<docknot> is a static web site generator with special support for managing
@@ -77,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
@@ -97,11 +106,19 @@ Like C<spin>, but convert a single file written in thread to HTML.
Update the DocKnot package configuration from an older format.
+=item update-spin
+
+Update an input tree for C<spin> to the latest expectations. This will, for
+example, convert old-style F<*.rpod> pointer files to new-style F<*.spin>
+pointer files.
+
=back
=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
@@ -129,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>
@@ -151,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>
@@ -182,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>
@@ -193,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
@@ -206,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>
@@ -285,6 +328,17 @@ recommended metadata path for a project).
=back
+=head2 update-spin
+
+=over 4
+
+=item I<path>
+
+The path to the spin input tree to update. If not given, defaults to the
+current directory.
+
+=back
+
=head1 DIAGNOSTICS
If B<docknot> fails with errors, see the underlying module for that subcommand
@@ -320,7 +374,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2016, 2018-2021 Russ Allbery <rra@cpan.org>
+Copyright 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"), to deal
diff --git a/cpanfile b/cpanfile
index 22a0136..2bb9b3e 100644
--- a/cpanfile
+++ b/cpanfile
@@ -12,9 +12,11 @@ requires 'IPC::System::Simple';
requires 'JSON::MaybeXS';
requires 'Kwalify';
requires 'List::SomeUtils', '0.07';
-requires 'Path::Tiny';
+requires 'Path::Iterator::Rule';
+requires 'Path::Tiny', '0.101';
requires 'Perl6::Slurp';
-requires 'Pod::Thread', '3.00';
+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 becc165..0103185 100644
--- a/docs/docknot.yaml
+++ b/docs/docknot.yaml
@@ -15,14 +15,14 @@ format: v1
name: DocKnot
maintainer: Russ Allbery <rra@cpan.org>
-version: '6.00'
+version: '6.01'
synopsis: Static web site and documentation generator
license:
name: Expat
copyrights:
- holder: Russ Allbery <rra@cpan.org>
- years: 1999-2021
+ years: 1999-2022
build:
type: Module::Build
@@ -141,9 +141,11 @@ requirements: |
* JSON::MaybeXS
* Kwalify
* List::SomeUtils 0.07 or later
- * Path::Tiny
+ * Path::Iterator::Rule
+ * Path::Tiny 0.101 or later
* Perl6::Slurp
- * Pod::Thread 3.00 or later
+ * 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.pm b/lib/App/DocKnot.pm
index 721bb30..c288ed9 100644
--- a/lib/App/DocKnot.pm
+++ b/lib/App/DocKnot.pm
@@ -11,7 +11,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot 6.00;
+package App::DocKnot 6.01;
use 5.024;
use autodie;
@@ -70,10 +70,21 @@ sub load_yaml_file {
# data elements set to false.
local $YAML::XS::Boolean = 'JSON::PP';
- # Load the metadata and check it against the schema.
- my $data_ref = YAML::XS::LoadFile($path);
+ # Load the metadata and check it against the schema. YAML::XS for some
+ # reason puts a newline before the system error part of an error message
+ # when loading a file, so clean up the error a bit.
my $schema_path = $self->appdata_path('schema', $schema . '.yaml');
- my $schema_ref = YAML::XS::LoadFile($schema_path);
+ my ($data_ref, $schema_ref);
+ eval {
+ $data_ref = YAML::XS::LoadFile($path);
+ $schema_ref = YAML::XS::LoadFile($schema_path);
+ };
+ if ($@) {
+ my $error = lcfirst($@);
+ chomp($error);
+ $error =~ s{ \n }{ }xms;
+ die "$error\n";
+ }
eval { validate($schema_ref, $data_ref) };
if ($@) {
my $errors = $@;
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm
index 7ef45e5..f032ab6 100644
--- a/lib/App/DocKnot/Command.pm
+++ b/lib/App/DocKnot/Command.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Command 6.00;
+package App::DocKnot::Command 6.01;
use 5.024;
use autodie;
@@ -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',
@@ -104,6 +110,11 @@ our %COMMANDS = (
options => ['metadata|m=s', 'output|o=s'],
maximum => 0,
},
+ 'update-spin' => {
+ method => 'update_spin',
+ module => 'App::DocKnot::Update',
+ maximum => 1,
+ },
);
##############################################################################
@@ -297,8 +308,9 @@ Perl 5.24 or later and the modules Date::Language, Date::Parse (both part of
TimeDate), File::BaseDir, File::ShareDir, Git::Repository, Image::Size,
IO::Compress::Xz (part of IO-Compress-Lzma), IO::Uncompress::Gunzip (part of
IO-Compress), IPC::Run, IPC::System::Simple, JSON::MaybeXS, Kwalify,
-List::SomeUtils, Path::Tiny, Perl6::Slurp, Template (part of Template
-Toolkit), and YAML::XS, all of which are available from CPAN.
+List::SomeUtils, Path::Iterator::Rule, Path::Tiny, Perl6::Slurp, Template
+(part of Template Toolkit), and YAML::XS, all of which are available from
+CPAN.
=head1 DESCRIPTION
@@ -337,7 +349,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2018-2021 Russ Allbery <rra@cpan.org>
+Copyright 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"), to deal
diff --git a/lib/App/DocKnot/Config.pm b/lib/App/DocKnot/Config.pm
index 2eebe32..6b6716d 100644
--- a/lib/App/DocKnot/Config.pm
+++ b/lib/App/DocKnot/Config.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Config 6.00;
+package App::DocKnot::Config 6.01;
use 5.024;
use autodie;
@@ -31,18 +31,9 @@ use YAML::XS ();
# metadata - Path to the docknot.yaml file
#
# Returns: Newly created object
-# Throws: Text exceptions on invalid metadata directory path
sub new {
my ($class, $args_ref) = @_;
-
- # Ensure we were given a valid metadata argument.
- my $metadata = $args_ref->{metadata} // 'docs/docknot.yaml';
- if (!-e $metadata) {
- croak("metadata path $metadata does not exist");
- }
-
- # Create and return the object.
- my $self = { metadata => $metadata };
+ my $self = { metadata => $args_ref->{metadata} // 'docs/docknot.yaml' };
bless($self, $class);
return $self;
}
@@ -116,7 +107,7 @@ __END__
=for stopwords
Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG Kwalify
-distdir
+distdir archivedir
=head1 NAME
@@ -179,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
@@ -186,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
@@ -237,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..ccbeff4 100644
--- a/lib/App/DocKnot/Dist.pm
+++ b/lib/App/DocKnot/Dist.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Dist 6.00;
+package App::DocKnot::Dist 6.01;
use 5.024;
use autodie;
@@ -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/Generate.pm b/lib/App/DocKnot/Generate.pm
index 8caa7a8..cab4365 100644
--- a/lib/App/DocKnot/Generate.pm
+++ b/lib/App/DocKnot/Generate.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Generate 6.00;
+package App::DocKnot::Generate 6.01;
use 5.024;
use autodie;
@@ -18,6 +18,7 @@ use parent qw(App::DocKnot);
use warnings;
use App::DocKnot::Config;
+use App::DocKnot::Util qw(print_fh);
use Carp qw(croak);
use Encode qw(encode);
use Template;
@@ -525,9 +526,9 @@ sub generate_output {
}
# Generate the output.
+ my $data = $self->generate($template);
open(my $outfh, '>', $output);
- print {$outfh} encode('utf-8', $self->generate($template))
- or croak("cannot write to $output: $!");
+ print_fh($outfh, $output, encode('utf-8', $data));
close($outfh);
return;
}
diff --git a/lib/App/DocKnot/Release.pm b/lib/App/DocKnot/Release.pm
new file mode 100644
index 0000000..b5d8d83
--- /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.01;
+
+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.pm b/lib/App/DocKnot/Spin.pm
index 73666e0..5f4632e 100644
--- a/lib/App/DocKnot/Spin.pm
+++ b/lib/App/DocKnot/Spin.pm
@@ -11,7 +11,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin 6.00;
+package App::DocKnot::Spin 6.01;
use 5.024;
use autodie;
@@ -23,15 +23,11 @@ use App::DocKnot::Spin::Sitemap;
use App::DocKnot::Spin::Thread;
use App::DocKnot::Spin::Versions;
use App::DocKnot::Util qw(is_newer print_checked print_fh);
-use Carp qw(croak);
-use Cwd qw(getcwd realpath);
-use File::Basename qw(fileparse);
-use File::Copy qw(copy);
-use File::Find qw(find finddepth);
-use File::Spec ();
use Git::Repository ();
use IPC::System::Simple qw(capture);
-use Pod::Thread 3.00 ();
+use Path::Iterator::Rule ();
+use Path::Tiny qw(path);
+use Pod::Thread 3.01 ();
use POSIX qw(strftime);
# The default list of files and/or directories to exclude from spinning. This
@@ -53,8 +49,8 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/';
# Build te page footer, which consists of the navigation links, the regular
# signature, and the last modified date.
#
-# $source - Full path to the source file
-# $out_path - Full path to the output file
+# $source - Path::Tiny path to the source file
+# $out_path - Path::Tiny path to the output file
# $id - CVS Id of the source file or undef if not known
# @templates - Two templates to use. The first will be used if the
# modification and current dates are the same, and the second
@@ -67,15 +63,14 @@ sub _footer {
my ($self, $source, $out_path, $id, @templates) = @_;
my $output = q{};
my $in_tree = 0;
- if ($self->{source} && $source =~ m{ \A \Q$self->{source}\E }xms) {
+ if ($self->{source} && $self->{source}->subsumes($source)) {
$in_tree = 1;
}
# Add the end-of-page navbar if we have sitemap information.
if ($self->{sitemap} && $self->{output}) {
- my $page = $out_path;
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
+ my $page = $out_path->relative($self->{output});
+ $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n";
}
# Figure out the modification dates. Use the RCS/CVS Id if available,
@@ -88,13 +83,13 @@ sub _footer {
}
} elsif ($self->{repository} && $in_tree) {
$modified
- = $self->{repository}->run('log', '-1', '--format=%ct', $source);
+ = $self->{repository}->run('log', '-1', '--format=%ct', "$source");
if ($modified) {
$modified = strftime('%Y-%m-%d', gmtime($modified));
}
}
if (!$modified) {
- $modified = strftime('%Y-%m-%d', gmtime((stat $source)[9]));
+ $modified = strftime('%Y-%m-%d', gmtime($source->stat()->[9]));
}
my $now = strftime('%Y-%m-%d', gmtime());
@@ -121,9 +116,8 @@ sub _footer {
# the output of an external converter.
sub _write_converter_output {
my ($self, $page_ref, $output, $footer) = @_;
- my $page = $output;
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- open(my $out_fh, '>', $output);
+ my $page = $output->relative($self->{output});
+ my $out_fh = $output->openw_utf8();
# Grab the first few lines of input, looking for a blurb and Id string.
# Give up if we encounter <body> first. Also look for a </head> tag and
@@ -211,7 +205,8 @@ sub _cvs2xhtml {
$style ||= $self->{style_url} . 'cvs.css';
# Separate the source file into a directory and filename.
- my ($name, $dir) = fileparse($source);
+ my $name = $source->basename();
+ my $dir = $source->parent();
# Construct the options to cvs2xhtml.
if ($options !~ m{ -n [ ] }xms) {
@@ -274,14 +269,12 @@ sub _pod2html {
# Grab the thread output.
my $data;
$podthread->output_string(\$data);
- $podthread->parse_file($source);
+ $podthread->parse_file("$source");
# Spin that thread into HTML.
my $page = $self->{thread}->spin_thread($data);
# Push the result through _write_converter_output.
- my $file = $source;
- $file =~ s{ [.] [^.]+ \z }{.html}xms;
my $footer = sub {
my ($blurb) = @_;
my $link = '<a href="%URL%">spun</a>';
@@ -305,7 +298,7 @@ sub _pod2html {
# Given a pointer file, read the master file name and any options, returning
# them as a list with the newlines chomped off.
#
-# $file - The path to the file to read
+# $file - Path::Tiny for the file to read
#
# Returns: List of the master file, any command-line options, and the style
# sheet to use, as strings
@@ -315,11 +308,7 @@ sub _read_pointer {
my ($self, $file) = @_;
# Read the pointer file.
- open(my $pointer, '<', $file);
- my $master = <$pointer>;
- my $options = <$pointer>;
- my $style = <$pointer>;
- close($pointer);
+ my ($master, $options, $style) = $file->lines_utf8();
# Clean up the contents.
if (!$master) {
@@ -339,29 +328,42 @@ sub _read_pointer {
return ($master, $options, $style);
}
-# This routine is called by File::Find for every file in the source tree. It
-# decides what to do with each file, whether spinning it or copying it.
+# Convert an input path to an output path.
+#
+# $input - Path::Tiny input path
+# $extension - If given, remove this extension and add .html in its place
+sub _output_for_file {
+ my ($self, $input, $extension) = @_;
+ my $output = $input->relative($self->{source})->absolute($self->{output});
+ if ($extension) {
+ my $output_file = $input->basename($extension) . '.html';
+ $output = $output->sibling($output_file);
+ }
+ return $output;
+}
+
+# Report an action to standard output.
+#
+# $action - String description of the action
+# $output - Output file generated
+sub _report_action {
+ my ($self, $action, $output) = @_;
+ my $shortout = $output->relative($self->{output});
+ print_checked("$action .../$shortout\n");
+ return;
+}
+
+# This routine is called for every file in the source tree. It decides what
+# to do with each file, whether spinning it or copying it.
+#
+# $input - Path::Tiny path to the input file
#
# Throws: Text exception on any processing error
# autodie exception if files could not be accessed or written
#
## no critic (Subroutines::ProhibitExcessComplexity)
sub _process_file {
- my ($self) = @_;
- my $file = $_;
- return if $file eq q{.};
- for my $regex ($self->{excludes}->@*) {
- if ($file =~ m{$regex}xms) {
- $File::Find::prune = 1;
- return;
- }
- }
- my $input = $File::Find::name;
- my $output = $input;
- $output =~ s{ \A \Q$self->{source}\E }{$self->{output}}xms
- or die "input file $file out of tree\n";
- my $shortout = $output;
- $shortout =~ s{ \A \Q$self->{output}\E }{...}xms;
+ my ($self, $input) = @_;
# Conversion rules for pointers. The key is the extension, the first
# value is the name of the command for the purposes of output, and the
@@ -376,87 +378,84 @@ sub _process_file {
#>>>
# Figure out what to do with the input.
- if (-d $file) {
- $self->{generated}{$output} = 1;
- if (-e $output && !-d $output) {
+ if ($input->is_dir()) {
+ my $output = $self->_output_for_file($input);
+ $self->{generated}{"$output"} = 1;
+ if ($output->exists() && !$output->is_dir()) {
die "cannot replace $output with a directory\n";
- } elsif (!-d $output) {
- print_checked("Creating $shortout\n");
- mkdir($output, 0755);
+ } elsif (!$output->is_dir()) {
+ $self->_report_action('Creating', $output);
+ $output->mkpath();
}
- my $rss_path = File::Spec->catfile($file, '.rss');
- if (-e $rss_path) {
- $self->{rss}->generate($rss_path, $file);
+ my $rss_path = path($input, '.rss');
+ if ($rss_path->exists()) {
+ $self->{rss}->generate("$rss_path", "$input");
}
- } elsif ($file =~ m{ [.] spin \z }xms) {
- $output =~ s{ [.] spin \z }{.html}xms;
- $shortout =~ s{ [.] spin \z }{.html}xms;
- $self->{generated}{$output} = 1;
- if ($self->{pointer}->is_out_of_date($input, $output)) {
- print_checked("Converting $shortout\n");
- $self->{pointer}->spin_pointer($input, $output);
+ } elsif ($input->basename() =~ m{ [.] spin \z }xms) {
+ my $output = $self->_output_for_file($input, '.spin');
+ $self->{generated}{"$output"} = 1;
+ if ($self->{pointer}->is_out_of_date("$input", "$output")) {
+ $self->_report_action('Converting', $output);
+ $self->{pointer}->spin_pointer("$input", "$output");
}
- } elsif ($file =~ m{ [.] th \z }xms) {
- $output =~ s{ [.] th \z }{.html}xms;
- $shortout =~ s{ [.] th \z }{.html}xms;
- $self->{generated}{$output} = 1;
+ } elsif ($input->basename() =~ m{ [.] th \z }xms) {
+ my $output = $self->_output_for_file($input, '.th');
+ $self->{generated}{"$output"} = 1;
# See if we're forced to regenerate the file because it is affected by
# a software release.
- if (-e $output && $self->{versions}) {
- my $relative = $input;
- $relative =~ s{ ^ \Q$self->{source}\E / }{}xms;
- my $time = $self->{versions}->latest_release($relative);
- return if is_newer($output, $file) && (stat($output))[9] >= $time;
+ if ($output->exists() && $self->{versions}) {
+ my $relative = $input->relative($self->{source});
+ my $time = $self->{versions}->latest_release("$relative");
+ return
+ if is_newer("$output", "$input")
+ && $output->stat()->[9] >= $time;
} else {
- return if is_newer($output, $file);
+ return if is_newer("$output", "$input");
}
# The output file is not newer. Respin it.
- print_checked("Spinning $shortout\n");
+ $self->_report_action('Spinning', $output);
$self->{thread}->spin_thread_file($input, $output);
} else {
- my ($extension) = ($file =~ m{ [.] ([^.]+) \z }xms);
+ my ($extension) = ($input->basename =~ m{ [.] ([^.]+) \z }xms);
if (defined($extension) && $rules{$extension}) {
my ($name, $sub) = $rules{$extension}->@*;
- $output =~ s{ [.] \Q$extension\E \z }{.html}xms;
- $shortout =~ s{ [.] \Q$extension\E \z }{.html}xms;
- $self->{generated}{$output} = 1;
+ my $output = $self->_output_for_file($input, $extension);
+ $self->{generated}{"$output"} = 1;
my ($source, $options, $style) = $self->_read_pointer($input);
return if is_newer($output, $input, $source);
- print_checked("Running $name for $shortout\n");
+ $self->_report_action("Running $name for", $output);
$self->$sub($source, $output, $options, $style);
} else {
- $self->{generated}{$output} = 1;
- return if is_newer($output, $file);
- print_checked("Updating $shortout\n");
- copy($file, $output)
- or die "copy of $input to $output failed: $!\n";
+ my $output = $self->_output_for_file($input);
+ $self->{generated}{"$output"} = 1;
+ return if is_newer("$output", "$input");
+ $self->_report_action('Updating', $output);
+ $input->copy($output);
}
}
return;
}
## use critic
-# This routine is called by File::Find for every file in the destination tree
-# in depth-first order, if the user requested file deletion of files not
-# generated from the source tree. It checks each file to see if it is in the
-# $self->{generated} hash that was generated during spin processing, and if
-# not, removes it.
+# This routine is called for every file in the destination tree in depth-first
+# order, if the user requested file deletion of files not generated from the
+# source tree. It checks each file to see if it is in the $self->{generated}
+# hash that was generated during spin processing, and if not, removes it.
+#
+# $file - Path::Tiny path to the file
#
# Throws: autodie exception on failure of rmdir or unlink
sub _delete_files {
- my ($self) = @_;
- return if $_ eq q{.};
- my $file = $File::Find::name;
- return if $self->{generated}{$file};
- my $shortfile = $file;
- $shortfile =~ s{ ^ \Q$self->{output}\E }{...}xms;
- print_checked("Deleting $shortfile\n");
- if (-d $file) {
+ my ($self, $file) = @_;
+ return if $self->{generated}{"$file"};
+ my $shortfile = $file->relative($self->{output});
+ print_checked("Deleting .../$shortfile\n");
+ if ($file->is_dir()) {
rmdir($file);
} else {
- unlink($file);
+ $file->remove();
}
return;
}
@@ -495,7 +494,6 @@ sub new {
my $self = {
delete => $args_ref->{delete},
excludes => [@excludes],
- rss => App::DocKnot::Spin::RSS->new(),
style_url => $style_url,
};
#>>>
@@ -514,44 +512,47 @@ sub spin {
# Reset data from a previous run.
delete $self->{repository};
+ delete $self->{rss};
delete $self->{sitemap};
delete $self->{versions};
# Canonicalize and check input.
- $input = realpath($input) or die "cannot canonicalize $input: $!\n";
- if (!-d $input) {
+ $input = path($input)->realpath();
+ if (!$input->is_dir()) {
die "input tree $input must be a directory\n";
}
$self->{source} = $input;
# Canonicalize and check output.
- if (!-d $output) {
- print_checked("Creating $output\n");
- mkdir($output, 0755);
+ $output = path($output);
+ if (!$output->is_dir()) {
+ for my $created ($output->mkpath()) {
+ print_checked("Creating $created\n");
+ }
}
- $output = realpath($output) or die "cannot canonicalize $output: $!\n";
+ $output = $output->realpath();
$self->{output} = $output;
# Read metadata from the top of the input directory.
- my $sitemap_path = File::Spec->catfile($input, '.sitemap');
- if (-e $sitemap_path) {
- $self->{sitemap} = App::DocKnot::Spin::Sitemap->new($sitemap_path);
+ my $sitemap_path = $input->child('.sitemap');
+ if ($sitemap_path->exists()) {
+ $self->{sitemap} = App::DocKnot::Spin::Sitemap->new("$sitemap_path");
}
- my $versions_path = File::Spec->catfile($input, '.versions');
- if (-e $versions_path) {
+ my $versions_path = $input->child('.versions');
+ if ($versions_path->exists()) {
$self->{versions} = App::DocKnot::Spin::Versions->new($versions_path);
}
- if (-d File::Spec->catdir($input, '.git')) {
+ if ($input->child('.git')->is_dir()) {
$self->{repository} = Git::Repository->new(work_tree => $input);
}
+ # Create a new RSS generator object.
+ $self->{rss} = App::DocKnot::Spin::RSS->new({ base => $input });
+
# Process an .rss file at the top of the tree, if present.
- my $rss_path = File::Spec->catfile($input, '.rss');
- if (-e $rss_path) {
- my $cwd = getcwd();
- chdir($input);
- $self->{rss}->generate($rss_path);
- chdir($cwd);
+ my $rss_path = $input->child('.rss');
+ if ($rss_path->exists()) {
+ $self->{rss}->generate("$rss_path", "$input");
}
# Create a new thread converter object.
@@ -571,7 +572,7 @@ sub spin {
#<<<
$self->{pointer} = App::DocKnot::Spin::Pointer->new(
{
- output => $output,
+ output => "$output",
sitemap => $self->{sitemap},
'style-url' => $self->{style_url},
thread => $self->{thread},
@@ -580,11 +581,20 @@ sub spin {
#>>>
# Process the input tree.
- my $preprocess = sub { my @files = sort(@_); return @files };
- my $wanted = sub { $self->_process_file(@_) };
- find({ preprocess => $preprocess, wanted => $wanted }, $input);
+ my $rule = Path::Iterator::Rule->new();
+ $rule = $rule->skip($rule->new()->name($self->{excludes}->@*));
+ my $iter = $rule->iter("$input", { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ $self->_process_file(path($file));
+ }
+
+ # Remove stray files from the output tree.
if ($self->{delete}) {
- finddepth(sub { $self->_delete_files(@_) }, $output);
+ my %options = (depthfirst => 1, follow_symlinks => 0);
+ $iter = $rule->iter("$output", \%options);
+ while (defined(my $file = $iter->())) {
+ $self->_delete_files(path($file));
+ }
}
return;
}
@@ -614,10 +624,10 @@ App::DocKnot::Spin - Static site builder supporting thread macro language
=head1 REQUIREMENTS
Perl 5.24 or later and the modules Git::Repository, Image::Size,
-List::SomeUtils, Path::Tiny, Pod::Thread, Template (part of Template Toolkit),
-and YAML::XS, all of which are available from CPAN. Also expects to find
-B<faq2html>, B<cvs2xhtml>, and B<cl2xhtml> on the user's PATH to convert
-certain types of files.
+List::SomeUtils, Path::Iterator::Rule, Path::Tiny, Pod::Thread, Template (part
+of Template Toolkit), and YAML::XS, all of which are available from CPAN.
+Also expects to find B<faq2html>, B<cvs2xhtml>, and B<cl2xhtml> on the user's
+PATH to convert certain types of files.
=head1 DESCRIPTION
@@ -730,7 +740,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/lib/App/DocKnot/Spin/Pointer.pm b/lib/App/DocKnot/Spin/Pointer.pm
index 05aa9ac..871e267 100644
--- a/lib/App/DocKnot/Spin/Pointer.pm
+++ b/lib/App/DocKnot/Spin/Pointer.pm
@@ -10,7 +10,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Pointer 6.00;
+package App::DocKnot::Spin::Pointer 6.01;
use 5.024;
use autodie;
@@ -18,12 +18,13 @@ use parent qw(App::DocKnot);
use warnings;
use App::DocKnot::Config;
-use App::DocKnot::Util qw(is_newer print_fh);
+use App::DocKnot::Util qw(is_newer);
use Carp qw(croak);
-use Encode qw(decode encode);
+use Encode qw(decode);
use File::BaseDir qw(config_files);
use IPC::System::Simple qw(capturex);
-use Kwalify qw(validate);
+use Path::Tiny qw(path);
+use Pod::Thread 3.01 ();
use POSIX qw(strftime);
use Template ();
use YAML::XS ();
@@ -41,12 +42,13 @@ my $URL = 'https://www.eyrie.org/~eagle/software/web/';
# $data_ref - Data from the pointer file
# path - Path to the Markdown file to convert
# style - Style sheet to use
+# $base - Base path of pointer file (for relative paths)
# $output - Path to the output file
#
# Throws: Text exception on conversion failure
sub _spin_markdown {
- my ($self, $data_ref, $output) = @_;
- my $source = $data_ref->{path};
+ my ($self, $data_ref, $base, $output) = @_;
+ my $source = path($data_ref->{path})->absolute($base);
# Do the Markdown conversion using pandoc.
my $html = capturex(
@@ -63,13 +65,12 @@ sub _spin_markdown {
# Construct the template variables.
my ($links, $navbar, $style);
if ($self->{sitemap}) {
- my $page = $output;
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- my @links = $self->{sitemap}->links($page);
+ my $page = $output->relative($self->{output});
+ my @links = $self->{sitemap}->links("/$page");
if (@links) {
$links = join(q{}, @links);
}
- my @navbar = $self->{sitemap}->navbar($page);
+ my @navbar = $self->{sitemap}->navbar("/$page");
if (@navbar) {
$navbar = join(q{}, @navbar);
}
@@ -82,7 +83,7 @@ sub _spin_markdown {
docknot_url => $URL,
html => decode('utf-8', $html),
links => $links,
- modified => strftime('%Y-%m-%d', gmtime((stat($source))[9])),
+ modified => strftime('%Y-%m-%d', gmtime($source->stat()->[9])),
navbar => $navbar,
now => strftime('%Y-%m-%d', gmtime()),
style => $style,
@@ -96,9 +97,7 @@ sub _spin_markdown {
or croak($self->{template}->error());
# Write the result to the output file.
- open(my $outfh, '>', $output);
- print_fh($outfh, $output, encode('utf-8', $result));
- close($outfh);
+ $output->spew_utf8($result);
return;
}
@@ -110,18 +109,20 @@ sub _spin_markdown {
# navbar - Whether to add a navigation bar
# path - Path to the POD file to convert
# style - Style sheet to use
+# $base - Base path of pointer file (for relative paths)
# $output - Path to the output file
#
# Throws: Text exception on conversion failure
sub _spin_pod {
- my ($self, $data_ref, $output) = @_;
- my $source = $data_ref->{path};
+ my ($self, $data_ref, $base, $output) = @_;
+ my $source = path($data_ref->{path})->absolute($base);
# Construct the Pod::Thread formatter object.
#<<<
my %options = (
contents => $data_ref->{options}{contents},
style => $data_ref->{style} // 'pod',
+ title => $data_ref->{title},
);
#<<<
if (exists($data_ref->{options}{navbar})) {
@@ -129,15 +130,12 @@ sub _spin_pod {
} else {
$options{navbar} = 1;
}
- if (exists($data_ref->{title})) {
- $options{title} = $data_ref->{title};
- }
my $podthread = Pod::Thread->new(%options);
# Convert the POD to thread.
my $data;
$podthread->output_string(\$data);
- $podthread->parse_file($source);
+ $podthread->parse_file("$source");
# Spin that page into HTML.
$self->{thread}->spin_thread_output($data, $source, 'POD', $output);
@@ -192,8 +190,8 @@ sub new {
# Check if the result of a pointer file needs to be regenerated.
#
-# $pointer - Pointer file to process
-# $output - Corresponding output path
+# $pointer - Path to pointer file
+# $output - Path to corresponding output file
#
# Returns: True if the output file does not exist or has a modification date
# older than either the pointer file or the underlying source file,
@@ -201,31 +199,35 @@ sub new {
# Throws: YAML::XS exception on invalid pointer
sub is_out_of_date {
my ($self, $pointer, $output) = @_;
+ $pointer = path($pointer);
my $data_ref = $self->load_yaml_file($pointer, 'pointer');
- if (!-e $data_ref->{path}) {
- die "$pointer: path $data_ref->{path} does not exist\n";
+ my $path = path($data_ref->{path})->absolute($pointer->parent());
+ if (!$path->exists()) {
+ die "$pointer: path $data_ref->{path} ($path) does not exist\n";
}
- return !is_newer($output, $pointer, $data_ref->{path});
+ return !is_newer($output, $pointer, $path);
}
# Process a given pointer file.
#
-# $pointer - Pointer file to process
-# $output - Corresponding output path
+# $pointer - Path to pointer file to process
+# $output - Path to corresponding output file
#
# Throws: YAML::XS exception on invalid pointer
# Text exception for missing input file
# Text exception on failure to convert the file
sub spin_pointer {
my ($self, $pointer, $output, $options_ref) = @_;
+ $pointer = path($pointer);
+ $output = path($output);
my $data_ref = $self->load_yaml_file($pointer, 'pointer');
$data_ref->{options} //= {};
# Dispatch to the appropriate conversion function.
if ($data_ref->{format} eq 'markdown') {
- $self->_spin_markdown($data_ref, $output);
+ $self->_spin_markdown($data_ref, $pointer->parent(), $output);
} elsif ($data_ref->{format} eq 'pod') {
- $self->_spin_pod($data_ref, $output);
+ $self->_spin_pod($data_ref, $pointer->parent(), $output);
} else {
die "$pointer: unknown output format $data_ref->{format}\n";
}
@@ -263,7 +265,7 @@ App::DocKnot::Spin::Pointer - Generate HTML from a pointer to an external file
=head1 REQUIREMENTS
Perl 5.24 or later and the modules File::ShareDir, Kwalify, List::SomeUtils,
-Pod::Thread, and YAML::XS, all of which are available from CPAN.
+Path::Tiny, Pod::Thread, and YAML::XS, all of which are available from CPAN.
=head1 DESCRIPTION
diff --git a/lib/App/DocKnot/Spin/RSS.pm b/lib/App/DocKnot/Spin/RSS.pm
index 2a7460a..3f8fb70 100644
--- a/lib/App/DocKnot/Spin/RSS.pm
+++ b/lib/App/DocKnot/Spin/RSS.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::RSS 6.00;
+package App::DocKnot::Spin::RSS 6.01;
use 5.024;
use autodie;
@@ -18,10 +18,9 @@ use warnings;
use App::DocKnot;
use App::DocKnot::Spin::Thread;
use App::DocKnot::Util qw(print_checked print_fh);
-use Cwd qw(getcwd);
use Date::Language ();
use Date::Parse qw(str2time);
-use File::Basename qw(fileparse);
+use Path::Tiny qw(path);
use Perl6::Slurp qw(slurp);
use POSIX qw(strftime);
@@ -123,23 +122,30 @@ sub _relative_url {
return ('../' x scalar(@base)) . $url;
}
-# Spin a file into HTML, changing directories to the directory of that file so
-# that relative file references resolve correctly.
+# Spin a file into HTML.
#
-# $file - Path to the file
+# $file - Path::Tiny path to the file
#
# Returns: Rendered HTML as a list with one element per line
sub _spin_file {
my ($self, $file) = @_;
- my $source = slurp($file);
- my $cwd = getcwd();
- my (undef, $dir) = fileparse($file);
- chdir($dir);
- my $page = $self->{spin}->spin_thread($source);
- chdir($cwd);
+ my $source = $file->slurp_utf8();
+ my $page = $self->{spin}->spin_thread($source, $file);
return map { "$_\n" } split(m{ \n }xms, $page);
}
+# Report an action to standard output.
+#
+# $action - String description of the action
+# $output - Output file generated
+# $base - Base path for all output
+sub _report_action {
+ my ($self, $action, $output) = @_;
+ my $shortout = $output->relative($self->{base} // path());
+ print_checked("$action .../$shortout\n");
+ return;
+}
+
##############################################################################
# Parsing
##############################################################################
@@ -158,7 +164,7 @@ sub _read_rfc2822_file {
# Parse the file. $key holds the last key seen, used to append
# continuation values to the previous key. $current holds the current
# block being parsed and @blocks all blocks seen so far.
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
while (defined(my $line = <$fh>)) {
if ($line =~ m{ \A \s* \z }xms) {
if ($key) {
@@ -208,7 +214,7 @@ sub _read_rfc2822_file {
# the changes into the provided array reference. Each element of the array
# will be a hash with keys title, date, link, and description.
#
-# $file - File to read
+# $file - Path::Tiny path to file to read
#
# Returns: List of reference to metadata hash and reference to a list of
# hashes of changes
@@ -273,7 +279,7 @@ sub _parse_changes {
# Format a journal post into HTML for inclusion in an RSS feed. This depends
# heavily on my personal layout for journal posts.
#
-# $file - Path to the journal post
+# $file - Path::Tiny path to the journal post
#
# Returns: HTML suitable for including in an RSS feed
sub _rss_journal {
@@ -303,7 +309,7 @@ sub _rss_journal {
# Format a review into HTML for inclusion in an RSS feed. This depends even
# more heavily on my personal layout for review posts.
#
-# $file - Path to the review
+# $file - Path::Tiny path to the review
#
# Returns: HTML suitable for inclusion in an RSS feed
sub _rss_review {
@@ -373,12 +379,13 @@ sub _rss_review {
# time as <lastBuildDate>; it's not completely clear to me that this is
# correct.
#
-# $fh - Output file handle
-# $file - Name of the output file
+# $file - Path::Tiny path to the output file
+# $base - Base Path::Tiny path for input files
# $metadata_ref - Hash of metadata for the RSS feed
# $entries_ref - Array of entries in the RSS feed
sub _rss_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $base, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
my $version = '1.25';
# Determine the current date and latest publication date of all of the
@@ -405,7 +412,7 @@ sub _rss_output {
<generator>DocKnot $App::DocKnot::VERSION</generator>
EOC
if ($metadata_ref->{'rss-base'}) {
- my ($name) = fileparse($file);
+ my $name = $file->basename();
my $url = $metadata_ref->{'rss-base'} . $name;
print_fh(
$fh,
@@ -427,9 +434,11 @@ EOC
$description =~ s{ \A (\s*) }{$1<p>}xms;
$description =~ s{ \n* \z }{</p>\n}xms;
} elsif ($entry_ref->{journal}) {
- $description = $self->_rss_journal($entry_ref->{journal});
+ my $path = path($entry_ref->{journal})->absolute($base);
+ $description = $self->_rss_journal($path);
} elsif ($entry_ref->{review}) {
- $description = $self->_rss_review($entry_ref->{review});
+ my $path = path($entry_ref->{review})->absolute($base);
+ $description = $self->_rss_review($path);
}
# Make all relative URLs absolute.
@@ -464,6 +473,7 @@ EOC
# Close the RSS structure.
print_fh($fh, $file, " </channel>\n</rss>\n");
+ close($fh);
return;
}
@@ -473,12 +483,12 @@ EOC
# Print out the thread version of the recent changes list.
#
-# $fh - File handle to which to output
-# $file - Name of the file for error reporting
+# $file - Path::Tiny output path
# $metadata_ref - RSS feed metadata
# $entries_ref - Entries
sub _thread_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
# Page prefix.
if ($metadata_ref->{'thread-prefix'}) {
@@ -520,6 +530,7 @@ sub _thread_output {
# Print out the end of the page.
print_fh($fh, $file, "\\signature\n");
+ close($fh);
return;
}
@@ -529,12 +540,12 @@ sub _thread_output {
# Translate the thread of a journal entry for inclusion in an index page.
#
-# $file - Path to the journal entry
+# $file - Path::Tiny to the journal entry
#
# Returns: Thread to include in the index page
sub _index_journal {
my ($self, $file, $url) = @_;
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
# Skip to the first \h1 and exclude it.
while (defined(my $line = <$fh>)) {
@@ -558,7 +569,7 @@ sub _index_journal {
# Translate the thread of a book review for inclusion into an index page.
#
-# $file - Path to the book review
+# $file - Path::Tiny to the book review
#
# Returns: Thread to include in the index page
sub _index_review {
@@ -571,7 +582,7 @@ sub _index_review {
# Scan for the author information and save it. Handle the case where the
# \header or \edited line is continued on the next line.
- open(my $fh, '<', $file);
+ my $fh = $file->openr_utf8();
while (defined(my $line = <$fh>)) {
if ($line =~ m{ \\ (?:header|edited) \s* \[ $char+ \] \s* \z }xms) {
$line .= <$fh>;
@@ -619,12 +630,13 @@ sub _index_review {
# Print out the index version of the recent changes list.
#
-# $fh - File handle to which to output
-# $file - Name of the file for error reporting
+# $file - Path::Tiny path to the output file
+# $base - Base Path::Tiny path for input files
# $metadata_ref - RSS feed metadata
# $entries_ref - Entries
sub _index_output {
- my ($self, $fh, $file, $metadata_ref, $entries_ref) = @_;
+ my ($self, $file, $base, $metadata_ref, $entries_ref) = @_;
+ my $fh = $file->openw_utf8();
# Output the prefix.
if ($metadata_ref->{'index-prefix'}) {
@@ -640,9 +652,11 @@ sub _index_output {
# Get the text of the entry.
my $text;
if ($entry_ref->{journal}) {
- $text = $self->_index_journal($entry_ref->{journal});
+ my $path = path($entry_ref->{journal})->absolute($base);
+ $text = $self->_index_journal($path);
} elsif ($entry_ref->{review}) {
- $text = $self->_index_review($entry_ref->{review});
+ my $path = path($entry_ref->{review})->absolute($base);
+ $text = $self->_index_review($path);
} else {
die "unknown entry type\n";
}
@@ -674,6 +688,7 @@ sub _index_output {
print_fh($fh, $file, $metadata_ref->{'index-suffix'}, "\n");
}
print_fh($fh, $file, "\\signature\n");
+ close($fh);
return;
}
@@ -683,8 +698,8 @@ sub _index_output {
# Create a new RSS generator object.
#
-# $args - Anonymous hash of arguments with the following keys:
-# base - Base path for output files
+# $args_ref - Anonymous hash of arguments with the following keys:
+# base - Path::Tiny base path for output files
#
# Returns: Newly created object
sub new {
@@ -692,7 +707,7 @@ sub new {
# Create and return the object.
my $self = {
- base => $args_ref->{base},
+ base => defined($args_ref->{base}) ? path($args_ref->{base}) : undef,
spin => App::DocKnot::Spin::Thread->new(),
};
bless($self, $class);
@@ -701,14 +716,13 @@ sub new {
# Generate specified output files from an .rss input file.
#
-# $source - Path to the .rss file
-# $base - Optional base path for output
+# $source - Path::Tiny path to the .rss file
+# $base - Optional Path::Tiny base path for output
sub generate {
my ($self, $source, $base) = @_;
+ $source = path($source);
$base //= $self->{base};
- if ($base) {
- $base =~ s{ /* \z}{/}xms;
- }
+ $base = defined($base) ? path($base) : path();
# Read in the changes.
my ($metadata_ref, $changes_ref) = $self->_parse_changes($source);
@@ -722,14 +736,13 @@ sub generate {
# Iterate through each specified output file.
for my $output (@output) {
my ($tags, $format, $file) = split(m{ : }xms, $output);
- my $path = ($base && $file !~ m{ \A / }xms) ? "$base$file" : $file;
- my $prettyfile = $path;
- if ($prettyfile !~ m{ \A / }xms) {
- $prettyfile = ".../$prettyfile";
+ $file = path($file);
+ if ($file->is_relative()) {
+ $file = $file->absolute($base);
}
# If the output file is newer than the input file, do nothing.
- next if (-e $path && -M $path <= -M $source);
+ next if ($file->exists() && -M "$file" <= -M "$source");
# Find all the changes of interest to this output file.
my @entries;
@@ -743,26 +756,21 @@ sub generate {
# Write the output.
if ($format eq 'thread') {
- print_checked("Generating thread file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_thread_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating thread file', $file);
+ $self->_thread_output($file, $metadata_ref, \@entries);
} elsif ($format eq 'rss') {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- print_checked("Generating RSS file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_rss_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating RSS file', $file);
+ $self->_rss_output($file, $base, $metadata_ref, \@entries);
} elsif ($format eq 'index') {
if (scalar(@entries) > $metadata_ref->{recent}) {
splice(@entries, $metadata_ref->{recent});
}
- print_checked("Generating index file $prettyfile\n");
- open(my $fh, '>', $path);
- $self->_index_output($fh, $path, $metadata_ref, \@entries);
- close($fh);
+ $self->_report_action('Generating index file', $file);
+ my $index_base = $source->parent();
+ $self->_index_output($file, $index_base, $metadata_ref, \@entries);
}
}
return;
@@ -792,9 +800,9 @@ App::DocKnot::Spin::RSS - Generate RSS and thread from a feed description file
=head1 REQUIREMENTS
-Perl 5.006 or later and the modules Date::Language, Date::Parse (both part of
-the TimeDate distribution), List::SomeUtils, and Perl6::Slurp, both of which
-are available from CPAN.
+Perl 5.24 or later and the modules Date::Language, Date::Parse (both part of
+the TimeDate distribution), List::SomeUtils, Path::Tiny, and Perl6::Slurp,
+both of which are available from CPAN.
=head1 DESCRIPTION
@@ -834,7 +842,7 @@ with one or more of the following keys, all of which are optional:
By default, App::DocKnot::Spin::RSS output files are relative to the current
working directory. If the C<base> argument is given, output files will be
relative to the value of C<base> instead. Output files specified as absolute
-paths will not be affected.
+paths will not be affected. C<base> may be a string or a Path::Tiny object.
=back
@@ -848,7 +856,8 @@ paths will not be affected.
Parse the input file FILE and generate the output files that it specifies.
BASE, if given, specifies the root directory for output files specified with
-relative paths, and overrides any C<base> argument given to new().
+relative paths, and overrides any C<base> argument given to new(). Both FILE
+and BASE may be strings or Path::Tiny objects.
=back
diff --git a/lib/App/DocKnot/Spin/Sitemap.pm b/lib/App/DocKnot/Spin/Sitemap.pm
index 22fbdd5..3cbc97a 100644
--- a/lib/App/DocKnot/Spin/Sitemap.pm
+++ b/lib/App/DocKnot/Spin/Sitemap.pm
@@ -12,7 +12,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Sitemap 6.00;
+package App::DocKnot::Spin::Sitemap 6.01;
use 5.024;
use autodie;
@@ -226,7 +226,7 @@ sub new {
# Return the <link> tags for a given output file, suitable for its <head>
# section.
#
-# $path - Path to the output, relative to the top of the web site
+# $path - URL path to the output with leading slash
#
# Returns: List of lines to add to the <head> section
sub links {
@@ -268,7 +268,7 @@ sub links {
# Return the navigation bar for a given output file.
#
-# $path - Path to the output, relative to the top of the web site
+# $path - URL path to the output with leading slash
#
# Returns: List of lines that create the navbar
sub navbar {
diff --git a/lib/App/DocKnot/Spin/Thread.pm b/lib/App/DocKnot/Spin/Thread.pm
index bf58dc0..0d84bd7 100644
--- a/lib/App/DocKnot/Spin/Thread.pm
+++ b/lib/App/DocKnot/Spin/Thread.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Thread 6.00;
+package App::DocKnot::Spin::Thread 6.01;
use 5.024;
use autodie;
@@ -17,9 +17,6 @@ use warnings;
use App::DocKnot;
use App::DocKnot::Util qw(print_fh);
-use Cwd qw(getcwd realpath);
-use File::Basename qw(fileparse);
-use File::Spec ();
use Git::Repository ();
use Image::Size qw(html_imgsize);
use Path::Tiny qw(path);
@@ -89,19 +86,37 @@ my %COMMANDS = (
# Input and output
##############################################################################
+# Determine the path to a file relative to the current file being processed.
+# If the current file being processed is standard input, the path is relative
+# to the current working directory.
+#
+# $path - File path as a string
+#
+# Returns: Path::Tiny object holding the absolute path
+sub _file_path {
+ my ($self, $file) = @_;
+ my $input_path = $self->{input}[-1][1];
+ if (defined($input_path)) {
+ my $path = $input_path->sibling($file);
+ return $path->exists() ? $path->realpath() : $path;
+ } else {
+ return path($file);
+ }
+}
+
# Read a file and check it for bad line endings.
#
-# $path - File path
+# $path - Path::Tiny object
#
# Returns: Contents of the file
sub _read_file {
- my ($self, $fh, $path) = @_;
- my $text = slurp($fh);
+ my ($self, $path) = @_;
+ my $text = $path->slurp_utf8();
# Check for broken line endings.
if ("\n" !~ m{ \015 }xms && $text =~ m{ \015 }xms) {
my $m = 'found CR characters; are your line endings correct?';
- $self->_warning($m);
+ $self->_warning($m, $path);
}
# Return the contents.
@@ -159,15 +174,24 @@ sub _output {
sub _fatal {
my ($self, $problem) = @_;
my (undef, $file, $lineno) = $self->{input}[-1]->@*;
+ $file //= q{-};
die "$file:$lineno: $problem\n";
}
# Warn about a problem with the current file and line.
#
# $problem - Warning message to report
+# $file - Optional path where the problem was seen, otherwise the current
+# input file is used
sub _warning {
- my ($self, $problem) = @_;
- my (undef, $file, $lineno) = $self->{input}[-1]->@*;
+ my ($self, $problem, $file) = @_;
+ my $lineno;
+ if (!defined($file)) {
+ (undef, $file, $lineno) = $self->{input}[-1]->@*;
+ $file //= q{-};
+ } else {
+ $lineno = 0;
+ }
warn "$file:$lineno: $problem\n";
return;
}
@@ -637,9 +661,9 @@ sub _parse {
# needs to access.
#
# $thread - Thread to spin
-# $in_path - Input file path if any, used for error reporting
+# $in_path - Input file path as a Path::Tiny object, or undef
# $out_fh - Output file handle to which to write the HTML
-# $out_path - Optional output file path for error reporting and page links
+# $out_path - Output file path as a Path::Tiny object, or undef
# $input_type - Optional one-word description of input type
sub _parse_document {
my ($self, $thread, $in_path, $out_fh, $out_path, $input_type) = @_;
@@ -653,7 +677,7 @@ sub _parse_document {
$self->{input_type} = $input_type // 'thread';
$self->{macro} = {};
$self->{out_fh} = $out_fh;
- $self->{out_path} = $out_path // q{-};
+ $self->{out_path} = $out_path;
$self->{rss} = [];
$self->{space} = q{};
$self->{state} = ['BLOCK'];
@@ -1019,9 +1043,9 @@ sub _cmd_heading {
$style = $self->_parse($style);
# Get the relative URL of the output page, used for sitemap information.
- my $page = $self->{out_path};
- if ($self->{output}) {
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
+ my $page;
+ if (defined($self->{out_path}) && defined($self->{output})) {
+ $page = $self->{out_path}->relative($self->{output});
}
# Build the page header.
@@ -1054,8 +1078,8 @@ sub _cmd_heading {
}
# Add <link> tags based on the sitemap.
- if ($self->{sitemap}) {
- my @links = $self->{sitemap}->links($page);
+ if ($self->{sitemap} && defined($page)) {
+ my @links = $self->{sitemap}->links("/$page");
if (@links) {
$output .= join(q{}, @links);
}
@@ -1066,17 +1090,15 @@ sub _cmd_heading {
# Add some generator comments.
my $date = strftime('%Y-%m-%d %T -0000', gmtime());
- my $from
- = $self->{input}[-1][1] eq q{-}
- ? q{}
- : ' from ' . fileparse($self->{input}[-1][1]);
+ my $input_path = $self->{input}[-1][1];
+ my $from = defined($input_path) ? ' from ' . $input_path->basename() : q{};
my $version = $App::DocKnot::VERSION;
$output .= "<!-- Spun$from by DocKnot $version on $date -->\n";
# Add the <body> tag and the navbar (if we have a sitemap).
$output .= "\n<body>\n";
- if ($self->{sitemap}) {
- my @navbar = $self->{sitemap}->navbar($page);
+ if ($self->{sitemap} && defined($page)) {
+ my @navbar = $self->{sitemap}->navbar("/$page");
if (@navbar) {
$output .= join(q{}, @navbar);
}
@@ -1096,7 +1118,8 @@ sub _cmd_image {
$text = $self->_parse($text);
# Determine the size attributes of the image if possible.
- my $size = -e $image ? q{ } . lc(html_imgsize($image)) : q{};
+ my $path = $self->_file_path($image);
+ my $size = $path->exists() ? q{ } . lc(html_imgsize("$path")) : q{};
# Generate the tag.
my $output = qq{<img src="$image" alt="$text"$size};
@@ -1108,7 +1131,7 @@ sub _cmd_image {
# not immediately, which may be a bit surprising.
sub _cmd_include {
my ($self, $file) = @_;
- $file = realpath($self->_parse($file));
+ $file = $self->_file_path($self->_parse($file));
# Read the thread, split it on paragraphs, and reverse it to make a stack.
my $thread = $self->_read_file($file);
@@ -1242,33 +1265,34 @@ sub _cmd_rss {
# address block.
sub _cmd_signature {
my ($self) = @_;
- my $source = $self->{input}[-1][1];
+ my $input_path = $self->{input}[-1][1];
my $output = $self->_border_end();
# If we're spinning from standard input to standard output, don't add any
# of the standard footer, just close the HTML tags.
- if ($source eq q{-} && $self->{out_path} eq q{-}) {
+ if (!defined($input_path) && !defined($self->{out_path})) {
$output .= "</body>\n</html>\n";
return (1, $output);
}
# Add the end-of-page navbar if we have sitemap information.
if ($self->{sitemap} && $self->{output}) {
- my $page = $self->{out_path};
- $page =~ s{ \A \Q$self->{output}\E }{}xms;
- $output .= join(q{}, $self->{sitemap}->navbar($page)) . "\n";
+ my $page = $self->{out_path}->relative($self->{output});
+ $output .= join(q{}, $self->{sitemap}->navbar("/$page")) . "\n";
}
# Figure out the modification dates. Use the Git repository if available.
my $now = strftime('%Y-%m-%d', gmtime());
my $modified = $now;
- if ($source ne q{-}) {
- $modified = strftime('%Y-%m-%d', gmtime((stat($source))[9]));
+ if (defined($input_path)) {
+ $modified = strftime('%Y-%m-%d', gmtime($input_path->stat()->[9]));
}
if ($self->{repository} && $self->{source}) {
- if (path($self->{source})->subsumes(path($source))) {
+ if (path($self->{source})->subsumes($input_path)) {
my $repository = $self->{repository};
- $modified = $repository->run('log', '-1', '--format=%ct', $source);
+ $modified = $self->{repository}->run(
+ 'log', '-1', '--format=%ct', "$input_path",
+ );
if ($modified) {
$modified = strftime('%Y-%m-%d', gmtime($modified));
}
@@ -1295,10 +1319,13 @@ sub _cmd_signature {
# enough and doesn't seem worth the trouble of another dependency.
sub _cmd_size {
my ($self, $file) = @_;
- $file = $self->_parse($file);
+ $file = $self->_file_path($self->_parse($file));
# Get the size of the file.
- my ($size) = (stat($file))[7];
+ my $size;
+ if ($file->exists()) {
+ $size = $file->stat()->[7];
+ }
if (!defined($size)) {
$self->_warning("cannot stat file $file: $!");
return (0, q{});
@@ -1394,6 +1421,10 @@ sub _cmd_version {
# Returns: Newly created object
sub new {
my ($class, $args_ref) = @_;
+ my $output;
+ if (defined($args_ref->{output})) {
+ $output = path($args_ref->{output});
+ }
# Add a trailing slash to the partial URL for style sheets.
my $style_url = $args_ref->{'style-url'} // q{};
@@ -1403,19 +1434,21 @@ sub new {
# Use a Git::Repository object to get modification timestamps if a source
# tree was specified and it appears to be a git repository.
- my $source = $args_ref->{source};
- my $repository;
- if (defined($source) && -d File::Spec->catdir($source, '.git')) {
- $repository = Git::Repository->new(work_tree => $source);
+ my ($source, $repository);
+ if (defined($args_ref->{source})) {
+ $source = path($args_ref->{source});
+ if ($source->child('.git')->is_dir()) {
+ $repository = Git::Repository->new(work_tree => "$source");
+ }
}
# Create and return the object.
#<<<
my $self = {
- output => $args_ref->{output},
+ output => $output,
repository => $repository,
sitemap => $args_ref->{sitemap},
- source => $args_ref->{source},
+ source => $source,
style_url => $style_url,
versions => $args_ref->{versions},
};
@@ -1427,14 +1460,15 @@ sub new {
# Convert thread to HTML and return the output as a string. The working
# directory still matters for file references in the thread.
#
-# $thread - Thread to spin
+# $thread - Thread to spin
+# $input - Optional input file path (for relative path and timestamps)
#
# Returns: Resulting HTML
sub spin_thread {
- my ($self, $thread) = @_;
+ my ($self, $thread, $input) = @_;
my $result;
open(my $out_fh, '>', \$result);
- $self->_parse_document($thread, q{-}, $out_fh, q{-});
+ $self->_parse_document($thread, $input, $out_fh, undef);
close($out_fh);
return $result;
}
@@ -1447,40 +1481,30 @@ sub spin_thread {
# Raises: Text exception on processing error
sub spin_thread_file {
my ($self, $input, $output) = @_;
- my $cwd = getcwd() or die "cannot get current directory: $!\n";
my $out_fh;
my $thread;
- # Read the input file. We do the work from the directory of the file to
- # ensure that relative file references resolve properly.
+ # Read the input file.
if (defined($input)) {
- my $path = realpath($input) or die "cannot canonicalize $input: $!\n";
- $input = $path;
- $thread = slurp($input);
- my (undef, $input_dir) = fileparse($input);
- chdir($input_dir);
+ $input = path($input)->realpath();
+ $thread = $input->slurp_utf8();
} else {
- $input = q{-};
$thread = slurp(\*STDIN);
}
# Open the output file.
if (defined($output)) {
- my $path = realpath($output)
- or die "cannot canonicalize $output: $!\n";
- $output = $path;
- open($out_fh, '>', $output);
+ $output = path($output)->absolute();
+ $out_fh = $output->openw_utf8();
} else {
- $output = q{-};
open($out_fh, '>&', 'STDOUT');
}
# Do the work.
$self->_parse_document($thread, $input, $out_fh, $output);
- # Clean up and restore the working directory.
+ # Clean up.
close($out_fh);
- chdir($cwd);
return;
}
@@ -1489,23 +1513,21 @@ sub spin_thread_file {
# output from some other conversion process.
#
# $thread - Thread to spin
-# $input - Original input file (for modification timestamps)
+# $input - Original input file path (for relative path and timestamps)
# $input_type - One-word description of input type for the page footer
# $output - Output file
#
# Returns: Resulting HTML
sub spin_thread_output {
my ($self, $thread, $input, $input_type, $output) = @_;
+ $input = path($input);
# Open the output file.
my $out_fh;
if (defined($output)) {
- my $path = realpath($output)
- or die "cannot canonicalize $output: $!\n";
- $output = $path;
- open($out_fh, '>', $output);
+ $output = path($output)->absolute();
+ $out_fh = $output->filehandle('>');
} else {
- $output = q{-};
open($out_fh, '>&', 'STDOUT');
}
@@ -1623,11 +1645,13 @@ data for the C<\release> and C<\version> commands.
=over 4
-=item spin_thread(THREAD)
+=item spin_thread(THREAD[, INPUT])
Convert the given thread to HTML, returning the result. When run via this
API, App::DocKnot::Spin::Thread will not be able to obtain sitemap information
even if a sitemap was provided and therefore will not add inter-page links.
+INPUT, if given, is the full path to the original source file, used for
+relative paths and modification time information.
=item spin_thread_file([INPUT[, OUTPUT]])
@@ -1647,8 +1671,9 @@ not given, write the results to standard output. This is like spin_thread()
but does use sitemap information and adds inter-page links. It should be used
when the thread input is the result of an intermediate conversion step of a
known input file. INPUT should be the full path to the original source file,
-used for modification time information. TYPE should be set to a one-word
-description of the format of the input file and is used for the page footer.
+used for relative paths and modification time information. TYPE should be set
+to a one-word description of the format of the input file and is used for the
+page footer.
=back
diff --git a/lib/App/DocKnot/Spin/Versions.pm b/lib/App/DocKnot/Spin/Versions.pm
index 679b368..1655ff0 100644
--- a/lib/App/DocKnot/Spin/Versions.pm
+++ b/lib/App/DocKnot/Spin/Versions.pm
@@ -12,12 +12,13 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Spin::Versions 6.00;
+package App::DocKnot::Spin::Versions 6.01;
use 5.024;
use autodie;
use warnings;
+use Path::Tiny qw(path);
use POSIX qw(mktime strftime);
##############################################################################
@@ -56,18 +57,20 @@ sub _datetime_to_seconds {
return mktime(@datetime);
}
-# Parse a .versions file and populate the App::DocKnot::Spin::Versions object.
-#
-# $path - Path to the .versions file
+# Parse the .versions file and populate the App::DocKnot::Spin::Versions
+# object.
#
# Raises: autodie exception on file read errors
# Text exception on file parsing errors
sub _read_data {
- my ($self, $path) = @_;
+ my ($self) = @_;
+ $self->{depends} = {};
+ $self->{versions} = {};
my $timestamp;
- open(my $fh, '<', $path);
- while (defined(my $line = <$fh>)) {
+ my $lineno = 0;
+ for my $line ($self->{path}->lines_utf8()) {
+ $lineno++;
next if $line =~ m{ \A \s* \z }xms;
next if $line =~ m{ \A \s* \# }xms;
@@ -75,17 +78,17 @@ sub _read_data {
my @depends;
if ($line =~ m{ \A \s }xms) {
if (!defined($timestamp)) {
- die "continuation without previous entry in $path\n";
+ die "continuation without previous entry in $self->{path}\n";
}
@depends = split(qr{ \s+ }xms, $line);
} else {
my @line = split(qr{ \s+ }xms, $line);
my ($package, $version, $date, $time, @files) = @line;
if (!defined($time)) {
- die "invalid line $. in $path\n";
+ die "invalid line $lineno in $self->{path}\n";
}
@depends = @files;
- $timestamp = _datetime_to_seconds($date, $time, $path);
+ $timestamp = _datetime_to_seconds($date, $time, $self->{path});
$date = strftime('%Y-%m-%d', gmtime($timestamp));
$self->{versions}{$package} = [$version, $date];
}
@@ -100,7 +103,6 @@ sub _read_data {
}
}
}
- close($fh);
return;
}
@@ -119,14 +121,11 @@ sub new {
my ($class, $path) = @_;
# Create an empty object.
- my $self = {
- depends => {},
- versions => {},
- };
+ my $self = { path => path($path) };
bless($self, $class);
# Parse the file into the newly-created object.
- $self->_read_data($path);
+ $self->_read_data();
# Return the populated object.
return $self;
@@ -134,13 +133,13 @@ sub new {
# Return the timestamp of the latest release affecting a different page.
#
-# $file - File name that may be listed as an affected file for a release
+# $file - File path that may be listed as an affected file for a release
#
# Returns: The timestamp in seconds since epoch of the latest release
# affecting that file, or 0 if there are none
sub latest_release {
my ($self, $file) = @_;
- return $self->{depends}{$file} // 0;
+ return $self->{depends}{"$file"} // 0;
}
# Return the release date for a given package.
@@ -154,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
@@ -189,7 +232,7 @@ App::DocKnot::Spin::Versions - Parse package release information for spin
=head1 REQUIREMENTS
-Perl 5.24 or later.
+Perl 5.24 or later and the Path::Tiny module, available from CPAN.
=head1 DESCRIPTION
@@ -271,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
@@ -284,7 +334,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2004, 2021 Russ Allbery <rra@cpan.org>
+Copyright 2004, 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/lib/App/DocKnot/Update.pm b/lib/App/DocKnot/Update.pm
index 5c6a999..78b33de 100644
--- a/lib/App/DocKnot/Update.pm
+++ b/lib/App/DocKnot/Update.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Update 6.00;
+package App::DocKnot::Update 6.01;
use 5.024;
use autodie;
@@ -17,10 +17,10 @@ use parent qw(App::DocKnot);
use warnings;
use Carp qw(croak);
-use File::Spec;
use JSON::MaybeXS qw(JSON);
use Kwalify qw(validate);
-use Perl6::Slurp;
+use Path::Iterator::Rule;
+use Path::Tiny qw(path);
use YAML::XS ();
# The older JSON metadata format stored text snippets in separate files in the
@@ -49,10 +49,10 @@ our @JSON_METADATA_FILES = qw(
#
# @path - The relative path of the file as a list of components
#
-# Returns: The absolute path in the metadata directory
+# Returns: Path::Tiny for the metadata file
sub _metadata_path {
my ($self, @path) = @_;
- return File::Spec->catdir($self->{metadata}, @path);
+ return path($self->{metadata}, @path);
}
# Internal helper routine to read a file from the package metadata directory
@@ -65,7 +65,8 @@ sub _metadata_path {
# Throws: slurp exception on failure to read the file
sub _load_metadata {
my ($self, @path) = @_;
- return slurp('<:utf8', $self->_metadata_path(@path));
+ my $path = $self->_metadata_path(@path);
+ return $path->slurp_utf8();
}
# Like _load_metadata, but interprets the contents of the metadata file as
@@ -158,6 +159,90 @@ sub _config_from_json {
}
##############################################################################
+# Spin helper methods
+##############################################################################
+
+# Given an old-format *.rpod pointer file, read the master file name and any
+# options. Return them in the structure used for *.spin pointer files.
+#
+# $path - Path::Tiny for the file to read
+#
+# Returns: Hash in the format of a *.spin pointer file
+# Throws: Text exception if no master file is present in the pointer
+# autodie exception if the pointer file could not be read
+sub _read_rpod_pointer {
+ my ($self, $path) = @_;
+
+ # Read the pointer file.
+ my ($master, $options, $style) = $path->lines_utf8();
+ if (!$master) {
+ die "no master file specified in $path\n";
+ }
+ chomp($master);
+
+ # Put the results into the correct format.
+ my %results = (format => 'pod', path => $master);
+ if (defined($style)) {
+ chomp($style);
+ $results{style} = $style;
+ }
+ if (defined($options)) {
+ if ($options =~ m{ -c ( \s | \z ) }xms) {
+ $results{options} = {
+ contents => JSON::MaybeXS::true,
+ navbar => JSON::MaybeXS::false,
+ };
+ }
+ if ($options =~ m{ -t \s+ (?: '(.*)' | ( [^\'] \S+ ) ) }xms) {
+ $results{title} = $1 || $2;
+ }
+ }
+
+ # Return the parsed file.
+ return \%results;
+}
+
+# Given its representation as a hash, write out a new-style *.spin file.
+#
+# $data_ref - Hash of data for the file
+# $path - Path to output file
+sub _write_spin_pointer {
+ my ($self, $data_ref, $path) = @_;
+
+ # Generate the YAML output and strip off the leading document separator.
+ local $YAML::XS::Boolean = 'JSON::PP';
+ my $yaml = YAML::XS::Dump($data_ref);
+ $yaml =~ s{ \A --- \n }{}xms;
+
+ # Write the output.
+ $path->spew_utf8($yaml);
+ return;
+}
+
+# Convert an *.rpod file to a *.spin file. Intended to be run via
+# Path::Iterator::Rule.
+#
+# $rpod_path - Path to *.rpod file
+# $repo - Optional Git::Repository object for input tree
+sub _convert_rpod_pointer {
+ my ($self, $rpod_path, $repo) = @_;
+
+ # Convert the file.
+ my $data_ref = $self->_read_rpod_pointer($rpod_path);
+ my $basename = $rpod_path->basename('.rpod');
+ my $spin_path = $rpod_path->sibling($basename . '.spin');
+ $self->_write_spin_pointer($data_ref, $spin_path);
+
+ # If we have a Git repository, update Git.
+ if (defined($repo)) {
+ my $root = path($repo->work_tree());
+ $repo->run('add', $spin_path->relative($root)->stringify());
+ $repo->run('rm', $rpod_path->relative($root)->stringify());
+ }
+ return;
+}
+
+##############################################################################
# Public Interface
##############################################################################
@@ -172,17 +257,9 @@ sub _config_from_json {
# Throws: Text exceptions on invalid metadata directory path
sub new {
my ($class, $args_ref) = @_;
-
- # Ensure we were given a valid metadata argument.
- my $metadata = $args_ref->{metadata} // 'docs/metadata';
- if (!-d $metadata) {
- croak("metadata path $metadata does not exist or is not a directory");
- }
-
- # Create and return the object.
my $self = {
- metadata => $metadata,
- output => $args_ref->{output} // 'docs/docknot.yaml',
+ metadata => path($args_ref->{metadata} // 'docs/metadata'),
+ output => path($args_ref->{output} // 'docs/docknot.yaml'),
};
bless($self, $class);
return $self;
@@ -197,6 +274,12 @@ sub new {
sub update {
my ($self) = @_;
+ # Ensure we were given a valid metadata argument.
+ if (!$self->{metadata}->is_dir()) {
+ my $metadata = $self->{metadata};
+ croak("metadata path $metadata does not exist or is not a directory");
+ }
+
# Tell YAML::XS that we'll be feeding it JSON::PP booleans.
local $YAML::XS::Boolean = 'JSON::PP';
@@ -274,7 +357,29 @@ sub update {
}
# Write the new YAML package configuration.
- YAML::XS::DumpFile($self->{output}, $data_ref);
+ YAML::XS::DumpFile($self->{output}->stringify(), $data_ref);
+ return;
+}
+
+# Update an input tree for spin to the current format.
+#
+# $path - Optional path to the spin input tree, defaults to current directory
+#
+# Raises: Text exception on failure
+sub update_spin {
+ my ($self, $path) = @_;
+ $path = defined($path) ? path($path) : path(q{.});
+ my $repo;
+ if ($path->child('.git')->is_dir()) {
+ $repo = Git::Repository->new(work_tree => "$path");
+ }
+
+ # Convert all *.rpod files to *.spin files.
+ my $rule = Path::Iterator::Rule->new()->name(qr{ [.] rpod \z }xms);
+ my $iter = $rule->iter($path, { follow_symlinks => 0 });
+ while (defined(my $file = $iter->())) {
+ $self->_convert_rpod_pointer(path($file), $repo);
+ }
return;
}
@@ -290,23 +395,27 @@ Allbery DocKnot MERCHANTABILITY NONINFRINGEMENT sublicense CPAN XDG
=head1 NAME
-App::DocKnot::Update - Update DocKnot package configuration for new formats
+App::DocKnot::Update - Update DocKnot input or package configuration
=head1 SYNOPSIS
use App::DocKnot::Update;
- my $reader = App::DocKnot::Update->new(
+
+ my $update = App::DocKnot::Update->new(
{
metadata => 'docs/metadata',
output => 'docs/docknot.yaml',
}
);
- my $config = $reader->update();
+ $update->update();
+
+ $update->update_spin('/path/to/spin/input');
=head1 REQUIREMENTS
-Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON,
-Perl6::Slurp, and YAML::XS, all of which are available from CPAN.
+Perl 5.24 or later and the modules Git::Repository, File::BaseDir,
+File::ShareDir, JSON::MaybeXS, Path::Iterator::Rule, Path::Tiny, Perl6::Slurp,
+and YAML::XS, all of which are available from CPAN.
=head1 DESCRIPTION
@@ -348,6 +457,13 @@ F<docs/docknot.yaml> relative to the current directory.
Load the legacy JSON metadata and write out the YAML equivalent.
+=item update_spin([PATH])
+
+Update the input tree for App::DocKnot::Spin to follow current expectations.
+PATH is the path to the input tree, which defaults to the current directory
+if not given. If the input tree is the working tree for a Git repository,
+any changes are also registered with Git (but not committed).
+
=back
=head1 AUTHOR
@@ -356,7 +472,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/Util.pm b/lib/App/DocKnot/Util.pm
index 203d3d3..e5e4014 100644
--- a/lib/App/DocKnot/Util.pm
+++ b/lib/App/DocKnot/Util.pm
@@ -9,7 +9,7 @@
# Modules and declarations
##############################################################################
-package App::DocKnot::Util 6.00;
+package App::DocKnot::Util 6.01;
use 5.024;
use autodie;
@@ -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
@@ -123,8 +188,8 @@ doesn't because print cannot be prototyped).
=item print_fh(FH, NAME, DATA[, DATA ...])
Writes the concatenation of the DATA elements (interpreted as scalar strings)
-to the file handle FH. NAME should be the name of the file open as FH, and is
-used for error reporting.
+to the file handle FH. NAME should be the name of (or Path::Tiny object for)
+the file open as FH, and is used for error reporting.
This is mostly equivalent to C<print {fh}> but throws a text exception in the
event of a failure.
@@ -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/cli/errors.t b/t/cli/errors.t
index 2336977..c311d14 100755
--- a/t/cli/errors.t
+++ b/t/cli/errors.t
@@ -10,9 +10,11 @@ use 5.024;
use autodie;
use warnings;
+use POSIX qw(LC_ALL setlocale);
use Test::More tests => 11;
# Isolate from the environment.
+setlocale(LC_ALL, 'C');
local $ENV{XDG_CONFIG_HOME} = '/nonexistent';
local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
@@ -61,7 +63,13 @@ is_error($@, 'generate-all: too many arguments', 'Too many arguments');
# Trigger an error in a submodule to test error rewriting.
eval { $docknot->run('generate', '-m', '/nonexistent', 'readme') };
-is_error($@, 'generate: metadata path /nonexistent does not exist');
+is_error(
+ $@,
+ (
+ 'generate: can\'t open \'/nonexistent\' for input:'
+ . ' No such file or directory'
+ ),
+);
# Check for a missing required argument.
eval { $docknot->run('dist') };
diff --git a/t/cli/spin.t b/t/cli/spin.t
index ac7894f..2522eda 100755
--- a/t/cli/spin.t
+++ b/t/cli/spin.t
@@ -76,7 +76,11 @@ print_fh($fh, $pointer_path, "format: pod\n");
print_fh($fh, $pointer_path, "path: $pod_source\n");
close($fh);
-# Spin a tree of files.
+# Spin a tree of files. Do this from the temporary directory because 6.00 had
+# a regression where docknot spin would fail if there were no package metadata
+# even though it didn't use it.
+my $cwd = getcwd();
+chdir($tempdir->dirname);
$expected = File::Spec->catfile($datadir, 'output');
capture_stdout {
$docknot->run(
@@ -84,6 +88,7 @@ capture_stdout {
$tempdir->dirname,
);
};
+chdir($cwd);
my $count = is_spin_output_tree($tempdir->dirname, $expected, 'spin');
# Spin a file with warnings. The specific warnings are checked in
diff --git a/t/data/generate/docknot/output/thread b/t/data/generate/docknot/output/thread
index 63e79c3..d1d89a0 100644
--- a/t/data/generate/docknot/output/thread
+++ b/t/data/generate/docknot/output/thread
@@ -117,9 +117,11 @@ The following additional Perl modules are required to use it:
\bullet(packed)[JSON::MaybeXS]
\bullet(packed)[Kwalify]
\bullet(packed)[List::SomeUtils 0.07 or later]
-\bullet(packed)[Path::Tiny]
+\bullet(packed)[Path::Iterator::Rule]
+\bullet(packed)[Path::Tiny 0.101 or later]
\bullet(packed)[Perl6::Slurp]
-\bullet(packed)[Pod::Thread 3.00 or later]
+\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]
@@ -197,7 +199,7 @@ license:
\block[
- Copyright 1999-2021
+ Copyright 1999-2022
Russ Allbery <rra@cpan.org>
Permission is hereby granted, free of charge, to any person obtaining
diff --git a/t/data/perltidyrc b/t/data/perltidyrc
index 431c311..dc3a2f7 100644
--- a/t/data/perltidyrc
+++ b/t/data/perltidyrc
@@ -6,7 +6,7 @@
# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
#
# Written by Russ Allbery <eagle@eyrie.org>
-# Copyright 2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2021-2022 Russ Allbery <eagle@eyrie.org>
# Copyright 2012-2013
# The Board of Trustees of the Leland Stanford Junior University
#
@@ -22,8 +22,9 @@
-boc # do not re-break lists, since perltidy is awful at this
-ce # cuddle braces around else
-l=79 # usually use 78, but don't want 79-long lines reformatted
+-nlop # disable vertical alignment of logical and ternary expressions
-pt=2 # don't add extra whitespace around parentheses
-sbt=2 # ...or square brackets
--sfs # no space before semicolon in for (not that I use this form)
+-nsfs # no space before semicolon in for (not that I use this form)
-nvc # disable vertical alignment of = and similar symbols
-xci # improve indentation of nested structures
diff --git a/t/data/spin/input/journal/.rss b/t/data/spin/input/journal/.rss
index 62f7c63..07c8e82 100644
--- a/t/data/spin/input/journal/.rss
+++ b/t/data/spin/input/journal/.rss
@@ -30,10 +30,10 @@ Index-Suffix:
Date: 2011-08-13 00:09
Title: NPR Top 100 SFF meme
Link: journal/2011-08/006.html
-Journal: journal/2011-08/006.th
+Journal: 2011-08/006.th
Tags: debian
Date: 2007-01-14 21:30
Title: Review: Fermat's Enigma
Link: reviews/books/1-250-30112-2.html
-Review: reviews/books/0-385-49362-2.th
+Review: ../reviews/books/0-385-49362-2.th
diff --git a/t/data/spin/update/input/module.rpod b/t/data/spin/update/input/module.rpod
new file mode 100644
index 0000000..5f75697
--- /dev/null
+++ b/t/data/spin/update/input/module.rpod
@@ -0,0 +1 @@
+/path/Module.pm
diff --git a/t/data/spin/update/input/readme.rpod b/t/data/spin/update/input/readme.rpod
new file mode 100644
index 0000000..042274b
--- /dev/null
+++ b/t/data/spin/update/input/readme.rpod
@@ -0,0 +1,2 @@
+/path/readme.pod
+-c -t 'Basic Information'
diff --git a/t/data/spin/update/input/script.rpod b/t/data/spin/update/input/script.rpod
new file mode 100644
index 0000000..00f79c7
--- /dev/null
+++ b/t/data/spin/update/input/script.rpod
@@ -0,0 +1,3 @@
+/path/script
+
+/~eagle/styles/script.css
diff --git a/t/data/spin/update/output/module.spin b/t/data/spin/update/output/module.spin
new file mode 100644
index 0000000..31b97ce
--- /dev/null
+++ b/t/data/spin/update/output/module.spin
@@ -0,0 +1,2 @@
+format: pod
+path: /path/Module.pm
diff --git a/t/data/spin/update/output/readme.spin b/t/data/spin/update/output/readme.spin
new file mode 100644
index 0000000..c78a5cf
--- /dev/null
+++ b/t/data/spin/update/output/readme.spin
@@ -0,0 +1,6 @@
+format: pod
+options:
+ contents: true
+ navbar: false
+path: /path/readme.pod
+title: Basic Information
diff --git a/t/data/spin/update/output/script.spin b/t/data/spin/update/output/script.spin
new file mode 100644
index 0000000..5be3d64
--- /dev/null
+++ b/t/data/spin/update/output/script.spin
@@ -0,0 +1,3 @@
+format: pod
+path: /path/script
+style: /~eagle/styles/script.css
diff --git a/t/dist/basic.t b/t/dist/basic.t
index 6fcf2bc..dcc6b1b 100755
--- a/t/dist/basic.t
+++ b/t/dist/basic.t
@@ -20,6 +20,7 @@ use File::Temp;
use Git::Repository;
use IPC::Run qw(run);
use IPC::System::Simple qw(capturex systemx);
+use List::Util qw(first);
use Test::More;
@@ -52,9 +53,13 @@ $repo->run(add => '-A', q{.});
$repo->run(commit => '-q', '-m', 'Initial commit');
# Check whether we have all the necessary tools to run the test.
+my @branches = $repo->run(
+ 'for-each-ref' => '--format=%(refname:short)', 'refs/heads/',
+);
+my $head = first { $_ eq 'main' || $_ eq 'master' } @branches;
my $result;
eval {
- my $archive = $repo->command(archive => 'HEAD');
+ my $archive = $repo->command(archive => '--prefix=foo/', $head);
my $out;
$result = run([qw(tar tf -)], '<', $archive->stdout, '>', \$out);
$archive->close();
diff --git a/t/lib/Test/DocKnot/Spin.pm b/t/lib/Test/DocKnot/Spin.pm
index c7c33b8..2c520a2 100644
--- a/t/lib/Test/DocKnot/Spin.pm
+++ b/t/lib/Test/DocKnot/Spin.pm
@@ -82,6 +82,10 @@ sub is_spin_output_tree {
# File::Find on the output directory.
my $check_output = sub {
my $file = $_;
+ if ($file eq '.git') {
+ $File::Find::prune = 1;
+ return;
+ }
return if -d $file;
# Determine the relative path and mark it as seen.
@@ -191,7 +195,7 @@ Russ Allbery <rra@cpan.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2021 Russ Allbery <rra@cpan.org>
+Copyright 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/t/lib/Test/RRA.pm b/t/lib/Test/RRA.pm
index c432d0c..2e34bf5 100644
--- a/t/lib/Test/RRA.pm
+++ b/t/lib/Test/RRA.pm
@@ -52,7 +52,7 @@ BEGIN {
# This version should match the corresponding rra-c-util release, but with
# two digits for the minor version, including a leading zero if necessary,
# so that it will sort properly.
- $VERSION = '10.01';
+ $VERSION = '10.02';
}
# Compare a string to the contents of a file, similar to the standard is()
diff --git a/t/lib/Test/RRA/Config.pm b/t/lib/Test/RRA/Config.pm
index 75419ea..a2ba32c 100644
--- a/t/lib/Test/RRA/Config.pm
+++ b/t/lib/Test/RRA/Config.pm
@@ -32,7 +32,7 @@ BEGIN {
# This version should match the corresponding rra-c-util release, but with
# two digits for the minor version, including a leading zero if necessary,
# so that it will sort properly.
- $VERSION = '10.01';
+ $VERSION = '10.02';
}
# If C_TAP_BUILD or C_TAP_SOURCE are set in the environment, look for
diff --git a/t/lib/Test/RRA/ModuleVersion.pm b/t/lib/Test/RRA/ModuleVersion.pm
index d01c14b..aed52c1 100644
--- a/t/lib/Test/RRA/ModuleVersion.pm
+++ b/t/lib/Test/RRA/ModuleVersion.pm
@@ -29,7 +29,7 @@ BEGIN {
# This version should match the corresponding rra-c-util release, but with
# two digits for the minor version, including a leading zero if necessary,
# so that it will sort properly.
- $VERSION = '10.01';
+ $VERSION = '10.02';
}
# A regular expression matching the version string for a module using the
@@ -103,9 +103,7 @@ sub _module_version {
my ($file) = @_;
open(my $data, q{<}, $file) or die "$0: cannot open $file: $!\n";
while (defined(my $line = <$data>)) {
- if ( $line =~ $REGEX_VERSION_PACKAGE
- || $line =~ $REGEX_VERSION_OLD)
- {
+ if ($line =~ $REGEX_VERSION_PACKAGE || $line =~ $REGEX_VERSION_OLD) {
my ($prefix, $version, $suffix) = ($1, $2, $3);
close($data) or die "$0: error reading from $file: $!\n";
return $version;
@@ -140,8 +138,8 @@ sub _update_module_version {
or die "$0: cannot create $file.new: $!\n";
SCAN:
while (defined(my $line = <$in>)) {
- if ( $line =~ s{ $REGEX_VERSION_PACKAGE }{$1$version$3}xms
- || $line =~ s{ $REGEX_VERSION_OLD }{$1$old_version$3}xms)
+ if ($line =~ s{ $REGEX_VERSION_PACKAGE }{$1$version$3}xms
+ || $line =~ s{ $REGEX_VERSION_OLD }{$1$old_version$3}xms)
{
print {$out} $line or die "$0: cannot write to $file.new: $!\n";
last SCAN;
@@ -265,7 +263,7 @@ Russ Allbery <eagle@eyrie.org>
=head1 COPYRIGHT AND LICENSE
-Copyright 2016, 2018-2020 Russ Allbery <eagle@eyrie.org>
+Copyright 2016, 2018-2020, 2022 Russ Allbery <eagle@eyrie.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/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');
diff --git a/t/spin/errors.t b/t/spin/errors.t
index d4cc565..c6e1ce6 100755
--- a/t/spin/errors.t
+++ b/t/spin/errors.t
@@ -43,5 +43,5 @@ my ($stdout, $stderr) = capture {
# Simplify the file name, and then check against the expected output.
$stderr =~ s{ ^ [^:]+/errors[.]th: }{errors.th:}xmsg;
-$stderr =~ s{ (cannot [ ] stat [^:]+): .* }{$1\n}xms;
+$stderr =~ s{ (cannot [ ] stat [ ] file [ ]) /[^:]+/([^/:]+) : .* }{$1$2\n}xms;
is($stderr, $EXPECTED_ERRORS, 'errors are correct');
diff --git a/t/spin/sitemap.t b/t/spin/sitemap.t
index 59f8fe3..b51d675 100755
--- a/t/spin/sitemap.t
+++ b/t/spin/sitemap.t
@@ -40,7 +40,7 @@ is_deeply(\@navbar, [], 'navbar for unknown page');
# exercised by the test of spinning a tree of files.
@links = $sitemap->links('/faqs/soundness-inn.html');
my @expected = (
- q{ <link rel="next" href="soundness-cnews.html"}
+ q{ <link rel="next" href="soundness-cnews.html"}
. qq{ title="Soundness for C News" />\n},
qq{ <link rel="up" href="./" title="FAQs and Documentation" />\n},
qq{ <link rel="top" href="../" />\n},
diff --git a/t/spin/tree.t b/t/spin/tree.t
index d2daab1..700f961 100755
--- a/t/spin/tree.t
+++ b/t/spin/tree.t
@@ -35,35 +35,35 @@ Generating RSS file .../changes.rss
Updating .../changes.rss
Spinning .../changes.html
Spinning .../index.html
-Updating .../names.png
-Spinning .../random.html
Creating .../journal
Generating index file .../journal/index.th
Generating RSS file .../journal/index.rss
Generating RSS file .../journal/debian.rss
Generating RSS file .../journal/reviews.rss
+Updating .../names.png
+Spinning .../random.html
+Creating .../reviews
+Creating .../software
+Creating .../usefor
+Creating .../journal/2011-08
Updating .../journal/debian.rss
Updating .../journal/index.rss
Spinning .../journal/index.html
Updating .../journal/reviews.rss
-Creating .../journal/2011-08
-Spinning .../journal/2011-08/006.html
-Creating .../reviews
Creating .../reviews/books
-Spinning .../reviews/books/0-385-49362-2.html
-Creating .../software
-Spinning .../software/index.html
Creating .../software/docknot
-Spinning .../software/docknot/index.html
-Creating .../software/docknot/api
-Converting .../software/docknot/api/app-docknot.html
-Creating .../usefor
-Spinning .../usefor/index.html
+Spinning .../software/index.html
Creating .../usefor/drafts
+Spinning .../usefor/index.html
+Spinning .../journal/2011-08/006.html
+Spinning .../reviews/books/0-385-49362-2.html
+Creating .../software/docknot/api
+Spinning .../software/docknot/index.html
Updating .../usefor/drafts/draft-ietf-usefor-message-id-01.txt
Updating .../usefor/drafts/draft-ietf-usefor-posted-mailed-01.txt
Updating .../usefor/drafts/draft-ietf-usefor-useage-01.txt
Updating .../usefor/drafts/draft-lindsey-usefor-signed-01.txt
+Converting .../software/docknot/api/app-docknot.html
OUTPUT
BEGIN { use_ok('App::DocKnot::Util', qw(print_fh)) }
diff --git a/t/spin/versions.t b/t/spin/versions.t
index 640e143..7f1e62b 100755
--- a/t/spin/versions.t
+++ b/t/spin/versions.t
@@ -19,8 +19,10 @@ use Test::More tests => 20;
require_ok('App::DocKnot::Spin::Versions');
-# All dates in the sample data are in America/Los_Angeles.
-local $ENV{TZ} = 'America/Los_Angeles';
+# All dates in the sample data are in America/Los_Angeles. Specify this in
+# the POSIX format in the hope this will also work on systems without tzinfo
+# installed.
+local $ENV{TZ} = 'PST8PDT,M3.2.0,M11.1.0';
tzset();
# Parse the file.
diff --git a/t/update/spin.t b/t/update/spin.t
new file mode 100755
index 0000000..c692c80
--- /dev/null
+++ b/t/update/spin.t
@@ -0,0 +1,65 @@
+#!/usr/bin/perl
+#
+# Tests for the spin part of the App::DocKnot::Update 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 File::Copy::Recursive qw(dircopy);
+use Git::Repository ();
+use Path::Tiny qw(path);
+use Test::DocKnot::Spin qw(is_spin_output_tree);
+
+use Test::More;
+
+# Isolate from the environment.
+local $ENV{XDG_CONFIG_HOME} = '/nonexistent';
+local $ENV{XDG_CONFIG_DIRS} = '/nonexistent';
+
+# Load the module.
+require_ok('App::DocKnot::Update');
+
+# Construct the source tree. Copy t/data/spin/update/input into a fresh Git
+# repository and commit it so that we can test the Git interaction.
+my $input = path('t', 'data', 'spin', 'update', 'input');
+my $tempdir = Path::Tiny->tempdir();
+Git::Repository->run('init', { cwd => "$tempdir", quiet => 1 });
+dircopy($input, "$tempdir")
+ or die "$0: cannot copy $input to $tempdir: $!\n";
+my $repo = Git::Repository->new(work_tree => "$tempdir");
+$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');
+
+# Update the tree.
+my $update = App::DocKnot::Update->new();
+$update->update_spin($tempdir);
+
+# Check the resulting output.
+my $expected = path('t', 'data', 'spin', 'update', 'output');
+my $count = is_spin_output_tree("$tempdir", "$expected", 'Tree updated');
+my @changes = grep { m{ deleted | new [ ] file }xms } $repo->run('status');
+@changes = map { [split(q{ })] } sort(@changes);
+is_deeply(
+ \@changes,
+ [
+ ['deleted:', 'module.rpod'],
+ ['deleted:', 'readme.rpod'],
+ ['deleted:', 'script.rpod'],
+ ['new', 'file:', 'module.spin'],
+ ['new', 'file:', 'readme.spin'],
+ ['new', 'file:', 'script.spin'],
+ ],
+ 'Git operations',
+);
+
+# Report the end of testing.
+done_testing($count + 2);