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
38 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
39 use Exporter qw(import);
41 use Params::Validate qw(validate_with :types);
42 use Debbugs::Common qw(:util :lock :quit :misc);
44 use Debbugs::Config qw(:config);
45 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
46 use Debbugs::Packages qw(makesourceversions make_source_versions getversions get_versions binary_to_source);
47 use Debbugs::Versions;
48 use Debbugs::Versions::Dpkg;
50 use File::Copy qw(copy);
51 use Encode qw(decode encode is_utf8);
53 use Storable qw(dclone);
54 use List::AllUtils qw(min max);
60 $DEBUG = 0 unless defined $DEBUG;
63 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
64 qw(isstrongseverity bug_presence split_status_fields),
66 read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
67 qw(lock_read_all_merged_bugs),
69 write => [qw(writebug makestatus unlockwritebug)],
71 versions => [qw(addfoundversions addfixedversions),
72 qw(removefoundversions removefixedversions)
74 hook => [qw(bughook bughook_archive)],
75 indexdb => [qw(generate_index_db_line)],
76 fields => [qw(%fields)],
79 Exporter::export_ok_tags(keys %EXPORT_TAGS);
80 $EXPORT_TAGS{all} = [@EXPORT_OK];
86 readbug($bug_num,$location)
89 Reads a summary file from the archive given a bug number and a bug
90 location. Valid locations are those understood by L</getbugcomponent>
94 # these probably shouldn't be imported by most people, but
95 # Debbugs::Control needs them, so they're now exportable
96 our %fields = (originator => 'submitter',
99 msgid => 'message-id',
100 'package' => 'package',
103 forwarded => 'forwarded-to',
104 mergedwith => 'merged-with',
105 severity => 'severity',
107 found_versions => 'found-in',
108 found_date => 'found-date',
109 fixed_versions => 'fixed-in',
110 fixed_date => 'fixed-date',
112 blockedby => 'blocked-by',
113 unarchived => 'unarchived',
114 summary => 'summary',
115 outlook => 'outlook',
116 affects => 'affects',
120 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
121 my @rfc1522_fields = qw(originator subject done forwarded owner);
124 return read_bug(bug => $_[0],
125 (@_ > 1)?(location => $_[1]):()
131 read_bug(bug => $bug_num,
132 location => 'archive',
134 read_bug(summary => 'path/to/bugnum.summary');
137 A more complete function than readbug; it enables you to pass a full
138 path to the summary file instead of the bug number and/or location.
144 =item bug -- the bug number
146 =item location -- optional location which is passed to getbugcomponent
148 =item summary -- complete path to the .summary file which will be read
150 =item lock -- whether to obtain a lock for the bug to prevent
151 something modifying it while the bug has been read. You B<must> call
152 C<unfilelock();> if something not undef is returned from read_bug.
154 =item locks -- hashref of already obtained locks; incremented as new
155 locks are needed, and decremented as locks are released on particular
160 One of C<bug> or C<summary> must be passed. This function will return
161 undef on failure, and will die if improper arguments are passed.
170 {bug => {type => SCALAR,
172 # something really stupid passes negative bugnumbers
175 location => {type => SCALAR|UNDEF,
178 summary => {type => SCALAR,
181 lock => {type => BOOLEAN,
184 locks => {type => HASHREF,
188 my %param = validate_with(params => \@_,
191 die "One of bug or summary must be passed to read_bug"
192 if not exists $param{bug} and not exists $param{summary};
196 if (not defined $param{summary}) {
198 ($lref,$location) = @param{qw(bug location)};
199 if (not defined $location) {
200 $location = getbuglocation($lref,'summary');
201 return undef if not defined $location;
203 $status = getbugcomponent($lref, 'summary', $location);
204 $log = getbugcomponent($lref, 'log' , $location);
205 return undef unless defined $status;
206 return undef if not -e $status;
209 $status = $param{summary};
211 $log =~ s/\.summary$/.log/;
212 ($location) = $status =~ m/(db-h|db|archive)/;
213 ($param{bug}) = $status =~ m/(\d+)\.summary$/;
216 filelock("$config{spool_dir}/lock/$param{bug}",exists $param{locks}?$param{locks}:());
218 my $status_fh = IO::File->new($status, 'r');
219 if (not defined $status_fh) {
220 warn "Unable to open $status for reading: $!";
222 unfilelock(exists $param{locks}?$param{locks}:());
226 binmode($status_fh,':encoding(UTF-8)');
233 while (<$status_fh>) {
236 $version = $1 if /^Format-Version: ([0-9]+)/i;
239 # Version 3 is the latest format version currently supported.
241 warn "Unsupported status version '$version'";
243 unfilelock(exists $param{locks}?$param{locks}:());
248 my %namemap = reverse %fields;
249 for my $line (@lines) {
250 if ($line =~ /(\S+?): (.*)/) {
251 my ($name, $value) = (lc $1, $2);
252 # this is a bit of a hack; we should never, ever have \r
253 # or \n in the fields of status. Kill them off here.
254 # [Eventually, this should be superfluous.]
255 $value =~ s/[\r\n]//g;
256 $data{$namemap{$name}} = $value if exists $namemap{$name};
259 for my $field (keys %fields) {
260 $data{$field} = '' unless exists $data{$field};
263 for my $field (@rfc1522_fields) {
264 $data{$field} = decode_rfc1522($data{$field});
267 $data{severity} = $config{default_severity} if $data{severity} eq '';
268 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
269 $data{$field} = [split ' ', $data{$field}];
271 for my $field (qw(found fixed)) {
272 # create the found/fixed hashes which indicate when a
273 # particular version was marked found or marked fixed.
274 @{$data{$field}}{@{$data{"${field}_versions"}}} =
275 (('') x (@{$data{"${field}_versions"}} - @{$data{"${field}_date"}}),
276 @{$data{"${field}_date"}});
279 my $status_modified = (stat($status))[9];
280 # Add log last modified time
281 $data{log_modified} = (stat($log))[9] // (stat("${log}.gz"))[9];
282 $data{last_modified} = max($status_modified,$data{log_modified});
283 $data{location} = $location;
284 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
285 $data{bug_num} = $param{bug};
287 # mergedwith occasionally is sorted badly. Fix it to always be sorted by <=>
288 # and not include this bug
289 if (defined $data{mergedwith} and
293 grep { $_ != $data{bug_num}}
295 split / /, $data{mergedwith}
301 =head2 split_status_fields
303 my @data = split_status_fields(@data);
305 Splits splittable status fields (like package, tags, blocks,
306 blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
307 passed @data intact using dclone.
309 In scalar context, returns only the first element of @data.
313 our $ditch_empty = sub{
315 my $splitter = shift @t;
316 return grep {length $_} map {split $splitter} @t;
319 my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
321 (package => \&splitpackages,
322 affects => \&splitpackages,
323 # Ideally we won't have to split source, but because some consumers of
324 # get_bug_status cannot handle arrayref, we will split it here.
325 source => \&splitpackages,
326 blocks => $ditch_empty_space,
327 blockedby => $ditch_empty_space,
328 # this isn't strictly correct, but we'll split both of them for
329 # the time being until we ditch all use of keywords everywhere
331 keywords => $ditch_empty_space,
332 tags => $ditch_empty_space,
333 found_versions => $ditch_empty_space,
334 fixed_versions => $ditch_empty_space,
335 mergedwith => $ditch_empty_space,
338 sub split_status_fields {
339 my @data = @{dclone(\@_)};
340 for my $data (@data) {
341 next if not defined $data;
342 croak "Passed an element which is not a hashref to split_status_field".ref($data) if
343 not (ref($data) and ref($data) eq 'HASH');
344 for my $field (keys %{$data}) {
345 next unless defined $data->{$field};
346 if (exists $split_fields{$field}) {
347 next if ref($data->{$field});
349 if (ref($split_fields{$field}) eq 'CODE') {
350 @elements = &{$split_fields{$field}}($data->{$field});
352 elsif (not ref($split_fields{$field}) or
353 UNIVERSAL::isa($split_fields{$field},'Regex')
355 @elements = split $split_fields{$field}, $data->{$field};
357 $data->{$field} = \@elements;
361 return wantarray?@data:$data[0];
364 =head2 join_status_fields
366 my @data = join_status_fields(@data);
368 Handles joining the splitable status fields. (Basically, the inverse
369 of split_status_fields.
371 Primarily called from makestatus, but may be useful for other
372 functions after calling split_status_fields (or for legacy functions
373 if we transition to split fields by default).
377 sub join_status_fields {
384 found_versions => ' ',
385 fixed_versions => ' ',
390 my @data = @{dclone(\@_)};
391 for my $data (@data) {
392 next if not defined $data;
393 croak "Passed an element which is not a hashref to split_status_field: ".
395 if ref($data) ne 'HASH';
396 for my $field (keys %{$data}) {
397 next unless defined $data->{$field};
398 next unless ref($data->{$field}) eq 'ARRAY';
399 next unless exists $join_fields{$field};
400 $data->{$field} = join($join_fields{$field},@{$data->{$field}});
403 return wantarray?@data:$data[0];
409 lockreadbug($bug_num,$location)
411 Performs a filelock, then reads the bug; the bug is unlocked if the
412 return is undefined, otherwise, you need to call unfilelock or
415 See readbug above for information on what this returns
420 my ($lref, $location) = @_;
421 return read_bug(bug => $lref, location => $location, lock => 1);
424 =head2 lockreadbugmerge
426 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
428 Performs a filelock, then reads the bug. If the bug is merged, locks
429 the merge lock. Returns a list of the number of locks and the bug
434 sub lockreadbugmerge {
435 my $data = lockreadbug(@_);
436 if (not defined $data) {
439 if (not length $data->{mergedwith}) {
443 filelock("$config{spool_dir}/lock/merge");
444 $data = lockreadbug(@_);
445 if (not defined $data) {
452 =head2 lock_read_all_merged_bugs
454 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
456 Performs a filelock, then reads the bug passed. If the bug is merged,
457 locks the merge lock, then reads and locks all of the other merged
458 bugs. Returns a list of the number of locks and the bug data for all
461 Will also return undef if any of the merged bugs failed to be read,
462 even if all of the others were read properly.
466 sub lock_read_all_merged_bugs {
467 my %param = validate_with(params => \@_,
468 spec => {bug => {type => SCALAR,
471 location => {type => SCALAR,
474 locks => {type => HASHREF,
480 my @data = read_bug(bug => $param{bug},
482 exists $param{location} ? (location => $param{location}):(),
483 exists $param{locks} ? (locks => $param{locks}):(),
485 if (not @data or not defined $data[0]) {
489 if (not length $data[0]->{mergedwith}) {
490 return ($locks,@data);
492 unfilelock(exists $param{locks}?$param{locks}:());
494 filelock("$config{spool_dir}/lock/merge",exists $param{locks}?$param{locks}:());
496 @data = read_bug(bug => $param{bug},
498 exists $param{location} ? (location => $param{location}):(),
499 exists $param{locks} ? (locks => $param{locks}):(),
501 if (not @data or not defined $data[0]) {
502 unfilelock(exists $param{locks}?$param{locks}:()); #for merge lock above
507 my @bugs = split / /, $data[0]->{mergedwith};
508 push @bugs, $param{bug};
509 for my $bug (@bugs) {
511 if ($bug != $param{bug}) {
513 read_bug(bug => $bug,
515 exists $param{location} ? (location => $param{location}):(),
516 exists $param{locks} ? (locks => $param{locks}):(),
518 if (not defined $newdata) {
520 unfilelock(exists $param{locks}?$param{locks}:());
523 warn "Unable to read bug: $bug while handling merged bug: $param{bug}";
528 # perform a sanity check to make sure that the merged bugs
529 # are all merged with eachother
530 # We do a cmp sort instead of an <=> sort here, because that's
533 join(' ',grep {$_ != $bug }
536 if ($newdata->{mergedwith} ne $expectmerge) {
538 unfilelock(exists $param{locks}?$param{locks}:());
540 die "Bug $param{bug} mergedwith differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
544 return ($locks,@data);
549 my $new_bug_num = new_bug(copy => $data->{bug_num});
551 Creates a new bug and returns the new bug number upon success.
559 validate_with(params => \@_,
560 spec => {copy => {type => SCALAR,
566 filelock("nextnumber.lock");
567 my $nn_fh = IO::File->new("nextnumber",'r') or
568 die "Unable to open nextnuber for reading: $!";
571 ($nn) = $nn =~ m/^(\d+)\n$/ or die "Bad format of nextnumber; is not exactly ".'^\d+\n$';
573 overwritefile("nextnumber",
576 my $nn_hash = get_hashname($nn);
578 my $c_hash = get_hashname($param{copy});
579 for my $file (qw(log status summary report)) {
580 copy("db-h/$c_hash/$param{copy}.$file",
581 "db-h/$nn_hash/${nn}.$file")
585 for my $file (qw(log status summary report)) {
586 overwritefile("db-h/$nn_hash/${nn}.$file",
591 # this probably needs to be munged to do something more elegant
592 # &bughook('new', $clone, $data);
599 my @v1fieldorder = qw(originator date subject msgid package
600 keywords done forwarded mergedwith severity);
604 my $content = makestatus($status,$version)
605 my $content = makestatus($status);
607 Creates the content for a status file based on the $status hashref
610 Really only useful for writebug
612 Currently defaults to version 2 (non-encoded rfc1522 names) but will
613 eventually default to version 3. If you care, you should specify a
619 my ($data,$version) = @_;
620 $version = 3 unless defined $version;
624 my %newdata = %$data;
625 for my $field (qw(found fixed)) {
626 if (exists $newdata{$field}) {
627 $newdata{"${field}_date"} =
628 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
631 %newdata = %{join_status_fields(\%newdata)};
633 %newdata = encode_utf8_structure(%newdata);
636 for my $field (@rfc1522_fields) {
637 $newdata{$field} = encode_rfc1522($newdata{$field});
641 # this is a bit of a hack; we should never, ever have \r or \n in
642 # the fields of status. Kill them off here. [Eventually, this
643 # should be superfluous.]
644 for my $field (keys %newdata) {
645 $newdata{$field} =~ s/[\r\n]//g if defined $newdata{$field};
649 for my $field (@v1fieldorder) {
650 if (exists $newdata{$field} and defined $newdata{$field}) {
651 $contents .= "$newdata{$field}\n";
656 } elsif ($version == 2 or $version == 3) {
657 # Version 2 or 3. Add a file format version number for the sake of
658 # further extensibility in the future.
659 $contents .= "Format-Version: $version\n";
660 for my $field (keys %fields) {
661 if (exists $newdata{$field} and defined $newdata{$field}
662 and $newdata{$field} ne '') {
663 # Output field names in proper case, e.g. 'Merged-With'.
664 my $properfield = $fields{$field};
665 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
666 my $data = $newdata{$field};
667 $contents .= "$properfield: $data\n";
676 writebug($bug_num,$status,$location,$minversion,$disablebughook)
678 Writes the bug status and summary files out.
680 Skips writing out a status file if minversion is 2
682 Does not call bughook if disablebughook is true.
687 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
690 my %outputs = (1 => 'status', 3 => 'summary');
691 for my $version (keys %outputs) {
692 next if defined $minversion and $version < $minversion;
693 my $status = getbugcomponent($ref, $outputs{$version}, $location);
694 die "can't find location for $ref" unless defined $status;
697 open $sfh,">","$status.new" or
698 die "opening $status.new: $!";
701 open $sfh,">","$status.new" or
702 die "opening $status.new: $!";
704 print {$sfh} makestatus($data, $version) or
705 die "writing $status.new: $!";
706 close($sfh) or die "closing $status.new: $!";
712 rename("$status.new",$status) || die "installing new $status: $!";
715 # $disablebughook is a bit of a hack to let format migration scripts use
716 # this function rather than having to duplicate it themselves.
717 &bughook($change,$ref,$data) unless $disablebughook;
720 =head2 unlockwritebug
722 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
724 Writes a bug, then calls unfilelock; see writebug for what these
736 The following functions are exported with the :versions tag
738 =head2 addfoundversions
740 addfoundversions($status,$package,$version,$isbinary);
742 All use of this should be phased out in favor of Debbugs::Control::fixed/found
747 sub addfoundversions {
751 my $isbinary = shift;
752 return unless defined $version;
753 undef $package if defined $package and $package =~ m[(?:\s|/)];
754 my $source = $package;
755 if (defined $package and $package =~ s/^src://) {
760 if (defined $package and $isbinary) {
761 my @srcinfo = binary_to_source(binary => $package,
762 version => $version);
764 # We know the source package(s). Use a fully-qualified version.
765 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
768 # Otherwise, an unqualified version will have to do.
772 # Strip off various kinds of brain-damage.
774 $version =~ s/ *\(.*\)//;
775 $version =~ s/ +[A-Za-z].*//;
777 foreach my $ver (split /[,\s]+/, $version) {
778 my $sver = defined($source) ? "$source/$ver" : '';
779 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
780 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
782 @{$data->{fixed_versions}} =
783 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
787 =head2 removefoundversions
789 removefoundversions($data,$package,$versiontoremove)
791 Removes found versions from $data
793 If a version is fully qualified (contains /) only versions matching
794 exactly are removed. Otherwise, all versions matching the version
797 Currently $package and $isbinary are entirely ignored, but accepted
798 for backwards compatibility.
802 sub removefoundversions {
806 my $isbinary = shift;
807 return unless defined $version;
809 foreach my $ver (split /[,\s]+/, $version) {
811 # fully qualified version
812 @{$data->{found_versions}} =
814 @{$data->{found_versions}};
817 # non qualified version; delete all matchers
818 @{$data->{found_versions}} =
819 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
820 @{$data->{found_versions}};
826 sub addfixedversions {
830 my $isbinary = shift;
831 return unless defined $version;
832 undef $package if defined $package and $package =~ m[(?:\s|/)];
833 my $source = $package;
835 if (defined $package and $isbinary) {
836 my @srcinfo = binary_to_source(binary => $package,
837 version => $version);
839 # We know the source package(s). Use a fully-qualified version.
840 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
843 # Otherwise, an unqualified version will have to do.
847 # Strip off various kinds of brain-damage.
849 $version =~ s/ *\(.*\)//;
850 $version =~ s/ +[A-Za-z].*//;
852 foreach my $ver (split /[,\s]+/, $version) {
853 my $sver = defined($source) ? "$source/$ver" : '';
854 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
855 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
857 @{$data->{found_versions}} =
858 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
862 sub removefixedversions {
866 my $isbinary = shift;
867 return unless defined $version;
869 foreach my $ver (split /[,\s]+/, $version) {
871 # fully qualified version
872 @{$data->{fixed_versions}} =
874 @{$data->{fixed_versions}};
877 # non qualified version; delete all matchers
878 @{$data->{fixed_versions}} =
879 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
880 @{$data->{fixed_versions}};
891 Split a package string from the status file into a list of package names.
897 return unless defined $pkgs;
898 return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
902 =head2 bug_archiveable
904 bug_archiveable(bug => $bug_num);
910 =item bug -- bug number (required)
912 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
914 =item version -- Debbugs::Version information (optional)
916 =item days_until -- return days until the bug can be archived
920 Returns 1 if the bug can be archived
921 Returns 0 if the bug cannot be archived
923 If days_until is true, returns the number of days until the bug can be
924 archived, -1 if it cannot be archived. 0 means that the bug can be
925 archived the next time the archiver runs.
927 Returns undef on failure.
931 # This will eventually need to be fixed before we start using mod_perl
932 our $version_cache = {};
934 my %param = validate_with(params => \@_,
935 spec => {bug => {type => SCALAR,
938 status => {type => HASHREF,
941 days_until => {type => BOOLEAN,
944 ignore_time => {type => BOOLEAN,
949 # This is what we return if the bug cannot be archived.
950 my $cannot_archive = $param{days_until}?-1:0;
951 # read the status information
952 my $status = $param{status};
953 if (not exists $param{status} or not defined $status) {
954 $status = read_bug(bug=>$param{bug});
955 if (not defined $status) {
956 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
960 # Bugs can be archived if they are
962 if (not defined $status->{done} or not length $status->{done}) {
963 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
964 return $cannot_archive
966 # Check to make sure that the bug has none of the unremovable tags set
967 if (@{$config{removal_unremovable_tags}}) {
968 for my $tag (split ' ', ($status->{keywords}||'')) {
969 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
970 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
971 return $cannot_archive;
976 # If we just are checking if the bug can be archived, we'll not even bother
977 # checking the versioning information if the bug has been -done for less than 28 days.
978 my $log_file = getbugcomponent($param{bug},'log');
979 if (not defined $log_file) {
980 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
981 return $cannot_archive;
983 my $max_log_age = max(map {$config{remove_age} - -M $_}
984 $log_file, map {my $log = getbugcomponent($_,'log');
985 defined $log ? ($log) : ();
987 split / /, $status->{mergedwith}
989 if (not $param{days_until} and not $param{ignore_time}
992 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
993 return $cannot_archive;
995 # At this point, we have to get the versioning information for this bug.
996 # We examine the set of distribution tags. If a bug has no distribution
997 # tags set, we assume a default set, otherwise we use the tags the bug
1000 # In cases where we are assuming a default set, if the severity
1001 # is strong, we use the strong severity default; otherwise, we
1002 # use the normal default.
1004 # There must be fixed_versions for us to look at the versioning
1006 my $min_fixed_time = time;
1007 my $min_archive_days = 0;
1008 if (@{$status->{fixed_versions}}) {
1010 @dist_tags{@{$config{removal_distribution_tags}}} =
1011 (1) x @{$config{removal_distribution_tags}};
1013 for my $tag (split ' ', ($status->{keywords}||'')) {
1014 next unless exists $config{distribution_aliases}{$tag};
1015 next unless $dist_tags{$config{distribution_aliases}{$tag}};
1016 $dists{$config{distribution_aliases}{$tag}} = 1;
1018 if (not keys %dists) {
1019 if (isstrongseverity($status->{severity})) {
1020 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
1021 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
1024 @dists{@{$config{removal_default_distribution_tags}}} =
1025 (1) x @{$config{removal_default_distribution_tags}};
1028 my %source_versions;
1029 my @sourceversions = get_versions(package => $status->{package},
1030 dist => [keys %dists],
1033 @source_versions{@sourceversions} = (1) x @sourceversions;
1034 # If the bug has not been fixed in the versions actually
1035 # distributed, then it cannot be archived.
1036 if ('found' eq max_buggy(bug => $param{bug},
1037 sourceversions => [keys %source_versions],
1038 found => $status->{found_versions},
1039 fixed => $status->{fixed_versions},
1040 version_cache => $version_cache,
1041 package => $status->{package},
1043 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
1044 return $cannot_archive;
1046 # Since the bug has at least been fixed in the architectures
1047 # that matters, we check to see how long it has been fixed.
1049 # If $param{ignore_time}, then we should ignore time.
1050 if ($param{ignore_time}) {
1051 return $param{days_until}?0:1;
1054 # To do this, we order the times from most recent to oldest;
1055 # when we come to the first found version, we stop.
1056 # If we run out of versions, we only report the time of the
1058 my %time_versions = get_versions(package => $status->{package},
1059 dist => [keys %dists],
1063 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
1064 my $buggy = buggy(bug => $param{bug},
1065 version => $version,
1066 found => $status->{found_versions},
1067 fixed => $status->{fixed_versions},
1068 version_cache => $version_cache,
1069 package => $status->{package},
1071 last if $buggy eq 'found';
1072 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
1074 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
1075 # if there are no versions in the archive at all, then
1076 # we can archive if enough days have passed
1079 # If $param{ignore_time}, then we should ignore time.
1080 if ($param{ignore_time}) {
1081 return $param{days_until}?0:1;
1083 # 6. at least 28 days have passed since the last action has occured or the bug was closed
1084 my $age = ceil($max_log_age);
1085 if ($age > 0 or $min_archive_days > 0) {
1086 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
1087 return $param{days_until}?max($age,$min_archive_days):0;
1090 return $param{days_until}?0:1;
1095 =head2 get_bug_status
1097 my $status = get_bug_status(bug => $nnn);
1099 my $status = get_bug_status($bug_num)
1105 =item bug -- scalar bug number
1107 =item status -- optional hashref of bug status as returned by readbug
1108 (can be passed to avoid rereading the bug information)
1110 =item bug_index -- optional tied index of bug status infomration;
1111 currently not correctly implemented.
1113 =item version -- optional version(s) to check package status at
1115 =item dist -- optional distribution(s) to check package status at
1117 =item arch -- optional architecture(s) to check package status at
1119 =item bugusertags -- optional hashref of bugusertags
1121 =item sourceversion -- optional arrayref of source/version; overrides
1122 dist, arch, and version. [The entries in this array must be in the
1123 "source/version" format.] Eventually this can be used to for caching.
1125 =item indicatesource -- if true, indicate which source packages this
1126 bug could belong to (or does belong to in the case of bugs assigned to
1127 a source package). Defaults to true.
1131 Note: Currently the version information is cached; this needs to be
1132 changed before using this function in long lived programs.
1136 Currently returns a hashref of status with the following keys.
1140 =item id -- bug number
1142 =item bug_num -- duplicate of id
1144 =item keywords -- tags set on the bug, including usertags if bugusertags passed.
1146 =item tags -- duplicate of keywords
1148 =item package -- name of package that the bug is assigned to
1150 =item severity -- severity of the bug
1152 =item pending -- pending state of the bug; one of following possible
1153 values; values listed later have precedence if multiple conditions are
1158 =item pending -- default state
1160 =item forwarded -- bug has been forwarded
1162 =item pending-fixed -- bug is tagged pending
1164 =item fixed -- bug is tagged fixed
1166 =item absent -- bug does not apply to this distribution/architecture
1168 =item done -- bug is resolved in this distribution/architecture
1172 =item location -- db-h or archive; the location in the filesystem
1174 =item subject -- title of the bug
1176 =item last_modified -- epoch that the bug was last modified
1178 =item date -- epoch that the bug was filed
1180 =item originator -- bug reporter
1182 =item log_modified -- epoch that the log file was last modified
1184 =item msgid -- Message id of the original bug report
1189 Other key/value pairs are returned but are not currently documented here.
1193 sub get_bug_status {
1198 {bug => {type => SCALAR,
1201 status => {type => HASHREF,
1204 bug_index => {type => OBJECT,
1207 version => {type => SCALAR|ARRAYREF,
1210 dist => {type => SCALAR|ARRAYREF,
1213 arch => {type => SCALAR|ARRAYREF,
1216 bugusertags => {type => HASHREF,
1219 sourceversions => {type => ARRAYREF,
1222 indicatesource => {type => BOOLEAN,
1225 binary_to_source_cache => {type => HASHREF,
1229 my %param = validate_with(params => \@_,
1234 if (defined $param{bug_index} and
1235 exists $param{bug_index}{$param{bug}}) {
1236 %status = %{ $param{bug_index}{$param{bug}} };
1237 $status{pending} = $status{ status };
1238 $status{id} = $param{bug};
1241 if (defined $param{status}) {
1242 %status = %{$param{status}};
1245 my $location = getbuglocation($param{bug}, 'summary');
1246 return {} if not defined $location or not length $location;
1247 %status = %{ readbug( $param{bug}, $location ) };
1249 $status{id} = $param{bug};
1251 if (defined $param{bugusertags}{$param{bug}}) {
1252 $status{keywords} = "" unless defined $status{keywords};
1253 $status{keywords} .= " " unless $status{keywords} eq "";
1254 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
1256 $status{tags} = $status{keywords};
1257 my %tags = map { $_ => 1 } split ' ', $status{tags};
1259 $status{package} = '' if not defined $status{package};
1260 $status{"package"} =~ s/\s*$//;
1262 $status{source} = binary_to_source(binary=>[split /\s*,\s*/, $status{package}],
1264 exists $param{binary_to_source_cache}?
1265 (cache =>$param{binary_to_source_cache}):(),
1268 $status{"package"} = 'unknown' if ($status{"package"} eq '');
1269 $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
1271 $status{"pending"} = 'pending';
1272 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
1273 $status{"pending"} = 'pending-fixed' if ($tags{pending});
1274 $status{"pending"} = 'fixed' if ($tags{fixed});
1277 my $presence = bug_presence(status => \%status,
1278 map{(exists $param{$_})?($_,$param{$_}):()}
1279 qw(bug sourceversions arch dist version found fixed package)
1281 if (defined $presence) {
1282 if ($presence eq 'fixed') {
1283 $status{pending} = 'done';
1285 elsif ($presence eq 'absent') {
1286 $status{pending} = 'absent';
1294 my $precence = bug_presence(bug => nnn,
1298 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
1299 is found, absent, fixed, or no information is available in the
1300 distribution (dist) and/or architecture (arch) specified.
1307 =item bug -- scalar bug number
1309 =item status -- optional hashref of bug status as returned by readbug
1310 (can be passed to avoid rereading the bug information)
1312 =item bug_index -- optional tied index of bug status infomration;
1313 currently not correctly implemented.
1315 =item version -- optional version to check package status at
1317 =item dist -- optional distribution to check package status at
1319 =item arch -- optional architecture to check package status at
1321 =item sourceversion -- optional arrayref of source/version; overrides
1322 dist, arch, and version. [The entries in this array must be in the
1323 "source/version" format.] Eventually this can be used to for caching.
1330 my %param = validate_with(params => \@_,
1331 spec => {bug => {type => SCALAR,
1334 status => {type => HASHREF,
1337 version => {type => SCALAR|ARRAYREF,
1340 dist => {type => SCALAR|ARRAYREF,
1343 arch => {type => SCALAR|ARRAYREF,
1346 sourceversions => {type => ARRAYREF,
1352 if (defined $param{status}) {
1353 %status = %{$param{status}};
1356 my $location = getbuglocation($param{bug}, 'summary');
1357 return {} if not length $location;
1358 %status = %{ readbug( $param{bug}, $location ) };
1362 my $pseudo_desc = getpseudodesc();
1363 if (not exists $param{sourceversions}) {
1365 # pseudopackages do not have source versions by definition.
1366 if (exists $pseudo_desc->{$status{package}}) {
1369 elsif (defined $param{version}) {
1370 foreach my $arch (make_list($param{arch})) {
1371 for my $package (split /\s*,\s*/, $status{package}) {
1372 my @temp = makesourceversions($package,
1374 make_list($param{version})
1376 @sourceversions{@temp} = (1) x @temp;
1379 } elsif (defined $param{dist}) {
1380 my %affects_distribution_tags;
1381 @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
1382 (1) x @{$config{affects_distribution_tags}};
1383 my $some_distributions_disallowed = 0;
1384 my %allowed_distributions;
1385 for my $tag (split ' ', ($status{keywords}||'')) {
1386 if (exists $config{distribution_aliases}{$tag} and
1387 exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
1388 $some_distributions_disallowed = 1;
1389 $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
1391 elsif (exists $affects_distribution_tags{$tag}) {
1392 $some_distributions_disallowed = 1;
1393 $allowed_distributions{$tag} = 1;
1396 my @archs = make_list(exists $param{arch}?$param{arch}:());
1397 GET_SOURCE_VERSIONS:
1398 foreach my $arch (@archs) {
1399 for my $package (split /\s*,\s*/, $status{package}) {
1402 if ($package =~ /^src:(.+)$/) {
1406 foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
1407 # if some distributions are disallowed,
1408 # and this isn't an allowed
1409 # distribution, then we ignore this
1410 # distribution for the purposees of
1412 if ($some_distributions_disallowed and
1413 not exists $allowed_distributions{$dist}) {
1416 push @versions, get_versions(package => $package,
1418 ($source?(arch => 'source'):
1419 (defined $arch?(arch => $arch):())),
1422 next unless @versions;
1423 my @temp = make_source_versions(package => $package,
1425 versions => \@versions,
1427 @sourceversions{@temp} = (1) x @temp;
1430 # this should really be split out into a subroutine,
1431 # but it'd touch so many things currently, that we fake
1432 # it; it's needed to properly handle bugs which are
1433 # erroneously assigned to the binary package, and we'll
1434 # probably have it go away eventually.
1435 if (not keys %sourceversions and (not @archs or defined $archs[0])) {
1437 goto GET_SOURCE_VERSIONS;
1441 # TODO: This should probably be handled further out for efficiency and
1442 # for more ease of distinguishing between pkg= and src= queries.
1443 # DLA: src= queries should just pass arch=source, and they'll be happy.
1444 @sourceversions = keys %sourceversions;
1447 @sourceversions = @{$param{sourceversions}};
1449 my $maxbuggy = 'undef';
1450 if (@sourceversions) {
1451 $maxbuggy = max_buggy(bug => $param{bug},
1452 sourceversions => \@sourceversions,
1453 found => $status{found_versions},
1454 fixed => $status{fixed_versions},
1455 package => $status{package},
1456 version_cache => $version_cache,
1459 elsif (defined $param{dist} and
1460 not exists $pseudo_desc->{$status{package}}) {
1463 if (length($status{done}) and
1464 (not @sourceversions or not @{$status{fixed_versions}})) {
1479 =item bug -- scalar bug number
1481 =item sourceversion -- optional arrayref of source/version; overrides
1482 dist, arch, and version. [The entries in this array must be in the
1483 "source/version" format.] Eventually this can be used to for caching.
1487 Note: Currently the version information is cached; this needs to be
1488 changed before using this function in long lived programs.
1493 my %param = validate_with(params => \@_,
1494 spec => {bug => {type => SCALAR,
1497 sourceversions => {type => ARRAYREF,
1500 found => {type => ARRAYREF,
1503 fixed => {type => ARRAYREF,
1506 package => {type => SCALAR,
1508 version_cache => {type => HASHREF,
1513 # Resolve bugginess states (we might be looking at multiple
1514 # architectures, say). Found wins, then fixed, then absent.
1515 my $maxbuggy = 'absent';
1516 for my $package (split /\s*,\s*/, $param{package}) {
1517 for my $version (@{$param{sourceversions}}) {
1518 my $buggy = buggy(bug => $param{bug},
1519 version => $version,
1520 found => $param{found},
1521 fixed => $param{fixed},
1522 version_cache => $param{version_cache},
1523 package => $package,
1525 if ($buggy eq 'found') {
1527 } elsif ($buggy eq 'fixed') {
1528 $maxbuggy = 'fixed';
1545 Returns the output of Debbugs::Versions::buggy for a particular
1546 package, version and found/fixed set. Automatically turns found, fixed
1547 and version into source/version strings.
1549 Caching can be had by using the version_cache, but no attempt to check
1550 to see if the on disk information is more recent than the cache is
1551 made. [This will need to be fixed for long-lived processes.]
1556 my %param = validate_with(params => \@_,
1557 spec => {bug => {type => SCALAR,
1560 found => {type => ARRAYREF,
1563 fixed => {type => ARRAYREF,
1566 version_cache => {type => HASHREF,
1569 package => {type => SCALAR,
1571 version => {type => SCALAR,
1575 my @found = @{$param{found}};
1576 my @fixed = @{$param{fixed}};
1577 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1578 # We have non-source version versions
1579 @found = makesourceversions($param{package},undef,
1582 @fixed = makesourceversions($param{package},undef,
1586 if ($param{version} !~ m{/}) {
1587 my ($version) = makesourceversions($param{package},undef,
1590 $param{version} = $version if defined $version;
1592 # Figure out which source packages we need
1594 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1595 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1596 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1597 $param{version} =~ m{/};
1599 if (not defined $param{version_cache} or
1600 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1601 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1602 foreach my $source (keys %sources) {
1603 my $srchash = substr $source, 0, 1;
1604 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1605 if (not defined $version_fh) {
1606 # We only want to warn if it's a package which actually has a maintainer
1607 my $maints = getmaintainers();
1608 next if not exists $maints->{$source};
1609 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1612 $version->load($version_fh);
1614 if (defined $param{version_cache}) {
1615 $param{version_cache}{join(',',sort keys %sources)} = $version;
1619 $version = $param{version_cache}{join(',',sort keys %sources)};
1621 return $version->buggy($param{version},\@found,\@fixed);
1624 sub isstrongseverity {
1625 my $severity = shift;
1626 $severity = $config{default_severity} if
1627 not defined $severity or $severity eq '';
1628 return grep { $_ eq $severity } @{$config{strong_severities}};
1633 =head2 generate_index_db_line
1635 my $data = read_bug(bug => $bug,
1636 location => $initialdir);
1637 # generate_index_db_line hasn't been written yet at all.
1638 my $line = generate_index_db_line($data);
1640 Returns a line for a bug suitable to be written out to index.db.
1644 sub generate_index_db_line {
1645 my ($data,$bug) = @_;
1647 # just in case someone has given us a split out data
1648 $data = join_status_fields($data);
1650 my $whendone = "open";
1651 my $severity = $config{default_severity};
1652 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1653 $pkglist =~ s/^,+//;
1654 $pkglist =~ s/,+$//;
1655 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1656 $whendone = "done" if defined $data->{done} and length $data->{done};
1657 $severity = $data->{severity} if length $data->{severity};
1658 return sprintf "%s %d %d %s [%s] %s %s\n",
1659 $pkglist, $data->{bug_num}//$bug, $data->{date}, $whendone,
1660 $data->{originator}, $severity, $data->{keywords};
1665 =head1 PRIVATE FUNCTIONS
1669 sub update_realtime {
1670 my ($file, %bugs) = @_;
1672 # update realtime index.db
1674 return () unless keys %bugs;
1675 my $idx_old = IO::File->new($file,'r')
1676 or die "Couldn't open ${file}: $!";
1677 my $idx_new = IO::File->new($file.'.new','w')
1678 or die "Couldn't open ${file}.new: $!";
1680 binmode($idx_old,':raw:utf8');
1681 binmode($idx_new,':raw:encoding(UTF-8)');
1682 my $min_bug = min(keys %bugs);
1686 while($line = <$idx_old>) {
1687 @line = split /\s/, $line;
1688 # Two cases; replacing existing line or adding new line
1689 if (exists $bugs{$line[1]}) {
1690 my $new = $bugs{$line[1]};
1691 delete $bugs{$line[1]};
1692 $min_bug = min(keys %bugs);
1693 if ($new eq "NOCHANGE") {
1694 print {$idx_new} $line;
1695 $changed_bugs{$line[1]} = $line;
1696 } elsif ($new eq "REMOVE") {
1697 $changed_bugs{$line[1]} = $line;
1699 print {$idx_new} $new;
1700 $changed_bugs{$line[1]} = $line;
1704 while ($line[1] > $min_bug) {
1705 print {$idx_new} $bugs{$min_bug};
1706 delete $bugs{$min_bug};
1707 last unless keys %bugs;
1708 $min_bug = min(keys %bugs);
1710 print {$idx_new} $line;
1712 last unless keys %bugs;
1714 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1716 print {$idx_new} <$idx_old>;
1721 rename("$file.new", $file);
1723 return %changed_bugs;
1726 sub bughook_archive {
1728 filelock("$config{spool_dir}/debbugs.trace.lock");
1729 appendfile("$config{spool_dir}/debbugs.trace","archive ".join(',',@refs)."\n");
1730 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1731 map{($_,'REMOVE')} @refs);
1732 update_realtime("$config{spool_dir}/index.archive.realtime",
1738 my ( $type, %bugs_temp ) = @_;
1739 filelock("$config{spool_dir}/debbugs.trace.lock");
1742 for my $bug (keys %bugs_temp) {
1743 my $data = $bugs_temp{$bug};
1744 appendfile("$config{spool_dir}/debbugs.trace","$type $bug\n",makestatus($data, 1));
1746 $bugs{$bug} = generate_index_db_line($data,$bug);
1748 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);