blob: 3607c7ae0d60870052969b52c431c5fa61db8da4 [file] [log] [blame]
# 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/>.
package Dpkg::Changelog::Entry::Debian;
use strict;
use warnings;
our $VERSION = "1.00";
use Exporter;
use Dpkg::Changelog::Entry;
use base qw(Exporter Dpkg::Changelog::Entry);
our @EXPORT_OK = qw($regex_header $regex_trailer find_closes);
use Date::Parse;
use Dpkg::Gettext;
use Dpkg::Control::Changelog;
use Dpkg::Version;
=encoding utf8
=head1 NAME
Dpkg::Changelog::Entry::Debian - represents a Debian changelog entry
=head1 DESCRIPTION
This object represents a Debian changelog entry. It implements the
generic interface Dpkg::Changelog::Entry. Only functions specific to this
implementation are described below.
=head1 VARIABLES
$regex_header, $regex_trailer are two regular expressions that can be used
to match a line and know whether it's a valid header/trailer line.
The matched content for $regex_header is the source package name ($1), the
version ($2), the target distributions ($3) and the options on the rest
of the line ($4). For $regex_trailer, it's the maintainer name ($1), its
email ($2), some blanks ($3) and the timestamp ($4).
=cut
my $name_chars = qr/[-+0-9a-z.]/i;
our $regex_header = qr/^(\w$name_chars*) \(([^\(\) \t]+)\)((?:\s+$name_chars+)+)\;(.*?)\s*$/i;
our $regex_trailer = qr/^ \-\- (.*) <(.*)>( ?)((\w+\,\s*)?\d{1,2}\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}(\s+\([^\\\(\)]\))?)\s*$/o;
=head1 FUNCTIONS
=over 4
=item my @items = $entry->get_change_items()
Return a list of change items. Each item contains at least one line.
A change line starting with an asterisk denotes the start of a new item.
Any change line like "[ Raphaël Hertzog ]" is treated like an item of its
own even if it starts a set of items attributed to this person (the
following line necessarily starts a new item).
=cut
sub get_change_items {
my ($self) = @_;
my (@items, @blanks, $item);
foreach my $line (@{$self->get_part("changes")}) {
if ($line =~ /^\s*\*/) {
push @items, $item if defined $item;
$item = "$line\n";
} elsif ($line =~ /^\s*\[\s[^\]]+\s\]\s*$/) {
push @items, $item if defined $item;
push @items, "$line\n";
$item = undef;
@blanks = ();
} elsif ($line =~ /^\s*$/) {
push @blanks, "$line\n";
} else {
if (defined $item) {
$item .= "@blanks$line\n";
} else {
$item = "$line\n";
}
@blanks = ();
}
}
push @items, $item if defined $item;
return @items;
}
=item my @errors = $entry->check_header()
=item my @errors = $entry->check_trailer()
Return a list of errors. Each item in the list is an error message
describing the problem. If the empty list is returned, no errors
have been found.
=cut
sub check_header {
my ($self) = @_;
my @errors;
if (defined($self->{header}) and $self->{header} =~ $regex_header) {
my $options = $4;
$options =~ s/^\s+//;
my %optdone;
foreach my $opt (split(/\s*,\s*/, $options)) {
unless ($opt =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i) {
push @errors, sprintf(_g("bad key-value after \`;': \`%s'"), $opt);
next;
}
my ($k, $v) = (ucfirst($1), $2);
if ($optdone{$k}) {
push @errors, sprintf(_g("repeated key-value %s"), $k);
}
$optdone{$k} = 1;
if ($k eq 'Urgency') {
push @errors, sprintf(_g("badly formatted urgency value: %s"), $v)
unless ($v =~ m/^([-0-9a-z]+)((\s+.*)?)$/i);
} elsif ($k =~ m/^X[BCS]+-/i) {
} else {
push @errors, sprintf(_g("unknown key-value %s"), $k);
}
}
} else {
push @errors, _g("the header doesn't match the expected regex");
}
return @errors;
}
sub check_trailer {
my ($self) = @_;
my @errors;
if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
if ($3 ne ' ') {
push @errors, _g("badly formatted trailer line");
}
unless (defined str2time($4)) {
push @errors, sprintf(_g("couldn't parse date %s"), $4);
}
} else {
push @errors, _g("the trailer doesn't match the expected regex");
}
return @errors;
}
=item $entry->normalize()
Normalize the content. Strip whitespaces at end of lines, use a single
empty line to separate each part.
=cut
sub normalize {
my ($self) = @_;
$self->SUPER::normalize();
#XXX: recreate header/trailer
}
sub get_source {
my ($self) = @_;
if (defined($self->{header}) and $self->{header} =~ $regex_header) {
return $1;
}
return undef;
}
sub get_version {
my ($self) = @_;
if (defined($self->{header}) and $self->{header} =~ $regex_header) {
return Dpkg::Version->new($2);
}
return undef;
}
sub get_distributions {
my ($self) = @_;
if (defined($self->{header}) and $self->{header} =~ $regex_header) {
my $value = $3;
$value =~ s/^\s+//;
my @dists = split(/\s+/, $value);
return @dists if wantarray;
return $dists[0];
}
return () if wantarray;
return undef;
}
sub get_optional_fields {
my ($self) = @_;
my $f = Dpkg::Control::Changelog->new();
if (defined($self->{header}) and $self->{header} =~ $regex_header) {
my $options = $4;
$options =~ s/^\s+//;
foreach my $opt (split(/\s*,\s*/, $options)) {
if ($opt =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i) {
$f->{$1} = $2;
}
}
}
my @closes = find_closes(join("\n", @{$self->{changes}}));
if (@closes) {
$f->{Closes} = join(" ", @closes);
}
return $f;
}
sub get_urgency {
my ($self) = @_;
my $f = $self->get_optional_fields();
if (exists $f->{Urgency}) {
$f->{Urgency} =~ s/\s.*$//;
return lc($f->{Urgency});
}
return undef;
}
sub get_maintainer {
my ($self) = @_;
if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
return "$1 <$2>";
}
return undef;
}
sub get_timestamp {
my ($self) = @_;
if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
return $4;
}
return undef;
}
=back
=head1 UTILITY FUNCTIONS
=head3 my @closed_bugs = find_closes($changes)
Takes one string as argument and finds "Closes: #123456, #654321" statements
as supported by the Debian Archive software in it. Returns all closed bug
numbers in an array.
=cut
sub find_closes {
my $changes = shift;
my %closes;
while ($changes &&
($changes =~ /closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*/ig)) {
$closes{$_} = 1 foreach($& =~ /\#?\s?(\d+)/g);
}
my @closes = sort { $a <=> $b } keys %closes;
return @closes;
}
=head1 AUTHOR
Raphaël Hertzog <hertzog@debian.org>.
=cut
1;