test: Move test_data_file() to test.h
[dpkg.git] / scripts / Dpkg / Changelog.pm
blob7e9297d2c3041c0a84e2913ed1d011c99cbced0f
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/>.
17 =encoding utf8
19 =head1 NAME
21 Dpkg::Changelog - base class to implement a changelog parser
23 =head1 DESCRIPTION
25 Dpkg::Changelog is a class representing a changelog file
26 as an array of changelog entries (Dpkg::Changelog::Entry).
27 By deriving this class and implementing its parse method, you
28 add the ability to fill this object with changelog entries.
30 =cut
32 package Dpkg::Changelog 2.00;
34 use strict;
35 use warnings;
37 use Carp;
39 use Dpkg::Gettext;
40 use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_WARN);
41 use Dpkg::Control;
42 use Dpkg::Control::Changelog;
43 use Dpkg::Control::Fields;
44 use Dpkg::Index;
45 use Dpkg::Version;
46 use Dpkg::Vendor qw(run_vendor_hook);
48 use parent qw(Dpkg::Interface::Storable);
50 use overload
51 '@{}' => sub { return $_[0]->{data} };
53 =head1 METHODS
55 =over 4
57 =item $c = Dpkg::Changelog->new(%options)
59 Creates a new changelog object.
61 =cut
63 sub new {
64 my ($this, %opts) = @_;
65 my $class = ref($this) || $this;
66 my $self = {
67 verbose => 1,
68 parse_errors => []
70 bless $self, $class;
71 $self->set_options(%opts);
72 return $self;
75 =item $c->set_options(%opts)
77 Change the value of some options. "verbose" (defaults to 1) defines
78 whether parse errors are displayed as warnings by default. "reportfile"
79 is a string to use instead of the name of the file parsed, in particular
80 in error messages. "range" defines the range of entries that we want to
81 parse, the parser will stop as soon as it has parsed enough data to
82 satisfy $c->get_range($opts{range}).
84 =cut
86 sub set_options {
87 my ($self, %opts) = @_;
88 $self->{$_} = $opts{$_} foreach keys %opts;
91 =item $count = $c->parse($fh, $description)
93 Read the filehandle and parse a changelog in it. The data in the object is
94 reset before parsing new data.
96 Returns the number of changelog entries that have been parsed with success.
98 This method needs to be implemented by one of the specialized changelog
99 format subclasses.
101 =item $count = $c->load($filename)
103 Parse $filename contents for a changelog.
105 Returns the number of changelog entries that have been parsed with success.
107 =item $c->reset_parse_errors()
109 Can be used to delete all information about errors occurred during
110 previous L<parse> runs.
112 =cut
114 sub reset_parse_errors {
115 my $self = shift;
116 $self->{parse_errors} = [];
119 =item $c->parse_error($file, $line_nr, $error, [$line])
121 Record a new parse error in $file at line $line_nr. The error message is
122 specified with $error and a copy of the line can be recorded in $line.
124 =cut
126 sub parse_error {
127 my ($self, $file, $line_nr, $error, $line) = @_;
129 push @{$self->{parse_errors}}, [ $file, $line_nr, $error, $line ];
131 if ($self->{verbose}) {
132 if ($line) {
133 warning("%20s(l$line_nr): $error\nLINE: $line", $file);
134 } else {
135 warning("%20s(l$line_nr): $error", $file);
140 =item $c->get_parse_errors()
142 Returns all error messages from the last L<parse> run.
143 If called in scalar context returns a human readable
144 string representation. If called in list context returns
145 an array of arrays. Each of these arrays contains
147 =over 4
149 =item 1.
151 a string describing the origin of the data (a filename usually). If the
152 reportfile configuration option was given, its value will be used instead.
154 =item 2.
156 the line number where the error occurred
158 =item 3.
160 an error description
162 =item 4.
164 the original line
166 =back
168 =cut
170 sub get_parse_errors {
171 my $self = shift;
173 if (wantarray) {
174 return @{$self->{parse_errors}};
175 } else {
176 my $res = '';
177 foreach my $e (@{$self->{parse_errors}}) {
178 if ($e->[3]) {
179 $res .= report(REPORT_WARN, g_("%s(l%s): %s\nLINE: %s"), @$e);
180 } else {
181 $res .= report(REPORT_WARN, g_('%s(l%s): %s'), @$e);
184 return $res;
188 =item $c->set_unparsed_tail($tail)
190 Add a string representing unparsed lines after the changelog entries.
191 Use undef as $tail to remove the unparsed lines currently set.
193 =item $c->get_unparsed_tail()
195 Return a string representing the unparsed lines after the changelog
196 entries. Returns undef if there's no such thing.
198 =cut
200 sub set_unparsed_tail {
201 my ($self, $tail) = @_;
202 $self->{unparsed_tail} = $tail;
205 sub get_unparsed_tail {
206 my $self = shift;
207 return $self->{unparsed_tail};
210 =item @{$c}
212 Returns all the Dpkg::Changelog::Entry objects contained in this changelog
213 in the order in which they have been parsed.
215 =item $c->get_range($range)
217 Returns an array (if called in list context) or a reference to an array of
218 Dpkg::Changelog::Entry objects which each represent one entry of the
219 changelog. $range is a hash reference describing the range of entries
220 to return. See section L<"RANGE SELECTION">.
222 =cut
224 sub _sanitize_range {
225 my ($self, $r) = @_;
226 my $data = $self->{data};
228 if (defined($r->{offset}) and not defined($r->{count})) {
229 warning(g_("'offset' without 'count' has no effect")) if $self->{verbose};
230 delete $r->{offset};
233 ## no critic (ControlStructures::ProhibitUntilBlocks)
234 if ((defined($r->{count}) || defined($r->{offset})) &&
235 (defined($r->{from}) || defined($r->{since}) ||
236 defined($r->{to}) || defined($r->{until})))
238 warning(g_("you can't combine 'count' or 'offset' with any other " .
239 'range option')) if $self->{verbose};
240 delete $r->{from};
241 delete $r->{since};
242 delete $r->{to};
243 delete $r->{until};
245 if (defined($r->{from}) && defined($r->{since})) {
246 warning(g_("you can only specify one of 'from' and 'since', using " .
247 "'since'")) if $self->{verbose};
248 delete $r->{from};
250 if (defined($r->{to}) && defined($r->{until})) {
251 warning(g_("you can only specify one of 'to' and 'until', using " .
252 "'until'")) if $self->{verbose};
253 delete $r->{to};
256 # Handle non-existing versions
257 my (%versions, @versions);
258 foreach my $entry (@{$data}) {
259 my $version = $entry->get_version();
260 next unless defined $version;
261 $versions{$version->as_string()} = 1;
262 push @versions, $version->as_string();
264 if ((defined($r->{since}) and not exists $versions{$r->{since}})) {
265 warning(g_("'%s' option specifies non-existing version '%s'"), 'since', $r->{since});
266 warning(g_('use newest entry that is earlier than the one specified'));
267 foreach my $v (@versions) {
268 if (version_compare_relation($v, REL_LT, $r->{since})) {
269 $r->{since} = $v;
270 last;
273 if (not exists $versions{$r->{since}}) {
274 # No version was earlier, include all
275 warning(g_('none found, starting from the oldest entry'));
276 delete $r->{since};
277 $r->{from} = $versions[-1];
280 if ((defined($r->{from}) and not exists $versions{$r->{from}})) {
281 warning(g_("'%s' option specifies non-existing version '%s'"), 'from', $r->{from});
282 warning(g_('use oldest entry that is later than the one specified'));
283 my $oldest;
284 foreach my $v (@versions) {
285 if (version_compare_relation($v, REL_GT, $r->{from})) {
286 $oldest = $v;
289 if (defined($oldest)) {
290 $r->{from} = $oldest;
291 } else {
292 warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'from', $r->{from});
293 delete $r->{from}; # No version was oldest
296 if (defined($r->{until}) and not exists $versions{$r->{until}}) {
297 warning(g_("'%s' option specifies non-existing version '%s'"), 'until', $r->{until});
298 warning(g_('use oldest entry that is later than the one specified'));
299 my $oldest;
300 foreach my $v (@versions) {
301 if (version_compare_relation($v, REL_GT, $r->{until})) {
302 $oldest = $v;
305 if (defined($oldest)) {
306 $r->{until} = $oldest;
307 } else {
308 warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'until', $r->{until});
309 delete $r->{until}; # No version was oldest
312 if (defined($r->{to}) and not exists $versions{$r->{to}}) {
313 warning(g_("'%s' option specifies non-existing version '%s'"), 'to', $r->{to});
314 warning(g_('use newest entry that is earlier than the one specified'));
315 foreach my $v (@versions) {
316 if (version_compare_relation($v, REL_LT, $r->{to})) {
317 $r->{to} = $v;
318 last;
321 if (not exists $versions{$r->{to}}) {
322 # No version was earlier
323 warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'to', $r->{to});
324 delete $r->{to};
328 if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) {
329 warning(g_("'since' option specifies most recent version '%s', ignoring"), $r->{since});
330 delete $r->{since};
332 if (defined($r->{until}) and $data->[-1]->get_version() eq $r->{until}) {
333 warning(g_("'until' option specifies oldest version '%s', ignoring"), $r->{until});
334 delete $r->{until};
336 ## use critic
339 sub get_range {
340 my ($self, $range) = @_;
341 $range //= {};
342 my $res = $self->_data_range($range);
343 return unless defined $res;
344 if (wantarray) {
345 return reverse @{$res} if $range->{reverse};
346 return @{$res};
347 } else {
348 return $res;
352 sub _is_full_range {
353 my ($self, $range) = @_;
355 return 1 if $range->{all};
357 # If no range delimiter is specified, we want everything.
358 foreach my $delim (qw(since until from to count offset)) {
359 return 0 if exists $range->{$delim};
362 return 1;
365 sub _data_range {
366 my ($self, $range) = @_;
368 my $data = $self->{data} or return;
370 return [ @$data ] if $self->_is_full_range($range);
372 $self->_sanitize_range($range);
374 my ($start, $end);
375 if (defined($range->{count})) {
376 my $offset = $range->{offset} // 0;
377 my $count = $range->{count};
378 # Convert count/offset in start/end
379 if ($offset > 0) {
380 $offset -= ($count < 0);
381 } elsif ($offset < 0) {
382 $offset = $#$data + ($count > 0) + $offset;
383 } else {
384 $offset = $#$data if $count < 0;
386 $start = $end = $offset;
387 $start += $count+1 if $count < 0;
388 $end += $count-1 if $count > 0;
389 # Check limits
390 $start = 0 if $start < 0;
391 return if $start > $#$data;
392 $end = $#$data if $end > $#$data;
393 return if $end < 0;
394 $end = $start if $end < $start;
395 return [ @{$data}[$start .. $end] ];
398 ## no critic (ControlStructures::ProhibitUntilBlocks)
399 my @result;
400 my $include = 1;
401 $include = 0 if defined($range->{to}) or defined($range->{until});
402 foreach my $entry (@{$data}) {
403 my $v = $entry->get_version();
404 $include = 1 if defined($range->{to}) and $v eq $range->{to};
405 last if defined($range->{since}) and $v eq $range->{since};
407 push @result, $entry if $include;
409 $include = 1 if defined($range->{until}) and $v eq $range->{until};
410 last if defined($range->{from}) and $v eq $range->{from};
412 ## use critic
414 return \@result if scalar(@result);
415 return;
418 =item $c->abort_early()
420 Returns true if enough data have been parsed to be able to return all
421 entries selected by the range set at creation (or with set_options).
423 =cut
425 sub abort_early {
426 my $self = shift;
428 my $data = $self->{data} or return;
429 my $r = $self->{range} or return;
430 my $count = $r->{count} // 0;
431 my $offset = $r->{offset} // 0;
433 return if $self->_is_full_range($r);
434 return if $offset < 0 or $count < 0;
435 if (defined($r->{count})) {
436 if ($offset > 0) {
437 $offset -= ($count < 0);
439 my $start = my $end = $offset;
440 $end += $count-1 if $count > 0;
441 return $start < @{$data} > $end;
444 return unless defined($r->{since}) or defined($r->{from});
445 foreach my $entry (@{$data}) {
446 my $v = $entry->get_version();
447 return 1 if defined($r->{since}) and $v eq $r->{since};
448 return 1 if defined($r->{from}) and $v eq $r->{from};
451 return;
454 =item $str = $c->output()
456 =item "$c"
458 Returns a string representation of the changelog (it's a concatenation of
459 the string representation of the individual changelog entries).
461 =item $c->output($fh)
463 Output the changelog to the given filehandle.
465 =cut
467 sub output {
468 my ($self, $fh) = @_;
469 my $str = '';
470 foreach my $entry (@{$self}) {
471 my $text = $entry->output();
472 print { $fh } $text if defined $fh;
473 $str .= $text if defined wantarray;
475 my $text = $self->get_unparsed_tail();
476 if (defined $text) {
477 print { $fh } $text if defined $fh;
478 $str .= $text if defined wantarray;
480 return $str;
483 =item $c->save($filename)
485 Save the changelog in the given file.
487 =cut
489 our ( @URGENCIES, %URGENCIES );
490 BEGIN {
491 @URGENCIES = qw(
493 medium
494 high
495 critical
496 emergency
498 my $i = 1;
499 %URGENCIES = map { $_ => $i++ } @URGENCIES;
502 sub _format_dpkg {
503 my ($self, $range) = @_;
505 my @data = $self->get_range($range) or return;
506 my $src = shift @data;
508 my $c = Dpkg::Control::Changelog->new();
509 $c->{Urgency} = $src->get_urgency() || 'unknown';
510 $c->{Source} = $src->get_source() || 'unknown';
511 $c->{Version} = $src->get_version() // 'unknown';
512 $c->{Distribution} = join ' ', $src->get_distributions();
513 $c->{Maintainer} = $src->get_maintainer() // '';
514 $c->{Date} = $src->get_timestamp() // '';
515 $c->{Timestamp} = $src->get_timepiece && $src->get_timepiece->epoch // '';
516 $c->{Changes} = $src->get_dpkg_changes();
518 # handle optional fields
519 my $opts = $src->get_optional_fields();
520 my %closes;
521 foreach my $f (keys %{$opts}) {
522 if ($f eq 'Urgency') {
523 # Already handled.
524 } elsif ($f eq 'Closes') {
525 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
526 } else {
527 field_transfer_single($opts, $c, $f);
531 foreach my $bin (@data) {
532 my $oldurg = $c->{Urgency} // '';
533 my $oldurgn = $URGENCIES{$c->{Urgency}} // -1;
534 my $newurg = $bin->get_urgency() // '';
535 my $newurgn = $URGENCIES{$newurg} // -1;
536 $c->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg;
537 $c->{Changes} .= "\n" . $bin->get_dpkg_changes();
539 # handle optional fields
540 $opts = $bin->get_optional_fields();
541 foreach my $f (keys %{$opts}) {
542 if ($f eq 'Closes') {
543 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
544 } elsif (not exists $c->{$f}) {
545 # Don't overwrite an existing field
546 field_transfer_single($opts, $c, $f);
551 if (scalar keys %closes) {
552 $c->{Closes} = join ' ', sort { $a <=> $b } keys %closes;
554 run_vendor_hook('post-process-changelog-entry', $c);
556 return $c;
559 sub _format_rfc822 {
560 my ($self, $range) = @_;
562 my @data = $self->get_range($range) or return;
563 my @ctrl;
565 foreach my $entry (@data) {
566 my $c = Dpkg::Control::Changelog->new();
567 $c->{Urgency} = $entry->get_urgency() || 'unknown';
568 $c->{Source} = $entry->get_source() || 'unknown';
569 $c->{Version} = $entry->get_version() // 'unknown';
570 $c->{Distribution} = join ' ', $entry->get_distributions();
571 $c->{Maintainer} = $entry->get_maintainer() // '';
572 $c->{Date} = $entry->get_timestamp() // '';
573 $c->{Timestamp} = $entry->get_timepiece && $entry->get_timepiece->epoch // '';
574 $c->{Changes} = $entry->get_dpkg_changes();
576 # handle optional fields
577 my $opts = $entry->get_optional_fields();
578 foreach my $f (keys %{$opts}) {
579 field_transfer_single($opts, $c, $f) unless exists $c->{$f};
582 run_vendor_hook('post-process-changelog-entry', $c);
584 push @ctrl, $c;
587 return @ctrl;
590 =item $control = $c->format_range($format, $range)
592 Formats the changelog into Dpkg::Control::Changelog objects representing the
593 entries selected by the optional range specifier (see L<"RANGE SELECTION">
594 for details). In scalar context returns a Dpkg::Index object containing the
595 selected entries, in list context returns an array of Dpkg::Control::Changelog
596 objects.
598 With format B<dpkg> the returned Dpkg::Control::Changelog object is coalesced
599 from the entries in the changelog that are part of the range requested,
600 with the fields described below, but considering that "selected entry"
601 means the first entry of the selected range.
603 With format B<rfc822> each returned Dpkg::Control::Changelog objects
604 represents one entry in the changelog that is part of the range requested,
605 with the fields described below, but considering that "selected entry"
606 means for each entry.
608 The different formats return undef if no entries are matched. The following
609 fields are contained in the object(s) returned:
611 =over 4
613 =item Source
615 package name (selected entry)
617 =item Version
619 packages' version (selected entry)
621 =item Distribution
623 target distribution (selected entry)
625 =item Urgency
627 urgency (highest of all entries in range)
629 =item Maintainer
631 person that created the (selected) entry
633 =item Date
635 date of the (selected) entry
637 =item Timestamp
639 date of the (selected) entry as a timestamp in seconds since the epoch
641 =item Closes
643 bugs closed by the (selected) entry/entries, sorted by bug number
645 =item Changes
647 content of the (selected) entry/entries
649 =back
651 =cut
653 sub format_range {
654 my ($self, $format, $range) = @_;
656 my @ctrl;
658 if ($format eq 'dpkg') {
659 @ctrl = $self->_format_dpkg($range);
660 } elsif ($format eq 'rfc822') {
661 @ctrl = $self->_format_rfc822($range);
662 } else {
663 croak "unknown changelog output format $format";
666 if (wantarray) {
667 return @ctrl;
668 } else {
669 my $index = Dpkg::Index->new(type => CTRL_CHANGELOG);
671 foreach my $c (@ctrl) {
672 $index->add($c);
675 return $index;
679 =back
681 =head1 RANGE SELECTION
683 A range selection is described by a hash reference where
684 the allowed keys and values are described below.
686 The following options take a version number as value.
688 =over 4
690 =item since
692 Causes changelog information from all versions strictly
693 later than B<version> to be used.
695 =item until
697 Causes changelog information from all versions strictly
698 earlier than B<version> to be used.
700 =item from
702 Similar to C<since> but also includes the information for the
703 specified B<version> itself.
705 =item to
707 Similar to C<until> but also includes the information for the
708 specified B<version> itself.
710 =back
712 The following options don't take version numbers as values:
714 =over 4
716 =item all
718 If set to a true value, all entries of the changelog are returned,
719 this overrides all other options.
721 =item count
723 Expects a signed integer as value. Returns C<value> entries from the
724 top of the changelog if set to a positive integer, and C<abs(value)>
725 entries from the tail if set to a negative integer.
727 =item offset
729 Expects a signed integer as value. Changes the starting point for
730 C<count>, either counted from the top (positive integer) or from
731 the tail (negative integer). C<offset> has no effect if C<count>
732 wasn't given as well.
734 =back
736 Some examples for the above options. Imagine an example changelog with
737 entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1.
739 Range Included entries
740 ----- ----------------
741 since => '2.0' 3.1, 3.0, 2.2
742 until => '2.0' 1.3, 1.2
743 from => '2.0' 3.1, 3.0, 2.2, 2.1, 2.0
744 to => '2.0' 2.0, 1.3, 1.2
745 count => 2 3.1, 3.0
746 count => -2 1.3, 1.2
747 count => 3, offset => 2 2.2, 2.1, 2.0
748 count => 2, offset => -3 2.0, 1.3
749 count => -2, offset => 3 3.0, 2.2
750 count => -2, offset => -3 2.2, 2.1
752 Any combination of one option of C<since> and C<from> and one of
753 C<until> and C<to> returns the intersection of the two results
754 with only one of the options specified.
756 =head1 CHANGES
758 =head2 Version 2.00 (dpkg 1.20.0)
760 Remove methods: $c->dpkg(), $c->rfc822().
762 =head2 Version 1.01 (dpkg 1.18.8)
764 New method: $c->format_range().
766 Deprecated methods: $c->dpkg(), $c->rfc822().
768 New field Timestamp in output formats.
770 =head2 Version 1.00 (dpkg 1.15.6)
772 Mark the module as public.
774 =cut