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 qw(lock_read_all_merged_bugs),
62 write => [qw(writebug makestatus unlockwritebug)],
63 versions => [qw(addfoundversions addfixedversions),
64 qw(removefoundversions removefixedversions)
66 hook => [qw(bughook bughook_archive)],
69 Exporter::export_ok_tags(qw(status read write versions hook));
70 $EXPORT_TAGS{all} = [@EXPORT_OK];
76 readbug($bug_num,$location)
79 Reads a summary file from the archive given a bug number and a bug
80 location. Valid locations are those understood by L</getbugcomponent>
85 my %fields = (originator => 'submitter',
88 msgid => 'message-id',
89 'package' => 'package',
92 forwarded => 'forwarded-to',
93 mergedwith => 'merged-with',
94 severity => 'severity',
96 found_versions => 'found-in',
97 found_date => 'found-date',
98 fixed_versions => 'fixed-in',
99 fixed_date => 'fixed-date',
101 blockedby => 'blocked-by',
102 unarchived => 'unarchived',
103 summary => 'summary',
104 affects => 'affects',
107 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
108 my @rfc1522_fields = qw(originator subject done forwarded owner);
111 return read_bug(bug => $_[0],
112 (@_ > 1)?(location => $_[1]):()
118 read_bug(bug => $bug_num,
119 location => 'archive',
121 read_bug(summary => 'path/to/bugnum.summary');
124 A more complete function than readbug; it enables you to pass a full
125 path to the summary file instead of the bug number and/or location.
131 =item bug -- the bug number
133 =item location -- optional location which is passed to getbugcomponent
135 =item summary -- complete path to the .summary file which will be read
137 =item lock -- whether to obtain a lock for the bug to prevent
138 something modifying it while the bug has been read. You B<must> call
139 C<unfilelock();> if something not undef is returned from read_bug.
143 One of C<bug> or C<summary> must be passed. This function will return
144 undef on failure, and will die if improper arguments are passed.
152 my %param = validate_with(params => \@_,
153 spec => {bug => {type => SCALAR,
157 # negative bugnumbers
160 location => {type => SCALAR|UNDEF,
163 summary => {type => SCALAR,
166 lock => {type => BOOLEAN,
171 die "One of bug or summary must be passed to read_bug"
172 if not exists $param{bug} and not exists $param{summary};
176 if (not defined $param{summary}) {
178 ($lref,$location) = @param{qw(bug location)};
179 if (not defined $location) {
180 $location = getbuglocation($lref,'summary');
181 return undef if not defined $location;
183 $status = getbugcomponent($lref, 'summary', $location);
184 $log = getbugcomponent($lref, 'log' , $location);
185 return undef unless defined $status;
188 $status = $param{summary};
190 $log =~ s/\.summary$/.log/;
191 ($location) = $status =~ m/(db-h|db|archive)/;
194 filelock("$config{spool_dir}/lock/$param{bug}");
196 my $status_fh = IO::File->new($status, 'r');
197 if (not defined $status_fh) {
198 warn "Unable to open $status for reading: $!";
210 while (<$status_fh>) {
213 $version = $1 if /^Format-Version: ([0-9]+)/i;
216 # Version 3 is the latest format version currently supported.
218 warn "Unsupported status version '$version'";
225 my %namemap = reverse %fields;
226 for my $line (@lines) {
227 if ($line =~ /(\S+?): (.*)/) {
228 my ($name, $value) = (lc $1, $2);
229 $data{$namemap{$name}} = $value if exists $namemap{$name};
232 for my $field (keys %fields) {
233 $data{$field} = '' unless exists $data{$field};
236 $data{severity} = $config{default_severity} if $data{severity} eq '';
237 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
238 $data{$field} = [split ' ', $data{$field}];
240 for my $field (qw(found fixed)) {
241 # create the found/fixed hashes which indicate when a
242 # particular version was marked found or marked fixed.
243 @{$data{$field}}{@{$data{"${field}_versions"}}} =
244 (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
245 @{$data{"${field}_date"}});
249 for my $field (@rfc1522_fields) {
250 $data{$field} = decode_rfc1522($data{$field});
253 # Add log last modified time
254 $data{log_modified} = (stat($log))[9];
255 $data{location} = $location;
256 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
257 $data{bug_num} = $param{bug};
264 lockreadbug($bug_num,$location)
266 Performs a filelock, then reads the bug; the bug is unlocked if the
267 return is undefined, otherwise, you need to call unfilelock or
270 See readbug above for information on what this returns
275 my ($lref, $location) = @_;
276 return read_bug(bug => $lref, location => $location, lock => 1);
279 =head2 lockreadbugmerge
281 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
283 Performs a filelock, then reads the bug. If the bug is merged, locks
284 the merge lock. Returns a list of the number of locks and the bug
289 sub lockreadbugmerge {
290 my ($bug_num,$location) = @_;
291 my $data = lockreadbug(@_);
292 if (not defined $data) {
295 if (not length $data->{mergedwith}) {
299 filelock("$config{spool_dir}/lock/merge");
300 $data = lockreadbug(@_);
301 if (not defined $data) {
308 =head2 lock_read_all_merged_bugs
310 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
312 Performs a filelock, then reads the bug passed. If the bug is merged,
313 locks the merge lock, then reads and locks all of the other merged
314 bugs. Returns a list of the number of locks and the bug data for all
317 Will also return undef if any of the merged bugs failed to be read,
318 even if all of the others were read properly.
322 sub lock_read_all_merged_bugs {
323 my ($bug_num,$location) = @_;
324 my @data = (lockreadbug(@_));
325 if (not @data and not defined $data[0]) {
328 if (not length $data[0]->{mergedwith}) {
332 filelock("$config{spool_dir}/lock/merge");
334 @data = (lockreadbug(@_));
335 if (not @data and not defined $data[0]) {
336 unfilelock(); #for merge lock above
340 my @bugs = split / /, $data[0]->{mergedwith};
341 for my $bug (@bugs) {
343 if ($bug ne $bug_num) {
344 $newdata = lockreadbug($bug,$location);
345 if (not defined $newdata) {
350 warn "Unable to read bug: $bug while handling merged bug: $bug_num";
351 return ($locks,undef);
356 # perform a sanity check to make sure that the merged bugs are
357 # all merged with eachother
358 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
359 if ($newdata->{mergedwith} ne $expectmerge) {
363 die "Bug $bug_num differs from bug $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
370 my @v1fieldorder = qw(originator date subject msgid package
371 keywords done forwarded mergedwith severity);
375 my $content = makestatus($status,$version)
376 my $content = makestatus($status);
378 Creates the content for a status file based on the $status hashref
381 Really only useful for writebug
383 Currently defaults to version 2 (non-encoded rfc1522 names) but will
384 eventually default to version 3. If you care, you should specify a
390 my ($data,$version) = @_;
391 $version = 2 unless defined $version;
395 my %newdata = %$data;
396 for my $field (qw(found fixed)) {
397 if (exists $newdata{$field}) {
398 $newdata{"${field}_date"} =
399 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
403 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
404 $newdata{$field} = join ' ', @{$newdata{$field}||[]};
408 for my $field (@rfc1522_fields) {
409 $newdata{$field} = encode_rfc1522($newdata{$field});
414 for my $field (@v1fieldorder) {
415 if (exists $newdata{$field} and defined $newdata{$field}) {
416 $contents .= "$newdata{$field}\n";
421 } elsif ($version == 2 or $version == 3) {
422 # Version 2 or 3. Add a file format version number for the sake of
423 # further extensibility in the future.
424 $contents .= "Format-Version: $version\n";
425 for my $field (keys %fields) {
426 if (exists $newdata{$field} and defined $newdata{$field}
427 and $newdata{$field} ne '') {
428 # Output field names in proper case, e.g. 'Merged-With'.
429 my $properfield = $fields{$field};
430 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
431 $contents .= "$properfield: $newdata{$field}\n";
441 writebug($bug_num,$status,$location,$minversion,$disablebughook)
443 Writes the bug status and summary files out.
445 Skips writting out a status file if minversion is 2
447 Does not call bughook if disablebughook is true.
452 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
455 my %outputs = (1 => 'status', 2 => 'summary');
456 for my $version (keys %outputs) {
457 next if defined $minversion and $version < $minversion;
458 my $status = getbugcomponent($ref, $outputs{$version}, $location);
459 die "can't find location for $ref" unless defined $status;
460 open(S,"> $status.new") || die "opening $status.new: $!";
461 print(S makestatus($data, $version)) ||
462 die "writing $status.new: $!";
463 close(S) || die "closing $status.new: $!";
469 rename("$status.new",$status) || die "installing new $status: $!";
472 # $disablebughook is a bit of a hack to let format migration scripts use
473 # this function rather than having to duplicate it themselves.
474 &bughook($change,$ref,$data) unless $disablebughook;
477 =head2 unlockwritebug
479 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
481 Writes a bug, then calls unfilelock; see writebug for what these
493 The following functions are exported with the :versions tag
495 =head2 addfoundversions
497 addfoundversions($status,$package,$version,$isbinary);
504 sub addfoundversions {
508 my $isbinary = shift;
509 return unless defined $version;
510 undef $package if $package =~ m[(?:\s|/)];
511 my $source = $package;
513 if (defined $package and $isbinary) {
514 my @srcinfo = binarytosource($package, $version, undef);
516 # We know the source package(s). Use a fully-qualified version.
517 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
520 # Otherwise, an unqualified version will have to do.
524 # Strip off various kinds of brain-damage.
526 $version =~ s/ *\(.*\)//;
527 $version =~ s/ +[A-Za-z].*//;
529 foreach my $ver (split /[,\s]+/, $version) {
530 my $sver = defined($source) ? "$source/$ver" : '';
531 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
532 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
534 @{$data->{fixed_versions}} =
535 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
539 =head2 removefoundversions
541 removefoundversions($data,$package,$versiontoremove)
543 Removes found versions from $data
545 If a version is fully qualified (contains /) only versions matching
546 exactly are removed. Otherwise, all versions matching the version
549 Currently $package and $isbinary are entirely ignored, but accepted
550 for backwards compatibilty.
554 sub removefoundversions {
558 my $isbinary = shift;
559 return unless defined $version;
561 foreach my $ver (split /[,\s]+/, $version) {
563 # fully qualified version
564 @{$data->{found_versions}} =
566 @{$data->{found_versions}};
569 # non qualified version; delete all matchers
570 @{$data->{found_versions}} =
571 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
572 @{$data->{found_versions}};
578 sub addfixedversions {
582 my $isbinary = shift;
583 return unless defined $version;
584 undef $package if defined $package and $package =~ m[(?:\s|/)];
585 my $source = $package;
587 if (defined $package and $isbinary) {
588 my @srcinfo = binarytosource($package, $version, undef);
590 # We know the source package(s). Use a fully-qualified version.
591 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
594 # Otherwise, an unqualified version will have to do.
598 # Strip off various kinds of brain-damage.
600 $version =~ s/ *\(.*\)//;
601 $version =~ s/ +[A-Za-z].*//;
603 foreach my $ver (split /[,\s]+/, $version) {
604 my $sver = defined($source) ? "$source/$ver" : '';
605 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
606 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
608 @{$data->{found_versions}} =
609 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
613 sub removefixedversions {
617 my $isbinary = shift;
618 return unless defined $version;
620 foreach my $ver (split /[,\s]+/, $version) {
622 # fully qualified version
623 @{$data->{fixed_versions}} =
625 @{$data->{fixed_versions}};
628 # non qualified version; delete all matchers
629 @{$data->{fixed_versions}} =
630 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
631 @{$data->{fixed_versions}};
642 Split a package string from the status file into a list of package names.
648 return unless defined $pkgs;
649 return map lc, split /[ \t?,()]+/, $pkgs;
653 =head2 bug_archiveable
655 bug_archiveable(bug => $bug_num);
661 =item bug -- bug number (required)
663 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
665 =item version -- Debbugs::Version information (optional)
667 =item days_until -- return days until the bug can be archived
671 Returns 1 if the bug can be archived
672 Returns 0 if the bug cannot be archived
674 If days_until is true, returns the number of days until the bug can be
675 archived, -1 if it cannot be archived. 0 means that the bug can be
676 archived the next time the archiver runs.
678 Returns undef on failure.
682 # This will eventually need to be fixed before we start using mod_perl
683 our $version_cache = {};
685 my %param = validate_with(params => \@_,
686 spec => {bug => {type => SCALAR,
689 status => {type => HASHREF,
692 days_until => {type => BOOLEAN,
695 ignore_time => {type => BOOLEAN,
700 # This is what we return if the bug cannot be archived.
701 my $cannot_archive = $param{days_until}?-1:0;
702 # read the status information
703 my $status = $param{status};
704 if (not exists $param{status} or not defined $status) {
705 $status = read_bug(bug=>$param{bug});
706 if (not defined $status) {
707 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
711 # Bugs can be archived if they are
713 if (not defined $status->{done} or not length $status->{done}) {
714 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
715 return $cannot_archive
717 # Check to make sure that the bug has none of the unremovable tags set
718 if (@{$config{removal_unremovable_tags}}) {
719 for my $tag (split ' ', ($status->{tags}||'')) {
720 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
721 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
722 return $cannot_archive;
727 # If we just are checking if the bug can be archived, we'll not even bother
728 # checking the versioning information if the bug has been -done for less than 28 days.
729 my $log_file = getbugcomponent($param{bug},'log');
730 if (not defined $log_file) {
731 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
732 return $cannot_archive;
734 my $max_log_age = max(map {$config{remove_age} - -M $_}
735 $log_file, map {my $log = getbugcomponent($_,'log');
736 defined $log ? ($log) : ();
738 split / /, $status->{mergedwith}
740 if (not $param{days_until} and not $param{ignore_time}
743 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
744 return $cannot_archive;
746 # At this point, we have to get the versioning information for this bug.
747 # We examine the set of distribution tags. If a bug has no distribution
748 # tags set, we assume a default set, otherwise we use the tags the bug
751 # In cases where we are assuming a default set, if the severity
752 # is strong, we use the strong severity default; otherwise, we
753 # use the normal default.
755 # There must be fixed_versions for us to look at the versioning
757 my $min_fixed_time = time;
758 my $min_archive_days = 0;
759 if (@{$status->{fixed_versions}}) {
761 @dist_tags{@{$config{removal_distribution_tags}}} =
762 (1) x @{$config{removal_distribution_tags}};
764 for my $tag (split ' ', ($status->{tags}||'')) {
765 next unless exists $config{distribution_aliases}{$tag};
766 next unless $dist_tags{$config{distribution_aliases}{$tag}};
767 $dists{$config{distribution_aliases}{$tag}} = 1;
769 if (not keys %dists) {
770 if (isstrongseverity($status->{severity})) {
771 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
772 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
775 @dists{@{$config{removal_default_distribution_tags}}} =
776 (1) x @{$config{removal_default_distribution_tags}};
780 my @sourceversions = get_versions(package => $status->{package},
781 dist => [keys %dists],
784 @source_versions{@sourceversions} = (1) x @sourceversions;
785 # If the bug has not been fixed in the versions actually
786 # distributed, then it cannot be archived.
787 if ('found' eq max_buggy(bug => $param{bug},
788 sourceversions => [keys %source_versions],
789 found => $status->{found_versions},
790 fixed => $status->{fixed_versions},
791 version_cache => $version_cache,
792 package => $status->{package},
794 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
795 return $cannot_archive;
797 # Since the bug has at least been fixed in the architectures
798 # that matters, we check to see how long it has been fixed.
800 # If $param{ignore_time}, then we should ignore time.
801 if ($param{ignore_time}) {
802 return $param{days_until}?0:1;
805 # To do this, we order the times from most recent to oldest;
806 # when we come to the first found version, we stop.
807 # If we run out of versions, we only report the time of the
809 my %time_versions = get_versions(package => $status->{package},
810 dist => [keys %dists],
814 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
815 my $buggy = buggy(bug => $param{bug},
817 found => $status->{found_versions},
818 fixed => $status->{fixed_versions},
819 version_cache => $version_cache,
820 package => $status->{package},
822 last if $buggy eq 'found';
823 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
825 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
826 # if there are no versions in the archive at all, then
827 # we can archive if enough days have passed
830 # If $param{ignore_time}, then we should ignore time.
831 if ($param{ignore_time}) {
832 return $param{days_until}?0:1;
834 # 6. at least 28 days have passed since the last action has occured or the bug was closed
835 my $age = ceil($max_log_age);
836 if ($age > 0 or $min_archive_days > 0) {
837 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
838 return $param{days_until}?max($age,$min_archive_days):0;
841 return $param{days_until}?0:1;
846 =head2 get_bug_status
848 my $status = get_bug_status(bug => $nnn);
850 my $status = get_bug_status($bug_num)
856 =item bug -- scalar bug number
858 =item status -- optional hashref of bug status as returned by readbug
859 (can be passed to avoid rereading the bug information)
861 =item bug_index -- optional tied index of bug status infomration;
862 currently not correctly implemented.
864 =item version -- optional version(s) to check package status at
866 =item dist -- optional distribution(s) to check package status at
868 =item arch -- optional architecture(s) to check package status at
870 =item bugusertags -- optional hashref of bugusertags
872 =item sourceversion -- optional arrayref of source/version; overrides
873 dist, arch, and version. [The entries in this array must be in the
874 "source/version" format.] Eventually this can be used to for caching.
876 =item indicatesource -- if true, indicate which source packages this
877 bug could belong to. Defaults to false. [Note that eventually we will
878 properly allow bugs that only affect a source package, and this will
883 Note: Currently the version information is cached; this needs to be
884 changed before using this function in long lived programs.
892 my %param = validate_with(params => \@_,
893 spec => {bug => {type => SCALAR,
896 status => {type => HASHREF,
899 bug_index => {type => OBJECT,
902 version => {type => SCALAR|ARRAYREF,
905 dist => {type => SCALAR|ARRAYREF,
908 arch => {type => SCALAR|ARRAYREF,
911 bugusertags => {type => HASHREF,
914 sourceversions => {type => ARRAYREF,
917 indicatesource => {type => BOOLEAN,
924 if (defined $param{bug_index} and
925 exists $param{bug_index}{$param{bug}}) {
926 %status = %{ $param{bug_index}{$param{bug}} };
927 $status{pending} = $status{ status };
928 $status{id} = $param{bug};
931 if (defined $param{status}) {
932 %status = %{$param{status}};
935 my $location = getbuglocation($param{bug}, 'summary');
936 return {} if not defined $location or not length $location;
937 %status = %{ readbug( $param{bug}, $location ) };
939 $status{id} = $param{bug};
941 if (defined $param{bugusertags}{$param{bug}}) {
942 $status{keywords} = "" unless defined $status{keywords};
943 $status{keywords} .= " " unless $status{keywords} eq "";
944 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
946 $status{tags} = $status{keywords};
947 my %tags = map { $_ => 1 } split ' ', $status{tags};
949 $status{"package"} =~ s/\s*$//;
950 if ($param{indicatesource} and $status{package} ne '') {
951 $status{source} = join(', ',binarytosource($status{package}));
954 $status{source} = 'unknown';
956 $status{"package"} = 'unknown' if ($status{"package"} eq '');
957 $status{"severity"} = 'normal' if ($status{"severity"} eq '');
959 $status{"pending"} = 'pending';
960 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
961 $status{"pending"} = 'pending-fixed' if ($tags{pending});
962 $status{"pending"} = 'fixed' if ($tags{fixed});
965 my $presence = bug_presence(status => \%status,
966 map{(exists $param{$_})?($_,$param{$_}):()}
967 qw(bug sourceversions arch dist version found fixed package)
969 if (defined $presence) {
970 if ($presence eq 'fixed') {
971 $status{pending} = 'done';
973 elsif ($presence eq 'absent') {
974 $status{pending} = 'absent';
982 my $precence = bug_presence(bug => nnn,
986 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
987 is found, absent, fixed, or no information is available in the
988 distribution (dist) and/or architecture (arch) specified.
995 =item bug -- scalar bug number
997 =item status -- optional hashref of bug status as returned by readbug
998 (can be passed to avoid rereading the bug information)
1000 =item bug_index -- optional tied index of bug status infomration;
1001 currently not correctly implemented.
1003 =item version -- optional version to check package status at
1005 =item dist -- optional distribution to check package status at
1007 =item arch -- optional architecture to check package status at
1009 =item sourceversion -- optional arrayref of source/version; overrides
1010 dist, arch, and version. [The entries in this array must be in the
1011 "source/version" format.] Eventually this can be used to for caching.
1018 my %param = validate_with(params => \@_,
1019 spec => {bug => {type => SCALAR,
1022 status => {type => HASHREF,
1025 version => {type => SCALAR|ARRAYREF,
1028 dist => {type => SCALAR|ARRAYREF,
1031 arch => {type => SCALAR|ARRAYREF,
1034 sourceversions => {type => ARRAYREF,
1040 if (defined $param{status}) {
1041 %status = %{$param{status}};
1044 my $location = getbuglocation($param{bug}, 'summary');
1045 return {} if not length $location;
1046 %status = %{ readbug( $param{bug}, $location ) };
1050 my $pseudo_desc = getpseudodesc();
1051 if (not exists $param{sourceversions}) {
1053 # pseudopackages do not have source versions by definition.
1054 if (exists $pseudo_desc->{$status{package}}) {
1057 elsif (defined $param{version}) {
1058 foreach my $arch (make_list($param{arch})) {
1059 for my $package (split /\s*,\s*/, $status{package}) {
1060 my @temp = makesourceversions($package,
1062 make_list($param{version})
1064 @sourceversions{@temp} = (1) x @temp;
1067 } elsif (defined $param{dist}) {
1068 foreach my $arch (make_list($param{arch})) {
1070 for my $package (split /\s*,\s*/, $status{package}) {
1071 foreach my $dist (make_list($param{dist})) {
1072 push @versions, getversions($package, $dist, $arch);
1074 my @temp = makesourceversions($package,
1078 @sourceversions{@temp} = (1) x @temp;
1083 # TODO: This should probably be handled further out for efficiency and
1084 # for more ease of distinguishing between pkg= and src= queries.
1085 # DLA: src= queries should just pass arch=source, and they'll be happy.
1086 @sourceversions = keys %sourceversions;
1089 @sourceversions = @{$param{sourceversions}};
1091 my $maxbuggy = 'undef';
1092 if (@sourceversions) {
1093 $maxbuggy = max_buggy(bug => $param{bug},
1094 sourceversions => \@sourceversions,
1095 found => $status{found_versions},
1096 fixed => $status{fixed_versions},
1097 package => $status{package},
1098 version_cache => $version_cache,
1101 elsif (defined $param{dist} and
1102 not exists $pseudo_desc->{$status{package}}) {
1105 if (length($status{done}) and
1106 (not @sourceversions or not @{$status{fixed_versions}})) {
1121 =item bug -- scalar bug number
1123 =item sourceversion -- optional arrayref of source/version; overrides
1124 dist, arch, and version. [The entries in this array must be in the
1125 "source/version" format.] Eventually this can be used to for caching.
1129 Note: Currently the version information is cached; this needs to be
1130 changed before using this function in long lived programs.
1135 my %param = validate_with(params => \@_,
1136 spec => {bug => {type => SCALAR,
1139 sourceversions => {type => ARRAYREF,
1142 found => {type => ARRAYREF,
1145 fixed => {type => ARRAYREF,
1148 package => {type => SCALAR,
1150 version_cache => {type => HASHREF,
1155 # Resolve bugginess states (we might be looking at multiple
1156 # architectures, say). Found wins, then fixed, then absent.
1157 my $maxbuggy = 'absent';
1158 for my $package (split /\s*,\s*/, $param{package}) {
1159 for my $version (@{$param{sourceversions}}) {
1160 my $buggy = buggy(bug => $param{bug},
1161 version => $version,
1162 found => $param{found},
1163 fixed => $param{fixed},
1164 version_cache => $param{version_cache},
1165 package => $package,
1167 if ($buggy eq 'found') {
1169 } elsif ($buggy eq 'fixed') {
1170 $maxbuggy = 'fixed';
1187 Returns the output of Debbugs::Versions::buggy for a particular
1188 package, version and found/fixed set. Automatically turns found, fixed
1189 and version into source/version strings.
1191 Caching can be had by using the version_cache, but no attempt to check
1192 to see if the on disk information is more recent than the cache is
1193 made. [This will need to be fixed for long-lived processes.]
1198 my %param = validate_with(params => \@_,
1199 spec => {bug => {type => SCALAR,
1202 found => {type => ARRAYREF,
1205 fixed => {type => ARRAYREF,
1208 version_cache => {type => HASHREF,
1211 package => {type => SCALAR,
1213 version => {type => SCALAR,
1217 my @found = @{$param{found}};
1218 my @fixed = @{$param{fixed}};
1219 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1220 # We have non-source version versions
1221 @found = makesourceversions($param{package},undef,
1224 @fixed = makesourceversions($param{package},undef,
1228 if ($param{version} !~ m{/}) {
1229 my ($version) = makesourceversions($param{package},undef,
1232 $param{version} = $version if defined $version;
1234 # Figure out which source packages we need
1236 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1237 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1238 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1239 $param{version} =~ m{/};
1241 if (not defined $param{version_cache} or
1242 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1243 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1244 foreach my $source (keys %sources) {
1245 my $srchash = substr $source, 0, 1;
1246 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1247 if (not defined $version_fh) {
1248 # We only want to warn if it's a package which actually has a maintainer
1249 my $maints = getmaintainers();
1250 next if not exists $maints->{$source};
1251 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1254 $version->load($version_fh);
1256 if (defined $param{version_cache}) {
1257 $param{version_cache}{join(',',sort keys %sources)} = $version;
1261 $version = $param{version_cache}{join(',',sort keys %sources)};
1263 return $version->buggy($param{version},\@found,\@fixed);
1266 sub isstrongseverity {
1267 my $severity = shift;
1268 $severity = $config{default_severity} if
1269 not defined $severity or $severity eq '';
1270 return grep { $_ eq $severity } @{$config{strong_severities}};
1274 =head1 PRIVATE FUNCTIONS
1278 sub update_realtime {
1279 my ($file, %bugs) = @_;
1281 # update realtime index.db
1283 return () unless keys %bugs;
1284 my $idx_old = IO::File->new($file,'r')
1285 or die "Couldn't open ${file}: $!";
1286 my $idx_new = IO::File->new($file.'.new','w')
1287 or die "Couldn't open ${file}.new: $!";
1289 my $min_bug = min(keys %bugs);
1293 while($line = <$idx_old>) {
1294 @line = split /\s/, $line;
1295 # Two cases; replacing existing line or adding new line
1296 if (exists $bugs{$line[1]}) {
1297 my $new = $bugs{$line[1]};
1298 delete $bugs{$line[1]};
1299 $min_bug = min(keys %bugs);
1300 if ($new eq "NOCHANGE") {
1301 print {$idx_new} $line;
1302 $changed_bugs{$line[1]} = $line;
1303 } elsif ($new eq "REMOVE") {
1304 $changed_bugs{$line[1]} = $line;
1306 print {$idx_new} $new;
1307 $changed_bugs{$line[1]} = $line;
1311 while ($line[1] > $min_bug) {
1312 print {$idx_new} $bugs{$min_bug};
1313 delete $bugs{$min_bug};
1314 last unless keys %bugs;
1315 $min_bug = min(keys %bugs);
1317 print {$idx_new} $line;
1319 last unless keys %bugs;
1321 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1323 print {$idx_new} <$idx_old>;
1328 rename("$file.new", $file);
1330 return %changed_bugs;
1333 sub bughook_archive {
1335 &filelock("$config{spool_dir}/debbugs.trace.lock");
1336 &appendfile("debbugs.trace","archive ".join(',',@refs)."\n");
1337 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1338 map{($_,'REMOVE')} @refs);
1339 update_realtime("$config{spool_dir}/index.archive.realtime",
1345 my ( $type, %bugs_temp ) = @_;
1346 &filelock("$config{spool_dir}/debbugs.trace.lock");
1349 for my $bug (keys %bugs_temp) {
1350 my $data = $bugs_temp{$bug};
1351 &appendfile("debbugs.trace","$type $bug\n",makestatus($data, 1));
1353 my $whendone = "open";
1354 my $severity = $config{default_severity};
1355 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1356 $pkglist =~ s/^,+//;
1357 $pkglist =~ s/,+$//;
1358 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1359 $whendone = "done" if defined $data->{done} and length $data->{done};
1360 $severity = $data->{severity} if length $data->{severity};
1362 my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1363 $pkglist, $bug, $data->{date}, $whendone,
1364 $data->{originator}, $severity, $data->{keywords};
1367 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);