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   360164 use strict;
  22         38  
  22         819  
4 22     22   88 use warnings;
  22         32  
  22         576  
5              
6 22     22   8224 use CPAN::Changes::Release;
  22         48  
  22         531  
7 22     22   109 use Scalar::Util ();
  22         27  
  22         262  
8 22     22   9895 use version ();
  22         33493  
  22         652  
9 22     22   11847 use Encode qw(decode FB_CROAK LEAVE_SRC);
  22         186269  
  22         39848  
10              
11             our $VERSION = '0.400002';
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 665 my $class = shift;
38 27         162 return bless {
39             preamble => '',
40             releases => {},
41             months => \%months,
42             @_,
43             }, $class;
44             }
45              
46             sub load {
47 18     18 1 1202 my ( $class, $file, @args ) = @_;
48              
49 18 50       748 open( my $fh, '<:raw', $file ) or die $!;
50              
51 18         34 my $content = do { local $/; <$fh> };
  18         60  
  18         394  
52              
53 18         102 close $fh;
54              
55             # if it's valid UTF-8, decode that. otherwise, assume latin 1 and leave it.
56 18         30 eval { $content = decode('UTF-8', $content, FB_CROAK | LEAVE_SRC) };
  18         507  
57              
58 18         3255 return $class->load_string( $content, @args );
59             }
60              
61             sub load_string {
62 22     22 1 1946 my ( $class, $string, @args ) = @_;
63              
64 22         79 my $changes = $class->new( @args );
65 22         38 my $preamble = '';
66 22         26 my ( @releases, $ingroup, $indent );
67              
68 22         1235 $string =~ s/(?:\015{1,2}\012|\015|\012)/\n/gs;
69 22         319 my @lines = split( "\n", $string );
70              
71             my $version_line_re
72             = $changes->{ next_token }
73 22 100       1445 ? qr/^(?:$version::LAX|$changes->{next_token})/
74             : qr/^$version::LAX/;
75              
76 22   66     397 $preamble .= shift( @lines ) . "\n" while @lines && $lines[ 0 ] !~ $version_line_re;
77              
78 22         53 for my $l ( @lines ) {
79              
80             # Version & Date
81 351 100       1121 if ( $l =~ $version_line_re ) {
82 78         432 my ( $v, $n ) = split m{\s[\W\s]*}, $l, 2;
83 78         95 my $match = '';
84 78         64 my $d;
85              
86             # munge date formats, save the remainder as note
87 78 100       162 if ( $n ) {
88             # unknown dates
89 75 100       2464 if ( $n =~ m{^($UNKNOWN_VALS)}i ) {
    100          
    100          
    100          
    100          
90 5         8 $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         5 $match = $1;
98 3 100       7 if ( $4 ) {
99              
100             # unfortunately ignores TZ data
101             $d = sprintf(
102             '%d-%02d-%02dT%sZ',
103 2         12 $5, $changes->{ months }->{ $2 },
104             $3, $4
105             );
106             }
107             else {
108             $d = sprintf( '%d-%02d-%02d',
109 1         7 $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         3 $match = $1;
119             $d = sprintf(
120             '%d-%02d-%02dT%s%s%02d:%02d',
121 1         13 $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         14 $d = sprintf( '%sT%sZ', $2, $3 );
132             }
133              
134             # start with W3CDTF, ignore rest
135             elsif ( $n =~ m{^($W3CDTF_REGEX)} ) {
136 63         125 $match = $1;
137 63         69 $d = $match;
138 63         127 $d =~ s{ }{T};
139             # Add UTC TZ if date ends at H:M, H:M:S or H:M:S.FS
140 63 100 100     457 $d .= 'Z' if length( $d ) == 16 || length( $d ) == 19 || $d =~ m{\.\d+$};
      100        
141             }
142              
143             # clean date from note
144 75         1003 $n =~ s{^\Q$match\E\s*}{};
145             }
146              
147 78 100       180 undef $d unless length $d;
148 78 100       169 undef $n unless length $n;
149              
150 78         346 push @releases,
151             CPAN::Changes::Release->new(
152             version => $v,
153             date => $d,
154             _parsed_date => $match,
155             note => $n,
156             );
157 78         85 $ingroup = undef;
158 78         71 $indent = undef;
159 78         139 next;
160             }
161              
162             # Grouping
163 273 100       514 if ( $l =~ m{^\s+\[\s*([^\[\]]+?)\s*\]\s*$} ) {
164 17         26 $ingroup = $1;
165 17         45 $releases[ -1 ]->add_group( $1 );
166 17         23 next;
167             }
168              
169 256 100       399 $ingroup = '' if !defined $ingroup;
170              
171 256 100       590 next if $l =~ m{^\s*$};
172              
173 136 100       242 if ( !defined $indent ) {
174 70 100       285 $indent
175             = $l =~ m{^(\s+)}
176             ? '\s' x length $1
177             : '';
178             }
179              
180 136         514 $l =~ s{^$indent}{};
181              
182             # Inconsistent indentation between releases
183 136 50 66     336 if ( $l =~ m{^\s} && !@{ $releases[ -1 ]->changes( $ingroup ) } ) {
  22         114  
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 136 100       276 if ( $l =~ m{^\s} ) {
191 22         58 $l =~ s{^\s+}{};
192 22         51 my $changeset = $releases[ -1 ]->changes( $ingroup );
193 22         61 $changeset->[ -1 ] .= " $l";
194             }
195              
196             # Start of Change line
197             else {
198 114         271 $l =~ s{^[^[:alnum:]]+\s}{}; # remove leading marker
199 114         421 $releases[ -1 ]->add_changes( { group => $ingroup }, $l );
200             }
201              
202             }
203              
204 22         77 $changes->preamble( $preamble );
205 22         68 $changes->releases( @releases );
206              
207 22         156 return $changes;
208             }
209              
210             sub preamble {
211 55     55 1 10232 my $self = shift;
212              
213 55 100       136 if ( @_ ) {
214 23         37 my $preamble = shift;
215 23         60 $preamble =~ s{\s+$}{}s;
216 23         53 $self->{ preamble } = $preamble;
217             }
218              
219 55         168 return $self->{ preamble };
220             }
221              
222             sub releases {
223 61     61 1 3597 my $self = shift;
224              
225 61 100       151 if ( @_ ) {
226 25         68 $self->{ releases } = {};
227 25         114 $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 174 50 0 174   457 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         100 my $next_token = $self->{ next_token };
246              
247             my $token_sort_function = sub {
248 3 100   3   11 $a->version =~ $next_token - $b->version =~ $next_token
249             or $sort_function->();
250 61         140 };
251              
252 61 100       127 my $sort = $next_token ? $token_sort_function : $sort_function;
253              
254 61         78 return sort $sort values %{ $self->{ releases } };
  61         484  
255             }
256              
257             sub add_release {
258 31     31 1 358 my $self = shift;
259              
260 31         71 for my $release ( @_ ) {
261 87 100       279 my $new = Scalar::Util::blessed $release ? $release
262             : CPAN::Changes::Release->new( %$release );
263 87         264 $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 324 my ( $self, $version ) = @_;
275              
276 4 50       15 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 621 my $self = shift;
288 11         18 my %args = @_;
289              
290 11         14 my %release_args;
291 11 100       25 $release_args{ group_sort } = $args{ group_sort } if $args{ group_sort };
292              
293 11         9 my $output;
294              
295 11 100       18 $output = $self->preamble . "\n\n" if $self->preamble;
296              
297 11         17 my @r = $self->releases;
298 11 50       71 @r = reverse @r unless $args{reverse}; # not a typo!
299              
300 11         44 $output .= $_->serialize( %release_args ) for @r;
301 11         50 $output =~ s/\n\n+\z/\n/;
302              
303 11         58 return $output;
304             }
305              
306             1;
307              
308             __END__