1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # [Other people have contributed to this file; their copyrights should
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
10 package Debbugs::Status;
14 Debbugs::Status -- Routines for dealing with summary and status files
23 This module is a replacement for the parts of errorlib.pl which write
24 and read status and summary files.
26 It also contains generic routines for returning information about the
27 status of a particular bug
36 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
37 use base qw(Exporter);
39 use Params::Validate qw(validate_with :types);
40 use Debbugs::Common qw(:util :lock :quit :misc);
41 use Debbugs::Config qw(:config);
42 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
43 use Debbugs::Packages qw(makesourceversions getversions get_versions binarytosource);
44 use Debbugs::Versions;
45 use Debbugs::Versions::Dpkg;
48 use List::Util qw(min max);
53 $DEBUG = 0 unless defined $DEBUG;
56 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
57 qw(isstrongseverity bug_presence),
59 read => [qw(readbug read_bug lockreadbug lockreadbugmerge)],
60 write => [qw(writebug makestatus unlockwritebug)],
61 versions => [qw(addfoundversions addfixedversions),
62 qw(removefoundversions removefixedversions)
64 hook => [qw(bughook bughook_archive)],
67 Exporter::export_ok_tags(qw(status read write versions hook));
68 $EXPORT_TAGS{all} = [@EXPORT_OK];
74 readbug($bug_num,$location)
77 Reads a summary file from the archive given a bug number and a bug
78 location. Valid locations are those understood by L</getbugcomponent>
83 my %fields = (originator => 'submitter',
86 msgid => 'message-id',
87 'package' => 'package',
90 forwarded => 'forwarded-to',
91 mergedwith => 'merged-with',
92 severity => 'severity',
94 found_versions => 'found-in',
95 found_date => 'found-date',
96 fixed_versions => 'fixed-in',
97 fixed_date => 'fixed-date',
99 blockedby => 'blocked-by',
100 unarchived => 'unarchived',
103 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
104 my @rfc1522_fields = qw(originator subject done forwarded owner);
107 return read_bug(bug => $_[0],
108 (@_ > 1)?(location => $_[1]):()
114 read_bug(bug => $bug_num,
115 location => 'archive',
117 read_bug(summary => 'path/to/bugnum.summary');
120 A more complete function than readbug; it enables you to pass a full
121 path to the summary file instead of the bug number and/or location.
127 =item bug -- the bug number
129 =item location -- optional location which is passed to getbugcomponent
131 =item summary -- complete path to the .summary file which will be read
133 =item lock -- whether to obtain a lock for the bug to prevent
134 something modifying it while the bug has been read. You B<must> call
135 C<unfilelock();> if something not undef is returned from read_bug.
139 One of C<bug> or C<summary> must be passed. This function will return
140 undef on failure, and will die if improper arguments are passed.
148 my %param = validate_with(params => \@_,
149 spec => {bug => {type => SCALAR,
153 # negative bugnumbers
156 location => {type => SCALAR|UNDEF,
159 summary => {type => SCALAR,
162 lock => {type => BOOLEAN,
167 die "One of bug or summary must be passed to read_bug"
168 if not exists $param{bug} and not exists $param{summary};
172 if (not defined $param{summary}) {
174 ($lref,$location) = @param{qw(bug location)};
175 if (not defined $location) {
176 $location = getbuglocation($lref,'summary');
177 return undef if not defined $location;
179 $status = getbugcomponent($lref, 'summary', $location);
180 $log = getbugcomponent($lref, 'log' , $location);
181 return undef unless defined $status;
184 $status = $param{summary};
186 $log =~ s/\.summary$/.log/;
187 ($location) = $status =~ m/(db-h|db|archive)/;
190 filelock("$config{spool_dir}/lock/$param{bug}");
192 my $status_fh = IO::File->new($status, 'r');
193 if (not defined $status_fh) {
194 warn "Unable to open $status for reading: $!";
206 while (<$status_fh>) {
209 $version = $1 if /^Format-Version: ([0-9]+)/i;
212 # Version 3 is the latest format version currently supported.
214 warn "Unsupported status version '$version'";
221 my %namemap = reverse %fields;
222 for my $line (@lines) {
223 if ($line =~ /(\S+?): (.*)/) {
224 my ($name, $value) = (lc $1, $2);
225 $data{$namemap{$name}} = $value if exists $namemap{$name};
228 for my $field (keys %fields) {
229 $data{$field} = '' unless exists $data{$field};
232 $data{severity} = $config{default_severity} if $data{severity} eq '';
233 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
234 $data{$field} = [split ' ', $data{$field}];
236 for my $field (qw(found fixed)) {
237 # create the found/fixed hashes which indicate when a
238 # particular version was marked found or marked fixed.
239 @{$data{$field}}{@{$data{"${field}_versions"}}} =
240 (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
241 @{$data{"${field}_date"}});
245 for my $field (@rfc1522_fields) {
246 $data{$field} = decode_rfc1522($data{$field});
249 # Add log last modified time
250 $data{log_modified} = (stat($log))[9];
251 $data{location} = $location;
252 $data{archived} = $location eq 'archive';
253 $data{bug_num} = $param{bug};
260 lockreadbug($bug_num,$location)
262 Performs a filelock, then reads the bug; the bug is unlocked if the
263 return is undefined, otherwise, you need to call unfilelock or
266 See readbug above for information on what this returns
271 my ($lref, $location) = @_;
272 return read_bug(bug => $lref, location => $location, lock => 1);
275 =head2 lockreadbugmerge
277 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
279 Performs a filelock, then reads the bug. If the bug is merged, locks
280 the merge lock. Returns a list of the number of locks and the bug
285 sub lockreadbugmerge {
286 my ($bug_num,$location) = @_;
287 my $data = lockreadbug(@_);
288 if (not defined $data) {
291 if (not length $data->{mergedwith}) {
295 filelock("$config{spool_dir}/lock/merge");
296 $data = lockreadbug(@_);
297 if (not defined $data) {
304 =head2 lock_read_all_merged_bugs
306 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
308 Performs a filelock, then reads the bug passed. If the bug is merged,
309 locks the merge lock, then reads and locks all of the other merged
310 bugs. Returns a list of the number of locks and the bug data for all
313 Will also return undef if any of the merged bugs failed to be read,
314 even if all of the others were read properly.
318 sub lock_read_all_merged_bugs {
319 my ($bug_num,$location) = @_;
320 my @data = (lockreadbug(@_));
321 if (not @data and not defined $data[0]) {
324 if (not length $data[0]->{mergedwith}) {
328 filelock("$config{spool_dir}/lock/merge");
330 @data = (lockreadbug(@_));
331 if (not @data and not defined $data[0]) {
332 unfilelock(); #for merge lock above
336 my @bugs = split / /, $data[0]->{mergedwith};
337 for my $bug (@bugs) {
339 if ($bug ne $bug_num) {
340 $newdata = lockreadbug($bug,$location);
341 if (not defined $newdata) {
346 warn "Unable to read bug: $bug while handling merged bug: $bug_num";
347 return ($locks,undef);
352 # perform a sanity check to make sure that the merged bugs are
353 # all merged with eachother
354 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
355 if ($newdata->{mergedwith} ne $expectmerge) {
359 die "Bug $bug_num differs from bug $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
366 my @v1fieldorder = qw(originator date subject msgid package
367 keywords done forwarded mergedwith severity);
371 my $content = makestatus($status,$version)
372 my $content = makestatus($status);
374 Creates the content for a status file based on the $status hashref
377 Really only useful for writebug
379 Currently defaults to version 2 (non-encoded rfc1522 names) but will
380 eventually default to version 3. If you care, you should specify a
386 my ($data,$version) = @_;
387 $version = 2 unless defined $version;
391 my %newdata = %$data;
392 for my $field (qw(found fixed)) {
393 if (exists $newdata{$field}) {
394 $newdata{"${field}_date"} =
395 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
399 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
400 $newdata{$field} = join ' ', @{$newdata{$field}||[]};
404 for my $field (@rfc1522_fields) {
405 $newdata{$field} = encode_rfc1522($newdata{$field});
410 for my $field (@v1fieldorder) {
411 if (exists $newdata{$field} and defined $newdata{$field}) {
412 $contents .= "$newdata{$field}\n";
417 } elsif ($version == 2 or $version == 3) {
418 # Version 2 or 3. Add a file format version number for the sake of
419 # further extensibility in the future.
420 $contents .= "Format-Version: $version\n";
421 for my $field (keys %fields) {
422 if (exists $newdata{$field} and defined $newdata{$field}
423 and $newdata{$field} ne '') {
424 # Output field names in proper case, e.g. 'Merged-With'.
425 my $properfield = $fields{$field};
426 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
427 $contents .= "$properfield: $newdata{$field}\n";
437 writebug($bug_num,$status,$location,$minversion,$disablebughook)
439 Writes the bug status and summary files out.
441 Skips writting out a status file if minversion is 2
443 Does not call bughook if disablebughook is true.
448 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
451 my %outputs = (1 => 'status', 2 => 'summary');
452 for my $version (keys %outputs) {
453 next if defined $minversion and $version < $minversion;
454 my $status = getbugcomponent($ref, $outputs{$version}, $location);
455 die "can't find location for $ref" unless defined $status;
456 open(S,"> $status.new") || die "opening $status.new: $!";
457 print(S makestatus($data, $version)) ||
458 die "writing $status.new: $!";
459 close(S) || die "closing $status.new: $!";
465 rename("$status.new",$status) || die "installing new $status: $!";
468 # $disablebughook is a bit of a hack to let format migration scripts use
469 # this function rather than having to duplicate it themselves.
470 &bughook($change,$ref,$data) unless $disablebughook;
473 =head2 unlockwritebug
475 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
477 Writes a bug, then calls unfilelock; see writebug for what these
489 The following functions are exported with the :versions tag
491 =head2 addfoundversions
493 addfoundversions($status,$package,$version,$isbinary);
500 sub addfoundversions {
504 my $isbinary = shift;
505 return unless defined $version;
506 undef $package if $package =~ m[(?:\s|/)];
507 my $source = $package;
509 if (defined $package and $isbinary) {
510 my @srcinfo = binarytosource($package, $version, undef);
512 # We know the source package(s). Use a fully-qualified version.
513 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
516 # Otherwise, an unqualified version will have to do.
520 # Strip off various kinds of brain-damage.
522 $version =~ s/ *\(.*\)//;
523 $version =~ s/ +[A-Za-z].*//;
525 foreach my $ver (split /[,\s]+/, $version) {
526 my $sver = defined($source) ? "$source/$ver" : '';
527 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
528 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
530 @{$data->{fixed_versions}} =
531 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
535 =head2 removefoundversions
537 removefoundversions($data,$package,$versiontoremove)
539 Removes found versions from $data
541 If a version is fully qualified (contains /) only versions matching
542 exactly are removed. Otherwise, all versions matching the version
545 Currently $package and $isbinary are entirely ignored, but accepted
546 for backwards compatibilty.
550 sub removefoundversions {
554 my $isbinary = shift;
555 return unless defined $version;
557 foreach my $ver (split /[,\s]+/, $version) {
559 # fully qualified version
560 @{$data->{found_versions}} =
562 @{$data->{found_versions}};
565 # non qualified version; delete all matchers
566 @{$data->{found_versions}} =
567 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
568 @{$data->{found_versions}};
574 sub addfixedversions {
578 my $isbinary = shift;
579 return unless defined $version;
580 undef $package if defined $package and $package =~ m[(?:\s|/)];
581 my $source = $package;
583 if (defined $package and $isbinary) {
584 my @srcinfo = binarytosource($package, $version, undef);
586 # We know the source package(s). Use a fully-qualified version.
587 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
590 # Otherwise, an unqualified version will have to do.
594 # Strip off various kinds of brain-damage.
596 $version =~ s/ *\(.*\)//;
597 $version =~ s/ +[A-Za-z].*//;
599 foreach my $ver (split /[,\s]+/, $version) {
600 my $sver = defined($source) ? "$source/$ver" : '';
601 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
602 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
604 @{$data->{found_versions}} =
605 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
609 sub removefixedversions {
613 my $isbinary = shift;
614 return unless defined $version;
616 foreach my $ver (split /[,\s]+/, $version) {
618 # fully qualified version
619 @{$data->{fixed_versions}} =
621 @{$data->{fixed_versions}};
624 # non qualified version; delete all matchers
625 @{$data->{fixed_versions}} =
626 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
627 @{$data->{fixed_versions}};
638 Split a package string from the status file into a list of package names.
644 return unless defined $pkgs;
645 return map lc, split /[ \t?,()]+/, $pkgs;
649 =head2 bug_archiveable
651 bug_archiveable(bug => $bug_num);
657 =item bug -- bug number (required)
659 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
661 =item version -- Debbugs::Version information (optional)
663 =item days_until -- return days until the bug can be archived
667 Returns 1 if the bug can be archived
668 Returns 0 if the bug cannot be archived
670 If days_until is true, returns the number of days until the bug can be
671 archived, -1 if it cannot be archived. 0 means that the bug can be
672 archived the next time the archiver runs.
674 Returns undef on failure.
678 # This will eventually need to be fixed before we start using mod_perl
679 our $version_cache = {};
681 my %param = validate_with(params => \@_,
682 spec => {bug => {type => SCALAR,
685 status => {type => HASHREF,
688 days_until => {type => BOOLEAN,
691 ignore_time => {type => BOOLEAN,
696 # This is what we return if the bug cannot be archived.
697 my $cannot_archive = $param{days_until}?-1:0;
698 # read the status information
699 my $status = $param{status};
700 if (not exists $param{status} or not defined $status) {
701 $status = read_bug(bug=>$param{bug});
702 if (not defined $status) {
703 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
707 # Bugs can be archived if they are
709 if (not defined $status->{done} or not length $status->{done}) {
710 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
711 return $cannot_archive
713 # Check to make sure that the bug has none of the unremovable tags set
714 if (@{$config{removal_unremovable_tags}}) {
715 for my $tag (split ' ', ($status->{tags}||'')) {
716 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
717 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
718 return $cannot_archive;
723 # If we just are checking if the bug can be archived, we'll not even bother
724 # checking the versioning information if the bug has been -done for less than 28 days.
725 my $log_file = getbugcomponent($param{bug},'log');
726 if (not defined $log_file) {
727 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
728 return $cannot_archive;
730 my $max_log_age = max(map {$config{remove_age} - -M $_}
731 $log_file, map {my $log = getbugcomponent($_,'log');
732 defined $log ? ($log) : ();
734 split / /, $status->{mergedwith}
736 if (not $param{days_until} and not $param{ignore_time}
739 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
740 return $cannot_archive;
742 # At this point, we have to get the versioning information for this bug.
743 # We examine the set of distribution tags. If a bug has no distribution
744 # tags set, we assume a default set, otherwise we use the tags the bug
747 # In cases where we are assuming a default set, if the severity
748 # is strong, we use the strong severity default; otherwise, we
749 # use the normal default.
751 # There must be fixed_versions for us to look at the versioning
753 my $min_fixed_time = time;
754 my $min_archive_days = 0;
755 if (@{$status->{fixed_versions}}) {
757 @dist_tags{@{$config{removal_distribution_tags}}} =
758 (1) x @{$config{removal_distribution_tags}};
760 for my $tag (split ' ', ($status->{tags}||'')) {
761 next unless exists $config{distribution_aliases}{$tag};
762 next unless $dist_tags{$config{distribution_aliases}{$tag}};
763 $dists{$config{distribution_aliases}{$tag}} = 1;
765 if (not keys %dists) {
766 if (isstrongseverity($status->{severity})) {
767 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
768 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
771 @dists{@{$config{removal_default_distribution_tags}}} =
772 (1) x @{$config{removal_default_distribution_tags}};
776 my @sourceversions = get_versions(package => $status->{package},
777 dist => [keys %dists],
780 @source_versions{@sourceversions} = (1) x @sourceversions;
781 # If the bug has not been fixed in the versions actually
782 # distributed, then it cannot be archived.
783 if ('found' eq max_buggy(bug => $param{bug},
784 sourceversions => [keys %source_versions],
785 found => $status->{found_versions},
786 fixed => $status->{fixed_versions},
787 version_cache => $version_cache,
788 package => $status->{package},
790 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
791 return $cannot_archive;
793 # Since the bug has at least been fixed in the architectures
794 # that matters, we check to see how long it has been fixed.
796 # If $param{ignore_time}, then we should ignore time.
797 if ($param{ignore_time}) {
798 return $param{days_until}?0:1;
801 # To do this, we order the times from most recent to oldest;
802 # when we come to the first found version, we stop.
803 # If we run out of versions, we only report the time of the
805 my %time_versions = get_versions(package => $status->{package},
806 dist => [keys %dists],
810 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
811 my $buggy = buggy(bug => $param{bug},
813 found => $status->{found_versions},
814 fixed => $status->{fixed_versions},
815 version_cache => $version_cache,
816 package => $status->{package},
818 last if $buggy eq 'found';
819 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
821 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
822 # if there are no versions in the archive at all, then
823 # we can archive if enough days have passed
826 # If $param{ignore_time}, then we should ignore time.
827 if ($param{ignore_time}) {
828 return $param{days_until}?0:1;
830 # 6. at least 28 days have passed since the last action has occured or the bug was closed
831 my $age = ceil($max_log_age);
832 if ($age > 0 or $min_archive_days > 0) {
833 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
834 return $param{days_until}?max($age,$min_archive_days):0;
837 return $param{days_until}?0:1;
842 =head2 get_bug_status
844 my $status = get_bug_status(bug => $nnn);
846 my $status = get_bug_status($bug_num)
852 =item bug -- scalar bug number
854 =item status -- optional hashref of bug status as returned by readbug
855 (can be passed to avoid rereading the bug information)
857 =item bug_index -- optional tied index of bug status infomration;
858 currently not correctly implemented.
860 =item version -- optional version(s) to check package status at
862 =item dist -- optional distribution(s) to check package status at
864 =item arch -- optional architecture(s) to check package status at
866 =item bugusertags -- optional hashref of bugusertags
868 =item sourceversion -- optional arrayref of source/version; overrides
869 dist, arch, and version. [The entries in this array must be in the
870 "source/version" format.] Eventually this can be used to for caching.
872 =item indicatesource -- if true, indicate which source packages this
873 bug could belong to. Defaults to false. [Note that eventually we will
874 properly allow bugs that only affect a source package, and this will
879 Note: Currently the version information is cached; this needs to be
880 changed before using this function in long lived programs.
888 my %param = validate_with(params => \@_,
889 spec => {bug => {type => SCALAR,
892 status => {type => HASHREF,
895 bug_index => {type => OBJECT,
898 version => {type => SCALAR|ARRAYREF,
901 dist => {type => SCALAR|ARRAYREF,
904 arch => {type => SCALAR|ARRAYREF,
907 bugusertags => {type => HASHREF,
910 sourceversions => {type => ARRAYREF,
913 indicatesource => {type => BOOLEAN,
920 if (defined $param{bug_index} and
921 exists $param{bug_index}{$param{bug}}) {
922 %status = %{ $param{bug_index}{$param{bug}} };
923 $status{pending} = $status{ status };
924 $status{id} = $param{bug};
927 if (defined $param{status}) {
928 %status = %{$param{status}};
931 my $location = getbuglocation($param{bug}, 'summary');
932 return {} if not defined $location or not length $location;
933 %status = %{ readbug( $param{bug}, $location ) };
935 $status{id} = $param{bug};
937 if (defined $param{bugusertags}{$param{bug}}) {
938 $status{keywords} = "" unless defined $status{keywords};
939 $status{keywords} .= " " unless $status{keywords} eq "";
940 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
942 $status{tags} = $status{keywords};
943 my %tags = map { $_ => 1 } split ' ', $status{tags};
945 $status{"package"} =~ s/\s*$//;
946 if ($param{indicatesource} and $status{package} ne '') {
947 $status{source} = join(', ',binarytosource($status{package}));
950 $status{source} = 'unknown';
952 $status{"package"} = 'unknown' if ($status{"package"} eq '');
953 $status{"severity"} = 'normal' if ($status{"severity"} eq '');
955 $status{"pending"} = 'pending';
956 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
957 $status{"pending"} = 'pending-fixed' if ($tags{pending});
958 $status{"pending"} = 'fixed' if ($tags{fixed});
961 my $presence = bug_presence(status => \%status,
962 map{(exists $param{$_})?($_,$param{$_}):()}
963 qw(bug sourceversions arch dist version found fixed package)
965 if (defined $presence) {
966 if ($presence eq 'fixed') {
967 $status{pending} = 'done';
969 elsif ($presence eq 'absent') {
970 $status{pending} = 'absent';
978 my $precence = bug_presence(bug => nnn,
982 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
983 is found, absent, fixed, or no information is available in the
984 distribution (dist) and/or architecture (arch) specified.
991 =item bug -- scalar bug number
993 =item status -- optional hashref of bug status as returned by readbug
994 (can be passed to avoid rereading the bug information)
996 =item bug_index -- optional tied index of bug status infomration;
997 currently not correctly implemented.
999 =item version -- optional version to check package status at
1001 =item dist -- optional distribution to check package status at
1003 =item arch -- optional architecture to check package status at
1005 =item sourceversion -- optional arrayref of source/version; overrides
1006 dist, arch, and version. [The entries in this array must be in the
1007 "source/version" format.] Eventually this can be used to for caching.
1014 my %param = validate_with(params => \@_,
1015 spec => {bug => {type => SCALAR,
1018 status => {type => HASHREF,
1021 version => {type => SCALAR|ARRAYREF,
1024 dist => {type => SCALAR|ARRAYREF,
1027 arch => {type => SCALAR|ARRAYREF,
1030 sourceversions => {type => ARRAYREF,
1036 if (defined $param{status}) {
1037 %status = %{$param{status}};
1040 my $location = getbuglocation($param{bug}, 'summary');
1041 return {} if not length $location;
1042 %status = %{ readbug( $param{bug}, $location ) };
1046 my $pseudo_desc = getpseudodesc();
1047 if (not exists $param{sourceversions}) {
1049 # pseudopackages do not have source versions by definition.
1050 if (exists $pseudo_desc->{$status{package}}) {
1053 elsif (defined $param{version}) {
1054 foreach my $arch (make_list($param{arch})) {
1055 for my $package (split /\s*,\s*/, $status{package}) {
1056 my @temp = makesourceversions($package,
1058 make_list($param{version})
1060 @sourceversions{@temp} = (1) x @temp;
1063 } elsif (defined $param{dist}) {
1064 foreach my $arch (make_list($param{arch})) {
1066 for my $package (split /\s*,\s*/, $status{package}) {
1067 foreach my $dist (make_list($param{dist})) {
1068 push @versions, getversions($package, $dist, $arch);
1070 my @temp = makesourceversions($package,
1074 @sourceversions{@temp} = (1) x @temp;
1079 # TODO: This should probably be handled further out for efficiency and
1080 # for more ease of distinguishing between pkg= and src= queries.
1081 # DLA: src= queries should just pass arch=source, and they'll be happy.
1082 @sourceversions = keys %sourceversions;
1085 @sourceversions = @{$param{sourceversions}};
1087 my $maxbuggy = 'undef';
1088 if (@sourceversions) {
1089 $maxbuggy = max_buggy(bug => $param{bug},
1090 sourceversions => \@sourceversions,
1091 found => $status{found_versions},
1092 fixed => $status{fixed_versions},
1093 package => $status{package},
1094 version_cache => $version_cache,
1097 elsif (defined $param{dist} and
1098 not exists $pseudo_desc->{$status{package}}) {
1101 if (length($status{done}) and
1102 (not @sourceversions or not @{$status{fixed_versions}})) {
1117 =item bug -- scalar bug number
1119 =item sourceversion -- optional arrayref of source/version; overrides
1120 dist, arch, and version. [The entries in this array must be in the
1121 "source/version" format.] Eventually this can be used to for caching.
1125 Note: Currently the version information is cached; this needs to be
1126 changed before using this function in long lived programs.
1131 my %param = validate_with(params => \@_,
1132 spec => {bug => {type => SCALAR,
1135 sourceversions => {type => ARRAYREF,
1138 found => {type => ARRAYREF,
1141 fixed => {type => ARRAYREF,
1144 package => {type => SCALAR,
1146 version_cache => {type => HASHREF,
1151 # Resolve bugginess states (we might be looking at multiple
1152 # architectures, say). Found wins, then fixed, then absent.
1153 my $maxbuggy = 'absent';
1154 for my $package (split /\s*,\s*/, $param{package}) {
1155 for my $version (@{$param{sourceversions}}) {
1156 my $buggy = buggy(bug => $param{bug},
1157 version => $version,
1158 found => $param{found},
1159 fixed => $param{fixed},
1160 version_cache => $param{version_cache},
1161 package => $package,
1163 if ($buggy eq 'found') {
1165 } elsif ($buggy eq 'fixed') {
1166 $maxbuggy = 'fixed';
1183 Returns the output of Debbugs::Versions::buggy for a particular
1184 package, version and found/fixed set. Automatically turns found, fixed
1185 and version into source/version strings.
1187 Caching can be had by using the version_cache, but no attempt to check
1188 to see if the on disk information is more recent than the cache is
1189 made. [This will need to be fixed for long-lived processes.]
1194 my %param = validate_with(params => \@_,
1195 spec => {bug => {type => SCALAR,
1198 found => {type => ARRAYREF,
1201 fixed => {type => ARRAYREF,
1204 version_cache => {type => HASHREF,
1207 package => {type => SCALAR,
1209 version => {type => SCALAR,
1213 my @found = @{$param{found}};
1214 my @fixed = @{$param{fixed}};
1215 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1216 # We have non-source version versions
1217 @found = makesourceversions($param{package},undef,
1220 @fixed = makesourceversions($param{package},undef,
1224 if ($param{version} !~ m{/}) {
1225 my ($version) = makesourceversions($param{package},undef,
1228 $param{version} = $version if defined $version;
1230 # Figure out which source packages we need
1232 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1233 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1234 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1235 $param{version} =~ m{/};
1237 if (not defined $param{version_cache} or
1238 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1239 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1240 foreach my $source (keys %sources) {
1241 my $srchash = substr $source, 0, 1;
1242 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1243 if (not defined $version_fh) {
1244 # We only want to warn if it's a package which actually has a maintainer
1245 my $maints = getmaintainers();
1246 next if not exists $maints->{$source};
1247 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1250 $version->load($version_fh);
1252 if (defined $param{version_cache}) {
1253 $param{version_cache}{join(',',sort keys %sources)} = $version;
1257 $version = $param{version_cache}{join(',',sort keys %sources)};
1259 return $version->buggy($param{version},\@found,\@fixed);
1262 sub isstrongseverity {
1263 my $severity = shift;
1264 $severity = $config{default_severity} if
1265 not defined $severity or $severity eq '';
1266 return grep { $_ eq $severity } @{$config{strong_severities}};
1270 =head1 PRIVATE FUNCTIONS
1274 sub update_realtime {
1275 my ($file, %bugs) = @_;
1277 # update realtime index.db
1279 return () unless keys %bugs;
1280 my $idx_old = IO::File->new($file,'r')
1281 or die "Couldn't open ${file}: $!";
1282 my $idx_new = IO::File->new($file.'.new','w')
1283 or die "Couldn't open ${file}.new: $!";
1285 my $min_bug = min(keys %bugs);
1289 while($line = <$idx_old>) {
1290 @line = split /\s/, $line;
1291 # Two cases; replacing existing line or adding new line
1292 if (exists $bugs{$line[1]}) {
1293 my $new = $bugs{$line[1]};
1294 delete $bugs{$line[1]};
1295 $min_bug = min(keys %bugs);
1296 if ($new eq "NOCHANGE") {
1297 print {$idx_new} $line;
1298 $changed_bugs{$line[1]} = $line;
1299 } elsif ($new eq "REMOVE") {
1300 $changed_bugs{$line[1]} = $line;
1302 print {$idx_new} $new;
1303 $changed_bugs{$line[1]} = $line;
1307 while ($line[1] > $min_bug) {
1308 print {$idx_new} $bugs{$min_bug};
1309 delete $bugs{$min_bug};
1310 last unless keys %bugs;
1311 $min_bug = min(keys %bugs);
1313 print {$idx_new} $line;
1315 last unless keys %bugs;
1317 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1319 print {$idx_new} <$idx_old>;
1324 rename("$file.new", $file);
1326 return %changed_bugs;
1329 sub bughook_archive {
1331 &filelock("$config{spool_dir}/debbugs.trace.lock");
1332 &appendfile("debbugs.trace","archive ".join(',',@refs)."\n");
1333 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1334 map{($_,'REMOVE')} @refs);
1335 update_realtime("$config{spool_dir}/index.archive.realtime",
1341 my ( $type, %bugs_temp ) = @_;
1342 &filelock("$config{spool_dir}/debbugs.trace.lock");
1345 for my $bug (keys %bugs_temp) {
1346 my $data = $bugs_temp{$bug};
1347 &appendfile("debbugs.trace","$type $bug\n",makestatus($data, 1));
1349 my $whendone = "open";
1350 my $severity = $config{default_severity};
1351 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1352 $pkglist =~ s/^,+//;
1353 $pkglist =~ s/,+$//;
1354 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1355 $whendone = "done" if defined $data->{done} and length $data->{done};
1356 $severity = $data->{severity} if length $data->{severity};
1358 my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1359 $pkglist, $bug, $data->{date}, $whendone,
1360 $data->{originator}, $severity, $data->{keywords};
1363 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);