File Coverage

blib/lib/CPAN/Changes.pm
Criterion Covered Total %
statement 123 128 96.0
branch 55 60 91.6
condition 12 20 60.0
subroutine 17 18 94.4
pod 10 10 100.0
total 217 236 91.9


line stmt bran cond sub pod time code
1             package CPAN::Changes;
2              
3 22     22   393889 use strict;
  22         45  
  22         991  
4 22     22   97 use warnings;
  22         34  
  22         619  
5              
6 22     22   8436 use CPAN::Changes::Release;
  22         51  
  22         649  
7 22     22   114 use Scalar::Util ();
  22         27  
  22         261  
8 22     22   10635 use version ();
  22         35113  
  22         640  
9 22     22   12925 use Encode qw(decode FB_CROAK LEAVE_SRC);
  22         192192  
  22         40701  
10              
11             our $VERSION = '0.400001';
12              
13             # From DateTime::Format::W3CDTF
14             our $W3CDTF_REGEX = qr{(\d\d\d\d) # Year
15             (?:-(\d\d) # -Month
16             (?:-(\d\d) # -Day
17             (?:[T\s]
18             (\d\d):(\d\d) # Hour:Minute
19             (?:
20             :(\d\d) # :Second
21             (\.\d+)? # .Fractional_Second
22             )?
23             ( Z # UTC
24             | [+-]\d\d:\d\d # Hour:Minute TZ offset
25             (?::\d\d)? # :Second TZ offset
26             )?)?)?)?}x;
27              
28             my @m = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
29             my %months = map { $m[ $_ ] => $_ + 1 } 0 .. 11;
30              
31             our $UNKNOWN_VALS = join( '|', (
32             'Unknown Release Date', 'Unknown', 'Not Released', 'Development Release',
33             'Development', 'Developer Release',
34             ) );
35              
36             sub new {
37 27     27 1 656 my $class = shift;
38 27         170 return bless {
39             preamble => '',
40             releases => {},
41             months => \%months,
42             @_,
43             }, $class;
44             }
45              
46             sub load {
47 18     18 1 1124 my ( $class, $file, @args ) = @_;
48              
49 18 50       1271 open( my $fh, '<:raw', $file ) or die $!;
50              
51 18         38 my $content = do { local $/; <$fh> };
  18         79  
  18         474  
52              
53 18         121 close $fh;
54              
55             # if it's valid UTF-8, decode that. otherwise, assume latin 1 and leave it.
56 18         33 eval { $content = decode('UTF-8', $content, FB_CROAK | LEAVE_SRC) };
  18         533  
57              
58 18         3896 return $class->load_string( $content, @args );
59             }
60              
61             sub load_string {
62 22     22 1 1946 my ( $class, $string, @args ) = @_;
63              
64 22         87 my $changes = $class->new( @args );
65 22         41 my $preamble = '';
66 22         27 my ( @releases, $ingroup, $indent );
67              
68 22         1342 $string =~ s/(?:\015{1,2}\012|\015|\012)/\n/gs;
69 22         488 my @lines = split( "\n", $string );
70              
71             my $version_line_re
72             = $changes->{ next_token }
73 22 100       1620 ? qr/^(?:$version::LAX|$changes->{next_token})/
74             : qr/^$version::LAX/;
75              
76 22   66     441 $preamble .= shift( @lines ) . "\n" while @lines && $lines[ 0 ] !~ $version_line_re;
77              
78 22         56 for my $l ( @lines ) {
79              
80             # Version & Date
81 346 100       1277 if ( $l =~ $version_line_re ) {
82 77         531 my ( $v, $n ) = split m{\s[\W\s]*}, $l, 2;
83 77         98 my $match = '';
84 77         74 my $d;
85              
86             # munge date formats, save the remainder as note
87 77 100       134 if ( $n ) {
88             # unknown dates
89 74 100       2743 if ( $n =~ m{^($UNKNOWN_VALS)}i ) {
    100          
    100          
    100          
    100          
90 5         7 $d = $1;
91 5         7 $match = $d;
92             }
93             # handle localtime-like timestamps
94             elsif ( $n
95             =~ m{^(\D{3}\s+(\D{3})\s+(\d{1,2})\s+([\d:]+)?\D*(\d{4}))} )
96             {
97 3         8 $match = $1;
98 3 100       7 if ( $4 ) {
99              
100             # unfortunately ignores TZ data
101             $d = sprintf(
102             '%d-%02d-%02dT%sZ',
103 2         14 $5, $changes->{ months }->{ $2 },
104             $3, $4
105             );
106             }
107             else {
108             $d = sprintf( '%d-%02d-%02d',
109 1         11 $5, $changes->{ months }->{ $2 }, $3 );
110             }
111             }
112              
113             # RFC 2822
114             elsif ( $n
115             =~ m{^(\D{3}, (\d{1,2}) (\D{3}) (\d{4}) (\d\d:\d\d:\d\d) ([+-])(\d{2})(\d{2}))}
116             )
117             {
118 1         2 $match = $1;
119             $d = sprintf(
120             '%d-%02d-%02dT%s%s%02d:%02d',
121 1         14 $4, $changes->{ months }->{ $3 },
122             $2, $5, $6, $7, $8
123             );
124             }
125              
126             # handle dist-zilla style, puts TZ data in note
127             elsif ( $n
128             =~ m{^((\d{4}-\d\d-\d\d)\s+(\d\d:\d\d(?::\d\d)?))(?:\s+[A-Za-z]+/[A-Za-z_-]+)} )
129             {
130 2         6 $match = $1;
131 2         15 $d = sprintf( '%sT%sZ', $2, $3 );
132             }
133              
134             # start with W3CDTF, ignore rest
135             elsif ( $n =~ m{^($W3CDTF_REGEX)} ) {
136 62         132 $match = $1;
137 62         68 $d = $match;
138 62         131 $d =~ s{ }{T};
139             # Add UTC TZ if date ends at H:M, H:M:S or H:M:S.FS
140 62 100 100     475 $d .= 'Z' if length( $d ) == 16 || length( $d ) == 19 || $d =~ m{\.\d+$};
      100        
141             }
142              
143             # clean date from note
144 74         1070 $n =~ s{^\Q$match\E\s*}{};
145             }
146              
147 77 100       196 undef $d unless length $d;
148 77 100       181 undef $n unless length $n;
149              
150 77         349 push @releases,
151             CPAN::Changes::Release->new(
152             version => $v,
153             date => $d,
154             _parsed_date => $match,
155             note => $n,
156             );
157 77         88 $ingroup = undef;
158 77         70 $indent = undef;
159 77         148 next;
160             }
161              
162             # Grouping
163 269 100       524 if ( $l =~ m{^\s+\[\s*([^\[\]]+?)\s*\]\s*$} ) {
164 17         31 $ingroup = $1;
165 17         59 $releases[ -1 ]->add_group( $1 );
166 17         25 next;
167             }
168              
169 252 100       397 $ingroup = '' if !defined $ingroup;
170              
171 252 100       636 next if $l =~ m{^\s*$};
172              
173 133 100       212 if ( !defined $indent ) {
174 69 100       267 $indent
175             = $l =~ m{^(\s+)}
176             ? '\s' x length $1
177             : '';
178             }
179              
180 133         559 $l =~ s{^$indent}{};
181              
182             # Inconsistent indentation between releases
183 133 50 66     402 if ( $l =~ m{^\s} && !@{ $releases[ -1 ]->changes( $ingroup ) } ) {
  21         58  
184 0         0 $l =~ m{^(\s+)};
185 0         0 $indent = $1;
186 0         0 $l =~ s{^\s+}{};
187             }
188              
189             # Change line cont'd
190 133 100       257 if ( $l =~ m{^\s} ) {
191 21         65 $l =~ s{^\s+}{};
192 21         49 my $changeset = $releases[ -1 ]->changes( $ingroup );
193 21         64 $changeset->[ -1 ] .= " $l";
194             }
195              
196             # Start of Change line
197             else {
198 112         279 $l =~ s{^[^[:alnum:]]+\s}{}; # remove leading marker
199 112         422 $releases[ -1 ]->add_changes( { group => $ingroup }, $l );
200             }
201              
202             }
203              
204 22         92 $changes->preamble( $preamble );
205 22         65 $changes->releases( @releases );
206              
207 22         172 return $changes;
208             }
209              
210             sub preamble {
211 55     55 1 8051 my $self = shift;
212              
213 55 100       149 if ( @_ ) {
214 23         40 my $preamble = shift;
215 23         70 $preamble =~ s{\s+$}{}s;
216 23         51 $self->{ preamble } = $preamble;
217             }
218              
219 55         159 return $self->{ preamble };
220             }
221              
222             sub releases {
223 61     61 1 2720 my $self = shift;
224              
225 61 100       181 if ( @_ ) {
226 25         56 $self->{ releases } = {};
227 25         113 $self->add_release( @_ );
228             }
229              
230             my $sort_function = sub {
231             ( eval {
232             ( my $v = $a->version ) =~ s/-TRIAL$//;
233             version->parse( $v );
234             }
235             || 0
236             ) <=> (
237 160 50 0 160   426 eval {
      0        
      50        
      50        
238             ( my $v = $b->version ) =~ s/-TRIAL$//;
239             version->parse( $v );
240             }
241             || 0
242             ) or ( $a->date || '' ) cmp( $b->date || '' );
243 61         271 };
244              
245 61         93 my $next_token = $self->{ next_token };
246              
247             my $token_sort_function = sub {
248 3 100   3   12 $a->version =~ $next_token - $b->version =~ $next_token
249             or $sort_function->();
250 61         153 };
251              
252 61 100       168 my $sort = $next_token ? $token_sort_function : $sort_function;
253              
254 61         66 return sort $sort values %{ $self->{ releases } };
  61         508  
255             }
256              
257             sub add_release {
258 31     31 1 375 my $self = shift;
259              
260 31         75 for my $release ( @_ ) {
261 86 100       262 my $new = Scalar::Util::blessed $release ? $release
262             : CPAN::Changes::Release->new( %$release );
263 86         266 $self->{ releases }->{ $new->version } = $new;
264             }
265             }
266              
267             sub delete_release {
268 0     0 1 0 my $self = shift;
269              
270 0         0 delete $self->{ releases }->{ $_ } for @_;
271             }
272              
273             sub release {
274 4     4 1 314 my ( $self, $version ) = @_;
275              
276 4 50       14 return unless exists $self->{ releases }->{ $version };
277 4         17 return $self->{ releases }->{ $version };
278             }
279              
280             sub delete_empty_groups {
281 2     2 1 7 my $self = shift;
282              
283 2         3 $_->delete_empty_groups for $self->releases;
284             }
285              
286             sub serialize {
287 11     11 1 777 my $self = shift;
288 11         20 my %args = @_;
289              
290 11         10 my %release_args;
291 11 100       26 $release_args{ group_sort } = $args{ group_sort } if $args{ group_sort };
292              
293 11         10 my $output;
294              
295 11 100       30 $output = $self->preamble . "\n\n" if $self->preamble;
296              
297 11         20 my @r = $self->releases;
298 11 50       37 @r = reverse @r unless $args{reverse}; # not a typo!
299              
300 11         41 $output .= $_->serialize( %release_args ) for @r;
301 11         52 $output =~ s/\n\n+\z/\n/;
302              
303 11         113 return $output;
304             }
305              
306             1;
307              
308             __END__