| # Copyright © 2005, 2007 Frank Lichtenheld <frank@lichtenheld.de> |
| # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org> |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| |
| =encoding utf8 |
| |
| =head1 NAME |
| |
| Dpkg::Changelog - base class to implement a changelog parser |
| |
| =head1 DESCRIPTION |
| |
| Dpkg::Changelog is a class representing a changelog file |
| as an array of changelog entries (Dpkg::Changelog::Entry). |
| By deriving this object and implementing its parse method, you |
| add the ability to fill this object with changelog entries. |
| |
| =head2 FUNCTIONS |
| |
| =cut |
| |
| package Dpkg::Changelog; |
| |
| use strict; |
| use warnings; |
| |
| our $VERSION = "1.00"; |
| |
| use Dpkg; |
| use Dpkg::Gettext; |
| use Dpkg::ErrorHandling qw(:DEFAULT report); |
| use Dpkg::Control; |
| use Dpkg::Control::Changelog; |
| use Dpkg::Control::Fields; |
| use Dpkg::Index; |
| use Dpkg::Version; |
| use Dpkg::Vendor qw(run_vendor_hook); |
| |
| use base qw(Dpkg::Interface::Storable); |
| |
| use overload |
| '@{}' => sub { return $_[0]->{data} }; |
| |
| =over 4 |
| |
| =item my $c = Dpkg::Changelog->new(%options) |
| |
| Creates a new changelog object. |
| |
| =cut |
| |
| sub new { |
| my ($this, %opts) = @_; |
| my $class = ref($this) || $this; |
| my $self = { |
| verbose => 1, |
| parse_errors => [] |
| }; |
| bless $self, $class; |
| $self->set_options(%opts); |
| return $self; |
| } |
| |
| =item $c->load($filename) |
| |
| Parse $filename as a changelog. |
| |
| =cut |
| |
| =item $c->set_options(%opts) |
| |
| Change the value of some options. "verbose" (defaults to 1) defines |
| whether parse errors are displayed as warnings by default. "reportfile" |
| is a string to use instead of the name of the file parsed, in particular |
| in error messages. "range" defines the range of entries that we want to |
| parse, the parser will stop as soon as it has parsed enough data to |
| satisfy $c->get_range($opts{'range'}). |
| |
| =cut |
| |
| sub set_options { |
| my ($self, %opts) = @_; |
| $self->{$_} = $opts{$_} foreach keys %opts; |
| } |
| |
| =item $c->reset_parse_errors() |
| |
| Can be used to delete all information about errors occurred during |
| previous L<parse> runs. |
| |
| =cut |
| |
| sub reset_parse_errors { |
| my ($self) = @_; |
| $self->{parse_errors} = []; |
| } |
| |
| =item $c->parse_error($line_nr, $error, [$line]) |
| |
| Record a new parse error at line $line_nr. The error message is specified |
| with $error and a copy of the line can be recorded in $line. |
| |
| =cut |
| |
| sub parse_error { |
| my ($self, $file, $line_nr, $error, $line) = @_; |
| shift; |
| |
| push @{$self->{parse_errors}}, [ @_ ]; |
| |
| if ($self->{verbose}) { |
| if ($line) { |
| warning("%20s(l$line_nr): $error\nLINE: $line", $file); |
| } else { |
| warning("%20s(l$line_nr): $error", $file); |
| } |
| } |
| } |
| |
| =item $c->get_parse_errors() |
| |
| Returns all error messages from the last L<parse> run. |
| If called in scalar context returns a human readable |
| string representation. If called in list context returns |
| an array of arrays. Each of these arrays contains |
| |
| =over 4 |
| |
| =item 1. |
| |
| a string describing the origin of the data (a filename usually). If the |
| reportfile configuration option was given, its value will be used instead. |
| |
| =item 2. |
| |
| the line number where the error occurred |
| |
| =item 3. |
| |
| an error description |
| |
| =item 4. |
| |
| the original line |
| |
| =back |
| |
| =cut |
| |
| sub get_parse_errors { |
| my ($self) = @_; |
| |
| if (wantarray) { |
| return @{$self->{parse_errors}}; |
| } else { |
| my $res = ""; |
| foreach my $e (@{$self->{parse_errors}}) { |
| if ($e->[3]) { |
| $res .= report(_g('warning'),_g("%s(l%s): %s\nLINE: %s"), @$e ); |
| } else { |
| $res .= report(_g('warning'),_g("%s(l%s): %s"), @$e ); |
| } |
| } |
| return $res; |
| } |
| } |
| |
| =item $c->set_unparsed_tail($tail) |
| |
| Add a string representing unparsed lines after the changelog entries. |
| Use undef as $tail to remove the unparsed lines currently set. |
| |
| =item $c->get_unparsed_tail() |
| |
| Return a string representing the unparsed lines after the changelog |
| entries. Returns undef if there's no such thing. |
| |
| =cut |
| |
| sub set_unparsed_tail { |
| my ($self, $tail) = @_; |
| $self->{'unparsed_tail'} = $tail; |
| } |
| |
| sub get_unparsed_tail { |
| my ($self) = @_; |
| return $self->{'unparsed_tail'}; |
| } |
| |
| =item @{$c} |
| |
| Returns all the Dpkg::Changelog::Entry objects contained in this changelog |
| in the order in which they have been parsed. |
| |
| =item $c->get_range($range) |
| |
| Returns an array (if called in list context) or a reference to an array of |
| Dpkg::Changelog::Entry objects which each represent one entry of the |
| changelog. $range is a hash reference describing the range of entries |
| to return. See section L<"RANGE SELECTION">. |
| |
| =cut |
| |
| sub __sanity_check_range { |
| my ($self, $r) = @_; |
| my $data = $self->{data}; |
| |
| if (defined($r->{offset}) and not defined($r->{count})) { |
| warning(_g("'offset' without 'count' has no effect")) if $self->{verbose}; |
| delete $r->{offset}; |
| } |
| |
| if ((defined($r->{count}) || defined($r->{offset})) && |
| (defined($r->{from}) || defined($r->{since}) || |
| defined($r->{to}) || defined($r->{'until'}))) |
| { |
| warning(_g("you can't combine 'count' or 'offset' with any other " . |
| "range option")) if $self->{verbose}; |
| delete $r->{from}; |
| delete $r->{since}; |
| delete $r->{to}; |
| delete $r->{'until'}; |
| } |
| if (defined($r->{from}) && defined($r->{since})) { |
| warning(_g("you can only specify one of 'from' and 'since', using " . |
| "'since'")) if $self->{verbose}; |
| delete $r->{from}; |
| } |
| if (defined($r->{to}) && defined($r->{'until'})) { |
| warning(_g("you can only specify one of 'to' and 'until', using " . |
| "'until'")) if $self->{verbose}; |
| delete $r->{to}; |
| } |
| |
| # Handle non-existing versions |
| my (%versions, @versions); |
| foreach my $entry (@{$data}) { |
| $versions{$entry->get_version()->as_string()} = 1; |
| push @versions, $entry->get_version()->as_string(); |
| } |
| if ((defined($r->{since}) and not exists $versions{$r->{since}})) { |
| warning(_g("'%s' option specifies non-existing version"), "since"); |
| warning(_g("use newest entry that is earlier than the one specified")); |
| foreach my $v (@versions) { |
| if (version_compare_relation($v, REL_LT, $r->{since})) { |
| $r->{since} = $v; |
| last; |
| } |
| } |
| if (not exists $versions{$r->{since}}) { |
| # No version was earlier, include all |
| warning(_g("none found, starting from the oldest entry")); |
| delete $r->{since}; |
| $r->{from} = $versions[-1]; |
| } |
| } |
| if ((defined($r->{from}) and not exists $versions{$r->{from}})) { |
| warning(_g("'%s' option specifies non-existing version"), "from"); |
| warning(_g("use oldest entry that is later than the one specified")); |
| my $oldest; |
| foreach my $v (@versions) { |
| if (version_compare_relation($v, REL_GT, $r->{from})) { |
| $oldest = $v; |
| } |
| } |
| if (defined($oldest)) { |
| $r->{from} = $oldest; |
| } else { |
| warning(_g("no such entry found, ignoring '%s' parameter"), "from"); |
| delete $r->{from}; # No version was oldest |
| } |
| } |
| if (defined($r->{'until'}) and not exists $versions{$r->{'until'}}) { |
| warning(_g("'%s' option specifies non-existing version"), "until"); |
| warning(_g("use oldest entry that is later than the one specified")); |
| my $oldest; |
| foreach my $v (@versions) { |
| if (version_compare_relation($v, REL_GT, $r->{'until'})) { |
| $oldest = $v; |
| } |
| } |
| if (defined($oldest)) { |
| $r->{'until'} = $oldest; |
| } else { |
| warning(_g("no such entry found, ignoring '%s' parameter"), "until"); |
| delete $r->{'until'}; # No version was oldest |
| } |
| } |
| if (defined($r->{to}) and not exists $versions{$r->{to}}) { |
| warning(_g("'%s' option specifies non-existing version"), "to"); |
| warning(_g("use newest entry that is earlier than the one specified")); |
| foreach my $v (@versions) { |
| if (version_compare_relation($v, REL_LT, $r->{to})) { |
| $r->{to} = $v; |
| last; |
| } |
| } |
| if (not exists $versions{$r->{to}}) { |
| # No version was earlier |
| warning(_g("no such entry found, ignoring '%s' parameter"), "to"); |
| delete $r->{to}; |
| } |
| } |
| |
| if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) { |
| warning(_g("'since' option specifies most recent version, ignoring")); |
| delete $r->{since}; |
| } |
| if (defined($r->{'until'}) and $data->[-1]->get_version() eq $r->{'until'}) { |
| warning(_g("'until' option specifies oldest version, ignoring")); |
| delete $r->{'until'}; |
| } |
| } |
| |
| sub get_range { |
| my ($self, $range) = @_; |
| $range = {} unless defined $range; |
| my $res = $self->_data_range($range); |
| if (defined $res) { |
| return @$res if wantarray; |
| return $res; |
| } else { |
| return () if wantarray; |
| return undef; |
| } |
| } |
| |
| sub _data_range { |
| my ($self, $range) = @_; |
| |
| my $data = $self->{data} or return undef; |
| |
| return [ @$data ] if $range->{all}; |
| |
| unless (grep { m/^(since|until|from|to|count|offset)$/ } keys %$range) { |
| return [ @$data ]; |
| } |
| |
| $self->__sanity_check_range($range); |
| |
| my ($start, $end); |
| if (defined($range->{count})) { |
| my $offset = $range->{offset} || 0; |
| my $count = $range->{count}; |
| # Convert count/offset in start/end |
| if ($offset > 0) { |
| $offset -= ($count < 0); |
| } elsif ($offset < 0) { |
| $offset = $#$data + ($count > 0) + $offset; |
| } else { |
| $offset = $#$data if $count < 0; |
| } |
| $start = $end = $offset; |
| $start += $count+1 if $count < 0; |
| $end += $count-1 if $count > 0; |
| # Check limits |
| $start = 0 if $start < 0; |
| return if $start > $#$data; |
| $end = $#$data if $end > $#$data; |
| return if $end < 0; |
| $end = $start if $end < $start; |
| return [ @{$data}[$start .. $end] ]; |
| } |
| |
| my @result; |
| my $include = 1; |
| $include = 0 if defined($range->{to}) or defined($range->{'until'}); |
| foreach (@$data) { |
| my $v = $_->get_version(); |
| $include = 1 if defined($range->{to}) and $v eq $range->{to}; |
| last if defined($range->{since}) and $v eq $range->{since}; |
| |
| push @result, $_ if $include; |
| |
| $include = 1 if defined($range->{'until'}) and $v eq $range->{'until'}; |
| last if defined($range->{from}) and $v eq $range->{from}; |
| } |
| |
| return \@result if scalar(@result); |
| return undef; |
| } |
| |
| =item $c->abort_early() |
| |
| Returns true if enough data have been parsed to be able to return all |
| entries selected by the range set at creation (or with set_options). |
| |
| =cut |
| |
| sub abort_early { |
| my ($self) = @_; |
| |
| my $data = $self->{data} or return; |
| my $r = $self->{range} or return; |
| my $count = $r->{count} || 0; |
| my $offset = $r->{offset} || 0; |
| |
| return if $r->{all}; |
| return unless grep { m/^(since|until|from|to|count|offset)$/ } keys %$r; |
| return if $offset < 0 or $count < 0; |
| if (defined($r->{count})) { |
| if ($offset > 0) { |
| $offset -= ($count < 0); |
| } |
| my $start = my $end = $offset; |
| $end += $count-1 if $count > 0; |
| return ($start < @$data and $end < @$data); |
| } |
| |
| return unless defined($r->{since}) or defined($r->{from}); |
| foreach (@$data) { |
| my $v = $_->get_version(); |
| return 1 if defined($r->{since}) and $v eq $r->{since}; |
| return 1 if defined($r->{from}) and $v eq $r->{from}; |
| } |
| |
| return; |
| } |
| |
| =item $c->save($filename) |
| |
| Save the changelog in the given file. |
| |
| =item $c->output() |
| |
| =item "$c" |
| |
| Returns a string representation of the changelog (it's a concatenation of |
| the string representation of the individual changelog entries). |
| |
| =item $c->output($fh) |
| |
| Output the changelog to the given filehandle. |
| |
| =cut |
| |
| sub output { |
| my ($self, $fh) = @_; |
| my $str = ""; |
| foreach my $entry (@{$self}) { |
| my $text = $entry->output(); |
| print $fh $text if defined $fh; |
| $str .= $text if defined wantarray; |
| } |
| my $text = $self->get_unparsed_tail(); |
| if (defined $text) { |
| print $fh $text if defined $fh; |
| $str .= $text if defined wantarray; |
| } |
| return $str; |
| } |
| |
| =item my $control = $c->dpkg($range) |
| |
| Returns a Dpkg::Control::Changelog object representing the entries selected |
| by the optional range specifier (see L<"RANGE SELECTION"> for details). |
| Returns undef in no entries are matched. |
| |
| The following fields are contained in the object: |
| |
| =over 4 |
| |
| =item Source |
| |
| package name (in the first entry) |
| |
| =item Version |
| |
| packages' version (from first entry) |
| |
| =item Distribution |
| |
| target distribution (from first entry) |
| |
| =item Urgency |
| |
| urgency (highest of all printed entries) |
| |
| =item Maintainer |
| |
| person that created the (first) entry |
| |
| =item Date |
| |
| date of the (first) entry |
| |
| =item Closes |
| |
| bugs closed by the entry/entries, sorted by bug number |
| |
| =item Changes |
| |
| content of the the entry/entries |
| |
| =back |
| |
| =cut |
| |
| our ( @URGENCIES, %URGENCIES ); |
| BEGIN { |
| @URGENCIES = qw(low medium high critical emergency); |
| my $i = 1; |
| %URGENCIES = map { $_ => $i++ } @URGENCIES; |
| } |
| |
| sub dpkg { |
| my ($self, $range) = @_; |
| |
| my @data = $self->get_range($range) or return undef; |
| my $entry = shift @data; |
| |
| my $f = Dpkg::Control::Changelog->new(); |
| $f->{Urgency} = $entry->get_urgency() || "unknown"; |
| $f->{Source} = $entry->get_source() || "unknown"; |
| $f->{Version} = $entry->get_version(); |
| $f->{Version} = "unknown" unless defined $f->{Version}; |
| $f->{Distribution} = join(" ", $entry->get_distributions()); |
| $f->{Maintainer} = $entry->get_maintainer() || ''; |
| $f->{Date} = $entry->get_timestamp() || ''; |
| $f->{Changes} = $entry->get_dpkg_changes(); |
| |
| # handle optional fields |
| my $opts = $entry->get_optional_fields(); |
| my %closes; |
| foreach (keys %$opts) { |
| if (/^Urgency$/i) { # Already dealt |
| } elsif (/^Closes$/i) { |
| $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes})); |
| } else { |
| field_transfer_single($opts, $f); |
| } |
| } |
| |
| foreach $entry (@data) { |
| my $oldurg = $f->{Urgency} || ''; |
| my $oldurgn = $URGENCIES{$f->{Urgency}} || -1; |
| my $newurg = $entry->get_urgency() || ''; |
| my $newurgn = $URGENCIES{$newurg} || -1; |
| $f->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg; |
| $f->{Changes} .= "\n" . $entry->get_dpkg_changes(); |
| |
| # handle optional fields |
| $opts = $entry->get_optional_fields(); |
| foreach (keys %$opts) { |
| if (/^Closes$/i) { |
| $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes})); |
| } elsif (not exists $f->{$_}) { # Don't overwrite an existing field |
| field_transfer_single($opts, $f); |
| } |
| } |
| } |
| |
| if (scalar keys %closes) { |
| $f->{Closes} = join " ", sort { $a <=> $b } keys %closes; |
| } |
| run_vendor_hook("post-process-changelog-entry", $f); |
| |
| return $f; |
| } |
| |
| =item my @controls = $c->rfc822($range) |
| |
| Returns a Dpkg::Index containing Dpkg::Control::Changelog objects where |
| each object represents one entry in the changelog that is part of the |
| range requested (see L<"RANGE SELECTION"> for details). For the format of |
| such an object see the description of the L<"dpkg"> method (while ignoring |
| the remarks about which values are taken from the first entry). |
| |
| =cut |
| |
| sub rfc822 { |
| my ($self, $range) = @_; |
| |
| my @data = $self->get_range($range) or return undef; |
| my $index = Dpkg::Index->new(type => CTRL_CHANGELOG); |
| |
| foreach my $entry (@data) { |
| my $f = Dpkg::Control::Changelog->new(); |
| $f->{Urgency} = $entry->get_urgency() || "unknown"; |
| $f->{Source} = $entry->get_source() || "unknown"; |
| $f->{Version} = $entry->get_version(); |
| $f->{Version} = "unknown" unless defined $f->{Version}; |
| $f->{Distribution} = join(" ", $entry->get_distributions()); |
| $f->{Maintainer} = $entry->get_maintainer() || ""; |
| $f->{Date} = $entry->get_timestamp() || ""; |
| $f->{Changes} = $entry->get_dpkg_changes(); |
| |
| # handle optional fields |
| my $opts = $entry->get_optional_fields(); |
| foreach (keys %$opts) { |
| field_transfer_single($opts, $f) unless exists $f->{$_}; |
| } |
| |
| run_vendor_hook("post-process-changelog-entry", $f); |
| |
| $index->add($f); |
| } |
| return $index; |
| } |
| |
| =back |
| |
| =head1 RANGE SELECTION |
| |
| A range selection is described by a hash reference where |
| the allowed keys and values are described below. |
| |
| The following options take a version number as value. |
| |
| =over 4 |
| |
| =item since |
| |
| Causes changelog information from all versions strictly |
| later than B<version> to be used. |
| |
| =item until |
| |
| Causes changelog information from all versions strictly |
| earlier than B<version> to be used. |
| |
| =item from |
| |
| Similar to C<since> but also includes the information for the |
| specified B<version> itself. |
| |
| =item to |
| |
| Similar to C<until> but also includes the information for the |
| specified B<version> itself. |
| |
| =back |
| |
| The following options don't take version numbers as values: |
| |
| =over 4 |
| |
| =item all |
| |
| If set to a true value, all entries of the changelog are returned, |
| this overrides all other options. |
| |
| =item count |
| |
| Expects a signed integer as value. Returns C<value> entries from the |
| top of the changelog if set to a positive integer, and C<abs(value)> |
| entries from the tail if set to a negative integer. |
| |
| =item offset |
| |
| Expects a signed integer as value. Changes the starting point for |
| C<count>, either counted from the top (positive integer) or from |
| the tail (negative integer). C<offset> has no effect if C<count> |
| wasn't given as well. |
| |
| =back |
| |
| Some examples for the above options. Imagine an example changelog with |
| entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1. |
| |
| Range Included entries |
| C<{ since =E<gt> '2.0' }> 3.1, 3.0, 2.2 |
| C<{ until =E<gt> '2.0' }> 1.3, 1.2 |
| C<{ from =E<gt> '2.0' }> 3.1, 3.0, 2.2, 2.1, 2.0 |
| C<{ to =E<gt> '2.0' }> 2.0, 1.3, 1.2 |
| C<{ count =E<gt> 2 }> 3.1, 3.0 |
| C<{ count =E<gt> -2 }> 1.3, 1.2 |
| C<{ count =E<gt> 3, offset=E<gt> 2 }> 2.2, 2.1, 2.0 |
| C<{ count =E<gt> 2, offset=E<gt> -3 }> 2.0, 1.3 |
| C<{ count =E<gt> -2, offset=E<gt> 3 }> 3.0, 2.2 |
| C<{ count =E<gt> -2, offset=E<gt> -3 }> 2.2, 2.1 |
| |
| Any combination of one option of C<since> and C<from> and one of |
| C<until> and C<to> returns the intersection of the two results |
| with only one of the options specified. |
| |
| =head1 AUTHOR |
| |
| Frank Lichtenheld, E<lt>frank@lichtenheld.deE<gt> |
| Raphaël Hertzog, E<lt>hertzog@debian.orgE<gt> |
| |
| =cut |
| 1; |