1 # Copyright © 2005, 2007 Frank Lichtenheld <frank@lichtenheld.de>
2 # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 Dpkg::Changelog - base class to implement a changelog parser
25 Dpkg::Changelog is a class representing a changelog file
26 as an array of changelog entries (Dpkg::Changelog::Entry).
27 By deriving this object and implementing its parse method, you
28 add the ability to fill this object with changelog entries.
32 package Dpkg::Changelog;
37 our $VERSION = '1.01';
42 use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_WARN);
44 use Dpkg::Control::Changelog;
45 use Dpkg::Control::Fields;
48 use Dpkg::Vendor qw(run_vendor_hook);
50 use parent qw(Dpkg::Interface::Storable);
53 '@{}' => sub { return $_[0]->{data} };
59 =item $c = Dpkg::Changelog->new(%options)
61 Creates a new changelog object.
66 my ($this, %opts) = @_;
67 my $class = ref($this) || $this;
73 $self->set_options(%opts);
77 =item $c->load($filename)
79 Parse $filename as a changelog.
83 =item $c->set_options(%opts)
85 Change the value of some options. "verbose" (defaults to 1) defines
86 whether parse errors are displayed as warnings by default. "reportfile"
87 is a string to use instead of the name of the file parsed, in particular
88 in error messages. "range" defines the range of entries that we want to
89 parse, the parser will stop as soon as it has parsed enough data to
90 satisfy $c->get_range($opts{range}).
95 my ($self, %opts) = @_;
96 $self->{$_} = $opts{$_} foreach keys %opts;
99 =item $c->reset_parse_errors()
101 Can be used to delete all information about errors occurred during
102 previous L<parse> runs.
106 sub reset_parse_errors {
108 $self->{parse_errors} = [];
111 =item $c->parse_error($file, $line_nr, $error, [$line])
113 Record a new parse error in $file at line $line_nr. The error message is
114 specified with $error and a copy of the line can be recorded in $line.
119 my ($self, $file, $line_nr, $error, $line) = @_;
121 push @{$self->{parse_errors}}, [ $file, $line_nr, $error, $line ];
123 if ($self->{verbose}) {
125 warning("%20s(l$line_nr): $error\nLINE: $line", $file);
127 warning("%20s(l$line_nr): $error", $file);
132 =item $c->get_parse_errors()
134 Returns all error messages from the last L<parse> run.
135 If called in scalar context returns a human readable
136 string representation. If called in list context returns
137 an array of arrays. Each of these arrays contains
143 a string describing the origin of the data (a filename usually). If the
144 reportfile configuration option was given, its value will be used instead.
148 the line number where the error occurred
162 sub get_parse_errors {
166 return @{$self->{parse_errors}};
169 foreach my $e (@{$self->{parse_errors}}) {
171 $res .= report(REPORT_WARN, g_("%s(l%s): %s\nLINE: %s"), @$e);
173 $res .= report(REPORT_WARN, g_('%s(l%s): %s'), @$e);
180 =item $c->set_unparsed_tail($tail)
182 Add a string representing unparsed lines after the changelog entries.
183 Use undef as $tail to remove the unparsed lines currently set.
185 =item $c->get_unparsed_tail()
187 Return a string representing the unparsed lines after the changelog
188 entries. Returns undef if there's no such thing.
192 sub set_unparsed_tail {
193 my ($self, $tail) = @_;
194 $self->{unparsed_tail} = $tail;
197 sub get_unparsed_tail {
199 return $self->{unparsed_tail};
204 Returns all the Dpkg::Changelog::Entry objects contained in this changelog
205 in the order in which they have been parsed.
207 =item $c->get_range($range)
209 Returns an array (if called in list context) or a reference to an array of
210 Dpkg::Changelog::Entry objects which each represent one entry of the
211 changelog. $range is a hash reference describing the range of entries
212 to return. See section L<"RANGE SELECTION">.
216 sub __sanity_check_range {
218 my $data = $self->{data};
220 if (defined($r->{offset}) and not defined($r->{count})) {
221 warning(g_("'offset' without 'count' has no effect")) if $self->{verbose};
225 ## no critic (ControlStructures::ProhibitUntilBlocks)
226 if ((defined($r->{count}) || defined($r->{offset})) &&
227 (defined($r->{from}) || defined($r->{since}) ||
228 defined($r->{to}) || defined($r->{until})))
230 warning(g_("you can't combine 'count' or 'offset' with any other " .
231 'range option')) if $self->{verbose};
237 if (defined($r->{from}) && defined($r->{since})) {
238 warning(g_("you can only specify one of 'from' and 'since', using " .
239 "'since'")) if $self->{verbose};
242 if (defined($r->{to}) && defined($r->{until})) {
243 warning(g_("you can only specify one of 'to' and 'until', using " .
244 "'until'")) if $self->{verbose};
248 # Handle non-existing versions
249 my (%versions, @versions);
250 foreach my $entry (@{$data}) {
251 my $version = $entry->get_version();
252 next unless defined $version;
253 $versions{$version->as_string()} = 1;
254 push @versions, $version->as_string();
256 if ((defined($r->{since}) and not exists $versions{$r->{since}})) {
257 warning(g_("'%s' option specifies non-existing version"), 'since');
258 warning(g_('use newest entry that is earlier than the one specified'));
259 foreach my $v (@versions) {
260 if (version_compare_relation($v, REL_LT, $r->{since})) {
265 if (not exists $versions{$r->{since}}) {
266 # No version was earlier, include all
267 warning(g_('none found, starting from the oldest entry'));
269 $r->{from} = $versions[-1];
272 if ((defined($r->{from}) and not exists $versions{$r->{from}})) {
273 warning(g_("'%s' option specifies non-existing version"), 'from');
274 warning(g_('use oldest entry that is later than the one specified'));
276 foreach my $v (@versions) {
277 if (version_compare_relation($v, REL_GT, $r->{from})) {
281 if (defined($oldest)) {
282 $r->{from} = $oldest;
284 warning(g_("no such entry found, ignoring '%s' parameter"), 'from');
285 delete $r->{from}; # No version was oldest
288 if (defined($r->{until}) and not exists $versions{$r->{until}}) {
289 warning(g_("'%s' option specifies non-existing version"), 'until');
290 warning(g_('use oldest entry that is later than the one specified'));
292 foreach my $v (@versions) {
293 if (version_compare_relation($v, REL_GT, $r->{until})) {
297 if (defined($oldest)) {
298 $r->{until} = $oldest;
300 warning(g_("no such entry found, ignoring '%s' parameter"), 'until');
301 delete $r->{until}; # No version was oldest
304 if (defined($r->{to}) and not exists $versions{$r->{to}}) {
305 warning(g_("'%s' option specifies non-existing version"), 'to');
306 warning(g_('use newest entry that is earlier than the one specified'));
307 foreach my $v (@versions) {
308 if (version_compare_relation($v, REL_LT, $r->{to})) {
313 if (not exists $versions{$r->{to}}) {
314 # No version was earlier
315 warning(g_("no such entry found, ignoring '%s' parameter"), 'to');
320 if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) {
321 warning(g_("'since' option specifies most recent version, ignoring"));
324 if (defined($r->{until}) and $data->[-1]->get_version() eq $r->{until}) {
325 warning(g_("'until' option specifies oldest version, ignoring"));
332 my ($self, $range) = @_;
334 my $res = $self->_data_range($range);
336 return @$res if wantarray;
344 my ($self, $range) = @_;
346 return 1 if $range->{all};
348 # If no range delimiter is specified, we want everything.
349 foreach my $delim (qw(since until from to count offset)) {
350 return 0 if exists $range->{$delim};
357 my ($self, $range) = @_;
359 my $data = $self->{data} or return;
361 return [ @$data ] if $self->_is_full_range($range);
363 $self->__sanity_check_range($range);
366 if (defined($range->{count})) {
367 my $offset = $range->{offset} // 0;
368 my $count = $range->{count};
369 # Convert count/offset in start/end
371 $offset -= ($count < 0);
372 } elsif ($offset < 0) {
373 $offset = $#$data + ($count > 0) + $offset;
375 $offset = $#$data if $count < 0;
377 $start = $end = $offset;
378 $start += $count+1 if $count < 0;
379 $end += $count-1 if $count > 0;
381 $start = 0 if $start < 0;
382 return if $start > $#$data;
383 $end = $#$data if $end > $#$data;
385 $end = $start if $end < $start;
386 return [ @{$data}[$start .. $end] ];
389 ## no critic (ControlStructures::ProhibitUntilBlocks)
392 $include = 0 if defined($range->{to}) or defined($range->{until});
393 foreach my $entry (@{$data}) {
394 my $v = $entry->get_version();
395 $include = 1 if defined($range->{to}) and $v eq $range->{to};
396 last if defined($range->{since}) and $v eq $range->{since};
398 push @result, $entry if $include;
400 $include = 1 if defined($range->{until}) and $v eq $range->{until};
401 last if defined($range->{from}) and $v eq $range->{from};
405 return \@result if scalar(@result);
409 =item $c->abort_early()
411 Returns true if enough data have been parsed to be able to return all
412 entries selected by the range set at creation (or with set_options).
419 my $data = $self->{data} or return;
420 my $r = $self->{range} or return;
421 my $count = $r->{count} // 0;
422 my $offset = $r->{offset} // 0;
424 return if $self->_is_full_range($r);
425 return if $offset < 0 or $count < 0;
426 if (defined($r->{count})) {
428 $offset -= ($count < 0);
430 my $start = my $end = $offset;
431 $end += $count-1 if $count > 0;
432 return ($start < @$data and $end < @$data);
435 return unless defined($r->{since}) or defined($r->{from});
436 foreach my $entry (@{$data}) {
437 my $v = $entry->get_version();
438 return 1 if defined($r->{since}) and $v eq $r->{since};
439 return 1 if defined($r->{from}) and $v eq $r->{from};
445 =item $c->save($filename)
447 Save the changelog in the given file.
453 Returns a string representation of the changelog (it's a concatenation of
454 the string representation of the individual changelog entries).
456 =item $c->output($fh)
458 Output the changelog to the given filehandle.
463 my ($self, $fh) = @_;
465 foreach my $entry (@{$self}) {
466 my $text = $entry->output();
467 print { $fh } $text if defined $fh;
468 $str .= $text if defined wantarray;
470 my $text = $self->get_unparsed_tail();
472 print { $fh } $text if defined $fh;
473 $str .= $text if defined wantarray;
478 our ( @URGENCIES, %URGENCIES );
480 @URGENCIES = qw(low medium high critical emergency);
482 %URGENCIES = map { $_ => $i++ } @URGENCIES;
486 my ($self, $range) = @_;
488 my @data = $self->get_range($range) or return;
489 my $src = shift @data;
491 my $f = Dpkg::Control::Changelog->new();
492 $f->{Urgency} = $src->get_urgency() || 'unknown';
493 $f->{Source} = $src->get_source() || 'unknown';
494 $f->{Version} = $src->get_version() // 'unknown';
495 $f->{Distribution} = join(' ', $src->get_distributions());
496 $f->{Maintainer} = $src->get_maintainer() // '';
497 $f->{Date} = $src->get_timestamp() // '';
498 $f->{Timestamp} = $src->get_timepiece && $src->get_timepiece->epoch // '';
499 $f->{Changes} = $src->get_dpkg_changes();
501 # handle optional fields
502 my $opts = $src->get_optional_fields();
504 foreach (keys %$opts) {
505 if (/^Urgency$/i) { # Already dealt
506 } elsif (/^Closes$/i) {
507 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
509 field_transfer_single($opts, $f);
513 foreach my $bin (@data) {
514 my $oldurg = $f->{Urgency} // '';
515 my $oldurgn = $URGENCIES{$f->{Urgency}} // -1;
516 my $newurg = $bin->get_urgency() // '';
517 my $newurgn = $URGENCIES{$newurg} // -1;
518 $f->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg;
519 $f->{Changes} .= "\n" . $bin->get_dpkg_changes();
521 # handle optional fields
522 $opts = $bin->get_optional_fields();
523 foreach (keys %$opts) {
525 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
526 } elsif (not exists $f->{$_}) { # Don't overwrite an existing field
527 field_transfer_single($opts, $f);
532 if (scalar keys %closes) {
533 $f->{Closes} = join ' ', sort { $a <=> $b } keys %closes;
535 run_vendor_hook('post-process-changelog-entry', $f);
541 my ($self, $range) = @_;
543 my @data = $self->get_range($range) or return;
546 foreach my $entry (@data) {
547 my $f = Dpkg::Control::Changelog->new();
548 $f->{Urgency} = $entry->get_urgency() || 'unknown';
549 $f->{Source} = $entry->get_source() || 'unknown';
550 $f->{Version} = $entry->get_version() // 'unknown';
551 $f->{Distribution} = join(' ', $entry->get_distributions());
552 $f->{Maintainer} = $entry->get_maintainer() // '';
553 $f->{Date} = $entry->get_timestamp() // '';
554 $f->{Timestamp} = $entry->get_timepiece && $entry->get_timepiece->epoch // '';
555 $f->{Changes} = $entry->get_dpkg_changes();
557 # handle optional fields
558 my $opts = $entry->get_optional_fields();
559 foreach (keys %$opts) {
560 field_transfer_single($opts, $f) unless exists $f->{$_};
563 run_vendor_hook('post-process-changelog-entry', $f);
571 =item $control = $c->format_range($format, $range)
573 Formats the changelog into Dpkg::Control::Changelog objects representing the
574 entries selected by the optional range specifier (see L<"RANGE SELECTION">
575 for details). In scalar context returns a Dpkg::Index object containing the
576 selected entries, in list context returns an array of Dpkg::Control::Changelog
579 With format B<dpkg> the returned Dpkg::Control::Changelog object is coalesced
580 from the entries in the changelog that are part of the range requested,
581 with the fields described below, but considering that "selected entry"
582 means the first entry of the selected range.
584 With format B<rfc822> each returned Dpkg::Control::Changelog objects
585 represents one entry in the changelog that is part of the range requested,
586 with the fields described below, but considering that "selected entry"
587 means for each entry.
589 The different formats return undef if no entries are matched. The following
590 fields are contained in the object(s) returned:
596 package name (selected entry)
600 packages' version (selected entry)
604 target distribution (selected entry)
608 urgency (highest of all entries in range)
612 person that created the (selected) entry
616 date of the (selected) entry
620 date of the (selected) entry as a timestamp in seconds since the epoch
624 bugs closed by the (selected) entry/entries, sorted by bug number
628 content of the (selected) entry/entries
635 my ($self, $format, $range) = @_;
639 if ($format eq 'dpkg') {
640 @ctrl = $self->_format_dpkg($range);
641 } elsif ($format eq 'rfc822') {
642 @ctrl = $self->_format_rfc822($range);
644 croak "unknown changelog output format $format";
650 my $index = Dpkg::Index->new(type => CTRL_CHANGELOG);
652 foreach my $f (@ctrl) {
660 =item $control = $c->dpkg($range)
662 This is a deprecated alias for $c->format_range('dpkg', $range).
667 my ($self, $range) = @_;
669 warnings::warnif('deprecated',
670 'deprecated method, please use format_range("dpkg", $range) instead');
672 return $self->format_range('dpkg', $range);
675 =item @controls = $c->rfc822($range)
677 This is a deprecated alias for C<scalar c->format_range('rfc822', $range)>.
682 my ($self, $range) = @_;
684 warnings::warnif('deprecated',
685 'deprecated method, please use format_range("rfc822", $range) instead');
687 return scalar $self->format_range('rfc822', $range);
692 =head1 RANGE SELECTION
694 A range selection is described by a hash reference where
695 the allowed keys and values are described below.
697 The following options take a version number as value.
703 Causes changelog information from all versions strictly
704 later than B<version> to be used.
708 Causes changelog information from all versions strictly
709 earlier than B<version> to be used.
713 Similar to C<since> but also includes the information for the
714 specified B<version> itself.
718 Similar to C<until> but also includes the information for the
719 specified B<version> itself.
723 The following options don't take version numbers as values:
729 If set to a true value, all entries of the changelog are returned,
730 this overrides all other options.
734 Expects a signed integer as value. Returns C<value> entries from the
735 top of the changelog if set to a positive integer, and C<abs(value)>
736 entries from the tail if set to a negative integer.
740 Expects a signed integer as value. Changes the starting point for
741 C<count>, either counted from the top (positive integer) or from
742 the tail (negative integer). C<offset> has no effect if C<count>
743 wasn't given as well.
747 Some examples for the above options. Imagine an example changelog with
748 entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1.
750 Range Included entries
751 ----- ----------------
752 since => '2.0' 3.1, 3.0, 2.2
753 until => '2.0' 1.3, 1.2
754 from => '2.0' 3.1, 3.0, 2.2, 2.1, 2.0
755 to => '2.0' 2.0, 1.3, 1.2
758 count => 3, offset => 2 2.2, 2.1, 2.0
759 count => 2, offset => -3 2.0, 1.3
760 count => -2, offset => 3 3.0, 2.2
761 count => -2, offset => -3 2.2, 2.1
763 Any combination of one option of C<since> and C<from> and one of
764 C<until> and C<to> returns the intersection of the two results
765 with only one of the options specified.
769 =head2 Version 1.01 (dpkg 1.18.8)
771 New method: $c->format_range().
773 Deprecated methods: $c->dpkg(), $c->rfc822().
775 New field Timestamp in output formats.
777 =head2 Version 1.00 (dpkg 1.15.6)
779 Mark the module as public.