use warnings;
use strict;
+use feature 'state';
+
use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
-use base qw(Exporter);
+use Exporter qw(import);
use Params::Validate qw(validate_with :types);
use Debbugs::Common qw(:util :lock :quit :misc);
use Encode qw(decode encode is_utf8);
use Storable qw(dclone);
-use List::Util qw(min max);
+use List::AllUtils qw(min max uniq);
+use DateTime::Format::Pg;
use Carp qw(croak);
@EXPORT = ();
%EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
qw(isstrongseverity bug_presence split_status_fields),
+ qw(get_bug_statuses),
],
read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
qw(lock_read_all_merged_bugs),
qw(removefoundversions removefixedversions)
],
hook => [qw(bughook bughook_archive)],
+ indexdb => [qw(generate_index_db_line)],
fields => [qw(%fields)],
);
@EXPORT_OK = ();
if (@_ == 1) {
unshift @_, 'bug';
}
+ state $spec =
+ {bug => {type => SCALAR,
+ optional => 1,
+ # something really stupid passes negative bugnumbers
+ regex => qr/^-?\d+/,
+ },
+ location => {type => SCALAR|UNDEF,
+ optional => 1,
+ },
+ summary => {type => SCALAR,
+ optional => 1,
+ },
+ lock => {type => BOOLEAN,
+ optional => 1,
+ },
+ locks => {type => HASHREF,
+ optional => 1,
+ },
+ };
my %param = validate_with(params => \@_,
- spec => {bug => {type => SCALAR,
- optional => 1,
- # something really
- # stupid passes
- # negative bugnumbers
- regex => qr/^-?\d+/,
- },
- location => {type => SCALAR|UNDEF,
- optional => 1,
- },
- summary => {type => SCALAR,
- optional => 1,
- },
- lock => {type => BOOLEAN,
- optional => 1,
- },
- locks => {type => HASHREF,
- optional => 1,
- },
- },
+ spec => $spec,
);
die "One of bug or summary must be passed to read_bug"
if not exists $param{bug} and not exists $param{summary};
my $status;
my $log;
my $location;
+ my $report;
if (not defined $param{summary}) {
my $lref;
($lref,$location) = @param{qw(bug location)};
}
$status = getbugcomponent($lref, 'summary', $location);
$log = getbugcomponent($lref, 'log' , $location);
+ $report = getbugcomponent($lref, 'report' , $location);
return undef unless defined $status;
return undef if not -e $status;
}
else {
$status = $param{summary};
$log = $status;
+ $report = $status;
$log =~ s/\.summary$/.log/;
+ $report =~ s/\.summary$/.report/;
($location) = $status =~ m/(db-h|db|archive)/;
+ ($param{bug}) = $status =~ m/(\d+)\.summary$/;
}
if ($param{lock}) {
filelock("$config{spool_dir}/lock/$param{bug}",exists $param{locks}?$param{locks}:());
my %data;
my @lines;
- my $version = 2;
+ my $version;
local $_;
while (<$status_fh>) {
chomp;
push @lines, $_;
- $version = $1 if /^Format-Version: ([0-9]+)/i;
+ if (not defined $version and
+ /^Format-Version: ([0-9]+)/i
+ ) {
+ $version = $1;
+ }
}
-
+ $version = 2 if not defined $version;
# Version 3 is the latest format version currently supported.
if ($version > 3) {
warn "Unsupported status version '$version'";
return undef;
}
- my %namemap = reverse %fields;
+ state $namemap = {reverse %fields};
for my $line (@lines) {
if ($line =~ /(\S+?): (.*)/) {
my ($name, $value) = (lc $1, $2);
# or \n in the fields of status. Kill them off here.
# [Eventually, this should be superfluous.]
$value =~ s/[\r\n]//g;
- $data{$namemap{$name}} = $value if exists $namemap{$name};
+ $data{$namemap->{$name}} = $value if exists $namemap->{$name};
}
}
for my $field (keys %fields) {
# create the found/fixed hashes which indicate when a
# particular version was marked found or marked fixed.
@{$data{$field}}{@{$data{"${field}_versions"}}} =
- (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
+ (('') x (@{$data{"${field}_versions"}} - @{$data{"${field}_date"}}),
@{$data{"${field}_date"}});
}
my $status_modified = (stat($status))[9];
# Add log last modified time
- $data{log_modified} = (stat($log))[9];
+ $data{log_modified} = (stat($log))[9] // (stat("${log}.gz"))[9];
+ my $report_modified = (stat($report))[9] // $data{log_modified};
$data{last_modified} = max($status_modified,$data{log_modified});
+ # if the date isn't set (ancient bug), use the smallest of any of the modified
+ if (not defined $data{date} or not length($data{date})) {
+ $data{date} = min($report_modified,$status_modified,$data{log_modified});
+ }
$data{location} = $location;
$data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
$data{bug_num} = $param{bug};
+ # mergedwith occasionally is sorted badly. Fix it to always be sorted by <=>
+ # and not include this bug
+ if (defined $data{mergedwith} and
+ $data{mergedwith}) {
+ $data{mergedwith} =
+ join(' ',
+ grep { $_ != $data{bug_num}}
+ sort { $a <=> $b }
+ split / /, $data{mergedwith}
+ );
+ }
return \%data;
}
return grep {length $_} map {split $splitter} @t;
};
-my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
+our $sort_and_unique = sub {
+ my @v;
+ my %u;
+ my $all_numeric = 1;
+ for my $v (@_) {
+ if ($all_numeric and $v =~ /\D/) {
+ $all_numeric = 0;
+ }
+ next if exists $u{$v};
+ $u{$v} = 1;
+ push @v, $v;
+ }
+ if ($all_numeric) {
+ return sort {$a <=> $b} @v;
+ } else {
+ return sort @v;
+ }
+};
+
+my $ditch_space_unique_and_sort = sub {return &{$sort_and_unique}(&{$ditch_empty}(' ',@_))};
my %split_fields =
(package => \&splitpackages,
affects => \&splitpackages,
- blocks => $ditch_empty_space,
- blockedby => $ditch_empty_space,
+ # Ideally we won't have to split source, but because some consumers of
+ # get_bug_status cannot handle arrayref, we will split it here.
+ source => \&splitpackages,
+ blocks => $ditch_space_unique_and_sort,
+ blockedby => $ditch_space_unique_and_sort,
# this isn't strictly correct, but we'll split both of them for
# the time being until we ditch all use of keywords everywhere
# from the code
- keywords => $ditch_empty_space,
- tags => $ditch_empty_space,
- found_versions => $ditch_empty_space,
- fixed_versions => $ditch_empty_space,
- mergedwith => $ditch_empty_space,
+ keywords => $ditch_space_unique_and_sort,
+ tags => $ditch_space_unique_and_sort,
+ found_versions => $ditch_space_unique_and_sort,
+ fixed_versions => $ditch_space_unique_and_sort,
+ mergedwith => $ditch_space_unique_and_sort,
);
sub split_status_fields {
=cut
sub lockreadbugmerge {
- my ($bug_num,$location) = @_;
my $data = lockreadbug(@_);
if (not defined $data) {
return (0,undef);
push @data,$newdata;
# perform a sanity check to make sure that the merged bugs
# are all merged with eachother
- my $expectmerge= join(' ',grep {$_ != $bug } sort { $a <=> $b } @bugs);
+ # We do a cmp sort instead of an <=> sort here, because that's
+ # what merge does
+ my $expectmerge=
+ join(' ',grep {$_ != $bug }
+ sort { $a <=> $b }
+ @bugs);
if ($newdata->{mergedwith} ne $expectmerge) {
for (1..$locks) {
unfilelock(exists $param{locks}?$param{locks}:());
}
- die "Bug $param{bug} differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
+ die "Bug $param{bug} mergedwith differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
}
}
}
Writes the bug status and summary files out.
-Skips writting out a status file if minversion is 2
+Skips writing out a status file if minversion is 2
Does not call bughook if disablebughook is true.
my $version = shift;
my $isbinary = shift;
return unless defined $version;
- undef $package if $package =~ m[(?:\s|/)];
+ undef $package if defined $package and $package =~ m[(?:\s|/)];
my $source = $package;
- if ($package =~ s/^src://) {
+ if (defined $package and $package =~ s/^src://) {
$isbinary = 0;
$source = $package;
}
number are removed.
Currently $package and $isbinary are entirely ignored, but accepted
-for backwards compatibilty.
+for backwards compatibility.
=cut
# If we just are checking if the bug can be archived, we'll not even bother
# checking the versioning information if the bug has been -done for less than 28 days.
my $log_file = getbugcomponent($param{bug},'log');
- if (not defined $log_file) {
+ if (not defined $log_file or not -e $log_file) {
print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
return $cannot_archive;
}
- my $max_log_age = max(map {$config{remove_age} - -M $_}
- $log_file, map {my $log = getbugcomponent($_,'log');
+ my @log_files = $log_file, (map {my $log = getbugcomponent($_,'log');
defined $log ? ($log) : ();
}
- split / /, $status->{mergedwith}
- );
+ split / /, $status->{mergedwith});
+ my $max_log_age = max(map {-e $_?($config{remove_age} - -M _):0}
+ @log_files);
if (not $param{days_until} and not $param{ignore_time}
and $max_log_age > 0
) {
if (@_ == 1) {
unshift @_, 'bug';
}
+ state $spec =
+ {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ status => {type => HASHREF,
+ optional => 1,
+ },
+ bug_index => {type => OBJECT,
+ optional => 1,
+ },
+ version => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ dist => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ arch => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ bugusertags => {type => HASHREF,
+ optional => 1,
+ },
+ sourceversions => {type => ARRAYREF,
+ optional => 1,
+ },
+ indicatesource => {type => BOOLEAN,
+ default => 1,
+ },
+ binary_to_source_cache => {type => HASHREF,
+ optional => 1,
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ };
my %param = validate_with(params => \@_,
- spec => {bug => {type => SCALAR,
- regex => qr/^\d+$/,
- },
- status => {type => HASHREF,
- optional => 1,
- },
- bug_index => {type => OBJECT,
- optional => 1,
- },
- version => {type => SCALAR|ARRAYREF,
- optional => 1,
- },
- dist => {type => SCALAR|ARRAYREF,
- optional => 1,
- },
- arch => {type => SCALAR|ARRAYREF,
- optional => 1,
- },
- bugusertags => {type => HASHREF,
- optional => 1,
- },
- sourceversions => {type => ARRAYREF,
- optional => 1,
- },
- indicatesource => {type => BOOLEAN,
- default => 1,
- },
- },
+ spec => $spec,
);
my %status;
if (defined $param{bug_index} and
exists $param{bug_index}{$param{bug}}) {
- %status = %{ $param{bug_index}{$param{bug}} };
- $status{pending} = $status{ status };
- $status{id} = $param{bug};
- return \%status;
+ %status = %{ $param{bug_index}{$param{bug}} };
+ $status{pending} = $status{ status };
+ $status{id} = $param{bug};
+ return \%status;
}
- if (defined $param{status}) {
- %status = %{$param{status}};
+ my $statuses = get_bug_statuses(@_);
+ if (exists $statuses->{$param{bug}}) {
+ return $statuses->{$param{bug}};
+ } else {
+ return {};
}
- else {
- my $location = getbuglocation($param{bug}, 'summary');
- return {} if not defined $location or not length $location;
- %status = %{ readbug( $param{bug}, $location ) };
- }
- $status{id} = $param{bug};
+}
- if (defined $param{bugusertags}{$param{bug}}) {
- $status{keywords} = "" unless defined $status{keywords};
- $status{keywords} .= " " unless $status{keywords} eq "";
- $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
+sub get_bug_statuses {
+ state $spec =
+ {bug => {type => SCALAR|ARRAYREF,
+ },
+ status => {type => HASHREF,
+ optional => 1,
+ },
+ bug_index => {type => OBJECT,
+ optional => 1,
+ },
+ version => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ dist => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ arch => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ bugusertags => {type => HASHREF,
+ optional => 1,
+ },
+ sourceversions => {type => ARRAYREF,
+ optional => 1,
+ },
+ indicatesource => {type => BOOLEAN,
+ default => 1,
+ },
+ binary_to_source_cache => {type => HASHREF,
+ optional => 1,
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ };
+ my %param = validate_with(params => \@_,
+ spec => $spec,
+ );
+ my $bin_to_src_cache = {};
+ if (defined $param{binary_to_source_cache}) {
+ $bin_to_src_cache = $param{binary_to_source_cache};
}
- $status{tags} = $status{keywords};
- my %tags = map { $_ => 1 } split ' ', $status{tags};
-
- $status{package} = '' if not defined $status{package};
- $status{"package"} =~ s/\s*$//;
-
- $status{source} = binary_to_source(binary=>[split /\s*,\s*/, $status{package}],
- source_only => 1,
- );
-
- $status{"package"} = 'unknown' if ($status{"package"} eq '');
- $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
-
- $status{"pending"} = 'pending';
- $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
- $status{"pending"} = 'pending-fixed' if ($tags{pending});
- $status{"pending"} = 'fixed' if ($tags{fixed});
-
+ my %status;
+ my %statuses;
+ if (defined $param{schema}) {
+ my @bug_statuses =
+ $param{schema}->resultset('BugStatus')->
+ search_rs({id => [make_list($param{bug})]},
+ {result_class => 'DBIx::Class::ResultClass::HashRefInflator'})->
+ all();
+ for my $bug_status (@bug_statuses) {
+ $statuses{$bug_status->{bug_num}} =
+ $bug_status;
+ for my $field (qw(blocks blockedby done),
+ qw(tags mergedwith)
+ ) {
+ $bug_status->{$field} //='';
+ }
+ $bug_status->{keywords} =
+ $bug_status->{tags};
+ $bug_status->{location} = $bug_status->{archived}?'archive':'db-h';
+ for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
+ $bug_status->{$field} = [split ' ', $bug_status->{$field} // ''];
+ }
+ for my $field (qw(found fixed)) {
+ # create the found/fixed hashes which indicate when a
+ # particular version was marked found or marked fixed.
+ @{$bug_status->{$field}}{@{$bug_status->{"${field}_versions"}}} =
+ (('') x (@{$bug_status->{"${field}_versions"}} -
+ @{$bug_status->{"${field}_date"}}),
+ @{$bug_status->{"${field}_date"}});
+ }
+ $bug_status->{id} = $bug_status->{bug_num};
+ }
+ } else {
+ for my $bug (make_list($param{bug})) {
+ if (defined $param{bug_index} and
+ exists $param{bug_index}{$bug}) {
+ my %status = %{$param{bug_index}{$bug}};
+ $status{pending} = $status{status};
+ $status{id} = $bug;
+ $statuses{$bug} = \%status;
+ }
+ elsif (defined $param{status} and
+ $param{status}{bug_num} == $bug
+ ) {
+ $statuses{$bug} = {%{$param{status}}};
+ } else {
+ my $location = getbuglocation($bug, 'summary');
+ next if not defined $location or not length $location;
+ my %status = %{ readbug( $bug, $location ) };
+ $status{id} = $bug;
+ $statuses{$bug} = \%status;
+ }
+ }
+ }
+ for my $bug (keys %statuses) {
+ my $status = $statuses{$bug};
- my $presence = bug_presence(status => \%status,
- map{(exists $param{$_})?($_,$param{$_}):()}
- qw(bug sourceversions arch dist version found fixed package)
- );
- if (defined $presence) {
- if ($presence eq 'fixed') {
- $status{pending} = 'done';
- }
- elsif ($presence eq 'absent') {
- $status{pending} = 'absent';
- }
+ if (defined $param{bugusertags}{$param{bug}}) {
+ $status->{keywords} = "" unless defined $status->{keywords};
+ $status->{keywords} .= " " unless $status->{keywords} eq "";
+ $status->{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
+ }
+ $status->{tags} = $status->{keywords};
+ my %tags = map { $_ => 1 } split ' ', $status->{tags};
+
+ $status->{package} = '' if not defined $status->{package};
+ $status->{"package"} =~ s/\s*$//;
+
+ $status->{source} = binary_to_source(binary=>[split /\s*,\s*/, $status->{package}],
+ source_only => 1,
+ cache => $bin_to_src_cache,
+ defined $param{schema}?
+ (schema => $param{schema}):(),
+ );
+
+ $status->{"package"} = 'unknown' if ($status->{"package"} eq '');
+ $status->{"severity"} = 'normal' if (not defined $status->{severity} or $status->{"severity"} eq '');
+
+ $status->{"pending"} = 'pending';
+ $status->{"pending"} = 'forwarded' if (length($status->{"forwarded"}));
+ $status->{"pending"} = 'pending-fixed' if ($tags{pending});
+ $status->{"pending"} = 'fixed' if ($tags{fixed});
+
+
+ my $presence = bug_presence(status => $status,
+ bug => $bug,
+ map{(exists $param{$_})?($_,$param{$_}):()}
+ qw(sourceversions arch dist version found fixed package)
+ );
+ if (defined $presence) {
+ if ($presence eq 'fixed') {
+ $status->{pending} = 'done';
+ } elsif ($presence eq 'absent') {
+ $status->{pending} = 'absent';
+ }
+ }
}
- return \%status;
+ return \%statuses;
}
=head2 bug_presence
return grep { $_ eq $severity } @{$config{strong_severities}};
}
+=head1 indexdb
+
+=head2 generate_index_db_line
+
+ my $data = read_bug(bug => $bug,
+ location => $initialdir);
+ # generate_index_db_line hasn't been written yet at all.
+ my $line = generate_index_db_line($data);
+
+Returns a line for a bug suitable to be written out to index.db.
+
+=cut
+
+sub generate_index_db_line {
+ my ($data,$bug) = @_;
+
+ # just in case someone has given us a split out data
+ $data = join_status_fields($data);
+
+ my $whendone = "open";
+ my $severity = $config{default_severity};
+ (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
+ $pkglist =~ s/^,+//;
+ $pkglist =~ s/,+$//;
+ $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
+ $whendone = "done" if defined $data->{done} and length $data->{done};
+ $severity = $data->{severity} if length $data->{severity};
+ return sprintf "%s %d %d %s [%s] %s %s\n",
+ $pkglist, $data->{bug_num}//$bug, $data->{date}, $whendone,
+ $data->{originator}, $severity, $data->{keywords};
+}
+
+
=head1 PRIVATE FUNCTIONS
my $data = $bugs_temp{$bug};
appendfile("$config{spool_dir}/debbugs.trace","$type $bug\n",makestatus($data, 1));
- my $whendone = "open";
- my $severity = $config{default_severity};
- (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
- $pkglist =~ s/^,+//;
- $pkglist =~ s/,+$//;
- $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
- $whendone = "done" if defined $data->{done} and length $data->{done};
- $severity = $data->{severity} if length $data->{severity};
-
- my $k = sprintf "%s %d %d %s [%s] %s %s\n",
- $pkglist, $bug, $data->{date}, $whendone,
- $data->{originator}, $severity, $data->{keywords};
- $bugs{$bug} = $k;
+ $bugs{$bug} = generate_index_db_line($data,$bug);
}
update_realtime("$config{spool_dir}/index.db.realtime", %bugs);