+ state $spec = {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ status => {type => HASHREF,
+ optional => 1,
+ },
+ days_until => {type => BOOLEAN,
+ default => 0,
+ },
+ ignore_time => {type => BOOLEAN,
+ default => 0,
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ };
+ my %param = validate_with(params => \@_,
+ spec => $spec,
+ );
+ # This is what we return if the bug cannot be archived.
+ my $cannot_archive = $param{days_until}?-1:0;
+ # read the status information
+ my $status = $param{status};
+ if (not exists $param{status} or not defined $status) {
+ $status = read_bug(bug=>$param{bug});
+ if (not defined $status) {
+ print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
+ return undef;
+ }
+ }
+ # Bugs can be archived if they are
+ # 1. Closed
+ if (not defined $status->{done} or not length $status->{done}) {
+ print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
+ return $cannot_archive
+ }
+ # Check to make sure that the bug has none of the unremovable tags set
+ if (@{$config{removal_unremovable_tags}}) {
+ for my $tag (split ' ', ($status->{keywords}||'')) {
+ if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
+ print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
+ return $cannot_archive;
+ }
+ }
+ }
+
+ # 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 or not -e $log_file) {
+ print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
+ return $cannot_archive;
+ }
+ my @log_files = $log_file, (map {my $log = getbugcomponent($_,'log');
+ defined $log ? ($log) : ();
+ }
+ 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
+ ) {
+ print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
+ return $cannot_archive;
+ }
+ # At this point, we have to get the versioning information for this bug.
+ # We examine the set of distribution tags. If a bug has no distribution
+ # tags set, we assume a default set, otherwise we use the tags the bug
+ # has set.
+
+ # In cases where we are assuming a default set, if the severity
+ # is strong, we use the strong severity default; otherwise, we
+ # use the normal default.
+
+ # There must be fixed_versions for us to look at the versioning
+ # information
+ my $min_fixed_time = time;
+ my $min_archive_days = 0;
+ if (@{$status->{fixed_versions}}) {
+ my %dist_tags;
+ @dist_tags{@{$config{removal_distribution_tags}}} =
+ (1) x @{$config{removal_distribution_tags}};
+ my %dists;
+ for my $tag (split ' ', ($status->{keywords}||'')) {
+ next unless exists $config{distribution_aliases}{$tag};
+ next unless $dist_tags{$config{distribution_aliases}{$tag}};
+ $dists{$config{distribution_aliases}{$tag}} = 1;
+ }
+ if (not keys %dists) {
+ if (isstrongseverity($status->{severity})) {
+ @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
+ (1) x @{$config{removal_strong_severity_default_distribution_tags}};
+ }
+ else {
+ @dists{@{$config{removal_default_distribution_tags}}} =
+ (1) x @{$config{removal_default_distribution_tags}};
+ }
+ }
+ my %source_versions;
+ my @sourceversions = get_versions(package => $status->{package},
+ dist => [keys %dists],
+ source => 1,
+ hash_slice(%param,'schema'),
+ );
+ @source_versions{@sourceversions} = (1) x @sourceversions;
+ # If the bug has not been fixed in the versions actually
+ # distributed, then it cannot be archived.
+ if ('found' eq max_buggy(bug => $param{bug},
+ sourceversions => [keys %source_versions],
+ found => $status->{found_versions},
+ fixed => $status->{fixed_versions},
+ version_cache => $version_cache,
+ package => $status->{package},
+ hash_slice(%param,'schema'),
+ )) {
+ print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
+ return $cannot_archive;
+ }
+ # Since the bug has at least been fixed in the architectures
+ # that matters, we check to see how long it has been fixed.
+
+ # If $param{ignore_time}, then we should ignore time.
+ if ($param{ignore_time}) {
+ return $param{days_until}?0:1;
+ }
+
+ # To do this, we order the times from most recent to oldest;
+ # when we come to the first found version, we stop.
+ # If we run out of versions, we only report the time of the
+ # last one.
+ my %time_versions = get_versions(package => $status->{package},
+ dist => [keys %dists],
+ source => 1,
+ time => 1,
+ hash_slice(%param,'schema'),
+ );
+ for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
+ my $buggy = buggy(bug => $param{bug},
+ version => $version,
+ found => $status->{found_versions},
+ fixed => $status->{fixed_versions},
+ version_cache => $version_cache,
+ package => $status->{package},
+ hash_slice(%param,'schema'),
+ );
+ last if $buggy eq 'found';
+ $min_fixed_time = min($time_versions{$version},$min_fixed_time);
+ }
+ $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
+ # if there are no versions in the archive at all, then
+ # we can archive if enough days have passed
+ if @sourceversions;
+ }
+ # If $param{ignore_time}, then we should ignore time.
+ if ($param{ignore_time}) {
+ return $param{days_until}?0:1;
+ }
+ # 6. at least 28 days have passed since the last action has occured or the bug was closed
+ my $age = ceil($max_log_age);
+ if ($age > 0 or $min_archive_days > 0) {
+ print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
+ return $param{days_until}?max($age,$min_archive_days):0;
+ }
+ else {
+ return $param{days_until}?0:1;
+ }
+}
+
+
+=head2 get_bug_status
+
+ my $status = get_bug_status(bug => $nnn);
+
+ my $status = get_bug_status($bug_num)
+
+=head3 Options
+
+=over
+
+=item bug -- scalar bug number
+
+=item status -- optional hashref of bug status as returned by readbug
+(can be passed to avoid rereading the bug information)
+
+=item bug_index -- optional tied index of bug status infomration;
+currently not correctly implemented.
+
+=item version -- optional version(s) to check package status at
+
+=item dist -- optional distribution(s) to check package status at
+
+=item arch -- optional architecture(s) to check package status at
+
+=item bugusertags -- optional hashref of bugusertags
+
+=item sourceversion -- optional arrayref of source/version; overrides
+dist, arch, and version. [The entries in this array must be in the
+"source/version" format.] Eventually this can be used to for caching.
+
+=item indicatesource -- if true, indicate which source packages this
+bug could belong to (or does belong to in the case of bugs assigned to
+a source package). Defaults to true.
+
+=back
+
+Note: Currently the version information is cached; this needs to be
+changed before using this function in long lived programs.
+
+=head3 Returns
+
+Currently returns a hashref of status with the following keys.
+
+=over
+
+=item id -- bug number
+
+=item bug_num -- duplicate of id
+
+=item keywords -- tags set on the bug, including usertags if bugusertags passed.
+
+=item tags -- duplicate of keywords
+
+=item package -- name of package that the bug is assigned to
+
+=item severity -- severity of the bug
+
+=item pending -- pending state of the bug; one of following possible
+values; values listed later have precedence if multiple conditions are
+satisifed:
+
+=over
+
+=item pending -- default state
+
+=item forwarded -- bug has been forwarded
+
+=item pending-fixed -- bug is tagged pending
+
+=item fixed -- bug is tagged fixed
+
+=item absent -- bug does not apply to this distribution/architecture
+
+=item done -- bug is resolved in this distribution/architecture
+
+=back
+
+=item location -- db-h or archive; the location in the filesystem
+
+=item subject -- title of the bug
+
+=item last_modified -- epoch that the bug was last modified
+
+=item date -- epoch that the bug was filed
+
+=item originator -- bug reporter
+
+=item log_modified -- epoch that the log file was last modified
+
+=item msgid -- Message id of the original bug report
+
+=back
+
+
+Other key/value pairs are returned but are not currently documented here.
+
+=cut
+
+sub get_bug_status {
+ 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 => $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;
+ }
+ my $statuses = get_bug_statuses(@_);
+ if (exists $statuses->{$param{bug}}) {
+ return $statuses->{$param{bug}};
+ } else {
+ return {};
+ }
+}
+
+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};
+ }
+ 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 affects)
+ ) {
+ $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};
+
+ 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->{"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 \%statuses;
+}
+
+=head2 bug_presence
+
+ my $precence = bug_presence(bug => nnn,
+ ...
+ );
+
+Returns 'found', 'absent', 'fixed' or undef based on whether the bug
+is found, absent, fixed, or no information is available in the
+distribution (dist) and/or architecture (arch) specified.
+
+
+=head3 Options
+
+=over
+
+=item bug -- scalar bug number
+
+=item status -- optional hashref of bug status as returned by readbug
+(can be passed to avoid rereading the bug information)
+
+=item bug_index -- optional tied index of bug status infomration;
+currently not correctly implemented.
+
+=item version -- optional version to check package status at
+
+=item dist -- optional distribution to check package status at
+
+=item arch -- optional architecture to check package status at
+
+=item sourceversion -- optional arrayref of source/version; overrides
+dist, arch, and version. [The entries in this array must be in the
+"source/version" format.] Eventually this can be used to for caching.
+
+=back
+
+=cut
+
+sub bug_presence {
+ my %param = validate_with(params => \@_,
+ spec => {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ status => {type => HASHREF,
+ optional => 1,
+ },
+ version => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ dist => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ arch => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ sourceversions => {type => ARRAYREF,
+ optional => 1,
+ },
+ },
+ );
+ my %status;
+ if (defined $param{status}) {
+ %status = %{$param{status}};
+ }
+ else {
+ my $location = getbuglocation($param{bug}, 'summary');
+ return {} if not length $location;
+ %status = %{ readbug( $param{bug}, $location ) };
+ }
+
+ my @sourceversions;
+ my $pseudo_desc = getpseudodesc();
+ if (not exists $param{sourceversions}) {
+ my %sourceversions;
+ # pseudopackages do not have source versions by definition.
+ if (exists $pseudo_desc->{$status{package}}) {
+ # do nothing.
+ }
+ elsif (defined $param{version}) {
+ foreach my $arch (make_list($param{arch})) {
+ for my $package (split /\s*,\s*/, $status{package}) {
+ my @temp = makesourceversions($package,
+ $arch,
+ make_list($param{version})
+ );
+ @sourceversions{@temp} = (1) x @temp;
+ }
+ }
+ } elsif (defined $param{dist}) {
+ my %affects_distribution_tags;
+ @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
+ (1) x @{$config{affects_distribution_tags}};
+ my $some_distributions_disallowed = 0;
+ my %allowed_distributions;
+ for my $tag (split ' ', ($status{keywords}||'')) {
+ if (exists $config{distribution_aliases}{$tag} and
+ exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
+ $some_distributions_disallowed = 1;
+ $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
+ }
+ elsif (exists $affects_distribution_tags{$tag}) {
+ $some_distributions_disallowed = 1;
+ $allowed_distributions{$tag} = 1;
+ }
+ }
+ my @archs = make_list(exists $param{arch}?$param{arch}:());
+ GET_SOURCE_VERSIONS:
+ foreach my $arch (@archs) {
+ for my $package (split /\s*,\s*/, $status{package}) {
+ my @versions = ();
+ my $source = 0;
+ if ($package =~ /^src:(.+)$/) {
+ $source = 1;
+ $package = $1;
+ }
+ foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
+ # if some distributions are disallowed,
+ # and this isn't an allowed
+ # distribution, then we ignore this
+ # distribution for the purposees of
+ # finding versions
+ if ($some_distributions_disallowed and
+ not exists $allowed_distributions{$dist}) {
+ next;
+ }
+ push @versions, get_versions(package => $package,
+ dist => $dist,
+ ($source?(arch => 'source'):
+ (defined $arch?(arch => $arch):())),
+ );
+ }
+ next unless @versions;
+ my @temp = make_source_versions(package => $package,
+ arch => $arch,
+ versions => \@versions,
+ );
+ @sourceversions{@temp} = (1) x @temp;
+ }
+ }
+ # this should really be split out into a subroutine,
+ # but it'd touch so many things currently, that we fake
+ # it; it's needed to properly handle bugs which are
+ # erroneously assigned to the binary package, and we'll
+ # probably have it go away eventually.
+ if (not keys %sourceversions and (not @archs or defined $archs[0])) {
+ @archs = (undef);
+ goto GET_SOURCE_VERSIONS;
+ }
+ }
+
+ # TODO: This should probably be handled further out for efficiency and
+ # for more ease of distinguishing between pkg= and src= queries.
+ # DLA: src= queries should just pass arch=source, and they'll be happy.
+ @sourceversions = keys %sourceversions;
+ }
+ else {
+ @sourceversions = @{$param{sourceversions}};
+ }
+ my $maxbuggy = 'undef';
+ if (@sourceversions) {
+ $maxbuggy = max_buggy(bug => $param{bug},
+ sourceversions => \@sourceversions,
+ found => $status{found_versions},
+ fixed => $status{fixed_versions},
+ package => $status{package},
+ version_cache => $version_cache,
+ );
+ }
+ elsif (defined $param{dist} and
+ not exists $pseudo_desc->{$status{package}}) {
+ return 'absent';
+ }
+ if (length($status{done}) and
+ (not @sourceversions or not @{$status{fixed_versions}})) {
+ return 'fixed';
+ }
+ return $maxbuggy;
+}
+
+
+=head2 max_buggy
+
+ max_buggy()
+
+=head3 Options
+
+=over
+
+=item bug -- scalar bug number
+
+=item sourceversion -- optional arrayref of source/version; overrides
+dist, arch, and version. [The entries in this array must be in the
+"source/version" format.] Eventually this can be used to for caching.
+
+=back
+
+Note: Currently the version information is cached; this needs to be
+changed before using this function in long lived programs.
+
+
+=cut
+sub max_buggy{
+ my %param = validate_with(params => \@_,
+ spec => {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ sourceversions => {type => ARRAYREF,
+ default => [],
+ },
+ found => {type => ARRAYREF,
+ default => [],
+ },
+ fixed => {type => ARRAYREF,
+ default => [],
+ },
+ package => {type => SCALAR,
+ },
+ version_cache => {type => HASHREF,
+ default => {},
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ },
+ );
+ # Resolve bugginess states (we might be looking at multiple
+ # architectures, say). Found wins, then fixed, then absent.
+ my $maxbuggy = 'absent';
+ for my $package (split /\s*,\s*/, $param{package}) {
+ for my $version (@{$param{sourceversions}}) {
+ my $buggy = buggy(bug => $param{bug},
+ version => $version,
+ found => $param{found},
+ fixed => $param{fixed},
+ version_cache => $param{version_cache},
+ package => $package,
+ );
+ if ($buggy eq 'found') {
+ return 'found';
+ } elsif ($buggy eq 'fixed') {
+ $maxbuggy = 'fixed';
+ }
+ }
+ }
+ return $maxbuggy;
+}
+
+
+=head2 buggy
+
+ buggy(bug => nnn,
+ found => \@found,
+ fixed => \@fixed,
+ package => 'foo',
+ version => '1.0',
+ );
+
+Returns the output of Debbugs::Versions::buggy for a particular
+package, version and found/fixed set. Automatically turns found, fixed
+and version into source/version strings.
+
+Caching can be had by using the version_cache, but no attempt to check
+to see if the on disk information is more recent than the cache is
+made. [This will need to be fixed for long-lived processes.]
+
+=cut
+
+sub buggy {