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-9 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 make_source_versions getversions get_versions binarytosource);
44 use Debbugs::Versions;
45 use Debbugs::Versions::Dpkg;
48 use Storable qw(dclone);
49 use List::Util qw(min max);
55 $DEBUG = 0 unless defined $DEBUG;
58 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
59 qw(isstrongseverity bug_presence split_status_fields),
61 read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
62 qw(lock_read_all_merged_bugs),
64 write => [qw(writebug makestatus unlockwritebug)],
65 versions => [qw(addfoundversions addfixedversions),
66 qw(removefoundversions removefixedversions)
68 hook => [qw(bughook bughook_archive)],
69 fields => [qw(%fields)],
72 Exporter::export_ok_tags(qw(status read write versions hook fields));
73 $EXPORT_TAGS{all} = [@EXPORT_OK];
79 readbug($bug_num,$location)
82 Reads a summary file from the archive given a bug number and a bug
83 location. Valid locations are those understood by L</getbugcomponent>
87 # these probably shouldn't be imported by most people, but
88 # Debbugs::Control needs them, so they're now exportable
89 our %fields = (originator => 'submitter',
92 msgid => 'message-id',
93 'package' => 'package',
96 forwarded => 'forwarded-to',
97 mergedwith => 'merged-with',
98 severity => 'severity',
100 found_versions => 'found-in',
101 found_date => 'found-date',
102 fixed_versions => 'fixed-in',
103 fixed_date => 'fixed-date',
105 blockedby => 'blocked-by',
106 unarchived => 'unarchived',
107 summary => 'summary',
108 affects => 'affects',
112 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
113 my @rfc1522_fields = qw(originator subject done forwarded owner);
116 return read_bug(bug => $_[0],
117 (@_ > 1)?(location => $_[1]):()
123 read_bug(bug => $bug_num,
124 location => 'archive',
126 read_bug(summary => 'path/to/bugnum.summary');
129 A more complete function than readbug; it enables you to pass a full
130 path to the summary file instead of the bug number and/or location.
136 =item bug -- the bug number
138 =item location -- optional location which is passed to getbugcomponent
140 =item summary -- complete path to the .summary file which will be read
142 =item lock -- whether to obtain a lock for the bug to prevent
143 something modifying it while the bug has been read. You B<must> call
144 C<unfilelock();> if something not undef is returned from read_bug.
148 One of C<bug> or C<summary> must be passed. This function will return
149 undef on failure, and will die if improper arguments are passed.
157 my %param = validate_with(params => \@_,
158 spec => {bug => {type => SCALAR,
162 # negative bugnumbers
165 location => {type => SCALAR|UNDEF,
168 summary => {type => SCALAR,
171 lock => {type => BOOLEAN,
176 die "One of bug or summary must be passed to read_bug"
177 if not exists $param{bug} and not exists $param{summary};
181 if (not defined $param{summary}) {
183 ($lref,$location) = @param{qw(bug location)};
184 if (not defined $location) {
185 $location = getbuglocation($lref,'summary');
186 return undef if not defined $location;
188 $status = getbugcomponent($lref, 'summary', $location);
189 $log = getbugcomponent($lref, 'log' , $location);
190 return undef unless defined $status;
191 return undef if not -e $status;
194 $status = $param{summary};
196 $log =~ s/\.summary$/.log/;
197 ($location) = $status =~ m/(db-h|db|archive)/;
200 filelock("$config{spool_dir}/lock/$param{bug}");
202 my $status_fh = IO::File->new($status, 'r');
203 if (not defined $status_fh) {
204 warn "Unable to open $status for reading: $!";
216 while (<$status_fh>) {
219 $version = $1 if /^Format-Version: ([0-9]+)/i;
222 # Version 3 is the latest format version currently supported.
224 warn "Unsupported status version '$version'";
231 my %namemap = reverse %fields;
232 for my $line (@lines) {
233 if ($line =~ /(\S+?): (.*)/) {
234 my ($name, $value) = (lc $1, $2);
235 $data{$namemap{$name}} = $value if exists $namemap{$name};
238 for my $field (keys %fields) {
239 $data{$field} = '' unless exists $data{$field};
242 $data{severity} = $config{default_severity} if $data{severity} eq '';
243 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
244 $data{$field} = [split ' ', $data{$field}];
246 for my $field (qw(found fixed)) {
247 # create the found/fixed hashes which indicate when a
248 # particular version was marked found or marked fixed.
249 @{$data{$field}}{@{$data{"${field}_versions"}}} =
250 (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
251 @{$data{"${field}_date"}});
255 for my $field (@rfc1522_fields) {
256 $data{$field} = decode_rfc1522($data{$field});
259 my $status_modified = (stat($status))[9];
260 # Add log last modified time
261 $data{log_modified} = (stat($log))[9];
262 $data{last_modified} = max($status_modified,$data{log_modified});
263 $data{location} = $location;
264 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
265 $data{bug_num} = $param{bug};
270 =head2 split_status_fields
272 my @data = split_status_fields(@data);
274 Splits splittable status fields (like package, tags, blocks,
275 blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
276 passed @data intact using dclone.
278 In scalar context, returns only the first element of @data.
282 our $ditch_empty = sub{
284 my $splitter = shift @t;
285 return grep {length $_} map {split $splitter} @t;
288 my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
290 (package => \&splitpackages,
291 affects => \&splitpackages,
292 blocks => $ditch_empty_space,
293 blockedby => $ditch_empty_space,
294 tags => sub {return &{$ditch_empty}(qr/\s*\,\s*/,@_)},
295 found_versions => $ditch_empty_space,
296 fixed_versions => $ditch_empty_space,
297 merged_with => $ditch_empty_space,
300 sub split_status_fields {
301 my @data = @{dclone(\@_)};
302 for my $data (@data) {
303 next if not defined $data;
304 croak "Passed an element which is not a hashref to split_status_field".ref($data) if
305 not (ref($data) and ref($data) eq 'HASH');
306 for my $field (keys %{$data}) {
307 next unless defined $data->{$field};
308 if (exists $split_fields{$field}) {
309 next if ref($data->{$field});
311 if (ref($split_fields{$field}) eq 'CODE') {
312 @elements = &{$split_fields{$field}}($data->{$field});
314 elsif (not ref($split_fields{$field}) or
315 UNIVERSAL::isa($split_fields{$field},'Regex')
317 @elements = split $split_fields{$field}, $data->{$field};
319 if (@elements != 1) {
320 $data->{$field} = \@elements;
323 $data->{$field} = $elements[0];
328 return wantarray?@data:$data[0];
331 =head2 join_status_fields
333 my @data = join_status_fields(@data);
335 Handles joining the splitable status fields. (Basically, the inverse
336 of split_status_fields.
338 Primarily called from makestatus, but may be useful for other
339 functions after calling split_status_fields (or for legacy functions
340 if we transition to split fields by default).
344 sub join_status_fields {
351 found_versions => ' ',
352 fixed_versions => ' ',
357 my @data = dclone(\@_);
358 for my $data (@data) {
359 next if not defined $data;
360 croak "Passed an element which is not a hashref to split_status_field" if
361 not (defined ref($data) and ref($data) eq 'HASH');
362 for my $field (keys %{$data}) {
363 next unless defined $data->{$field};
364 next unless defined(ref($data->{$field}))
365 and ref($data->{$field}) eq 'ARRAY';
366 next unless exists $join_fields{$field};
367 $data->{$field} = join($join_fields{$field},@{$data->{$field}});
370 return wantarray?@data:$data[0];
376 lockreadbug($bug_num,$location)
378 Performs a filelock, then reads the bug; the bug is unlocked if the
379 return is undefined, otherwise, you need to call unfilelock or
382 See readbug above for information on what this returns
387 my ($lref, $location) = @_;
388 return read_bug(bug => $lref, location => $location, lock => 1);
391 =head2 lockreadbugmerge
393 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
395 Performs a filelock, then reads the bug. If the bug is merged, locks
396 the merge lock. Returns a list of the number of locks and the bug
401 sub lockreadbugmerge {
402 my ($bug_num,$location) = @_;
403 my $data = lockreadbug(@_);
404 if (not defined $data) {
407 if (not length $data->{mergedwith}) {
411 filelock("$config{spool_dir}/lock/merge");
412 $data = lockreadbug(@_);
413 if (not defined $data) {
420 =head2 lock_read_all_merged_bugs
422 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
424 Performs a filelock, then reads the bug passed. If the bug is merged,
425 locks the merge lock, then reads and locks all of the other merged
426 bugs. Returns a list of the number of locks and the bug data for all
429 Will also return undef if any of the merged bugs failed to be read,
430 even if all of the others were read properly.
434 sub lock_read_all_merged_bugs {
435 my ($bug_num,$location) = @_;
437 my @data = (lockreadbug(@_));
438 if (not @data or not defined $data[0]) {
442 if (not length $data[0]->{mergedwith}) {
443 return ($locks,@data);
447 filelock("$config{spool_dir}/lock/merge");
449 @data = (lockreadbug(@_));
450 if (not @data or not defined $data[0]) {
451 unfilelock(); #for merge lock above
456 my @bugs = split / /, $data[0]->{mergedwith};
457 for my $bug (@bugs) {
459 if ($bug ne $bug_num) {
460 $newdata = lockreadbug($bug,$location);
461 if (not defined $newdata) {
466 warn "Unable to read bug: $bug while handling merged bug: $bug_num";
472 # perform a sanity check to make sure that the merged bugs are
473 # all merged with eachother
474 my $expectmerge= join(' ',grep {$_ != $bug } sort { $a <=> $b } (@bugs,$bug_num));
475 if ($newdata->{mergedwith} ne $expectmerge) {
479 die "Bug $bug_num differs from bug $bug: ($newdata->{bug_num}: $newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
482 return ($locks,@data);
486 my @v1fieldorder = qw(originator date subject msgid package
487 keywords done forwarded mergedwith severity);
491 my $content = makestatus($status,$version)
492 my $content = makestatus($status);
494 Creates the content for a status file based on the $status hashref
497 Really only useful for writebug
499 Currently defaults to version 2 (non-encoded rfc1522 names) but will
500 eventually default to version 3. If you care, you should specify a
506 my ($data,$version) = @_;
507 $version = 2 unless defined $version;
511 my %newdata = %$data;
512 for my $field (qw(found fixed)) {
513 if (exists $newdata{$field}) {
514 $newdata{"${field}_date"} =
515 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
518 %newdata = %{join_status_fields(\%newdata)};
519 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
520 $newdata{$field} = join ' ', @{$newdata{$field}||[]};
524 for my $field (@rfc1522_fields) {
525 $newdata{$field} = encode_rfc1522($newdata{$field});
530 for my $field (@v1fieldorder) {
531 if (exists $newdata{$field} and defined $newdata{$field}) {
532 $contents .= "$newdata{$field}\n";
537 } elsif ($version == 2 or $version == 3) {
538 # Version 2 or 3. Add a file format version number for the sake of
539 # further extensibility in the future.
540 $contents .= "Format-Version: $version\n";
541 for my $field (keys %fields) {
542 if (exists $newdata{$field} and defined $newdata{$field}
543 and $newdata{$field} ne '') {
544 # Output field names in proper case, e.g. 'Merged-With'.
545 my $properfield = $fields{$field};
546 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
547 $contents .= "$properfield: $newdata{$field}\n";
557 writebug($bug_num,$status,$location,$minversion,$disablebughook)
559 Writes the bug status and summary files out.
561 Skips writting out a status file if minversion is 2
563 Does not call bughook if disablebughook is true.
568 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
571 my %outputs = (1 => 'status', 2 => 'summary');
572 for my $version (keys %outputs) {
573 next if defined $minversion and $version < $minversion;
574 my $status = getbugcomponent($ref, $outputs{$version}, $location);
575 die "can't find location for $ref" unless defined $status;
576 open(S,"> $status.new") || die "opening $status.new: $!";
577 print(S makestatus($data, $version)) ||
578 die "writing $status.new: $!";
579 close(S) || die "closing $status.new: $!";
585 rename("$status.new",$status) || die "installing new $status: $!";
588 # $disablebughook is a bit of a hack to let format migration scripts use
589 # this function rather than having to duplicate it themselves.
590 &bughook($change,$ref,$data) unless $disablebughook;
593 =head2 unlockwritebug
595 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
597 Writes a bug, then calls unfilelock; see writebug for what these
609 The following functions are exported with the :versions tag
611 =head2 addfoundversions
613 addfoundversions($status,$package,$version,$isbinary);
620 sub addfoundversions {
624 my $isbinary = shift;
625 return unless defined $version;
626 undef $package if $package =~ m[(?:\s|/)];
627 my $source = $package;
629 if (defined $package and $isbinary) {
630 my @srcinfo = binarytosource($package, $version, undef);
632 # We know the source package(s). Use a fully-qualified version.
633 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
636 # Otherwise, an unqualified version will have to do.
640 # Strip off various kinds of brain-damage.
642 $version =~ s/ *\(.*\)//;
643 $version =~ s/ +[A-Za-z].*//;
645 foreach my $ver (split /[,\s]+/, $version) {
646 my $sver = defined($source) ? "$source/$ver" : '';
647 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
648 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
650 @{$data->{fixed_versions}} =
651 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
655 =head2 removefoundversions
657 removefoundversions($data,$package,$versiontoremove)
659 Removes found versions from $data
661 If a version is fully qualified (contains /) only versions matching
662 exactly are removed. Otherwise, all versions matching the version
665 Currently $package and $isbinary are entirely ignored, but accepted
666 for backwards compatibilty.
670 sub removefoundversions {
674 my $isbinary = shift;
675 return unless defined $version;
677 foreach my $ver (split /[,\s]+/, $version) {
679 # fully qualified version
680 @{$data->{found_versions}} =
682 @{$data->{found_versions}};
685 # non qualified version; delete all matchers
686 @{$data->{found_versions}} =
687 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
688 @{$data->{found_versions}};
694 sub addfixedversions {
698 my $isbinary = shift;
699 return unless defined $version;
700 undef $package if defined $package and $package =~ m[(?:\s|/)];
701 my $source = $package;
703 if (defined $package and $isbinary) {
704 my @srcinfo = binarytosource($package, $version, undef);
706 # We know the source package(s). Use a fully-qualified version.
707 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
710 # Otherwise, an unqualified version will have to do.
714 # Strip off various kinds of brain-damage.
716 $version =~ s/ *\(.*\)//;
717 $version =~ s/ +[A-Za-z].*//;
719 foreach my $ver (split /[,\s]+/, $version) {
720 my $sver = defined($source) ? "$source/$ver" : '';
721 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
722 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
724 @{$data->{found_versions}} =
725 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
729 sub removefixedversions {
733 my $isbinary = shift;
734 return unless defined $version;
736 foreach my $ver (split /[,\s]+/, $version) {
738 # fully qualified version
739 @{$data->{fixed_versions}} =
741 @{$data->{fixed_versions}};
744 # non qualified version; delete all matchers
745 @{$data->{fixed_versions}} =
746 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
747 @{$data->{fixed_versions}};
758 Split a package string from the status file into a list of package names.
764 return unless defined $pkgs;
765 return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
769 =head2 bug_archiveable
771 bug_archiveable(bug => $bug_num);
777 =item bug -- bug number (required)
779 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
781 =item version -- Debbugs::Version information (optional)
783 =item days_until -- return days until the bug can be archived
787 Returns 1 if the bug can be archived
788 Returns 0 if the bug cannot be archived
790 If days_until is true, returns the number of days until the bug can be
791 archived, -1 if it cannot be archived. 0 means that the bug can be
792 archived the next time the archiver runs.
794 Returns undef on failure.
798 # This will eventually need to be fixed before we start using mod_perl
799 our $version_cache = {};
801 my %param = validate_with(params => \@_,
802 spec => {bug => {type => SCALAR,
805 status => {type => HASHREF,
808 days_until => {type => BOOLEAN,
811 ignore_time => {type => BOOLEAN,
816 # This is what we return if the bug cannot be archived.
817 my $cannot_archive = $param{days_until}?-1:0;
818 # read the status information
819 my $status = $param{status};
820 if (not exists $param{status} or not defined $status) {
821 $status = read_bug(bug=>$param{bug});
822 if (not defined $status) {
823 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
827 # Bugs can be archived if they are
829 if (not defined $status->{done} or not length $status->{done}) {
830 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
831 return $cannot_archive
833 # Check to make sure that the bug has none of the unremovable tags set
834 if (@{$config{removal_unremovable_tags}}) {
835 for my $tag (split ' ', ($status->{tags}||'')) {
836 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
837 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
838 return $cannot_archive;
843 # If we just are checking if the bug can be archived, we'll not even bother
844 # checking the versioning information if the bug has been -done for less than 28 days.
845 my $log_file = getbugcomponent($param{bug},'log');
846 if (not defined $log_file) {
847 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
848 return $cannot_archive;
850 my $max_log_age = max(map {$config{remove_age} - -M $_}
851 $log_file, map {my $log = getbugcomponent($_,'log');
852 defined $log ? ($log) : ();
854 split / /, $status->{mergedwith}
856 if (not $param{days_until} and not $param{ignore_time}
859 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
860 return $cannot_archive;
862 # At this point, we have to get the versioning information for this bug.
863 # We examine the set of distribution tags. If a bug has no distribution
864 # tags set, we assume a default set, otherwise we use the tags the bug
867 # In cases where we are assuming a default set, if the severity
868 # is strong, we use the strong severity default; otherwise, we
869 # use the normal default.
871 # There must be fixed_versions for us to look at the versioning
873 my $min_fixed_time = time;
874 my $min_archive_days = 0;
875 if (@{$status->{fixed_versions}}) {
877 @dist_tags{@{$config{removal_distribution_tags}}} =
878 (1) x @{$config{removal_distribution_tags}};
880 for my $tag (split ' ', ($status->{tags}||'')) {
881 next unless exists $config{distribution_aliases}{$tag};
882 next unless $dist_tags{$config{distribution_aliases}{$tag}};
883 $dists{$config{distribution_aliases}{$tag}} = 1;
885 if (not keys %dists) {
886 if (isstrongseverity($status->{severity})) {
887 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
888 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
891 @dists{@{$config{removal_default_distribution_tags}}} =
892 (1) x @{$config{removal_default_distribution_tags}};
896 my @sourceversions = get_versions(package => $status->{package},
897 dist => [keys %dists],
900 @source_versions{@sourceversions} = (1) x @sourceversions;
901 # If the bug has not been fixed in the versions actually
902 # distributed, then it cannot be archived.
903 if ('found' eq max_buggy(bug => $param{bug},
904 sourceversions => [keys %source_versions],
905 found => $status->{found_versions},
906 fixed => $status->{fixed_versions},
907 version_cache => $version_cache,
908 package => $status->{package},
910 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
911 return $cannot_archive;
913 # Since the bug has at least been fixed in the architectures
914 # that matters, we check to see how long it has been fixed.
916 # If $param{ignore_time}, then we should ignore time.
917 if ($param{ignore_time}) {
918 return $param{days_until}?0:1;
921 # To do this, we order the times from most recent to oldest;
922 # when we come to the first found version, we stop.
923 # If we run out of versions, we only report the time of the
925 my %time_versions = get_versions(package => $status->{package},
926 dist => [keys %dists],
930 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
931 my $buggy = buggy(bug => $param{bug},
933 found => $status->{found_versions},
934 fixed => $status->{fixed_versions},
935 version_cache => $version_cache,
936 package => $status->{package},
938 last if $buggy eq 'found';
939 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
941 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
942 # if there are no versions in the archive at all, then
943 # we can archive if enough days have passed
946 # If $param{ignore_time}, then we should ignore time.
947 if ($param{ignore_time}) {
948 return $param{days_until}?0:1;
950 # 6. at least 28 days have passed since the last action has occured or the bug was closed
951 my $age = ceil($max_log_age);
952 if ($age > 0 or $min_archive_days > 0) {
953 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
954 return $param{days_until}?max($age,$min_archive_days):0;
957 return $param{days_until}?0:1;
962 =head2 get_bug_status
964 my $status = get_bug_status(bug => $nnn);
966 my $status = get_bug_status($bug_num)
972 =item bug -- scalar bug number
974 =item status -- optional hashref of bug status as returned by readbug
975 (can be passed to avoid rereading the bug information)
977 =item bug_index -- optional tied index of bug status infomration;
978 currently not correctly implemented.
980 =item version -- optional version(s) to check package status at
982 =item dist -- optional distribution(s) to check package status at
984 =item arch -- optional architecture(s) to check package status at
986 =item bugusertags -- optional hashref of bugusertags
988 =item sourceversion -- optional arrayref of source/version; overrides
989 dist, arch, and version. [The entries in this array must be in the
990 "source/version" format.] Eventually this can be used to for caching.
992 =item indicatesource -- if true, indicate which source packages this
993 bug could belong to (or does belong to in the case of bugs assigned to
994 a source package). Defaults to true.
998 Note: Currently the version information is cached; this needs to be
999 changed before using this function in long lived programs.
1003 sub get_bug_status {
1007 my %param = validate_with(params => \@_,
1008 spec => {bug => {type => SCALAR,
1011 status => {type => HASHREF,
1014 bug_index => {type => OBJECT,
1017 version => {type => SCALAR|ARRAYREF,
1020 dist => {type => SCALAR|ARRAYREF,
1023 arch => {type => SCALAR|ARRAYREF,
1026 bugusertags => {type => HASHREF,
1029 sourceversions => {type => ARRAYREF,
1032 indicatesource => {type => BOOLEAN,
1039 if (defined $param{bug_index} and
1040 exists $param{bug_index}{$param{bug}}) {
1041 %status = %{ $param{bug_index}{$param{bug}} };
1042 $status{pending} = $status{ status };
1043 $status{id} = $param{bug};
1046 if (defined $param{status}) {
1047 %status = %{$param{status}};
1050 my $location = getbuglocation($param{bug}, 'summary');
1051 return {} if not defined $location or not length $location;
1052 %status = %{ readbug( $param{bug}, $location ) };
1054 $status{id} = $param{bug};
1056 if (defined $param{bugusertags}{$param{bug}}) {
1057 $status{keywords} = "" unless defined $status{keywords};
1058 $status{keywords} .= " " unless $status{keywords} eq "";
1059 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
1061 $status{tags} = $status{keywords};
1062 my %tags = map { $_ => 1 } split ' ', $status{tags};
1064 $status{package} = '' if not defined $status{package};
1065 $status{"package"} =~ s/\s*$//;
1066 # if we aren't supposed to indicate the source, we'll return
1068 $status{source} = 'unknown';
1069 if ($param{indicatesource}) {
1070 my @packages = split /\s*,\s*/, $status{package};
1072 for my $package (@packages) {
1073 next if $package eq '';
1074 if ($package =~ /^src\:$/) {
1078 push @source, binarytosource($package);
1082 $status{source} = join(', ',@source);
1086 $status{"package"} = 'unknown' if ($status{"package"} eq '');
1087 $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
1089 $status{"pending"} = 'pending';
1090 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
1091 $status{"pending"} = 'pending-fixed' if ($tags{pending});
1092 $status{"pending"} = 'fixed' if ($tags{fixed});
1095 my $presence = bug_presence(status => \%status,
1096 map{(exists $param{$_})?($_,$param{$_}):()}
1097 qw(bug sourceversions arch dist version found fixed package)
1099 if (defined $presence) {
1100 if ($presence eq 'fixed') {
1101 $status{pending} = 'done';
1103 elsif ($presence eq 'absent') {
1104 $status{pending} = 'absent';
1112 my $precence = bug_presence(bug => nnn,
1116 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
1117 is found, absent, fixed, or no information is available in the
1118 distribution (dist) and/or architecture (arch) specified.
1125 =item bug -- scalar bug number
1127 =item status -- optional hashref of bug status as returned by readbug
1128 (can be passed to avoid rereading the bug information)
1130 =item bug_index -- optional tied index of bug status infomration;
1131 currently not correctly implemented.
1133 =item version -- optional version to check package status at
1135 =item dist -- optional distribution to check package status at
1137 =item arch -- optional architecture to check package status at
1139 =item sourceversion -- optional arrayref of source/version; overrides
1140 dist, arch, and version. [The entries in this array must be in the
1141 "source/version" format.] Eventually this can be used to for caching.
1148 my %param = validate_with(params => \@_,
1149 spec => {bug => {type => SCALAR,
1152 status => {type => HASHREF,
1155 version => {type => SCALAR|ARRAYREF,
1158 dist => {type => SCALAR|ARRAYREF,
1161 arch => {type => SCALAR|ARRAYREF,
1164 sourceversions => {type => ARRAYREF,
1170 if (defined $param{status}) {
1171 %status = %{$param{status}};
1174 my $location = getbuglocation($param{bug}, 'summary');
1175 return {} if not length $location;
1176 %status = %{ readbug( $param{bug}, $location ) };
1180 my $pseudo_desc = getpseudodesc();
1181 if (not exists $param{sourceversions}) {
1183 # pseudopackages do not have source versions by definition.
1184 if (exists $pseudo_desc->{$status{package}}) {
1187 elsif (defined $param{version}) {
1188 foreach my $arch (make_list($param{arch})) {
1189 for my $package (split /\s*,\s*/, $status{package}) {
1190 my @temp = makesourceversions($package,
1192 make_list($param{version})
1194 @sourceversions{@temp} = (1) x @temp;
1197 } elsif (defined $param{dist}) {
1198 my %affects_distribution_tags;
1199 @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
1200 (1) x @{$config{affects_distribution_tags}};
1201 my $some_distributions_disallowed = 0;
1202 my %allowed_distributions;
1203 for my $tag (split ' ', ($status{tags}||'')) {
1204 if (exists $config{distribution_aliases}{$tag} and
1205 exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
1206 $some_distributions_disallowed = 1;
1207 $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
1209 elsif (exists $affects_distribution_tags{$tag}) {
1210 $some_distributions_disallowed = 1;
1211 $allowed_distributions{$tag} = 1;
1214 foreach my $arch (make_list(exists $param{arch}?$param{arch}:undef)) {
1215 for my $package (split /\s*,\s*/, $status{package}) {
1218 if ($package =~ /^src:(.+)$/) {
1222 foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
1223 # if some distributions are disallowed,
1224 # and this isn't an allowed
1225 # distribution, then we ignore this
1226 # distribution for the purposees of
1228 if ($some_distributions_disallowed and
1229 not exists $allowed_distributions{$dist}) {
1232 push @versions, get_versions(package => $package,
1234 ($source?(arch => 'source'):
1235 (defined $arch?(arch => $arch):())),
1238 next unless @versions;
1239 my @temp = make_source_versions(package => $package,
1241 versions => \@versions,
1243 @sourceversions{@temp} = (1) x @temp;
1248 # TODO: This should probably be handled further out for efficiency and
1249 # for more ease of distinguishing between pkg= and src= queries.
1250 # DLA: src= queries should just pass arch=source, and they'll be happy.
1251 @sourceversions = keys %sourceversions;
1254 @sourceversions = @{$param{sourceversions}};
1256 my $maxbuggy = 'undef';
1257 if (@sourceversions) {
1258 $maxbuggy = max_buggy(bug => $param{bug},
1259 sourceversions => \@sourceversions,
1260 found => $status{found_versions},
1261 fixed => $status{fixed_versions},
1262 package => $status{package},
1263 version_cache => $version_cache,
1266 elsif (defined $param{dist} and
1267 not exists $pseudo_desc->{$status{package}}) {
1270 if (length($status{done}) and
1271 (not @sourceversions or not @{$status{fixed_versions}})) {
1286 =item bug -- scalar bug number
1288 =item sourceversion -- optional arrayref of source/version; overrides
1289 dist, arch, and version. [The entries in this array must be in the
1290 "source/version" format.] Eventually this can be used to for caching.
1294 Note: Currently the version information is cached; this needs to be
1295 changed before using this function in long lived programs.
1300 my %param = validate_with(params => \@_,
1301 spec => {bug => {type => SCALAR,
1304 sourceversions => {type => ARRAYREF,
1307 found => {type => ARRAYREF,
1310 fixed => {type => ARRAYREF,
1313 package => {type => SCALAR,
1315 version_cache => {type => HASHREF,
1320 # Resolve bugginess states (we might be looking at multiple
1321 # architectures, say). Found wins, then fixed, then absent.
1322 my $maxbuggy = 'absent';
1323 for my $package (split /\s*,\s*/, $param{package}) {
1324 for my $version (@{$param{sourceversions}}) {
1325 my $buggy = buggy(bug => $param{bug},
1326 version => $version,
1327 found => $param{found},
1328 fixed => $param{fixed},
1329 version_cache => $param{version_cache},
1330 package => $package,
1332 if ($buggy eq 'found') {
1334 } elsif ($buggy eq 'fixed') {
1335 $maxbuggy = 'fixed';
1352 Returns the output of Debbugs::Versions::buggy for a particular
1353 package, version and found/fixed set. Automatically turns found, fixed
1354 and version into source/version strings.
1356 Caching can be had by using the version_cache, but no attempt to check
1357 to see if the on disk information is more recent than the cache is
1358 made. [This will need to be fixed for long-lived processes.]
1363 my %param = validate_with(params => \@_,
1364 spec => {bug => {type => SCALAR,
1367 found => {type => ARRAYREF,
1370 fixed => {type => ARRAYREF,
1373 version_cache => {type => HASHREF,
1376 package => {type => SCALAR,
1378 version => {type => SCALAR,
1382 my @found = @{$param{found}};
1383 my @fixed = @{$param{fixed}};
1384 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1385 # We have non-source version versions
1386 @found = makesourceversions($param{package},undef,
1389 @fixed = makesourceversions($param{package},undef,
1393 if ($param{version} !~ m{/}) {
1394 my ($version) = makesourceversions($param{package},undef,
1397 $param{version} = $version if defined $version;
1399 # Figure out which source packages we need
1401 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1402 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1403 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1404 $param{version} =~ m{/};
1406 if (not defined $param{version_cache} or
1407 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1408 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1409 foreach my $source (keys %sources) {
1410 my $srchash = substr $source, 0, 1;
1411 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1412 if (not defined $version_fh) {
1413 # We only want to warn if it's a package which actually has a maintainer
1414 my $maints = getmaintainers();
1415 next if not exists $maints->{$source};
1416 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1419 $version->load($version_fh);
1421 if (defined $param{version_cache}) {
1422 $param{version_cache}{join(',',sort keys %sources)} = $version;
1426 $version = $param{version_cache}{join(',',sort keys %sources)};
1428 return $version->buggy($param{version},\@found,\@fixed);
1431 sub isstrongseverity {
1432 my $severity = shift;
1433 $severity = $config{default_severity} if
1434 not defined $severity or $severity eq '';
1435 return grep { $_ eq $severity } @{$config{strong_severities}};
1439 =head1 PRIVATE FUNCTIONS
1443 sub update_realtime {
1444 my ($file, %bugs) = @_;
1446 # update realtime index.db
1448 return () unless keys %bugs;
1449 my $idx_old = IO::File->new($file,'r')
1450 or die "Couldn't open ${file}: $!";
1451 my $idx_new = IO::File->new($file.'.new','w')
1452 or die "Couldn't open ${file}.new: $!";
1454 my $min_bug = min(keys %bugs);
1458 while($line = <$idx_old>) {
1459 @line = split /\s/, $line;
1460 # Two cases; replacing existing line or adding new line
1461 if (exists $bugs{$line[1]}) {
1462 my $new = $bugs{$line[1]};
1463 delete $bugs{$line[1]};
1464 $min_bug = min(keys %bugs);
1465 if ($new eq "NOCHANGE") {
1466 print {$idx_new} $line;
1467 $changed_bugs{$line[1]} = $line;
1468 } elsif ($new eq "REMOVE") {
1469 $changed_bugs{$line[1]} = $line;
1471 print {$idx_new} $new;
1472 $changed_bugs{$line[1]} = $line;
1476 while ($line[1] > $min_bug) {
1477 print {$idx_new} $bugs{$min_bug};
1478 delete $bugs{$min_bug};
1479 last unless keys %bugs;
1480 $min_bug = min(keys %bugs);
1482 print {$idx_new} $line;
1484 last unless keys %bugs;
1486 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1488 print {$idx_new} <$idx_old>;
1493 rename("$file.new", $file);
1495 return %changed_bugs;
1498 sub bughook_archive {
1500 &filelock("$config{spool_dir}/debbugs.trace.lock");
1501 &appendfile("debbugs.trace","archive ".join(',',@refs)."\n");
1502 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1503 map{($_,'REMOVE')} @refs);
1504 update_realtime("$config{spool_dir}/index.archive.realtime",
1510 my ( $type, %bugs_temp ) = @_;
1511 &filelock("$config{spool_dir}/debbugs.trace.lock");
1514 for my $bug (keys %bugs_temp) {
1515 my $data = $bugs_temp{$bug};
1516 &appendfile("debbugs.trace","$type $bug\n",makestatus($data, 1));
1518 my $whendone = "open";
1519 my $severity = $config{default_severity};
1520 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1521 $pkglist =~ s/^,+//;
1522 $pkglist =~ s/,+$//;
1523 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1524 $whendone = "done" if defined $data->{done} and length $data->{done};
1525 $severity = $data->{severity} if length $data->{severity};
1527 my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1528 $pkglist, $bug, $data->{date}, $whendone,
1529 $data->{originator}, $severity, $data->{keywords};
1532 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);