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 :utf8);
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 binary_to_source);
44 use Debbugs::Versions;
45 use Debbugs::Versions::Dpkg;
47 use File::Copy qw(copy);
48 use Encode qw(decode encode is_utf8);
50 use Storable qw(dclone);
51 use List::Util qw(min max);
57 $DEBUG = 0 unless defined $DEBUG;
60 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
61 qw(isstrongseverity bug_presence split_status_fields),
63 read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
64 qw(lock_read_all_merged_bugs),
66 write => [qw(writebug makestatus unlockwritebug)],
68 versions => [qw(addfoundversions addfixedversions),
69 qw(removefoundversions removefixedversions)
71 hook => [qw(bughook bughook_archive)],
72 fields => [qw(%fields)],
75 Exporter::export_ok_tags(keys %EXPORT_TAGS);
76 $EXPORT_TAGS{all} = [@EXPORT_OK];
82 readbug($bug_num,$location)
85 Reads a summary file from the archive given a bug number and a bug
86 location. Valid locations are those understood by L</getbugcomponent>
90 # these probably shouldn't be imported by most people, but
91 # Debbugs::Control needs them, so they're now exportable
92 our %fields = (originator => 'submitter',
95 msgid => 'message-id',
96 'package' => 'package',
99 forwarded => 'forwarded-to',
100 mergedwith => 'merged-with',
101 severity => 'severity',
103 found_versions => 'found-in',
104 found_date => 'found-date',
105 fixed_versions => 'fixed-in',
106 fixed_date => 'fixed-date',
108 blockedby => 'blocked-by',
109 unarchived => 'unarchived',
110 summary => 'summary',
111 outlook => 'outlook',
112 affects => 'affects',
116 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
117 my @rfc1522_fields = qw(originator subject done forwarded owner);
120 return read_bug(bug => $_[0],
121 (@_ > 1)?(location => $_[1]):()
127 read_bug(bug => $bug_num,
128 location => 'archive',
130 read_bug(summary => 'path/to/bugnum.summary');
133 A more complete function than readbug; it enables you to pass a full
134 path to the summary file instead of the bug number and/or location.
140 =item bug -- the bug number
142 =item location -- optional location which is passed to getbugcomponent
144 =item summary -- complete path to the .summary file which will be read
146 =item lock -- whether to obtain a lock for the bug to prevent
147 something modifying it while the bug has been read. You B<must> call
148 C<unfilelock();> if something not undef is returned from read_bug.
150 =item locks -- hashref of already obtained locks; incremented as new
151 locks are needed, and decremented as locks are released on particular
156 One of C<bug> or C<summary> must be passed. This function will return
157 undef on failure, and will die if improper arguments are passed.
165 my %param = validate_with(params => \@_,
166 spec => {bug => {type => SCALAR,
170 # negative bugnumbers
173 location => {type => SCALAR|UNDEF,
176 summary => {type => SCALAR,
179 lock => {type => BOOLEAN,
182 locks => {type => HASHREF,
187 die "One of bug or summary must be passed to read_bug"
188 if not exists $param{bug} and not exists $param{summary};
192 if (not defined $param{summary}) {
194 ($lref,$location) = @param{qw(bug location)};
195 if (not defined $location) {
196 $location = getbuglocation($lref,'summary');
197 return undef if not defined $location;
199 $status = getbugcomponent($lref, 'summary', $location);
200 $log = getbugcomponent($lref, 'log' , $location);
201 return undef unless defined $status;
202 return undef if not -e $status;
205 $status = $param{summary};
207 $log =~ s/\.summary$/.log/;
208 ($location) = $status =~ m/(db-h|db|archive)/;
211 filelock("$config{spool_dir}/lock/$param{bug}",exists $param{locks}?$param{locks}:());
213 my $status_fh = IO::File->new($status, 'r');
214 if (not defined $status_fh) {
215 warn "Unable to open $status for reading: $!";
217 unfilelock(exists $param{locks}?$param{locks}:());
221 binmode($status_fh,':encoding(UTF-8)');
228 while (<$status_fh>) {
231 $version = $1 if /^Format-Version: ([0-9]+)/i;
234 # Version 3 is the latest format version currently supported.
236 warn "Unsupported status version '$version'";
238 unfilelock(exists $param{locks}?$param{locks}:());
243 my %namemap = reverse %fields;
244 for my $line (@lines) {
245 if ($line =~ /(\S+?): (.*)/) {
246 my ($name, $value) = (lc $1, $2);
247 # this is a bit of a hack; we should never, ever have \r
248 # or \n in the fields of status. Kill them off here.
249 # [Eventually, this should be superfluous.]
250 $value =~ s/[\r\n]//g;
251 $data{$namemap{$name}} = $value if exists $namemap{$name};
254 for my $field (keys %fields) {
255 $data{$field} = '' unless exists $data{$field};
258 for my $field (@rfc1522_fields) {
259 $data{$field} = decode_rfc1522($data{$field});
262 $data{severity} = $config{default_severity} if $data{severity} eq '';
263 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
264 $data{$field} = [split ' ', $data{$field}];
266 for my $field (qw(found fixed)) {
267 # create the found/fixed hashes which indicate when a
268 # particular version was marked found or marked fixed.
269 @{$data{$field}}{@{$data{"${field}_versions"}}} =
270 (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
271 @{$data{"${field}_date"}});
274 my $status_modified = (stat($status))[9];
275 # Add log last modified time
276 $data{log_modified} = (stat($log))[9];
277 $data{last_modified} = max($status_modified,$data{log_modified});
278 $data{location} = $location;
279 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
280 $data{bug_num} = $param{bug};
285 =head2 split_status_fields
287 my @data = split_status_fields(@data);
289 Splits splittable status fields (like package, tags, blocks,
290 blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
291 passed @data intact using dclone.
293 In scalar context, returns only the first element of @data.
297 our $ditch_empty = sub{
299 my $splitter = shift @t;
300 return grep {length $_} map {split $splitter} @t;
303 my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
305 (package => \&splitpackages,
306 affects => \&splitpackages,
307 blocks => $ditch_empty_space,
308 blockedby => $ditch_empty_space,
309 # this isn't strictly correct, but we'll split both of them for
310 # the time being until we ditch all use of keywords everywhere
312 keywords => $ditch_empty_space,
313 tags => $ditch_empty_space,
314 found_versions => $ditch_empty_space,
315 fixed_versions => $ditch_empty_space,
316 mergedwith => $ditch_empty_space,
319 sub split_status_fields {
320 my @data = @{dclone(\@_)};
321 for my $data (@data) {
322 next if not defined $data;
323 croak "Passed an element which is not a hashref to split_status_field".ref($data) if
324 not (ref($data) and ref($data) eq 'HASH');
325 for my $field (keys %{$data}) {
326 next unless defined $data->{$field};
327 if (exists $split_fields{$field}) {
328 next if ref($data->{$field});
330 if (ref($split_fields{$field}) eq 'CODE') {
331 @elements = &{$split_fields{$field}}($data->{$field});
333 elsif (not ref($split_fields{$field}) or
334 UNIVERSAL::isa($split_fields{$field},'Regex')
336 @elements = split $split_fields{$field}, $data->{$field};
338 $data->{$field} = \@elements;
342 return wantarray?@data:$data[0];
345 =head2 join_status_fields
347 my @data = join_status_fields(@data);
349 Handles joining the splitable status fields. (Basically, the inverse
350 of split_status_fields.
352 Primarily called from makestatus, but may be useful for other
353 functions after calling split_status_fields (or for legacy functions
354 if we transition to split fields by default).
358 sub join_status_fields {
365 found_versions => ' ',
366 fixed_versions => ' ',
371 my @data = @{dclone(\@_)};
372 for my $data (@data) {
373 next if not defined $data;
374 croak "Passed an element which is not a hashref to split_status_field: ".
376 if ref($data) ne 'HASH';
377 for my $field (keys %{$data}) {
378 next unless defined $data->{$field};
379 next unless ref($data->{$field}) eq 'ARRAY';
380 next unless exists $join_fields{$field};
381 $data->{$field} = join($join_fields{$field},@{$data->{$field}});
384 return wantarray?@data:$data[0];
390 lockreadbug($bug_num,$location)
392 Performs a filelock, then reads the bug; the bug is unlocked if the
393 return is undefined, otherwise, you need to call unfilelock or
396 See readbug above for information on what this returns
401 my ($lref, $location) = @_;
402 return read_bug(bug => $lref, location => $location, lock => 1);
405 =head2 lockreadbugmerge
407 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
409 Performs a filelock, then reads the bug. If the bug is merged, locks
410 the merge lock. Returns a list of the number of locks and the bug
415 sub lockreadbugmerge {
416 my ($bug_num,$location) = @_;
417 my $data = lockreadbug(@_);
418 if (not defined $data) {
421 if (not length $data->{mergedwith}) {
425 filelock("$config{spool_dir}/lock/merge");
426 $data = lockreadbug(@_);
427 if (not defined $data) {
434 =head2 lock_read_all_merged_bugs
436 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
438 Performs a filelock, then reads the bug passed. If the bug is merged,
439 locks the merge lock, then reads and locks all of the other merged
440 bugs. Returns a list of the number of locks and the bug data for all
443 Will also return undef if any of the merged bugs failed to be read,
444 even if all of the others were read properly.
448 sub lock_read_all_merged_bugs {
449 my %param = validate_with(params => \@_,
450 spec => {bug => {type => SCALAR,
453 location => {type => SCALAR,
456 locks => {type => HASHREF,
462 my @data = read_bug(bug => $param{bug},
464 exists $param{location} ? (location => $param{location}):(),
465 exists $param{locks} ? (locks => $param{locks}):(),
467 if (not @data or not defined $data[0]) {
471 if (not length $data[0]->{mergedwith}) {
472 return ($locks,@data);
474 unfilelock(exists $param{locks}?$param{locks}:());
476 filelock("$config{spool_dir}/lock/merge",exists $param{locks}?$param{locks}:());
478 @data = read_bug(bug => $param{bug},
480 exists $param{location} ? (location => $param{location}):(),
481 exists $param{locks} ? (locks => $param{locks}):(),
483 if (not @data or not defined $data[0]) {
484 unfilelock(exists $param{locks}?$param{locks}:()); #for merge lock above
489 my @bugs = split / /, $data[0]->{mergedwith};
490 push @bugs, $param{bug};
491 for my $bug (@bugs) {
493 if ($bug != $param{bug}) {
495 read_bug(bug => $bug,
497 exists $param{location} ? (location => $param{location}):(),
498 exists $param{locks} ? (locks => $param{locks}):(),
500 if (not defined $newdata) {
502 unfilelock(exists $param{locks}?$param{locks}:());
505 warn "Unable to read bug: $bug while handling merged bug: $param{bug}";
510 # perform a sanity check to make sure that the merged bugs
511 # are all merged with eachother
512 my $expectmerge= join(' ',grep {$_ != $bug } sort { $a <=> $b } @bugs);
513 if ($newdata->{mergedwith} ne $expectmerge) {
515 unfilelock(exists $param{locks}?$param{locks}:());
517 die "Bug $param{bug} differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
521 return ($locks,@data);
526 my $new_bug_num = new_bug(copy => $data->{bug_num});
528 Creates a new bug and returns the new bug number upon success.
536 validate_with(params => \@_,
537 spec => {copy => {type => SCALAR,
543 filelock("nextnumber.lock");
544 my $nn_fh = IO::File->new("nextnumber",'r') or
545 die "Unable to open nextnuber for reading: $!";
548 ($nn) = $nn =~ m/^(\d+)\n$/ or die "Bad format of nextnumber; is not exactly ".'^\d+\n$';
550 overwritefile("nextnumber",
553 my $nn_hash = get_hashname($nn);
555 my $c_hash = get_hashname($param{copy});
556 for my $file (qw(log status summary report)) {
557 copy("db-h/$c_hash/$param{copy}.$file",
558 "db-h/$nn_hash/${nn}.$file")
562 for my $file (qw(log status summary report)) {
563 overwritefile("db-h/$nn_hash/${nn}.$file",
568 # this probably needs to be munged to do something more elegant
569 # &bughook('new', $clone, $data);
576 my @v1fieldorder = qw(originator date subject msgid package
577 keywords done forwarded mergedwith severity);
581 my $content = makestatus($status,$version)
582 my $content = makestatus($status);
584 Creates the content for a status file based on the $status hashref
587 Really only useful for writebug
589 Currently defaults to version 2 (non-encoded rfc1522 names) but will
590 eventually default to version 3. If you care, you should specify a
596 my ($data,$version) = @_;
597 $version = 3 unless defined $version;
601 my %newdata = %$data;
602 for my $field (qw(found fixed)) {
603 if (exists $newdata{$field}) {
604 $newdata{"${field}_date"} =
605 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
608 %newdata = %{join_status_fields(\%newdata)};
610 %newdata = encode_utf8_structure(%newdata);
613 for my $field (@rfc1522_fields) {
614 $newdata{$field} = encode_rfc1522($newdata{$field});
618 # this is a bit of a hack; we should never, ever have \r or \n in
619 # the fields of status. Kill them off here. [Eventually, this
620 # should be superfluous.]
621 for my $field (keys %newdata) {
622 $newdata{$field} =~ s/[\r\n]//g if defined $newdata{$field};
626 for my $field (@v1fieldorder) {
627 if (exists $newdata{$field} and defined $newdata{$field}) {
628 $contents .= "$newdata{$field}\n";
633 } elsif ($version == 2 or $version == 3) {
634 # Version 2 or 3. Add a file format version number for the sake of
635 # further extensibility in the future.
636 $contents .= "Format-Version: $version\n";
637 for my $field (keys %fields) {
638 if (exists $newdata{$field} and defined $newdata{$field}
639 and $newdata{$field} ne '') {
640 # Output field names in proper case, e.g. 'Merged-With'.
641 my $properfield = $fields{$field};
642 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
643 my $data = $newdata{$field};
644 $contents .= "$properfield: $data\n";
653 writebug($bug_num,$status,$location,$minversion,$disablebughook)
655 Writes the bug status and summary files out.
657 Skips writting out a status file if minversion is 2
659 Does not call bughook if disablebughook is true.
664 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
667 my %outputs = (1 => 'status', 3 => 'summary');
668 for my $version (keys %outputs) {
669 next if defined $minversion and $version < $minversion;
670 my $status = getbugcomponent($ref, $outputs{$version}, $location);
671 die "can't find location for $ref" unless defined $status;
674 open $sfh,">","$status.new" or
675 die "opening $status.new: $!";
678 open $sfh,">","$status.new" or
679 die "opening $status.new: $!";
681 print {$sfh} makestatus($data, $version) or
682 die "writing $status.new: $!";
683 close($sfh) or die "closing $status.new: $!";
689 rename("$status.new",$status) || die "installing new $status: $!";
692 # $disablebughook is a bit of a hack to let format migration scripts use
693 # this function rather than having to duplicate it themselves.
694 &bughook($change,$ref,$data) unless $disablebughook;
697 =head2 unlockwritebug
699 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
701 Writes a bug, then calls unfilelock; see writebug for what these
713 The following functions are exported with the :versions tag
715 =head2 addfoundversions
717 addfoundversions($status,$package,$version,$isbinary);
719 All use of this should be phased out in favor of Debbugs::Control::fixed/found
724 sub addfoundversions {
728 my $isbinary = shift;
729 return unless defined $version;
730 undef $package if $package =~ m[(?:\s|/)];
731 my $source = $package;
732 if ($package =~ s/^src://) {
737 if (defined $package and $isbinary) {
738 my @srcinfo = binary_to_source(binary => $package,
739 version => $version);
741 # We know the source package(s). Use a fully-qualified version.
742 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
745 # Otherwise, an unqualified version will have to do.
749 # Strip off various kinds of brain-damage.
751 $version =~ s/ *\(.*\)//;
752 $version =~ s/ +[A-Za-z].*//;
754 foreach my $ver (split /[,\s]+/, $version) {
755 my $sver = defined($source) ? "$source/$ver" : '';
756 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
757 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
759 @{$data->{fixed_versions}} =
760 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
764 =head2 removefoundversions
766 removefoundversions($data,$package,$versiontoremove)
768 Removes found versions from $data
770 If a version is fully qualified (contains /) only versions matching
771 exactly are removed. Otherwise, all versions matching the version
774 Currently $package and $isbinary are entirely ignored, but accepted
775 for backwards compatibilty.
779 sub removefoundversions {
783 my $isbinary = shift;
784 return unless defined $version;
786 foreach my $ver (split /[,\s]+/, $version) {
788 # fully qualified version
789 @{$data->{found_versions}} =
791 @{$data->{found_versions}};
794 # non qualified version; delete all matchers
795 @{$data->{found_versions}} =
796 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
797 @{$data->{found_versions}};
803 sub addfixedversions {
807 my $isbinary = shift;
808 return unless defined $version;
809 undef $package if defined $package and $package =~ m[(?:\s|/)];
810 my $source = $package;
812 if (defined $package and $isbinary) {
813 my @srcinfo = binary_to_source(binary => $package,
814 version => $version);
816 # We know the source package(s). Use a fully-qualified version.
817 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
820 # Otherwise, an unqualified version will have to do.
824 # Strip off various kinds of brain-damage.
826 $version =~ s/ *\(.*\)//;
827 $version =~ s/ +[A-Za-z].*//;
829 foreach my $ver (split /[,\s]+/, $version) {
830 my $sver = defined($source) ? "$source/$ver" : '';
831 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
832 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
834 @{$data->{found_versions}} =
835 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
839 sub removefixedversions {
843 my $isbinary = shift;
844 return unless defined $version;
846 foreach my $ver (split /[,\s]+/, $version) {
848 # fully qualified version
849 @{$data->{fixed_versions}} =
851 @{$data->{fixed_versions}};
854 # non qualified version; delete all matchers
855 @{$data->{fixed_versions}} =
856 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
857 @{$data->{fixed_versions}};
868 Split a package string from the status file into a list of package names.
874 return unless defined $pkgs;
875 return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
879 =head2 bug_archiveable
881 bug_archiveable(bug => $bug_num);
887 =item bug -- bug number (required)
889 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
891 =item version -- Debbugs::Version information (optional)
893 =item days_until -- return days until the bug can be archived
897 Returns 1 if the bug can be archived
898 Returns 0 if the bug cannot be archived
900 If days_until is true, returns the number of days until the bug can be
901 archived, -1 if it cannot be archived. 0 means that the bug can be
902 archived the next time the archiver runs.
904 Returns undef on failure.
908 # This will eventually need to be fixed before we start using mod_perl
909 our $version_cache = {};
911 my %param = validate_with(params => \@_,
912 spec => {bug => {type => SCALAR,
915 status => {type => HASHREF,
918 days_until => {type => BOOLEAN,
921 ignore_time => {type => BOOLEAN,
926 # This is what we return if the bug cannot be archived.
927 my $cannot_archive = $param{days_until}?-1:0;
928 # read the status information
929 my $status = $param{status};
930 if (not exists $param{status} or not defined $status) {
931 $status = read_bug(bug=>$param{bug});
932 if (not defined $status) {
933 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
937 # Bugs can be archived if they are
939 if (not defined $status->{done} or not length $status->{done}) {
940 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
941 return $cannot_archive
943 # Check to make sure that the bug has none of the unremovable tags set
944 if (@{$config{removal_unremovable_tags}}) {
945 for my $tag (split ' ', ($status->{keywords}||'')) {
946 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
947 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
948 return $cannot_archive;
953 # If we just are checking if the bug can be archived, we'll not even bother
954 # checking the versioning information if the bug has been -done for less than 28 days.
955 my $log_file = getbugcomponent($param{bug},'log');
956 if (not defined $log_file) {
957 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
958 return $cannot_archive;
960 my $max_log_age = max(map {$config{remove_age} - -M $_}
961 $log_file, map {my $log = getbugcomponent($_,'log');
962 defined $log ? ($log) : ();
964 split / /, $status->{mergedwith}
966 if (not $param{days_until} and not $param{ignore_time}
969 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
970 return $cannot_archive;
972 # At this point, we have to get the versioning information for this bug.
973 # We examine the set of distribution tags. If a bug has no distribution
974 # tags set, we assume a default set, otherwise we use the tags the bug
977 # In cases where we are assuming a default set, if the severity
978 # is strong, we use the strong severity default; otherwise, we
979 # use the normal default.
981 # There must be fixed_versions for us to look at the versioning
983 my $min_fixed_time = time;
984 my $min_archive_days = 0;
985 if (@{$status->{fixed_versions}}) {
987 @dist_tags{@{$config{removal_distribution_tags}}} =
988 (1) x @{$config{removal_distribution_tags}};
990 for my $tag (split ' ', ($status->{keywords}||'')) {
991 next unless exists $config{distribution_aliases}{$tag};
992 next unless $dist_tags{$config{distribution_aliases}{$tag}};
993 $dists{$config{distribution_aliases}{$tag}} = 1;
995 if (not keys %dists) {
996 if (isstrongseverity($status->{severity})) {
997 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
998 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
1001 @dists{@{$config{removal_default_distribution_tags}}} =
1002 (1) x @{$config{removal_default_distribution_tags}};
1005 my %source_versions;
1006 my @sourceversions = get_versions(package => $status->{package},
1007 dist => [keys %dists],
1010 @source_versions{@sourceversions} = (1) x @sourceversions;
1011 # If the bug has not been fixed in the versions actually
1012 # distributed, then it cannot be archived.
1013 if ('found' eq max_buggy(bug => $param{bug},
1014 sourceversions => [keys %source_versions],
1015 found => $status->{found_versions},
1016 fixed => $status->{fixed_versions},
1017 version_cache => $version_cache,
1018 package => $status->{package},
1020 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
1021 return $cannot_archive;
1023 # Since the bug has at least been fixed in the architectures
1024 # that matters, we check to see how long it has been fixed.
1026 # If $param{ignore_time}, then we should ignore time.
1027 if ($param{ignore_time}) {
1028 return $param{days_until}?0:1;
1031 # To do this, we order the times from most recent to oldest;
1032 # when we come to the first found version, we stop.
1033 # If we run out of versions, we only report the time of the
1035 my %time_versions = get_versions(package => $status->{package},
1036 dist => [keys %dists],
1040 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
1041 my $buggy = buggy(bug => $param{bug},
1042 version => $version,
1043 found => $status->{found_versions},
1044 fixed => $status->{fixed_versions},
1045 version_cache => $version_cache,
1046 package => $status->{package},
1048 last if $buggy eq 'found';
1049 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
1051 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
1052 # if there are no versions in the archive at all, then
1053 # we can archive if enough days have passed
1056 # If $param{ignore_time}, then we should ignore time.
1057 if ($param{ignore_time}) {
1058 return $param{days_until}?0:1;
1060 # 6. at least 28 days have passed since the last action has occured or the bug was closed
1061 my $age = ceil($max_log_age);
1062 if ($age > 0 or $min_archive_days > 0) {
1063 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
1064 return $param{days_until}?max($age,$min_archive_days):0;
1067 return $param{days_until}?0:1;
1072 =head2 get_bug_status
1074 my $status = get_bug_status(bug => $nnn);
1076 my $status = get_bug_status($bug_num)
1082 =item bug -- scalar bug number
1084 =item status -- optional hashref of bug status as returned by readbug
1085 (can be passed to avoid rereading the bug information)
1087 =item bug_index -- optional tied index of bug status infomration;
1088 currently not correctly implemented.
1090 =item version -- optional version(s) to check package status at
1092 =item dist -- optional distribution(s) to check package status at
1094 =item arch -- optional architecture(s) to check package status at
1096 =item bugusertags -- optional hashref of bugusertags
1098 =item sourceversion -- optional arrayref of source/version; overrides
1099 dist, arch, and version. [The entries in this array must be in the
1100 "source/version" format.] Eventually this can be used to for caching.
1102 =item indicatesource -- if true, indicate which source packages this
1103 bug could belong to (or does belong to in the case of bugs assigned to
1104 a source package). Defaults to true.
1108 Note: Currently the version information is cached; this needs to be
1109 changed before using this function in long lived programs.
1113 Currently returns a hashref of status with the following keys.
1117 =item id -- bug number
1119 =item bug_num -- duplicate of id
1121 =item keywords -- tags set on the bug, including usertags if bugusertags passed.
1123 =item tags -- duplicate of keywords
1125 =item package -- name of package that the bug is assigned to
1127 =item severity -- severity of the bug
1129 =item pending -- pending state of the bug; one of following possible
1130 values; values listed later have precedence if multiple conditions are
1135 =item pending -- default state
1137 =item forwarded -- bug has been forwarded
1139 =item pending-fixed -- bug is tagged pending
1141 =item fixed -- bug is tagged fixed
1143 =item absent -- bug does not apply to this distribution/architecture
1145 =item done -- bug is resolved in this distribution/architecture
1149 =item location -- db-h or archive; the location in the filesystem
1151 =item subject -- title of the bug
1153 =item last_modified -- epoch that the bug was last modified
1155 =item date -- epoch that the bug was filed
1157 =item originator -- bug reporter
1159 =item log_modified -- epoch that the log file was last modified
1161 =item msgid -- Message id of the original bug report
1166 Other key/value pairs are returned but are not currently documented here.
1170 sub get_bug_status {
1174 my %param = validate_with(params => \@_,
1175 spec => {bug => {type => SCALAR,
1178 status => {type => HASHREF,
1181 bug_index => {type => OBJECT,
1184 version => {type => SCALAR|ARRAYREF,
1187 dist => {type => SCALAR|ARRAYREF,
1190 arch => {type => SCALAR|ARRAYREF,
1193 bugusertags => {type => HASHREF,
1196 sourceversions => {type => ARRAYREF,
1199 indicatesource => {type => BOOLEAN,
1206 if (defined $param{bug_index} and
1207 exists $param{bug_index}{$param{bug}}) {
1208 %status = %{ $param{bug_index}{$param{bug}} };
1209 $status{pending} = $status{ status };
1210 $status{id} = $param{bug};
1213 if (defined $param{status}) {
1214 %status = %{$param{status}};
1217 my $location = getbuglocation($param{bug}, 'summary');
1218 return {} if not defined $location or not length $location;
1219 %status = %{ readbug( $param{bug}, $location ) };
1221 $status{id} = $param{bug};
1223 if (defined $param{bugusertags}{$param{bug}}) {
1224 $status{keywords} = "" unless defined $status{keywords};
1225 $status{keywords} .= " " unless $status{keywords} eq "";
1226 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
1228 $status{tags} = $status{keywords};
1229 my %tags = map { $_ => 1 } split ' ', $status{tags};
1231 $status{package} = '' if not defined $status{package};
1232 $status{"package"} =~ s/\s*$//;
1234 $status{source} = binary_to_source(binary=>[split /\s*,\s*/, $status{package}],
1238 $status{"package"} = 'unknown' if ($status{"package"} eq '');
1239 $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
1241 $status{"pending"} = 'pending';
1242 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
1243 $status{"pending"} = 'pending-fixed' if ($tags{pending});
1244 $status{"pending"} = 'fixed' if ($tags{fixed});
1247 my $presence = bug_presence(status => \%status,
1248 map{(exists $param{$_})?($_,$param{$_}):()}
1249 qw(bug sourceversions arch dist version found fixed package)
1251 if (defined $presence) {
1252 if ($presence eq 'fixed') {
1253 $status{pending} = 'done';
1255 elsif ($presence eq 'absent') {
1256 $status{pending} = 'absent';
1264 my $precence = bug_presence(bug => nnn,
1268 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
1269 is found, absent, fixed, or no information is available in the
1270 distribution (dist) and/or architecture (arch) specified.
1277 =item bug -- scalar bug number
1279 =item status -- optional hashref of bug status as returned by readbug
1280 (can be passed to avoid rereading the bug information)
1282 =item bug_index -- optional tied index of bug status infomration;
1283 currently not correctly implemented.
1285 =item version -- optional version to check package status at
1287 =item dist -- optional distribution to check package status at
1289 =item arch -- optional architecture to check package status at
1291 =item sourceversion -- optional arrayref of source/version; overrides
1292 dist, arch, and version. [The entries in this array must be in the
1293 "source/version" format.] Eventually this can be used to for caching.
1300 my %param = validate_with(params => \@_,
1301 spec => {bug => {type => SCALAR,
1304 status => {type => HASHREF,
1307 version => {type => SCALAR|ARRAYREF,
1310 dist => {type => SCALAR|ARRAYREF,
1313 arch => {type => SCALAR|ARRAYREF,
1316 sourceversions => {type => ARRAYREF,
1322 if (defined $param{status}) {
1323 %status = %{$param{status}};
1326 my $location = getbuglocation($param{bug}, 'summary');
1327 return {} if not length $location;
1328 %status = %{ readbug( $param{bug}, $location ) };
1332 my $pseudo_desc = getpseudodesc();
1333 if (not exists $param{sourceversions}) {
1335 # pseudopackages do not have source versions by definition.
1336 if (exists $pseudo_desc->{$status{package}}) {
1339 elsif (defined $param{version}) {
1340 foreach my $arch (make_list($param{arch})) {
1341 for my $package (split /\s*,\s*/, $status{package}) {
1342 my @temp = makesourceversions($package,
1344 make_list($param{version})
1346 @sourceversions{@temp} = (1) x @temp;
1349 } elsif (defined $param{dist}) {
1350 my %affects_distribution_tags;
1351 @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
1352 (1) x @{$config{affects_distribution_tags}};
1353 my $some_distributions_disallowed = 0;
1354 my %allowed_distributions;
1355 for my $tag (split ' ', ($status{keywords}||'')) {
1356 if (exists $config{distribution_aliases}{$tag} and
1357 exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
1358 $some_distributions_disallowed = 1;
1359 $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
1361 elsif (exists $affects_distribution_tags{$tag}) {
1362 $some_distributions_disallowed = 1;
1363 $allowed_distributions{$tag} = 1;
1366 my @archs = make_list(exists $param{arch}?$param{arch}:());
1367 GET_SOURCE_VERSIONS:
1368 foreach my $arch (@archs) {
1369 for my $package (split /\s*,\s*/, $status{package}) {
1372 if ($package =~ /^src:(.+)$/) {
1376 foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
1377 # if some distributions are disallowed,
1378 # and this isn't an allowed
1379 # distribution, then we ignore this
1380 # distribution for the purposees of
1382 if ($some_distributions_disallowed and
1383 not exists $allowed_distributions{$dist}) {
1386 push @versions, get_versions(package => $package,
1388 ($source?(arch => 'source'):
1389 (defined $arch?(arch => $arch):())),
1392 next unless @versions;
1393 my @temp = make_source_versions(package => $package,
1395 versions => \@versions,
1397 @sourceversions{@temp} = (1) x @temp;
1400 # this should really be split out into a subroutine,
1401 # but it'd touch so many things currently, that we fake
1402 # it; it's needed to properly handle bugs which are
1403 # erroneously assigned to the binary package, and we'll
1404 # probably have it go away eventually.
1405 if (not keys %sourceversions and (not @archs or defined $archs[0])) {
1407 goto GET_SOURCE_VERSIONS;
1411 # TODO: This should probably be handled further out for efficiency and
1412 # for more ease of distinguishing between pkg= and src= queries.
1413 # DLA: src= queries should just pass arch=source, and they'll be happy.
1414 @sourceversions = keys %sourceversions;
1417 @sourceversions = @{$param{sourceversions}};
1419 my $maxbuggy = 'undef';
1420 if (@sourceversions) {
1421 $maxbuggy = max_buggy(bug => $param{bug},
1422 sourceversions => \@sourceversions,
1423 found => $status{found_versions},
1424 fixed => $status{fixed_versions},
1425 package => $status{package},
1426 version_cache => $version_cache,
1429 elsif (defined $param{dist} and
1430 not exists $pseudo_desc->{$status{package}}) {
1433 if (length($status{done}) and
1434 (not @sourceversions or not @{$status{fixed_versions}})) {
1449 =item bug -- scalar bug number
1451 =item sourceversion -- optional arrayref of source/version; overrides
1452 dist, arch, and version. [The entries in this array must be in the
1453 "source/version" format.] Eventually this can be used to for caching.
1457 Note: Currently the version information is cached; this needs to be
1458 changed before using this function in long lived programs.
1463 my %param = validate_with(params => \@_,
1464 spec => {bug => {type => SCALAR,
1467 sourceversions => {type => ARRAYREF,
1470 found => {type => ARRAYREF,
1473 fixed => {type => ARRAYREF,
1476 package => {type => SCALAR,
1478 version_cache => {type => HASHREF,
1483 # Resolve bugginess states (we might be looking at multiple
1484 # architectures, say). Found wins, then fixed, then absent.
1485 my $maxbuggy = 'absent';
1486 for my $package (split /\s*,\s*/, $param{package}) {
1487 for my $version (@{$param{sourceversions}}) {
1488 my $buggy = buggy(bug => $param{bug},
1489 version => $version,
1490 found => $param{found},
1491 fixed => $param{fixed},
1492 version_cache => $param{version_cache},
1493 package => $package,
1495 if ($buggy eq 'found') {
1497 } elsif ($buggy eq 'fixed') {
1498 $maxbuggy = 'fixed';
1515 Returns the output of Debbugs::Versions::buggy for a particular
1516 package, version and found/fixed set. Automatically turns found, fixed
1517 and version into source/version strings.
1519 Caching can be had by using the version_cache, but no attempt to check
1520 to see if the on disk information is more recent than the cache is
1521 made. [This will need to be fixed for long-lived processes.]
1526 my %param = validate_with(params => \@_,
1527 spec => {bug => {type => SCALAR,
1530 found => {type => ARRAYREF,
1533 fixed => {type => ARRAYREF,
1536 version_cache => {type => HASHREF,
1539 package => {type => SCALAR,
1541 version => {type => SCALAR,
1545 my @found = @{$param{found}};
1546 my @fixed = @{$param{fixed}};
1547 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1548 # We have non-source version versions
1549 @found = makesourceversions($param{package},undef,
1552 @fixed = makesourceversions($param{package},undef,
1556 if ($param{version} !~ m{/}) {
1557 my ($version) = makesourceversions($param{package},undef,
1560 $param{version} = $version if defined $version;
1562 # Figure out which source packages we need
1564 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1565 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1566 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1567 $param{version} =~ m{/};
1569 if (not defined $param{version_cache} or
1570 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1571 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1572 foreach my $source (keys %sources) {
1573 my $srchash = substr $source, 0, 1;
1574 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1575 if (not defined $version_fh) {
1576 # We only want to warn if it's a package which actually has a maintainer
1577 my $maints = getmaintainers();
1578 next if not exists $maints->{$source};
1579 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1582 $version->load($version_fh);
1584 if (defined $param{version_cache}) {
1585 $param{version_cache}{join(',',sort keys %sources)} = $version;
1589 $version = $param{version_cache}{join(',',sort keys %sources)};
1591 return $version->buggy($param{version},\@found,\@fixed);
1594 sub isstrongseverity {
1595 my $severity = shift;
1596 $severity = $config{default_severity} if
1597 not defined $severity or $severity eq '';
1598 return grep { $_ eq $severity } @{$config{strong_severities}};
1602 =head1 PRIVATE FUNCTIONS
1606 sub update_realtime {
1607 my ($file, %bugs) = @_;
1609 # update realtime index.db
1611 return () unless keys %bugs;
1612 my $idx_old = IO::File->new($file,'r')
1613 or die "Couldn't open ${file}: $!";
1614 my $idx_new = IO::File->new($file.'.new','w')
1615 or die "Couldn't open ${file}.new: $!";
1617 my $min_bug = min(keys %bugs);
1621 while($line = <$idx_old>) {
1622 @line = split /\s/, $line;
1623 # Two cases; replacing existing line or adding new line
1624 if (exists $bugs{$line[1]}) {
1625 my $new = $bugs{$line[1]};
1626 delete $bugs{$line[1]};
1627 $min_bug = min(keys %bugs);
1628 if ($new eq "NOCHANGE") {
1629 print {$idx_new} $line;
1630 $changed_bugs{$line[1]} = $line;
1631 } elsif ($new eq "REMOVE") {
1632 $changed_bugs{$line[1]} = $line;
1634 print {$idx_new} $new;
1635 $changed_bugs{$line[1]} = $line;
1639 while ($line[1] > $min_bug) {
1640 print {$idx_new} $bugs{$min_bug};
1641 delete $bugs{$min_bug};
1642 last unless keys %bugs;
1643 $min_bug = min(keys %bugs);
1645 print {$idx_new} $line;
1647 last unless keys %bugs;
1649 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1651 print {$idx_new} <$idx_old>;
1656 rename("$file.new", $file);
1658 return %changed_bugs;
1661 sub bughook_archive {
1663 filelock("$config{spool_dir}/debbugs.trace.lock");
1664 appendfile("$config{spool_dir}/debbugs.trace","archive ".join(',',@refs)."\n");
1665 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1666 map{($_,'REMOVE')} @refs);
1667 update_realtime("$config{spool_dir}/index.archive.realtime",
1673 my ( $type, %bugs_temp ) = @_;
1674 filelock("$config{spool_dir}/debbugs.trace.lock");
1677 for my $bug (keys %bugs_temp) {
1678 my $data = $bugs_temp{$bug};
1679 appendfile("$config{spool_dir}/debbugs.trace","$type $bug\n",makestatus($data, 1));
1681 my $whendone = "open";
1682 my $severity = $config{default_severity};
1683 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1684 $pkglist =~ s/^,+//;
1685 $pkglist =~ s/,+$//;
1686 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1687 $whendone = "done" if defined $data->{done} and length $data->{done};
1688 $severity = $data->{severity} if length $data->{severity};
1690 my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1691 $pkglist, $bug, $data->{date}, $whendone,
1692 $data->{originator}, $severity, $data->{keywords};
1695 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);