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{bug_num} = $param{bug};
259 lockreadbug($bug_num,$location)
261 Performs a filelock, then reads the bug; the bug is unlocked if the
262 return is undefined, otherwise, you need to call unfilelock or
265 See readbug above for information on what this returns
270 my ($lref, $location) = @_;
271 return read_bug(bug => $lref, location => $location, lock => 1);
274 =head2 lockreadbugmerge
276 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
278 Performs a filelock, then reads the bug. If the bug is merged, locks
279 the merge lock. Returns a list of the number of locks and the bug
284 sub lockreadbugmerge {
285 my ($bug_num,$location) = @_;
286 my $data = lockreadbug(@_);
287 if (not defined $data) {
290 if (not length $data->{mergedwith}) {
294 filelock("$config{spool_dir}/lock/merge");
295 $data = lockreadbug(@_);
296 if (not defined $data) {
303 =head2 lock_read_all_merged_bugs
305 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
307 Performs a filelock, then reads the bug passed. If the bug is merged,
308 locks the merge lock, then reads and locks all of the other merged
309 bugs. Returns a list of the number of locks and the bug data for all
312 Will also return undef if any of the merged bugs failed to be read,
313 even if all of the others were read properly.
317 sub lock_read_all_merged_bugs {
318 my ($bug_num,$location) = @_;
319 my @data = (lockreadbug(@_));
320 if (not @data and not defined $data[0]) {
323 if (not length $data[0]->{mergedwith}) {
327 filelock("$config{spool_dir}/lock/merge");
329 @data = (lockreadbug(@_));
330 if (not @data and not defined $data[0]) {
331 unfilelock(); #for merge lock above
335 my @bugs = split / /, $data[0]->{mergedwith};
336 for my $bug (@bugs) {
338 if ($bug ne $bug_num) {
339 $newdata = lockreadbug($bug,$location);
340 if (not defined $newdata) {
345 warn "Unable to read bug: $bug while handling merged bug: $bug_num";
346 return ($locks,undef);
351 # perform a sanity check to make sure that the merged bugs are
352 # all merged with eachother
353 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
354 if ($newdata->{mergedwith} ne $expectmerge) {
358 die "Bug $bug_num differs from bug $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
365 my @v1fieldorder = qw(originator date subject msgid package
366 keywords done forwarded mergedwith severity);
370 my $content = makestatus($status,$version)
371 my $content = makestatus($status);
373 Creates the content for a status file based on the $status hashref
376 Really only useful for writebug
378 Currently defaults to version 2 (non-encoded rfc1522 names) but will
379 eventually default to version 3. If you care, you should specify a
385 my ($data,$version) = @_;
386 $version = 2 unless defined $version;
390 my %newdata = %$data;
391 for my $field (qw(found fixed)) {
392 if (exists $newdata{$field}) {
393 $newdata{"${field}_date"} =
394 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
398 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
399 $newdata{$field} = join ' ', @{$newdata{$field}||[]};
403 for my $field (@rfc1522_fields) {
404 $newdata{$field} = encode_rfc1522($newdata{$field});
409 for my $field (@v1fieldorder) {
410 if (exists $newdata{$field} and defined $newdata{$field}) {
411 $contents .= "$newdata{$field}\n";
416 } elsif ($version == 2 or $version == 3) {
417 # Version 2 or 3. Add a file format version number for the sake of
418 # further extensibility in the future.
419 $contents .= "Format-Version: $version\n";
420 for my $field (keys %fields) {
421 if (exists $newdata{$field} and defined $newdata{$field}
422 and $newdata{$field} ne '') {
423 # Output field names in proper case, e.g. 'Merged-With'.
424 my $properfield = $fields{$field};
425 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
426 $contents .= "$properfield: $newdata{$field}\n";
436 writebug($bug_num,$status,$location,$minversion,$disablebughook)
438 Writes the bug status and summary files out.
440 Skips writting out a status file if minversion is 2
442 Does not call bughook if disablebughook is true.
447 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
450 my %outputs = (1 => 'status', 2 => 'summary');
451 for my $version (keys %outputs) {
452 next if defined $minversion and $version < $minversion;
453 my $status = getbugcomponent($ref, $outputs{$version}, $location);
454 die "can't find location for $ref" unless defined $status;
455 open(S,"> $status.new") || die "opening $status.new: $!";
456 print(S makestatus($data, $version)) ||
457 die "writing $status.new: $!";
458 close(S) || die "closing $status.new: $!";
464 rename("$status.new",$status) || die "installing new $status: $!";
467 # $disablebughook is a bit of a hack to let format migration scripts use
468 # this function rather than having to duplicate it themselves.
469 &bughook($change,$ref,$data) unless $disablebughook;
472 =head2 unlockwritebug
474 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
476 Writes a bug, then calls unfilelock; see writebug for what these
488 The following functions are exported with the :versions tag
490 =head2 addfoundversions
492 addfoundversions($status,$package,$version,$isbinary);
499 sub addfoundversions {
503 my $isbinary = shift;
504 return unless defined $version;
505 undef $package if $package =~ m[(?:\s|/)];
506 my $source = $package;
508 if (defined $package and $isbinary) {
509 my @srcinfo = binarytosource($package, $version, undef);
511 # We know the source package(s). Use a fully-qualified version.
512 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
515 # Otherwise, an unqualified version will have to do.
519 # Strip off various kinds of brain-damage.
521 $version =~ s/ *\(.*\)//;
522 $version =~ s/ +[A-Za-z].*//;
524 foreach my $ver (split /[,\s]+/, $version) {
525 my $sver = defined($source) ? "$source/$ver" : '';
526 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
527 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
529 @{$data->{fixed_versions}} =
530 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
534 =head2 removefoundversions
536 removefoundversions($data,$package,$versiontoremove)
538 Removes found versions from $data
540 If a version is fully qualified (contains /) only versions matching
541 exactly are removed. Otherwise, all versions matching the version
544 Currently $package and $isbinary are entirely ignored, but accepted
545 for backwards compatibilty.
549 sub removefoundversions {
553 my $isbinary = shift;
554 return unless defined $version;
556 foreach my $ver (split /[,\s]+/, $version) {
558 # fully qualified version
559 @{$data->{found_versions}} =
561 @{$data->{found_versions}};
564 # non qualified version; delete all matchers
565 @{$data->{found_versions}} =
566 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
567 @{$data->{found_versions}};
573 sub addfixedversions {
577 my $isbinary = shift;
578 return unless defined $version;
579 undef $package if defined $package and $package =~ m[(?:\s|/)];
580 my $source = $package;
582 if (defined $package and $isbinary) {
583 my @srcinfo = binarytosource($package, $version, undef);
585 # We know the source package(s). Use a fully-qualified version.
586 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
589 # Otherwise, an unqualified version will have to do.
593 # Strip off various kinds of brain-damage.
595 $version =~ s/ *\(.*\)//;
596 $version =~ s/ +[A-Za-z].*//;
598 foreach my $ver (split /[,\s]+/, $version) {
599 my $sver = defined($source) ? "$source/$ver" : '';
600 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
601 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
603 @{$data->{found_versions}} =
604 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
608 sub removefixedversions {
612 my $isbinary = shift;
613 return unless defined $version;
615 foreach my $ver (split /[,\s]+/, $version) {
617 # fully qualified version
618 @{$data->{fixed_versions}} =
620 @{$data->{fixed_versions}};
623 # non qualified version; delete all matchers
624 @{$data->{fixed_versions}} =
625 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
626 @{$data->{fixed_versions}};
637 Split a package string from the status file into a list of package names.
643 return unless defined $pkgs;
644 return map lc, split /[ \t?,()]+/, $pkgs;
648 =head2 bug_archiveable
650 bug_archiveable(bug => $bug_num);
656 =item bug -- bug number (required)
658 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
660 =item version -- Debbugs::Version information (optional)
662 =item days_until -- return days until the bug can be archived
666 Returns 1 if the bug can be archived
667 Returns 0 if the bug cannot be archived
669 If days_until is true, returns the number of days until the bug can be
670 archived, -1 if it cannot be archived. 0 means that the bug can be
671 archived the next time the archiver runs.
673 Returns undef on failure.
677 # This will eventually need to be fixed before we start using mod_perl
678 our $version_cache = {};
680 my %param = validate_with(params => \@_,
681 spec => {bug => {type => SCALAR,
684 status => {type => HASHREF,
687 days_until => {type => BOOLEAN,
690 ignore_time => {type => BOOLEAN,
695 # This is what we return if the bug cannot be archived.
696 my $cannot_archive = $param{days_until}?-1:0;
697 # read the status information
698 my $status = $param{status};
699 if (not exists $param{status} or not defined $status) {
700 $status = read_bug(bug=>$param{bug});
701 if (not defined $status) {
702 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
706 # Bugs can be archived if they are
708 if (not defined $status->{done} or not length $status->{done}) {
709 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
710 return $cannot_archive
712 # Check to make sure that the bug has none of the unremovable tags set
713 if (@{$config{removal_unremovable_tags}}) {
714 for my $tag (split ' ', ($status->{tags}||'')) {
715 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
716 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
717 return $cannot_archive;
722 # If we just are checking if the bug can be archived, we'll not even bother
723 # checking the versioning information if the bug has been -done for less than 28 days.
724 my $log_file = getbugcomponent($param{bug},'log');
725 if (not defined $log_file) {
726 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
727 return $cannot_archive;
729 my $max_log_age = max(map {$config{remove_age} - -M $_}
730 $log_file, map {my $log = getbugcomponent($_,'log');
731 defined $log ? ($log) : ();
733 split / /, $status->{mergedwith}
735 if (not $param{days_until} and not $param{ignore_time}
738 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
739 return $cannot_archive;
741 # At this point, we have to get the versioning information for this bug.
742 # We examine the set of distribution tags. If a bug has no distribution
743 # tags set, we assume a default set, otherwise we use the tags the bug
746 # In cases where we are assuming a default set, if the severity
747 # is strong, we use the strong severity default; otherwise, we
748 # use the normal default.
750 # There must be fixed_versions for us to look at the versioning
752 my $min_fixed_time = time;
753 my $min_archive_days = 0;
754 if (@{$status->{fixed_versions}}) {
756 @dist_tags{@{$config{removal_distribution_tags}}} =
757 (1) x @{$config{removal_distribution_tags}};
759 for my $tag (split ' ', ($status->{tags}||'')) {
760 next unless exists $config{distribution_aliases}{$tag};
761 next unless $dist_tags{$config{distribution_aliases}{$tag}};
762 $dists{$config{distribution_aliases}{$tag}} = 1;
764 if (not keys %dists) {
765 if (isstrongseverity($status->{severity})) {
766 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
767 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
770 @dists{@{$config{removal_default_distribution_tags}}} =
771 (1) x @{$config{removal_default_distribution_tags}};
775 my @sourceversions = get_versions(package => $status->{package},
776 dist => [keys %dists],
779 @source_versions{@sourceversions} = (1) x @sourceversions;
780 # If the bug has not been fixed in the versions actually
781 # distributed, then it cannot be archived.
782 if ('found' eq max_buggy(bug => $param{bug},
783 sourceversions => [keys %source_versions],
784 found => $status->{found_versions},
785 fixed => $status->{fixed_versions},
786 version_cache => $version_cache,
787 package => $status->{package},
789 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
790 return $cannot_archive;
792 # Since the bug has at least been fixed in the architectures
793 # that matters, we check to see how long it has been fixed.
795 # If $param{ignore_time}, then we should ignore time.
796 if ($param{ignore_time}) {
797 return $param{days_until}?0:1;
800 # To do this, we order the times from most recent to oldest;
801 # when we come to the first found version, we stop.
802 # If we run out of versions, we only report the time of the
804 my %time_versions = get_versions(package => $status->{package},
805 dist => [keys %dists],
809 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
810 my $buggy = buggy(bug => $param{bug},
812 found => $status->{found_versions},
813 fixed => $status->{fixed_versions},
814 version_cache => $version_cache,
815 package => $status->{package},
817 last if $buggy eq 'found';
818 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
820 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
821 # if there are no versions in the archive at all, then
822 # we can archive if enough days have passed
825 # If $param{ignore_time}, then we should ignore time.
826 if ($param{ignore_time}) {
827 return $param{days_until}?0:1;
829 # 6. at least 28 days have passed since the last action has occured or the bug was closed
830 my $age = ceil($max_log_age);
831 if ($age > 0 or $min_archive_days > 0) {
832 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
833 return $param{days_until}?max($age,$min_archive_days):0;
836 return $param{days_until}?0:1;
841 =head2 get_bug_status
843 my $status = get_bug_status(bug => $nnn);
845 my $status = get_bug_status($bug_num)
851 =item bug -- scalar bug number
853 =item status -- optional hashref of bug status as returned by readbug
854 (can be passed to avoid rereading the bug information)
856 =item bug_index -- optional tied index of bug status infomration;
857 currently not correctly implemented.
859 =item version -- optional version(s) to check package status at
861 =item dist -- optional distribution(s) to check package status at
863 =item arch -- optional architecture(s) to check package status at
865 =item bugusertags -- optional hashref of bugusertags
867 =item sourceversion -- optional arrayref of source/version; overrides
868 dist, arch, and version. [The entries in this array must be in the
869 "source/version" format.] Eventually this can be used to for caching.
871 =item indicatesource -- if true, indicate which source packages this
872 bug could belong to. Defaults to false. [Note that eventually we will
873 properly allow bugs that only affect a source package, and this will
878 Note: Currently the version information is cached; this needs to be
879 changed before using this function in long lived programs.
887 my %param = validate_with(params => \@_,
888 spec => {bug => {type => SCALAR,
891 status => {type => HASHREF,
894 bug_index => {type => OBJECT,
897 version => {type => SCALAR|ARRAYREF,
900 dist => {type => SCALAR|ARRAYREF,
903 arch => {type => SCALAR|ARRAYREF,
906 bugusertags => {type => HASHREF,
909 sourceversions => {type => ARRAYREF,
912 indicatesource => {type => BOOLEAN,
919 if (defined $param{bug_index} and
920 exists $param{bug_index}{$param{bug}}) {
921 %status = %{ $param{bug_index}{$param{bug}} };
922 $status{pending} = $status{ status };
923 $status{id} = $param{bug};
926 if (defined $param{status}) {
927 %status = %{$param{status}};
930 my $location = getbuglocation($param{bug}, 'summary');
931 return {} if not defined $location or not length $location;
932 %status = %{ readbug( $param{bug}, $location ) };
934 $status{id} = $param{bug};
936 if (defined $param{bugusertags}{$param{bug}}) {
937 $status{keywords} = "" unless defined $status{keywords};
938 $status{keywords} .= " " unless $status{keywords} eq "";
939 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
941 $status{tags} = $status{keywords};
942 my %tags = map { $_ => 1 } split ' ', $status{tags};
944 $status{"package"} =~ s/\s*$//;
945 if ($param{indicatesource} and $status{package} ne '') {
946 $status{source} = join(', ',binarytosource($status{package}));
949 $status{source} = 'unknown';
951 $status{"package"} = 'unknown' if ($status{"package"} eq '');
952 $status{"severity"} = 'normal' if ($status{"severity"} eq '');
954 $status{"pending"} = 'pending';
955 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
956 $status{"pending"} = 'pending-fixed' if ($tags{pending});
957 $status{"pending"} = 'fixed' if ($tags{fixed});
960 my $presence = bug_presence(map{(exists $param{$_})?($_,$param{$_}):()}
961 qw(bug sourceversions arch dist version found fixed package)
963 if (defined $presence) {
964 if ($presence eq 'fixed') {
965 $status{pending} = 'done';
967 elsif ($presence eq 'absent') {
968 $status{pending} = 'absent';
976 my $precence = bug_presence(bug => nnn,
980 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
981 is found, absent, fixed, or no information is available in the
982 distribution (dist) and/or architecture (arch) specified.
989 =item bug -- scalar bug number
991 =item status -- optional hashref of bug status as returned by readbug
992 (can be passed to avoid rereading the bug information)
994 =item bug_index -- optional tied index of bug status infomration;
995 currently not correctly implemented.
997 =item version -- optional version to check package status at
999 =item dist -- optional distribution to check package status at
1001 =item arch -- optional architecture to check package status at
1003 =item sourceversion -- optional arrayref of source/version; overrides
1004 dist, arch, and version. [The entries in this array must be in the
1005 "source/version" format.] Eventually this can be used to for caching.
1012 my %param = validate_with(params => \@_,
1013 spec => {bug => {type => SCALAR,
1016 status => {type => HASHREF,
1019 version => {type => SCALAR|ARRAYREF,
1022 dist => {type => SCALAR|ARRAYREF,
1025 arch => {type => SCALAR|ARRAYREF,
1028 sourceversions => {type => ARRAYREF,
1034 if (defined $param{status}) {
1035 %status = %{$param{status}};
1038 my $location = getbuglocation($param{bug}, 'summary');
1039 return {} if not length $location;
1040 %status = %{ readbug( $param{bug}, $location ) };
1044 my $pseudo_desc = getpseudodesc();
1045 if (not exists $param{sourceversions}) {
1047 # pseudopackages do not have source versions by definition.
1048 if (exists $pseudo_desc->{$status{package}}) {
1051 elsif (defined $param{version}) {
1052 foreach my $arch (make_list($param{arch})) {
1053 for my $package (split /\s*,\s*/, $status{package}) {
1054 my @temp = makesourceversions($package,
1056 make_list($param{version})
1058 @sourceversions{@temp} = (1) x @temp;
1061 } elsif (defined $param{dist}) {
1062 foreach my $arch (make_list($param{arch})) {
1064 for my $package (split /\s*,\s*/, $status{package}) {
1065 foreach my $dist (make_list($param{dist})) {
1066 push @versions, getversions($package, $dist, $arch);
1068 my @temp = makesourceversions($package,
1072 @sourceversions{@temp} = (1) x @temp;
1077 # TODO: This should probably be handled further out for efficiency and
1078 # for more ease of distinguishing between pkg= and src= queries.
1079 # DLA: src= queries should just pass arch=source, and they'll be happy.
1080 @sourceversions = keys %sourceversions;
1083 @sourceversions = @{$param{sourceversions}};
1085 my $maxbuggy = 'undef';
1086 if (@sourceversions) {
1087 $maxbuggy = max_buggy(bug => $param{bug},
1088 sourceversions => \@sourceversions,
1089 found => $status{found_versions},
1090 fixed => $status{fixed_versions},
1091 package => $status{package},
1092 version_cache => $version_cache,
1095 elsif (defined $param{dist} and
1096 not exists $pseudo_desc->{$status{package}}) {
1099 if (length($status{done}) and
1100 (not @sourceversions or not @{$status{fixed_versions}})) {
1115 =item bug -- scalar bug number
1117 =item sourceversion -- optional arrayref of source/version; overrides
1118 dist, arch, and version. [The entries in this array must be in the
1119 "source/version" format.] Eventually this can be used to for caching.
1123 Note: Currently the version information is cached; this needs to be
1124 changed before using this function in long lived programs.
1129 my %param = validate_with(params => \@_,
1130 spec => {bug => {type => SCALAR,
1133 sourceversions => {type => ARRAYREF,
1136 found => {type => ARRAYREF,
1139 fixed => {type => ARRAYREF,
1142 package => {type => SCALAR,
1144 version_cache => {type => HASHREF,
1149 # Resolve bugginess states (we might be looking at multiple
1150 # architectures, say). Found wins, then fixed, then absent.
1151 my $maxbuggy = 'absent';
1152 for my $package (split /\s*,\s*/, $param{package}) {
1153 for my $version (@{$param{sourceversions}}) {
1154 my $buggy = buggy(bug => $param{bug},
1155 version => $version,
1156 found => $param{found},
1157 fixed => $param{fixed},
1158 version_cache => $param{version_cache},
1159 package => $package,
1161 if ($buggy eq 'found') {
1163 } elsif ($buggy eq 'fixed') {
1164 $maxbuggy = 'fixed';
1181 Returns the output of Debbugs::Versions::buggy for a particular
1182 package, version and found/fixed set. Automatically turns found, fixed
1183 and version into source/version strings.
1185 Caching can be had by using the version_cache, but no attempt to check
1186 to see if the on disk information is more recent than the cache is
1187 made. [This will need to be fixed for long-lived processes.]
1192 my %param = validate_with(params => \@_,
1193 spec => {bug => {type => SCALAR,
1196 found => {type => ARRAYREF,
1199 fixed => {type => ARRAYREF,
1202 version_cache => {type => HASHREF,
1205 package => {type => SCALAR,
1207 version => {type => SCALAR,
1211 my @found = @{$param{found}};
1212 my @fixed = @{$param{fixed}};
1213 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1214 # We have non-source version versions
1215 @found = makesourceversions($param{package},undef,
1218 @fixed = makesourceversions($param{package},undef,
1222 if ($param{version} !~ m{/}) {
1223 my ($version) = makesourceversions($param{package},undef,
1226 $param{version} = $version if defined $version;
1228 # Figure out which source packages we need
1230 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1231 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1232 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1233 $param{version} =~ m{/};
1235 if (not defined $param{version_cache} or
1236 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1237 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1238 foreach my $source (keys %sources) {
1239 my $srchash = substr $source, 0, 1;
1240 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1241 if (not defined $version_fh) {
1242 # We only want to warn if it's a package which actually has a maintainer
1243 my $maints = getmaintainers();
1244 next if not exists $maints->{$source};
1245 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1248 $version->load($version_fh);
1250 if (defined $param{version_cache}) {
1251 $param{version_cache}{join(',',sort keys %sources)} = $version;
1255 $version = $param{version_cache}{join(',',sort keys %sources)};
1257 return $version->buggy($param{version},\@found,\@fixed);
1260 sub isstrongseverity {
1261 my $severity = shift;
1262 $severity = $config{default_severity} if
1263 not defined $severity or $severity eq '';
1264 return grep { $_ eq $severity } @{$config{strong_severities}};
1268 =head1 PRIVATE FUNCTIONS
1272 sub update_realtime {
1273 my ($file, %bugs) = @_;
1275 # update realtime index.db
1277 return () unless keys %bugs;
1278 my $idx_old = IO::File->new($file,'r')
1279 or die "Couldn't open ${file}: $!";
1280 my $idx_new = IO::File->new($file.'.new','w')
1281 or die "Couldn't open ${file}.new: $!";
1283 my $min_bug = min(keys %bugs);
1287 while($line = <$idx_old>) {
1288 @line = split /\s/, $line;
1289 # Two cases; replacing existing line or adding new line
1290 if (exists $bugs{$line[1]}) {
1291 my $new = $bugs{$line[1]};
1292 delete $bugs{$line[1]};
1293 $min_bug = min(keys %bugs);
1294 if ($new eq "NOCHANGE") {
1295 print {$idx_new} $line;
1296 $changed_bugs{$line[1]} = $line;
1297 } elsif ($new eq "REMOVE") {
1298 $changed_bugs{$line[1]} = $line;
1300 print {$idx_new} $new;
1301 $changed_bugs{$line[1]} = $line;
1305 while ($line[1] > $min_bug) {
1306 print {$idx_new} $bugs{$min_bug};
1307 delete $bugs{$min_bug};
1308 last unless keys %bugs;
1309 $min_bug = min(keys %bugs);
1311 print {$idx_new} $line;
1313 last unless keys %bugs;
1315 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1317 print {$idx_new} <$idx_old>;
1322 rename("$file.new", $file);
1324 return %changed_bugs;
1327 sub bughook_archive {
1329 &filelock("$config{spool_dir}/debbugs.trace.lock");
1330 &appendfile("debbugs.trace","archive ".join(',',@refs)."\n");
1331 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1332 map{($_,'REMOVE')} @refs);
1333 update_realtime("$config{spool_dir}/index.archive.realtime",
1339 my ( $type, %bugs_temp ) = @_;
1340 &filelock("$config{spool_dir}/debbugs.trace.lock");
1343 for my $bug (keys %bugs_temp) {
1344 my $data = $bugs_temp{$bug};
1345 &appendfile("debbugs.trace","$type $bug\n",makestatus($data, 1));
1347 my $whendone = "open";
1348 my $severity = $config{default_severity};
1349 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1350 $pkglist =~ s/^,+//;
1351 $pkglist =~ s/,+$//;
1352 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1353 $whendone = "done" if defined $data->{done} and length $data->{done};
1354 $severity = $data->{severity} if length $data->{severity};
1356 my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1357 $pkglist, $bug, $data->{date}, $whendone,
1358 $data->{originator}, $severity, $data->{keywords};
1361 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);