File Coverage

lib/Module/Metadata/Changes.pm
Criterion Covered Total %
statement 169 256 66.0
branch 34 70 48.5
condition 19 43 44.1
subroutine 25 31 80.6
pod 13 15 86.6
total 260 415 62.6


line stmt bran cond sub pod time code
1             package Module::Metadata::Changes;
2              
3 1     1   52332 use strict;
  1         1  
  1         22  
4 1     1   3 use warnings;
  1         1  
  1         23  
5              
6 1     1   571 use Config::IniFiles;
  1         22842  
  1         32  
7              
8 1     1   463 use DateTime::Format::W3CDTF;
  1         302784  
  1         36  
9              
10 1     1   467 use File::Slurper 'read_lines';
  1         9830  
  1         46  
11              
12 1     1   415 use HTML::Entities::Interpolate;
  1         4819  
  1         4  
13 1     1   832 use HTML::Template;
  1         9226  
  1         30  
14              
15 1     1   478 use Moo;
  1         6881  
  1         3  
16              
17 1     1   955 use Try::Tiny;
  1         1  
  1         46  
18              
19 1     1   449 use Types::Standard qw/Any ArrayRef Bool Str/;
  1         51755  
  1         10  
20              
21 1     1   1260 use version;
  1         1573  
  1         5  
22              
23             has changes =>
24             (
25             default => sub{return []},
26             is => 'rw',
27             isa => ArrayRef,
28             required => 0,
29             );
30              
31             has config =>
32             (
33             default => sub{return ''},
34             is => 'rw',
35             isa => Any,
36             required => 0,
37             );
38              
39             has convert =>
40             (
41             default => sub{return 0},
42             is => 'rw',
43             isa => Bool,
44             required => 0,
45             );
46              
47             has errstr =>
48             (
49             default => sub{return ''},
50             is => 'rw',
51             isa => Str,
52             required => 0,
53             );
54              
55             has inFileName =>
56             (
57             default => sub{return ''},
58             is => 'rw',
59             isa => Str,
60             required => 0,
61             );
62              
63             has module_name =>
64             (
65             default => sub{return ''},
66             is => 'rw',
67             isa => Str,
68             required => 0,
69             );
70              
71             has outFileName =>
72             (
73             default => sub{return 'Changelog.ini'},
74             is => 'rw',
75             isa => Str,
76             required => 0,
77             );
78              
79             has pathForHTML =>
80             (
81             default => sub{return '/dev/run/html/assets/templates/module/metadata/changes'},
82             is => 'rw',
83             isa => Str,
84             required => 0,
85             );
86              
87             has release =>
88             (
89             default => sub{return ''},
90             is => 'rw',
91             isa => Str,
92             required => 0,
93             );
94              
95             has table =>
96             (
97             default => sub{return 0},
98             is => 'rw',
99             isa => Bool,
100             required => 0,
101             );
102              
103             has urlForCSS =>
104             (
105             default => sub{return '/assets/css/module/metadata/changes/ini.css'},
106             is => 'rw',
107             isa => Str,
108             required => 0,
109             );
110              
111             has verbose =>
112             (
113             default => sub{return 0},
114             is => 'rw',
115             isa => Bool,
116             required => 0,
117             );
118              
119             has webPage =>
120             (
121             default => sub{return 0},
122             is => 'rw',
123             isa => Bool,
124             required => 0,
125             );
126              
127             our $VERSION = '2.12';
128              
129             # -----------------------------------------------
130              
131             sub BUILD
132             {
133 1     1 0 14 my($self) = @_;
134              
135 1 50       14 if ($self -> webPage)
136             {
137 0         0 $self -> table(1);
138             }
139              
140             } # End of BUILD.
141              
142             # ------------------------------------------------
143              
144             sub get_latest_release
145             {
146 1     1 1 644 my($self) = @_;
147 1         22 my(@release) = $self -> config -> GroupMembers('V');
148              
149 1         30 my(@output);
150             my($release);
151 0         0 my($version);
152              
153 1         2 for $release (@release)
154             {
155 29         49 ($version = $release) =~ s/^V //;
156              
157 29         85 push @output, version -> new($version);
158             }
159              
160 1         4 @output = reverse sort{$a cmp $b} @output;
  28         43  
161              
162 1         3 my($result) = {};
163              
164 1 50       3 if ($#output >= 0)
165             {
166 1         5 my($section) = "V $output[0]";
167              
168 1         1 my($token);
169              
170 1         18 for $token ($self -> config -> Parameters($section) )
171             {
172 2         64 $$result{$token} = $self -> config -> val($section, $token);
173             }
174             }
175              
176 1         30 return $result;
177              
178             } # End of get_latest_release.
179              
180             # ------------------------------------------------
181              
182             sub get_latest_version
183             {
184 1     1 1 6 my($self) = @_;
185 1         15 my(@release) = $self -> config -> GroupMembers('V');
186              
187 1         18 my(@output);
188             my($release);
189 0         0 my($version);
190              
191 1         2 for $release (@release)
192             {
193 29         41 ($version = $release) =~ s/^V //;
194              
195 29         76 push @output, version -> new($version);
196             }
197              
198 1         3 @output = reverse sort{$a cmp $b} @output;
  28         42  
199              
200 1 50       16 return $#output >= 0 ? $output[0] : '';
201              
202             } # End of get_latest_version.
203              
204             # -----------------------------------------------
205              
206             sub log
207             {
208 34     34 0 425 my($self, $s) = @_;
209 34   50     88 $s ||= '';
210              
211 34 50       748 if ($self -> verbose)
212             {
213 0         0 print STDERR "$s\n";
214             }
215              
216             } # End of log.
217              
218             # -----------------------------------------------
219              
220             sub parse_datetime
221             {
222 242     242 1 285 my($self, $candidate) = @_;
223              
224             # One of the modules DateTime::Format::HTTP or DateTime::Format::Strptime
225             # can return 'No input string', so we use it as well.
226              
227 242 50       584 if (length($candidate) == 0)
228             {
229 0         0 return 'No input string';
230             }
231              
232 242         308 my($date) = $self -> parse_datetime_1($candidate);
233              
234 242 50       469 if ($date eq 'Could not parse date')
235             {
236 242         349 $date = $self -> parse_datetime_2('%A%n%B%n%d%n%Y', $candidate);
237              
238 242 100       38671 if ($date eq 'Could not parse date')
239             {
240 215         767 $candidate =~ s/(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s*//;
241 215         347 $date = $self -> parse_datetime_2('%B%n%d%n%Y', $candidate);
242             }
243             }
244              
245 242   33     14214 return $@ || $date;
246              
247             } # End of parse_datetime.
248              
249             # -----------------------------------------------
250              
251             sub parse_datetime_1
252             {
253 242     242 1 225 my($self, $candidate) = @_;
254              
255 242         200 my($date);
256              
257 242         1483 require 'DateTime/Format/HTTP.pm';
258              
259             try
260             {
261 242     242   5726 $date = DateTime::Format::HTTP -> parse_datetime($candidate);
262             }
263             catch
264             {
265 242     242   8522 $date = 'Could not parse date';
266 242         4825 };
267              
268 242         999 return $date;
269              
270             } # End of parse_datetime_1.
271              
272             # -----------------------------------------------
273              
274             sub parse_datetime_2
275             {
276 457     457 1 566 my($self, $pattern, $candidate) = @_;
277 457         1024 $candidate =~ s/([0-9]+)(st|nd|rd|th)/$1/; # Zap st from 1st, etc.
278              
279 457         2324 require 'DateTime/Format/Strptime.pm';
280              
281 457         39368 my($parser) = DateTime::Format::Strptime -> new(pattern => $pattern);
282              
283 457   100     398861 return $parser -> parse_datetime($candidate) || 'Could not parse date';
284              
285             } # End of parse_datetime_2.
286              
287             # -----------------------------------------------
288              
289             sub read
290             {
291 0     0 1 0 my($self, $in_file_name) = @_;
292 0   0     0 $in_file_name ||= $self -> inFileName || 'Changelog.ini';
      0        
293              
294 0         0 $self -> config(Config::IniFiles -> new(-file => $in_file_name) );
295              
296             # Return object for method chaining.
297              
298 0         0 return $self -> validate($in_file_name);
299              
300             } # End of read.
301              
302             # -----------------------------------------------
303              
304             sub reader
305             {
306 1     1 1 5 my($self, $in_file_name) = @_;
307 1   0     2 $in_file_name ||= $self -> inFileName || (-e 'Changes' ? 'Changes' : 'CHANGES');
      33        
308 1         7 my(@line) = read_lines $in_file_name;
309              
310 1         410 $self -> log("Input file: $in_file_name");
311              
312             # Get module name from the first line.
313             # 1st guess at format: /Revision history for Perl extension Local::Wine./.
314              
315 1         9 my($line) = shift @line;
316 1         10 $line =~ s/\s+$//;
317 1         4 $line =~ s/\s*\.\s*$//;
318 1         10 my(@field) = split(/\s+/, $line);
319 1         4 my($module_name) = $field[$#field];
320 1 50       6 my($ok) = $module_name ? 1 : 0;
321              
322             # 2nd guess at format: X::Y somewhere in the first line. This overrides the first guess.
323              
324 1 50       5 if (! $ok)
325             {
326 0         0 @field = split(/\s+/, $line);
327              
328 0         0 my($field);
329              
330 0         0 for $field (@field)
331             {
332 0 0       0 if ($field =~ /^.+::.+$/)
333             {
334 0         0 $module_name = $field;
335              
336 0         0 last;
337             }
338             }
339             }
340              
341 1         25 $self -> module_name($module_name);
342 1         26 $self -> log("Module: $module_name");
343              
344             # Return object for method chaining.
345              
346 1         9 return $self -> transform(@line);
347              
348             } # End of reader.
349              
350             # -----------------------------------------------
351              
352             sub report
353             {
354 0     0 1 0 my($self) = @_;
355 0         0 my($module_name) = $self -> config -> val('Module', 'Name');
356 0         0 my($width) = 15;
357              
358 0         0 my(@output);
359              
360 0         0 push @output, ['Module', $module_name];
361 0         0 push @output, ['-' x $width, '-' x $width];
362              
363 0         0 my($found) = 0;
364 0         0 my(@release) = $self -> config -> GroupMembers('V');
365              
366 0         0 my($date, $deploy_action, $deploy_reason);
367 0         0 my($release);
368 0         0 my($version);
369              
370 0         0 for $release (@release)
371             {
372 0         0 ($version = $release) =~ s/^V //;
373              
374 0 0 0     0 next if ($self -> release && ($version ne $self -> release) );
375              
376 0         0 $date = $self -> config -> val($release, 'Date');
377 0         0 $deploy_action = $self -> config -> val($release, 'Deploy.Action');
378 0         0 $deploy_reason = $self -> config -> val($release, 'Deploy.Reason');
379 0         0 $found = 1;
380              
381 0         0 push @output, ['Version', $version];
382 0         0 push @output, ['Date', $date];
383              
384 0 0       0 if ($deploy_action)
385             {
386 0         0 push @output, ['Deploy.Action', $deploy_action];
387 0         0 push @output, ['Deploy.Reason', $deploy_reason];
388             }
389              
390 0         0 push @output, ['-' x $width, '-' x $width];
391             }
392              
393 0 0       0 if (! $found)
394             {
395 0         0 push @output, ['Warning', "V @{[$self -> release]} not found"];
  0         0  
396             }
397              
398 0 0       0 if ($self -> table)
399             {
400 0         0 $self -> report_as_html(@output);
401             }
402             else
403             {
404             # Report as text.
405              
406 0         0 for (@output)
407             {
408 0         0 printf "%-${width}s %s\n", $$_[0], $$_[1];
409             }
410             }
411              
412             } # End of report.
413              
414             # -----------------------------------------------
415              
416             sub report_as_html
417             {
418 0     0 1 0 my($self, @output) = @_;
419 0         0 my($template) = HTML::Template -> new(path => $self -> pathForHTML, filename => 'ini.table.tmpl');
420             @output = map
421             {
422 0         0 {
423             th => $Entitize{$$_[0]},
424 0 0       0 td => $Entitize{$$_[1]},
425             td_class => $$_[0] =~ /Deploy/ ? 'ini_deploy' : 'ini_td',
426             }
427             } @output;
428              
429 0         0 $template -> param(tr_loop => [@output]);
430              
431 0         0 my($content) = $template -> output();
432              
433 0 0       0 if ($self -> webPage)
434             {
435 0         0 $template = HTML::Template -> new(path => $self -> pathForHTML, filename => 'ini.page.tmpl');
436              
437 0         0 $template -> param(content => $content);
438 0         0 $template -> param(url_for_css => $self -> urlForCSS);
439              
440 0         0 $content = $template -> output();
441             }
442              
443 0         0 print $content;
444              
445             } # End of report_as_html.
446              
447             # -----------------------------------------------
448              
449             sub run
450             {
451 1     1 1 706 my($self) = @_;
452              
453             # If converting, inFileName is the name of an old-style Changes/CHANGES file,
454             # and outFileName is the name of a new-style Changelog.ini file.
455             # If reporting on a specific release, inFileName is the name of
456             # a new-style Changelog.ini file.
457              
458 1 50       15 if ($self -> convert)
459             {
460 1 50       16 $self -> inFileName('Changes') if (! $self -> inFileName);
461 1         18 $self -> reader($self -> inFileName) -> writer($self -> outFileName);
462             }
463             else
464             {
465 0 0       0 $self -> inFileName('Changelog.ini') if (! $self -> inFileName);
466 0         0 $self -> read($self -> inFileName);
467 0         0 $self -> report;
468             }
469              
470             # Return 0 for success in case someone wants to know.
471              
472 1         5 return 0;
473              
474             } # End of run.
475              
476             # -----------------------------------------------
477              
478             sub transform
479             {
480 1     1 1 26 my($self, @line) = @_;
481 1         2 my($count) = 0;
482              
483 1         2 my($current_version, $current_date, @comment);
484 0         0 my($date);
485 0         0 my(@field);
486 0         0 my($line);
487 0         0 my($release, @release);
488 0         0 my($version);
489              
490 1         2 for $line (@line)
491             {
492 322         284 $count++;
493              
494 322         1178 $line =~ s/^\s+//;
495 322         1304 $line =~ s/\s+$//;
496              
497 322 100       750 next if (length($line) == 0);
498 253 50       456 next if ($line =~ /^#/);
499              
500             # Try to get the version number and date.
501             # Each release is expected to start with one of:
502             # o 1.05 Fri Jan 25 10:08:00 2008
503             # o 4.30 - Friday, April 25, 2008
504             # o 4.08 - Thursday, March 15th, 2006
505             # Squash spaces.
506              
507 253         703 $line =~ tr/ / /s;
508              
509             # Remove commas (from dates) if the line starts with a digit (which is assumed to be a version #).
510              
511 253 100       698 $line =~ s/,//g if ($line =~ /^v?\d/);
512 253         904 @field = split(/\s(?:-\s)?/, $line, 2);
513              
514             # The "" keeps version happy.
515              
516             try
517             {
518 253     253   6199 $version = version -> new("$field[0]");
519 253         1445 };
520              
521 253 100       2847 $date = defined $field[1] ? $self -> parse_datetime($field[1]) : 'No input string';
522              
523 253 100 66     3364 if (! defined $version || ($version eq '0') || ($date eq 'Could not parse date') || ($date =~ /No input string/) )
      100        
      100        
524             {
525             # We got an error. So assume it's commentary on the current release.
526             # If the line starts with EOT, jam a '-' in front of it to escape it,
527             # since Config::IniFiles uses EOT to terminate multi-line comments.
528              
529 223 50       684 $line = "-$line" if (substr($line, 0, 3) eq 'EOT');
530              
531 223         639 push @comment, $line;
532             }
533             else
534             {
535             # We got a version and a date. Assume it's a new release.
536             # Step 1: Wrap up the last version, if any.
537              
538 30 50 33     1213 if ($version && $date)
539             {
540 30         1038 $self -> log("Processing: V $version $date");
541             }
542              
543 30 100       411 if ($current_version)
544             {
545 29         140 $release = {Version => $current_version, Date => $current_date, Comments => [@comment]};
546              
547 29         48 push @release, $release;
548             }
549              
550             # Step 2: Start the new version.
551              
552 30 100 100     310 if ($current_version && ($version eq $current_version) )
553             {
554 1         5 $self -> errstr("V $version found with dates $current_date and $date");
555              
556 1         73 $self -> log($self -> errstr);
557             }
558              
559 30         86 @comment = ();
560 30         28 $current_date = $date;
561 30         61 $current_version = $version;
562             }
563             }
564              
565             # Step 3: Wrap up the last version, if any.
566              
567 1 50       10 if ($current_version)
568             {
569 1         10 $release = {Version => $current_version, Date => $current_date, Comments => [@comment]};
570              
571 1         2 push @release, $release;
572             }
573              
574             # Scan the releases looking for security advisories.
575              
576 1         3 my($security);
577              
578 1         4 for $release (0 .. $#release)
579             {
580 30         19 $security = 0;
581              
582 30         17 for $line (@{$release[$release]{'Comments'} })
  30         44  
583             {
584 204 100       584 if ($line =~ /Security/i)
585             {
586 5         4 $security = 1;
587              
588 5         4 last;
589             }
590             }
591              
592 30 100       36 if ($security)
593             {
594 5         6 $release[$release]{'Deploy.Action'} = 'Upgrade';
595 5         6 $release[$release]{'Deploy.Reason'} = 'Security';
596             }
597             }
598              
599 1         31 $self -> changes([@release]);
600              
601             # Return object for method chaining.
602              
603 1         131 return $self;
604              
605             } # End of transform.
606              
607             # -----------------------------------------------
608              
609             sub validate
610             {
611 0     0 1 0 my($self, $in_file_name) = @_;
612              
613             # Validate existence of Module section.
614              
615 0 0       0 if (! $self -> config -> SectionExists('Module') )
616             {
617 0         0 die "Error: Section 'Module' is missing from $in_file_name";
618             }
619              
620             # Validate existence of Name within Module section.
621              
622 0         0 my($module_name) = $self -> config -> val('Module', 'Name');
623              
624 0 0       0 if (! defined $module_name)
625             {
626 0         0 die "Error: Section 'Module' is missing a 'Name' token in $in_file_name";
627             }
628              
629             # Validate existence of Releases.
630              
631 0         0 my(@release) = $self -> config -> GroupMembers('V');
632              
633 0 0       0 if ($#release < 0)
634             {
635 0         0 die "Error: No releases (sections like [V \$version]) found in $in_file_name";
636             }
637              
638 0         0 my($parser) = DateTime::Format::W3CDTF -> new;
639              
640 0         0 my($candidate);
641             my($date);
642 0         0 my($release);
643 0         0 my($version);
644              
645 0         0 for $release (@release)
646             {
647 0         0 ($version = $release) =~ s/^V //;
648              
649             # Validate Date within each Release.
650              
651 0         0 $candidate = $self -> config -> val($release, 'Date');
652              
653             try
654             {
655 0     0   0 $date = $parser -> parse_datetime($candidate);
656             }
657             catch
658             {
659 0     0   0 die "Error: Date $candidate is not in W3CDTF format";
660             }
661 0         0 }
662              
663 0         0 $self -> log("Successful validation of file: $in_file_name");
664              
665             # Return object for method chaining.
666              
667 0         0 return $self;
668              
669             } # End of validate.
670              
671             # -----------------------------------------------
672              
673             sub writer
674             {
675 1     1 1 9 my($self, $output_file_name) = @_;
676 1   0     3 $output_file_name ||= $self -> outFileName || 'Changelog.ini';
      33        
677              
678 1         11 $self -> config(Config::IniFiles -> new);
679 1         145 $self -> config -> AddSection('Module');
680 1         72 $self -> config -> newval('Module', 'Name', $self -> module_name);
681 1         92 $self -> config -> newval('Module', 'Changelog.Creator', __PACKAGE__ . " V $VERSION");
682 1         55 $self -> config -> newval('Module', 'Changelog.Parser', "Config::IniFiles V $Config::IniFiles::VERSION");
683              
684             # Sort by version number to put the latest version at the top of the file.
685              
686 1         40 my($section);
687              
688 1         2 for my $r (reverse sort{$$a{'Version'} cmp $$b{'Version'} } @{$self -> changes})
  69         137  
  1         43  
689             {
690 30         1150 $section = "V $$r{'Version'}";
691              
692 30         411 $self -> config -> AddSection($section);
693 30         1735 $self -> config -> newval($section, 'Date', $$r{'Date'});
694              
695             # Put these near the top of this release's notes.
696              
697 30 100       1192 if ($$r{'Deploy.Action'})
698             {
699 5         68 $self -> config -> newval($section, 'Deploy.Action', $$r{'Deploy.Action'});
700 5   50     261 $self -> config -> newval($section, 'Deploy.Reason', $$r{'Deploy.Reason'} || '');
701             }
702              
703 30         595 $self -> config -> newval($section, 'Comments', @{$$r{'Comments'} });
  30         103  
704             }
705              
706 1         51 $self -> config -> WriteConfig($output_file_name);
707              
708 1         4246 $self -> log("Output file: $output_file_name");
709              
710             # Return object for method chaining.
711              
712 1         7 return $self;
713              
714             } # End of writer.
715              
716             # -----------------------------------------------
717              
718             1;
719              
720             =head1 NAME
721              
722             Module::Metadata::Changes - Manage machine-readable Changes/CHANGES/Changelog.ini files
723              
724             =head1 Synopsis
725              
726             =head2 One-liners
727              
728             These examples use Changes/CHANGES and Changelog.ini in the 'current' directory.
729              
730             The command line options (except for -h) correspond to the options documented under L</Constructor and initialization>, below.
731              
732             shell>ini.report.pl -h
733             shell>ini.report.pl -c
734             shell>ini.report.pl -r 1.23
735             shell>sudo ini.report.pl -w > /var/www/Changelog.html
736             shell>perl -MModule::Metadata::Changes -e 'Module::Metadata::Changes->new(convert => 1)->run'
737             shell>perl -MModule::Metadata::Changes -e 'print Module::Metadata::Changes->new->read->get_latest_version'
738             shell>perl -MModule::Metadata::Changes -e 'print Module::Metadata::Changes->new->read->report'
739             shell>perl -MModule::Metadata::Changes -e 'print Module::Metadata::Changes->new(release=>"2.00")->read->report'
740              
741             L<Module::Metadata::Changes> ships with C<ini.report.pl> in the bin/ directory. It is installed along with the module.
742              
743             Also, L<Module::Metadata::Changes> uses L<Config::IniFiles> to read and write Changelog.ini files.
744              
745             =head2 Reporters
746              
747             With a script like this:
748              
749             #!/usr/bin/env perl
750              
751             use feature 'say';
752             use strict;
753             use warnings;
754              
755             use File::chdir; # For magic $CWD.
756              
757             use Module::Metadata::Changes;
758              
759             # ------------------------------------------------
760              
761             my($work) = "$ENV{HOME}/perl.modules";
762             my($m) = Module::Metadata::Changes -> new;
763              
764             opendir(INX, $work) || die "Can't opendir($work)";
765             my(@name) = sort grep{! /^\.\.?$/} readdir INX;
766             closedir INX;
767              
768             my($config);
769             my($version);
770              
771             for my $name (@name)
772             {
773             $CWD = "$work/$name"; # Does a chdir.
774             $version = $m -> read -> get_latest_version;
775             $config = $m -> config; # Must call read() before config().
776              
777             say $config -> val('Module', 'Name'), " V $version ", $config -> val("V $version", 'Date');
778             }
779              
780             you can get a report of the latest version number, from Changelog.ini, for each module in your vast library.
781              
782             =head1 Description
783              
784             L<Module::Metadata::Changes> is a pure Perl module.
785              
786             It allows you to convert old-style Changes/CHANGES files, and to read and write Changelog.ini files.
787              
788             =head1 Distributions
789              
790             This module is available as a Unix-style distro (*.tgz).
791              
792             See http://savage.net.au/Perl-modules.html for details.
793              
794             See http://savage.net.au/Perl-modules/html/installing-a-module.html for
795             help on unpacking and installing.
796              
797             =head1 Constructor and initialization
798              
799             new(...) returns an object of type L<Module::Metadata::Changes>.
800              
801             This is the class contructor.
802              
803             Usage: C<< Module::Metadata::Changes -> new() >>.
804              
805             This method takes a hash of options. There are no mandatory options.
806              
807             Call C<new()> as C<< new(option_1 => value_1, option_2 => value_2, ...) >>.
808              
809             Available options:
810              
811             =over 4
812              
813             =item o convert
814              
815             This takes the value 0 or 1.
816              
817             The default is 0.
818              
819             If the value is 0, calling C<run()> calls C<read()> and C<report()>.
820              
821             If the value is 1, calling C<run()> calls C<writer(reader() )>.
822              
823             =item o inFileName
824              
825             The default is 'Changes' (or, if absent, 'CHANGES') when calling C<reader()>, and
826             'Changelog.ini' when calling C<read()>.
827              
828             =item o outFileName
829              
830             The default is 'Changelog.ini'.
831              
832             =item o pathForHTML
833              
834             This is path to the HTML::Template-style templates used by the 'table' and 'webPage' options.
835              
836             The default is '/dev/shm/html/assets/templates/module/metadata/changes'.
837              
838             =item o release
839              
840             The default is ''.
841              
842             If this option has a non-empty value, the value is assumed to be a release/version number.
843              
844             In that case, reports (text, HTML) are restricted to only the given version.
845              
846             The default ('') means reports contain all versions.
847              
848             'release' was chosen, rather than 'version', in order to avoid a clash with 'verbose',
849             since all options could then be abbreviated to 1 letter (when running ini.report.pl).
850              
851             Also, a lot of other software uses -r to refer to release/version.
852              
853             =item o table
854              
855             This takes the value 0 or 1.
856              
857             The default is 0.
858              
859             This option is only used when C<report()> is called.
860              
861             If the value is 0, calling C<report()> outputs a text report.
862              
863             If the value is 1, calling C<report()> outputs a HTML report.
864              
865             By default, the HTML report will just be a HTML table.
866              
867             However, if the 'webPage' option is 1, the HTML will be a complete web page.
868              
869             =item o urlForCSS
870              
871             The default is '/assets/css/module/metadata/changes/ini.css'.
872              
873             This is only used if the 'webPage' option is 1.
874              
875             =item o verbose
876              
877             This takes the value 0 or 1.
878              
879             The default is 0.
880              
881             If the value is 1, write progress reports to STDERR.
882              
883             =item o webPage
884              
885             This takes the value 0 or 1.
886              
887             The default is 0.
888              
889             A value of 1 automatically sets 'table' to 1.
890              
891             If the value is 0, the 'table' option outputs just a HTML table.
892              
893             If the value is 1, the 'table' option outputs a complete web page.
894              
895             =back
896              
897             =head1 Methods
898              
899             =head2 o config()
900              
901             Returns the L<Config::IniFiles> object, from which you can extract all the data.
902              
903             This method I<must> be called after calling C<read()>.
904              
905             See C<scripts/report.names.pl> for sample code.
906              
907             The names of the sections, [Module] and [V 1.23], and the keys under each, are documented in the FAQ.
908              
909             =head2 o errstr()
910              
911             Returns the last error message, or ''.
912              
913             =head2 o get_latest_release()
914              
915             Returns an hash ref of details for the latest release.
916              
917             Returns {} if there is no such release.
918              
919             The hash keys are (most of) the reserved tokens, as discussed below in the FAQ.
920              
921             Some reserved tokens, such as EOT, make no sense as hash keys.
922              
923             =head2 o get_latest_version()
924              
925             Returns the version number of the latest version.
926              
927             Returns '' if there is no such version.
928              
929             =head2 o parse_datetime()
930              
931             Used by C<transform()>.
932              
933             =head2 o parse_datetime_1()
934              
935             Used by C<transform()>.
936              
937             =head2 o parse_datetime_2()
938              
939             Used by C<transform()>.
940              
941             =head2 o read([$input_file_name])
942              
943             This method reads the given file, using L<Config::IniFiles>.
944              
945             The $input_file_name is optional. It defaults to 'Changelog.ini'.
946              
947             See config().
948              
949             Return value: The object, for method chaining.
950              
951             =head2 o reader([$input_file_name])
952              
953             This method parses the given file, assuming it is format is the common-or-garden Changes/CHANGES style.
954              
955             The $input_file_name is optional. It defaults to 'Changes' (or, if absent, 'CHANGES').
956              
957             C<reader()> calls C<module_name()> to save the module name for use by other methods.
958              
959             C<reader()> calls C<transform()>.
960              
961             Return value: An arrayref of hashrefs, i.e. the return value of C<transform()>.
962              
963             This value is suitable for passing to C<writer()>.
964              
965             =head2 o report()
966              
967             Displays various items for one or all releases.
968              
969             If the 'release' option to C<new()> was not used, displays items for all releases.
970              
971             If 'release' was used, restrict the report to just that release/version.
972              
973             If either the 'table' or 'webPage' options to C<new()> were used, output HTML by calling C<report_as_html()>.
974              
975             If these latter 2 options were not used, output text.
976              
977             HTML is escaped using L<HTML::Entities::Interpolate>.
978              
979             Output is to STDOUT.
980              
981             Clearly, you should not use -v to get logging output when using text or HTML output.
982              
983             =head2 o report_as_html()
984              
985             Displays various items as HTML for one or all releases.
986              
987             If the 'release' option to C<new()> was not used, displays items for all releases.
988              
989             If 'release' was used, restrict the report to just that release/version.
990              
991             Warning: This method must be called via the C<report()> method.
992              
993             Output is to STDOUT.
994              
995             =head2 o run()
996              
997             Use the options passed to C<new()> to determine what to do.
998              
999             Calling C<< new(convert => 1) >> and then C<run()> will cause C<writer(reader() )> to be called.
1000              
1001             If you do not set 'convert' to 1 (i.e. use 0 - the default), C<run()> will call C<read()> and C<report()>.
1002              
1003             Return value: 0.
1004              
1005             =head2 o transform(@line)
1006              
1007             Transform the memory-based version of Changes/CHANGES into an arrayref of hashrefs, where each array element
1008             holds data for 1 version.
1009              
1010             Must be called by C<reader()>.
1011              
1012             The array is the text read in from Changes/CHANGES.
1013              
1014             C<transform()> stores the arrayref of hashrefs in $obj -> changes(), for use by C<writer()>.
1015              
1016             Return value: The object, for method chaining.
1017              
1018             =head2 o validate($file_name)
1019              
1020             This method is used by C<read()> to validate the contents of the file read in.
1021              
1022             C<validate()> does not read the file.
1023              
1024             C<validate()> calls die when a validation test fails.
1025              
1026             The file name is just used for reporting.
1027              
1028             Return value: The object, for method chaining.
1029              
1030             =head2 o writer([$output_file_name])
1031              
1032             This method writes the arrayref stored in $obj -> changes(), using L<Config::IniFiles>, to the given file.
1033              
1034             See C<transform()>.
1035              
1036             The $output_file_name is optional. It defaults to 'Changelog.ini'.
1037              
1038             Return value: The object, for method chaining.
1039              
1040             =head1 FAQ
1041              
1042             =over 4
1043              
1044             =item o Are there any things I should look out for?
1045              
1046             =over 4
1047              
1048             =item o Invalid dates
1049              
1050             Invalid dates in Changes/CHANGES cannot be distinguished from comments. That means that if the output file is
1051             missing one or more versions, it is because of those invalid dates.
1052              
1053             =item o Invalid day-of-week (dow)
1054              
1055             If Changes/CHANGES includes the dow, it is not cross-checked with the date, so if the dow is wrong,
1056             you will not get an error generated.
1057              
1058             =back
1059              
1060             =item o How do I display Changelog.ini?
1061              
1062             See C<bin/ini.report.pl>. It outputs text or HTML.
1063              
1064             =item o What is the format of Changelog.ini?
1065              
1066             See also the next question.
1067              
1068             See C<scripts/report.names.pl> for sample code.
1069              
1070             Here is a sample:
1071              
1072             [Module]
1073             Name=CGI::Session
1074             Changelog.Creator=Module::Metadata::Changes V 1.00
1075             Changelog.Parser=Config::IniFiles V 2.39
1076              
1077             [V 4.30]
1078             Date=2008-04-25T00:00:00
1079             Comments= <<EOT
1080             * FIX: Patch POD for CGI::Session in various places, to emphasize even more that auto-flushing is
1081             unreliable, and that flush() should always be called explicitly before the program exits.
1082             The changes are a new section just after SYNOPSIS and DESCRIPTION, and the PODs for flush(),
1083             and delete(). See RT#17299 and RT#34668
1084             * NEW: Add t/new_with_undef.t and t/load_with_undef.t to explicitly demonstrate the effects of
1085             calling new() and load() with various types of undefined or fake parameters. See RT#34668
1086             EOT
1087              
1088             [V 4.10]
1089             Date=2006-03-28T00:00:00
1090             Deploy.Action=Upgrade
1091             Deploy.Reason=Security
1092             Comments= <<EOT
1093             * SECURITY: Hopefully this settles all of the problems with symlinks. Both the file
1094             and db_file drivers now use O_NOFOLLOW with open when the file should exist and
1095             O_EXCL|O_CREAT when creating the file. Tests added for symlinks. (Matt LeBlanc)
1096             * SECURITY: sqlite driver no longer attempts to use /tmp/sessions.sqlt when no
1097             Handle or DataSource is specified. This was a mistake from a security standpoint
1098             as anyone on the machine would then be able to create and therefore insert data
1099             into your sessions. (Matt LeBlanc)
1100             * NEW: name is now an instance method (RT#17979) (Matt LeBlanc)
1101             EOT
1102              
1103             =item o What are the reserved tokens in this format?
1104              
1105             I am using tokens to refer to both things in [] such as Module, and things on the left hand side
1106             of the = signs, such as Date.
1107              
1108             And yes, these tokens are case-sensitive.
1109              
1110             Under the [Module] section, the tokens are:
1111              
1112             =over 4
1113              
1114             =item o Changelog.Creator
1115              
1116             sample: Changelog.Creator=Module::Metadata::Changes V 2.00
1117              
1118             =item o Changelog.Parser
1119              
1120             Sample: Changelog.Parser=Config::IniFiles V 2.66
1121              
1122             =item o Name
1123              
1124             Sample: Name=Manage::Module::Changes
1125              
1126             =back
1127              
1128             Under each version (section), whose name is like [V 1.23], the token are as follows.
1129              
1130             L<Config::IniFiles> calls the V in [V 1.23] a Group Name.
1131              
1132             =over 4
1133              
1134             =item o Comments
1135              
1136             Sample: Comments=- Original version
1137              
1138             =item o Date
1139              
1140             The datetime of the release, in W3CDTF format.
1141              
1142             Sample: Date=2008-05-02T15:15:45
1143              
1144             I know the embedded 'T' makes this format a bit harder to read, but the idea is that such files
1145             will normally be processed by a program.
1146              
1147             =item o Deploy.Action
1148              
1149             The module author makes this recommendation to the end user.
1150              
1151             This enables the end user to quickly grep the Changelog.ini, or the output of C<ini.report.pl>,
1152             for things like security fixes and API changes.
1153              
1154             Run 'bin/ini.report.pl -h' for help.
1155              
1156             Suggestions:
1157              
1158             Deploy.Action=Upgrade
1159             Deploy.Reason=(Security|Major bug fix)
1160              
1161             Deploy.Action=Upgrade with caution
1162             Deploy.Reason=(Major|Minor) API change/Development version
1163              
1164             Alternately, the classic syslog tokens could perhaps be used:
1165              
1166             Debug/Info/Notice/Warning/Error/Critical/Alert/Emergency.
1167              
1168             I think the values for these 2 tokens (Deploy.*) should be kept terse, and the Comments section used
1169             for an expanded explanation, if necessary.
1170              
1171             Omitting Deploy.Action simply means the module author leaves it up to the end user to
1172             read the comments and make up their own mind.
1173              
1174             C<reader()> called directly, or via C<ini.report.pl -c> (i.e. old format to ini format converter),
1175             inserts these 2 tokens if it sees the word /Security/i in the Comments. It is a crude but automatic warning
1176             to end users. The HTML output options (C<-t> and C<-w>) use red text via CSS to highlight these 2 tokens.
1177              
1178             Of course security is best handled by the module author explicitly inserting a suitable note.
1179              
1180             And, lastly, any such note is purely up to the judgement of the author, which means differences in
1181             opinion are inevitable.
1182              
1183             =item o Deploy.Reason
1184              
1185             The module author gives this reason for their recommended action.
1186              
1187             =item o EOT
1188              
1189             Config::IniFiles uses EOT to terminate multi-line comments.
1190              
1191             If C<transform()> finds a line beginning with EOT, it jams a '-' in front of it.
1192              
1193             =back
1194              
1195             =item o Why are there not more reserved tokens?
1196              
1197             Various reasons:
1198              
1199             =over 4
1200              
1201             =item o Any one person, or any group, can standardize on their own tokens
1202              
1203             Obviously, it would help if they advertised their choice, firstly so as to get as
1204             many people as possible using the same tokens, and secondly to get agreement on the
1205             interpretation of those choices.
1206              
1207             Truely, there is no point in any particular token if it is not given a consistent meaning.
1208              
1209             =item o You can simply add your own to your Changelog.ini file
1210              
1211             They will then live on as part of the file.
1212              
1213             =back
1214              
1215             Special processing is normally only relevant when converting an old-style Changes/CHANGES file
1216             to a new-style Changelog.ini file.
1217              
1218             However, if you think the new tokens are important enough to be displayed as part of the text
1219             and HTML format reports, let me know.
1220              
1221             I have deliberately not included the Comments in reports since you can always just examine the
1222             Changelog.ini file itself for such items. But that too could be changed.
1223              
1224             =item o Are single-line comments acceptable?
1225              
1226             Sure. Here is one:
1227              
1228             Comments=* INTERNAL: No Changes since 4.20_1. Declaring stable.
1229              
1230             The '*' is not special, it is just part of the comment.
1231              
1232             =item o What is with the datetime format?
1233              
1234             It is called W3CDTF format. See:
1235              
1236             http://search.cpan.org/dist/DateTime-Format-W3CDTF/
1237              
1238             See also ISO8601 format:
1239              
1240             http://search.cpan.org/dist/DateTime-Format-ISO8601/
1241              
1242             =item o Why this file format?
1243              
1244             Various reasons:
1245              
1246             =over 4
1247              
1248             =item o [Module] allows for [Script], [Library], and so on.
1249              
1250             =item o *.ini files are easy for beginners to comprehend
1251              
1252             =item o Other formats were considered. I made a decision
1253              
1254             There is no perfect format which will please everyone.
1255              
1256             Various references, in no particular order:
1257              
1258             http://use.perl.org/~miyagawa/journal/34850
1259              
1260             http://use.perl.org/~hex/journal/34864
1261              
1262             http://redhanded.hobix.com/inspect/yamlIsJson.html
1263              
1264             http://use.perl.org/article.pl?sid=07/09/06/0324215
1265              
1266             http://use.perl.org/comments.pl?sid=36862&cid=57590
1267              
1268             http://use.perl.org/~RGiersig/journal/34370/
1269              
1270             =item o The module L<Config::IniFiles> already existed, for reading and writing this format
1271              
1272             Specifically, L<Config::IniFiles> allows for here documents, which I use to hold the comments
1273             authors produce for most of their releases.
1274              
1275             =back
1276              
1277             =item o What is the difference between release and version?
1278              
1279             I am using release to refer not just to the version number, but also to all the notes
1280             relating to that version.
1281              
1282             And by notes I mean everything in one section under the name [V $version].
1283              
1284             =item o Will you switch to YAML or XML format?
1285              
1286             YAML? No, never. It is targetted at other situations, and while it can be used for simple
1287             applications like this, it can't be hand-written I<by beginners>.
1288              
1289             And it's unreasonable to force people to write a simple program to write a simple YAML file.
1290              
1291             XML? Nope. It is great in I<some> situations, but too visually dense and slow to write for this one.
1292              
1293             =item o What about adding Changed Requirements to the file?
1294              
1295             No. That info will be in the changed C<Build.PL> or C<Makefile.PL> files.
1296              
1297             It is a pointless burden to make the module author I<also> add that to Changelog.ini.
1298              
1299             =item o Who said you had the power to decide on this format?
1300              
1301             No-one. But I do have the time and the inclination to maintain L<Module::Metadata::Changes>
1302             indefinitely.
1303              
1304             Also, I had a pressing need for a better way to manage metadata pertaining my own modules,
1305             for use in my database of modules.
1306              
1307             One of the reports I produce from this database is visible here:
1308              
1309             http://savage.net.au/Perl-modules.html
1310              
1311             Ideally, there will come a time when all of your modules, if not the whole of CPAN,
1312             will have Changelog.ini files, so producing such a report will be easy, and hence will be
1313             that much more likely to happen.
1314              
1315             =item o Why not use, say, L<Config::Tiny> to process Changelog.ini files?
1316              
1317             Because L<Config::Tiny> contains this line, 's/\s\;\s.+$//g;', so it will mangle
1318             text containing English semi-colons.
1319              
1320             Also, authors add comments per release, and most C<Config::*> modules only handle lines
1321             of the type X=Y.
1322              
1323             =item o How are the old Changes/CHANGES files parsed?
1324              
1325             The first line is scanned looking for /X::Y/ or /X\.$/. And yes, it fails for modules
1326             which identify themselves like Fuse-PDF not at the end of the line.
1327              
1328             Then lines looking something like /$a_version_number ... $a_datetime/ are searched for.
1329             This is deemed to be the start of information pertaining to a specific release.
1330              
1331             Everything up to the next release, or EOF, is deemed to belong to the release just
1332             identified.
1333              
1334             This means a line containing a version number without a date is not recognized as a new release,
1335             so that that line and the following comments are added to the 'current' release info.
1336              
1337             For an example of this, process the C<Changes> file from CGI::Session (t/Changes), and scan the
1338             output for '[4.00_01]', which you will see contains stuff for V 3.12, 3.8 and 3.x.
1339              
1340             See above, under the list of reserved tokens, for how security advisories are inserted in the output
1341             stream.
1342              
1343             =item o Is this conversion process perfect?
1344              
1345             Well, no, actually, but it will be as good as I can make it.
1346              
1347             For example, version numbers like '3.x' are turned into '3.'.
1348              
1349             You will simply have to scrutinize (which means 'read I<carefully>') the output of this conversion process.
1350              
1351             If a Changes/CHANGES file is not handled by the current version, log a bug report on Request Tracker:
1352             http://rt.cpan.org/Public/
1353              
1354             =item o How are datetimes in old-style files parsed?
1355              
1356             Firstly try L<DateTime::Format::HTTP>, and if that fails, try these steps:
1357              
1358             =over 4
1359              
1360             =item o Strip 'st' from 1st, 'nd' from 2nd, etc
1361              
1362             =item o Try L<DateTime::Format::Strptime>
1363              
1364             =item o If that fails, strip Monday, etc, and retry L<DateTime::Format::Strptime>
1365              
1366             I noticed some dates were invalid because the day of the week did not match
1367             the day of the month. So, I arbitrarily chop the day of the week, and retry.
1368              
1369             =back
1370              
1371             Other date parsing modules are L<Date::Manip>, L<Date::Parse> and L<Regexp::Common::time>.
1372              
1373             =item o Why did you choose these 2 modules?
1374              
1375             I had a look at a few Changes/CHANGES files, and these made sense.
1376              
1377             If appropriate, other modules can be added to the algorithm.
1378              
1379             See the discussion on this page (search for 'parse multiple formats'):
1380              
1381             http://datetime.perl.org/index.cgi?FAQBasicUsage
1382              
1383             If things get more complicated, I will reconsider using L<DateTime::Format::Builder>.
1384              
1385             =item o What happens for 2 releases on the same day?
1386              
1387             It depends whether or not the version numbers are different.
1388              
1389             The C<Changes> file for L<CGI::Session> contains 2 references to version 4.06 :-(.
1390              
1391             As long as the version numbers are different, the date does not actually matter.
1392              
1393             =item o Will a new file format mean more work for those who maintain CPAN?
1394              
1395             Yes, I am afraid so, unless they completely ignore me!
1396              
1397             But I am hopeful this will lead to less work overall.
1398              
1399             =item o Why did you not use the C<Template Toolkit> for the HTML?
1400              
1401             It is too complex for this tiny project.
1402              
1403             =item o Where do I go for support?
1404              
1405             Log a bug report on Request Tracker: http://rt.cpan.org/Public/
1406              
1407             If it concerns failure to convert a specific Changes/CHANGES file, just provide the name of
1408             the module and the version number.
1409              
1410             It would help - if the problem is failure to parse a specific datetime format - if you could
1411             advise me on a suitable C<DateTime::Format::*> module to use.
1412              
1413             =back
1414              
1415             =head1 Machine-Readable Change Log
1416              
1417             The file Changes was converted into Changelog.ini by L<Module::Metadata::Changes>.
1418              
1419             =head1 Version Numbers
1420              
1421             Version numbers < 1.00 represent development versions. From 1.00 up, they are production versions.
1422              
1423             =head1 Repository
1424              
1425             L<https://github.com/ronsavage/Module-Metadata-Changes>.
1426              
1427             =head1 Support
1428              
1429             Email the author, or log a bug on RT:
1430              
1431             L<https://rt.cpan.org/Public/Dist/Display.html?Name=Module::Metadata::Changes>.
1432              
1433             =head1 See Also
1434              
1435             L<App::ParseCPANChanges>.
1436              
1437             L<CPAN::Changes>
1438              
1439             L<Module::Changes>
1440              
1441             =head1 Author
1442              
1443             L<Module::Metadata::Changes> was written by Ron Savage I<E<lt>ron@savage.net.auE<gt>> in 2008.
1444              
1445             Home page: http://savage.net.au/index.html
1446              
1447             =head1 Copyright
1448              
1449             Australian copyright (c) 2008, Ron Savage.
1450             All Programs of mine are 'OSI Certified Open Source Software';
1451             you can redistribute them and/or modify them under the terms of
1452             The Perl License, a copy of which is available at:
1453             http://dev.perl.org/licenses/
1454              
1455             =cut