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);
42 use Debbugs::Config qw(:config);
43 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
44 use Debbugs::Packages qw(makesourceversions make_source_versions getversions get_versions binary_to_source);
45 use Debbugs::Versions;
46 use Debbugs::Versions::Dpkg;
48 use File::Copy qw(copy);
49 use Encode qw(decode encode is_utf8);
51 use Storable qw(dclone);
52 use List::Util qw(min max);
58 $DEBUG = 0 unless defined $DEBUG;
61 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
62 qw(isstrongseverity bug_presence split_status_fields),
64 read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
65 qw(lock_read_all_merged_bugs),
67 write => [qw(writebug makestatus unlockwritebug)],
69 versions => [qw(addfoundversions addfixedversions),
70 qw(removefoundversions removefixedversions)
72 hook => [qw(bughook bughook_archive)],
73 indexdb => [qw(generate_index_db_line)],
74 fields => [qw(%fields)],
77 Exporter::export_ok_tags(keys %EXPORT_TAGS);
78 $EXPORT_TAGS{all} = [@EXPORT_OK];
84 readbug($bug_num,$location)
87 Reads a summary file from the archive given a bug number and a bug
88 location. Valid locations are those understood by L</getbugcomponent>
92 # these probably shouldn't be imported by most people, but
93 # Debbugs::Control needs them, so they're now exportable
94 our %fields = (originator => 'submitter',
97 msgid => 'message-id',
98 'package' => 'package',
101 forwarded => 'forwarded-to',
102 mergedwith => 'merged-with',
103 severity => 'severity',
105 found_versions => 'found-in',
106 found_date => 'found-date',
107 fixed_versions => 'fixed-in',
108 fixed_date => 'fixed-date',
110 blockedby => 'blocked-by',
111 unarchived => 'unarchived',
112 summary => 'summary',
113 outlook => 'outlook',
114 affects => 'affects',
118 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
119 my @rfc1522_fields = qw(originator subject done forwarded owner);
122 return read_bug(bug => $_[0],
123 (@_ > 1)?(location => $_[1]):()
129 read_bug(bug => $bug_num,
130 location => 'archive',
132 read_bug(summary => 'path/to/bugnum.summary');
135 A more complete function than readbug; it enables you to pass a full
136 path to the summary file instead of the bug number and/or location.
142 =item bug -- the bug number
144 =item location -- optional location which is passed to getbugcomponent
146 =item summary -- complete path to the .summary file which will be read
148 =item lock -- whether to obtain a lock for the bug to prevent
149 something modifying it while the bug has been read. You B<must> call
150 C<unfilelock();> if something not undef is returned from read_bug.
152 =item locks -- hashref of already obtained locks; incremented as new
153 locks are needed, and decremented as locks are released on particular
158 One of C<bug> or C<summary> must be passed. This function will return
159 undef on failure, and will die if improper arguments are passed.
167 my %param = validate_with(params => \@_,
168 spec => {bug => {type => SCALAR,
172 # negative bugnumbers
175 location => {type => SCALAR|UNDEF,
178 summary => {type => SCALAR,
181 lock => {type => BOOLEAN,
184 locks => {type => HASHREF,
189 die "One of bug or summary must be passed to read_bug"
190 if not exists $param{bug} and not exists $param{summary};
194 if (not defined $param{summary}) {
196 ($lref,$location) = @param{qw(bug location)};
197 if (not defined $location) {
198 $location = getbuglocation($lref,'summary');
199 return undef if not defined $location;
201 $status = getbugcomponent($lref, 'summary', $location);
202 $log = getbugcomponent($lref, 'log' , $location);
203 return undef unless defined $status;
204 return undef if not -e $status;
207 $status = $param{summary};
209 $log =~ s/\.summary$/.log/;
210 ($location) = $status =~ m/(db-h|db|archive)/;
211 ($param{bug}) = $status =~ m/(\d+)\.summary$/;
214 filelock("$config{spool_dir}/lock/$param{bug}",exists $param{locks}?$param{locks}:());
216 my $status_fh = IO::File->new($status, 'r');
217 if (not defined $status_fh) {
218 warn "Unable to open $status for reading: $!";
220 unfilelock(exists $param{locks}?$param{locks}:());
224 binmode($status_fh,':encoding(UTF-8)');
231 while (<$status_fh>) {
234 $version = $1 if /^Format-Version: ([0-9]+)/i;
237 # Version 3 is the latest format version currently supported.
239 warn "Unsupported status version '$version'";
241 unfilelock(exists $param{locks}?$param{locks}:());
246 my %namemap = reverse %fields;
247 for my $line (@lines) {
248 if ($line =~ /(\S+?): (.*)/) {
249 my ($name, $value) = (lc $1, $2);
250 # this is a bit of a hack; we should never, ever have \r
251 # or \n in the fields of status. Kill them off here.
252 # [Eventually, this should be superfluous.]
253 $value =~ s/[\r\n]//g;
254 $data{$namemap{$name}} = $value if exists $namemap{$name};
257 for my $field (keys %fields) {
258 $data{$field} = '' unless exists $data{$field};
261 for my $field (@rfc1522_fields) {
262 $data{$field} = decode_rfc1522($data{$field});
265 $data{severity} = $config{default_severity} if $data{severity} eq '';
266 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
267 $data{$field} = [split ' ', $data{$field}];
269 for my $field (qw(found fixed)) {
270 # create the found/fixed hashes which indicate when a
271 # particular version was marked found or marked fixed.
272 @{$data{$field}}{@{$data{"${field}_versions"}}} =
273 (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
274 @{$data{"${field}_date"}});
277 my $status_modified = (stat($status))[9];
278 # Add log last modified time
279 $data{log_modified} = (stat($log))[9];
280 $data{last_modified} = max($status_modified,$data{log_modified});
281 $data{location} = $location;
282 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
283 $data{bug_num} = $param{bug};
288 =head2 split_status_fields
290 my @data = split_status_fields(@data);
292 Splits splittable status fields (like package, tags, blocks,
293 blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
294 passed @data intact using dclone.
296 In scalar context, returns only the first element of @data.
300 our $ditch_empty = sub{
302 my $splitter = shift @t;
303 return grep {length $_} map {split $splitter} @t;
306 my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
308 (package => \&splitpackages,
309 affects => \&splitpackages,
310 blocks => $ditch_empty_space,
311 blockedby => $ditch_empty_space,
312 # this isn't strictly correct, but we'll split both of them for
313 # the time being until we ditch all use of keywords everywhere
315 keywords => $ditch_empty_space,
316 tags => $ditch_empty_space,
317 found_versions => $ditch_empty_space,
318 fixed_versions => $ditch_empty_space,
319 mergedwith => $ditch_empty_space,
322 sub split_status_fields {
323 my @data = @{dclone(\@_)};
324 for my $data (@data) {
325 next if not defined $data;
326 croak "Passed an element which is not a hashref to split_status_field".ref($data) if
327 not (ref($data) and ref($data) eq 'HASH');
328 for my $field (keys %{$data}) {
329 next unless defined $data->{$field};
330 if (exists $split_fields{$field}) {
331 next if ref($data->{$field});
333 if (ref($split_fields{$field}) eq 'CODE') {
334 @elements = &{$split_fields{$field}}($data->{$field});
336 elsif (not ref($split_fields{$field}) or
337 UNIVERSAL::isa($split_fields{$field},'Regex')
339 @elements = split $split_fields{$field}, $data->{$field};
341 $data->{$field} = \@elements;
345 return wantarray?@data:$data[0];
348 =head2 join_status_fields
350 my @data = join_status_fields(@data);
352 Handles joining the splitable status fields. (Basically, the inverse
353 of split_status_fields.
355 Primarily called from makestatus, but may be useful for other
356 functions after calling split_status_fields (or for legacy functions
357 if we transition to split fields by default).
361 sub join_status_fields {
368 found_versions => ' ',
369 fixed_versions => ' ',
374 my @data = @{dclone(\@_)};
375 for my $data (@data) {
376 next if not defined $data;
377 croak "Passed an element which is not a hashref to split_status_field: ".
379 if ref($data) ne 'HASH';
380 for my $field (keys %{$data}) {
381 next unless defined $data->{$field};
382 next unless ref($data->{$field}) eq 'ARRAY';
383 next unless exists $join_fields{$field};
384 $data->{$field} = join($join_fields{$field},@{$data->{$field}});
387 return wantarray?@data:$data[0];
393 lockreadbug($bug_num,$location)
395 Performs a filelock, then reads the bug; the bug is unlocked if the
396 return is undefined, otherwise, you need to call unfilelock or
399 See readbug above for information on what this returns
404 my ($lref, $location) = @_;
405 return read_bug(bug => $lref, location => $location, lock => 1);
408 =head2 lockreadbugmerge
410 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
412 Performs a filelock, then reads the bug. If the bug is merged, locks
413 the merge lock. Returns a list of the number of locks and the bug
418 sub lockreadbugmerge {
419 my ($bug_num,$location) = @_;
420 my $data = lockreadbug(@_);
421 if (not defined $data) {
424 if (not length $data->{mergedwith}) {
428 filelock("$config{spool_dir}/lock/merge");
429 $data = lockreadbug(@_);
430 if (not defined $data) {
437 =head2 lock_read_all_merged_bugs
439 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
441 Performs a filelock, then reads the bug passed. If the bug is merged,
442 locks the merge lock, then reads and locks all of the other merged
443 bugs. Returns a list of the number of locks and the bug data for all
446 Will also return undef if any of the merged bugs failed to be read,
447 even if all of the others were read properly.
451 sub lock_read_all_merged_bugs {
452 my %param = validate_with(params => \@_,
453 spec => {bug => {type => SCALAR,
456 location => {type => SCALAR,
459 locks => {type => HASHREF,
465 my @data = read_bug(bug => $param{bug},
467 exists $param{location} ? (location => $param{location}):(),
468 exists $param{locks} ? (locks => $param{locks}):(),
470 if (not @data or not defined $data[0]) {
474 if (not length $data[0]->{mergedwith}) {
475 return ($locks,@data);
477 unfilelock(exists $param{locks}?$param{locks}:());
479 filelock("$config{spool_dir}/lock/merge",exists $param{locks}?$param{locks}:());
481 @data = read_bug(bug => $param{bug},
483 exists $param{location} ? (location => $param{location}):(),
484 exists $param{locks} ? (locks => $param{locks}):(),
486 if (not @data or not defined $data[0]) {
487 unfilelock(exists $param{locks}?$param{locks}:()); #for merge lock above
492 my @bugs = split / /, $data[0]->{mergedwith};
493 push @bugs, $param{bug};
494 for my $bug (@bugs) {
496 if ($bug != $param{bug}) {
498 read_bug(bug => $bug,
500 exists $param{location} ? (location => $param{location}):(),
501 exists $param{locks} ? (locks => $param{locks}):(),
503 if (not defined $newdata) {
505 unfilelock(exists $param{locks}?$param{locks}:());
508 warn "Unable to read bug: $bug while handling merged bug: $param{bug}";
513 # perform a sanity check to make sure that the merged bugs
514 # are all merged with eachother
515 my $expectmerge= join(' ',grep {$_ != $bug } sort { $a <=> $b } @bugs);
516 if ($newdata->{mergedwith} ne $expectmerge) {
518 unfilelock(exists $param{locks}?$param{locks}:());
520 die "Bug $param{bug} differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
524 return ($locks,@data);
529 my $new_bug_num = new_bug(copy => $data->{bug_num});
531 Creates a new bug and returns the new bug number upon success.
539 validate_with(params => \@_,
540 spec => {copy => {type => SCALAR,
546 filelock("nextnumber.lock");
547 my $nn_fh = IO::File->new("nextnumber",'r') or
548 die "Unable to open nextnuber for reading: $!";
551 ($nn) = $nn =~ m/^(\d+)\n$/ or die "Bad format of nextnumber; is not exactly ".'^\d+\n$';
553 overwritefile("nextnumber",
556 my $nn_hash = get_hashname($nn);
558 my $c_hash = get_hashname($param{copy});
559 for my $file (qw(log status summary report)) {
560 copy("db-h/$c_hash/$param{copy}.$file",
561 "db-h/$nn_hash/${nn}.$file")
565 for my $file (qw(log status summary report)) {
566 overwritefile("db-h/$nn_hash/${nn}.$file",
571 # this probably needs to be munged to do something more elegant
572 # &bughook('new', $clone, $data);
579 my @v1fieldorder = qw(originator date subject msgid package
580 keywords done forwarded mergedwith severity);
584 my $content = makestatus($status,$version)
585 my $content = makestatus($status);
587 Creates the content for a status file based on the $status hashref
590 Really only useful for writebug
592 Currently defaults to version 2 (non-encoded rfc1522 names) but will
593 eventually default to version 3. If you care, you should specify a
599 my ($data,$version) = @_;
600 $version = 3 unless defined $version;
604 my %newdata = %$data;
605 for my $field (qw(found fixed)) {
606 if (exists $newdata{$field}) {
607 $newdata{"${field}_date"} =
608 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
611 %newdata = %{join_status_fields(\%newdata)};
613 %newdata = encode_utf8_structure(%newdata);
616 for my $field (@rfc1522_fields) {
617 $newdata{$field} = encode_rfc1522($newdata{$field});
621 # this is a bit of a hack; we should never, ever have \r or \n in
622 # the fields of status. Kill them off here. [Eventually, this
623 # should be superfluous.]
624 for my $field (keys %newdata) {
625 $newdata{$field} =~ s/[\r\n]//g if defined $newdata{$field};
629 for my $field (@v1fieldorder) {
630 if (exists $newdata{$field} and defined $newdata{$field}) {
631 $contents .= "$newdata{$field}\n";
636 } elsif ($version == 2 or $version == 3) {
637 # Version 2 or 3. Add a file format version number for the sake of
638 # further extensibility in the future.
639 $contents .= "Format-Version: $version\n";
640 for my $field (keys %fields) {
641 if (exists $newdata{$field} and defined $newdata{$field}
642 and $newdata{$field} ne '') {
643 # Output field names in proper case, e.g. 'Merged-With'.
644 my $properfield = $fields{$field};
645 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
646 my $data = $newdata{$field};
647 $contents .= "$properfield: $data\n";
656 writebug($bug_num,$status,$location,$minversion,$disablebughook)
658 Writes the bug status and summary files out.
660 Skips writing out a status file if minversion is 2
662 Does not call bughook if disablebughook is true.
667 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
670 my %outputs = (1 => 'status', 3 => 'summary');
671 for my $version (keys %outputs) {
672 next if defined $minversion and $version < $minversion;
673 my $status = getbugcomponent($ref, $outputs{$version}, $location);
674 die "can't find location for $ref" unless defined $status;
677 open $sfh,">","$status.new" or
678 die "opening $status.new: $!";
681 open $sfh,">","$status.new" or
682 die "opening $status.new: $!";
684 print {$sfh} makestatus($data, $version) or
685 die "writing $status.new: $!";
686 close($sfh) or die "closing $status.new: $!";
692 rename("$status.new",$status) || die "installing new $status: $!";
695 # $disablebughook is a bit of a hack to let format migration scripts use
696 # this function rather than having to duplicate it themselves.
697 &bughook($change,$ref,$data) unless $disablebughook;
700 =head2 unlockwritebug
702 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
704 Writes a bug, then calls unfilelock; see writebug for what these
716 The following functions are exported with the :versions tag
718 =head2 addfoundversions
720 addfoundversions($status,$package,$version,$isbinary);
722 All use of this should be phased out in favor of Debbugs::Control::fixed/found
727 sub addfoundversions {
731 my $isbinary = shift;
732 return unless defined $version;
733 undef $package if $package =~ m[(?:\s|/)];
734 my $source = $package;
735 if ($package =~ s/^src://) {
740 if (defined $package and $isbinary) {
741 my @srcinfo = binary_to_source(binary => $package,
742 version => $version);
744 # We know the source package(s). Use a fully-qualified version.
745 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
748 # Otherwise, an unqualified version will have to do.
752 # Strip off various kinds of brain-damage.
754 $version =~ s/ *\(.*\)//;
755 $version =~ s/ +[A-Za-z].*//;
757 foreach my $ver (split /[,\s]+/, $version) {
758 my $sver = defined($source) ? "$source/$ver" : '';
759 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
760 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
762 @{$data->{fixed_versions}} =
763 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
767 =head2 removefoundversions
769 removefoundversions($data,$package,$versiontoremove)
771 Removes found versions from $data
773 If a version is fully qualified (contains /) only versions matching
774 exactly are removed. Otherwise, all versions matching the version
777 Currently $package and $isbinary are entirely ignored, but accepted
778 for backwards compatibility.
782 sub removefoundversions {
786 my $isbinary = shift;
787 return unless defined $version;
789 foreach my $ver (split /[,\s]+/, $version) {
791 # fully qualified version
792 @{$data->{found_versions}} =
794 @{$data->{found_versions}};
797 # non qualified version; delete all matchers
798 @{$data->{found_versions}} =
799 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
800 @{$data->{found_versions}};
806 sub addfixedversions {
810 my $isbinary = shift;
811 return unless defined $version;
812 undef $package if defined $package and $package =~ m[(?:\s|/)];
813 my $source = $package;
815 if (defined $package and $isbinary) {
816 my @srcinfo = binary_to_source(binary => $package,
817 version => $version);
819 # We know the source package(s). Use a fully-qualified version.
820 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
823 # Otherwise, an unqualified version will have to do.
827 # Strip off various kinds of brain-damage.
829 $version =~ s/ *\(.*\)//;
830 $version =~ s/ +[A-Za-z].*//;
832 foreach my $ver (split /[,\s]+/, $version) {
833 my $sver = defined($source) ? "$source/$ver" : '';
834 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
835 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
837 @{$data->{found_versions}} =
838 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
842 sub removefixedversions {
846 my $isbinary = shift;
847 return unless defined $version;
849 foreach my $ver (split /[,\s]+/, $version) {
851 # fully qualified version
852 @{$data->{fixed_versions}} =
854 @{$data->{fixed_versions}};
857 # non qualified version; delete all matchers
858 @{$data->{fixed_versions}} =
859 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
860 @{$data->{fixed_versions}};
871 Split a package string from the status file into a list of package names.
877 return unless defined $pkgs;
878 return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
882 =head2 bug_archiveable
884 bug_archiveable(bug => $bug_num);
890 =item bug -- bug number (required)
892 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
894 =item version -- Debbugs::Version information (optional)
896 =item days_until -- return days until the bug can be archived
900 Returns 1 if the bug can be archived
901 Returns 0 if the bug cannot be archived
903 If days_until is true, returns the number of days until the bug can be
904 archived, -1 if it cannot be archived. 0 means that the bug can be
905 archived the next time the archiver runs.
907 Returns undef on failure.
911 # This will eventually need to be fixed before we start using mod_perl
912 our $version_cache = {};
914 my %param = validate_with(params => \@_,
915 spec => {bug => {type => SCALAR,
918 status => {type => HASHREF,
921 days_until => {type => BOOLEAN,
924 ignore_time => {type => BOOLEAN,
929 # This is what we return if the bug cannot be archived.
930 my $cannot_archive = $param{days_until}?-1:0;
931 # read the status information
932 my $status = $param{status};
933 if (not exists $param{status} or not defined $status) {
934 $status = read_bug(bug=>$param{bug});
935 if (not defined $status) {
936 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
940 # Bugs can be archived if they are
942 if (not defined $status->{done} or not length $status->{done}) {
943 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
944 return $cannot_archive
946 # Check to make sure that the bug has none of the unremovable tags set
947 if (@{$config{removal_unremovable_tags}}) {
948 for my $tag (split ' ', ($status->{keywords}||'')) {
949 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
950 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
951 return $cannot_archive;
956 # If we just are checking if the bug can be archived, we'll not even bother
957 # checking the versioning information if the bug has been -done for less than 28 days.
958 my $log_file = getbugcomponent($param{bug},'log');
959 if (not defined $log_file) {
960 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
961 return $cannot_archive;
963 my $max_log_age = max(map {$config{remove_age} - -M $_}
964 $log_file, map {my $log = getbugcomponent($_,'log');
965 defined $log ? ($log) : ();
967 split / /, $status->{mergedwith}
969 if (not $param{days_until} and not $param{ignore_time}
972 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
973 return $cannot_archive;
975 # At this point, we have to get the versioning information for this bug.
976 # We examine the set of distribution tags. If a bug has no distribution
977 # tags set, we assume a default set, otherwise we use the tags the bug
980 # In cases where we are assuming a default set, if the severity
981 # is strong, we use the strong severity default; otherwise, we
982 # use the normal default.
984 # There must be fixed_versions for us to look at the versioning
986 my $min_fixed_time = time;
987 my $min_archive_days = 0;
988 if (@{$status->{fixed_versions}}) {
990 @dist_tags{@{$config{removal_distribution_tags}}} =
991 (1) x @{$config{removal_distribution_tags}};
993 for my $tag (split ' ', ($status->{keywords}||'')) {
994 next unless exists $config{distribution_aliases}{$tag};
995 next unless $dist_tags{$config{distribution_aliases}{$tag}};
996 $dists{$config{distribution_aliases}{$tag}} = 1;
998 if (not keys %dists) {
999 if (isstrongseverity($status->{severity})) {
1000 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
1001 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
1004 @dists{@{$config{removal_default_distribution_tags}}} =
1005 (1) x @{$config{removal_default_distribution_tags}};
1008 my %source_versions;
1009 my @sourceversions = get_versions(package => $status->{package},
1010 dist => [keys %dists],
1013 @source_versions{@sourceversions} = (1) x @sourceversions;
1014 # If the bug has not been fixed in the versions actually
1015 # distributed, then it cannot be archived.
1016 if ('found' eq max_buggy(bug => $param{bug},
1017 sourceversions => [keys %source_versions],
1018 found => $status->{found_versions},
1019 fixed => $status->{fixed_versions},
1020 version_cache => $version_cache,
1021 package => $status->{package},
1023 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
1024 return $cannot_archive;
1026 # Since the bug has at least been fixed in the architectures
1027 # that matters, we check to see how long it has been fixed.
1029 # If $param{ignore_time}, then we should ignore time.
1030 if ($param{ignore_time}) {
1031 return $param{days_until}?0:1;
1034 # To do this, we order the times from most recent to oldest;
1035 # when we come to the first found version, we stop.
1036 # If we run out of versions, we only report the time of the
1038 my %time_versions = get_versions(package => $status->{package},
1039 dist => [keys %dists],
1043 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
1044 my $buggy = buggy(bug => $param{bug},
1045 version => $version,
1046 found => $status->{found_versions},
1047 fixed => $status->{fixed_versions},
1048 version_cache => $version_cache,
1049 package => $status->{package},
1051 last if $buggy eq 'found';
1052 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
1054 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
1055 # if there are no versions in the archive at all, then
1056 # we can archive if enough days have passed
1059 # If $param{ignore_time}, then we should ignore time.
1060 if ($param{ignore_time}) {
1061 return $param{days_until}?0:1;
1063 # 6. at least 28 days have passed since the last action has occured or the bug was closed
1064 my $age = ceil($max_log_age);
1065 if ($age > 0 or $min_archive_days > 0) {
1066 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
1067 return $param{days_until}?max($age,$min_archive_days):0;
1070 return $param{days_until}?0:1;
1075 =head2 get_bug_status
1077 my $status = get_bug_status(bug => $nnn);
1079 my $status = get_bug_status($bug_num)
1085 =item bug -- scalar bug number
1087 =item status -- optional hashref of bug status as returned by readbug
1088 (can be passed to avoid rereading the bug information)
1090 =item bug_index -- optional tied index of bug status infomration;
1091 currently not correctly implemented.
1093 =item version -- optional version(s) to check package status at
1095 =item dist -- optional distribution(s) to check package status at
1097 =item arch -- optional architecture(s) to check package status at
1099 =item bugusertags -- optional hashref of bugusertags
1101 =item sourceversion -- optional arrayref of source/version; overrides
1102 dist, arch, and version. [The entries in this array must be in the
1103 "source/version" format.] Eventually this can be used to for caching.
1105 =item indicatesource -- if true, indicate which source packages this
1106 bug could belong to (or does belong to in the case of bugs assigned to
1107 a source package). Defaults to true.
1111 Note: Currently the version information is cached; this needs to be
1112 changed before using this function in long lived programs.
1116 Currently returns a hashref of status with the following keys.
1120 =item id -- bug number
1122 =item bug_num -- duplicate of id
1124 =item keywords -- tags set on the bug, including usertags if bugusertags passed.
1126 =item tags -- duplicate of keywords
1128 =item package -- name of package that the bug is assigned to
1130 =item severity -- severity of the bug
1132 =item pending -- pending state of the bug; one of following possible
1133 values; values listed later have precedence if multiple conditions are
1138 =item pending -- default state
1140 =item forwarded -- bug has been forwarded
1142 =item pending-fixed -- bug is tagged pending
1144 =item fixed -- bug is tagged fixed
1146 =item absent -- bug does not apply to this distribution/architecture
1148 =item done -- bug is resolved in this distribution/architecture
1152 =item location -- db-h or archive; the location in the filesystem
1154 =item subject -- title of the bug
1156 =item last_modified -- epoch that the bug was last modified
1158 =item date -- epoch that the bug was filed
1160 =item originator -- bug reporter
1162 =item log_modified -- epoch that the log file was last modified
1164 =item msgid -- Message id of the original bug report
1169 Other key/value pairs are returned but are not currently documented here.
1173 sub get_bug_status {
1177 my %param = validate_with(params => \@_,
1178 spec => {bug => {type => SCALAR,
1181 status => {type => HASHREF,
1184 bug_index => {type => OBJECT,
1187 version => {type => SCALAR|ARRAYREF,
1190 dist => {type => SCALAR|ARRAYREF,
1193 arch => {type => SCALAR|ARRAYREF,
1196 bugusertags => {type => HASHREF,
1199 sourceversions => {type => ARRAYREF,
1202 indicatesource => {type => BOOLEAN,
1209 if (defined $param{bug_index} and
1210 exists $param{bug_index}{$param{bug}}) {
1211 %status = %{ $param{bug_index}{$param{bug}} };
1212 $status{pending} = $status{ status };
1213 $status{id} = $param{bug};
1216 if (defined $param{status}) {
1217 %status = %{$param{status}};
1220 my $location = getbuglocation($param{bug}, 'summary');
1221 return {} if not defined $location or not length $location;
1222 %status = %{ readbug( $param{bug}, $location ) };
1224 $status{id} = $param{bug};
1226 if (defined $param{bugusertags}{$param{bug}}) {
1227 $status{keywords} = "" unless defined $status{keywords};
1228 $status{keywords} .= " " unless $status{keywords} eq "";
1229 $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
1231 $status{tags} = $status{keywords};
1232 my %tags = map { $_ => 1 } split ' ', $status{tags};
1234 $status{package} = '' if not defined $status{package};
1235 $status{"package"} =~ s/\s*$//;
1237 $status{source} = binary_to_source(binary=>[split /\s*,\s*/, $status{package}],
1241 $status{"package"} = 'unknown' if ($status{"package"} eq '');
1242 $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
1244 $status{"pending"} = 'pending';
1245 $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
1246 $status{"pending"} = 'pending-fixed' if ($tags{pending});
1247 $status{"pending"} = 'fixed' if ($tags{fixed});
1250 my $presence = bug_presence(status => \%status,
1251 map{(exists $param{$_})?($_,$param{$_}):()}
1252 qw(bug sourceversions arch dist version found fixed package)
1254 if (defined $presence) {
1255 if ($presence eq 'fixed') {
1256 $status{pending} = 'done';
1258 elsif ($presence eq 'absent') {
1259 $status{pending} = 'absent';
1267 my $precence = bug_presence(bug => nnn,
1271 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
1272 is found, absent, fixed, or no information is available in the
1273 distribution (dist) and/or architecture (arch) specified.
1280 =item bug -- scalar bug number
1282 =item status -- optional hashref of bug status as returned by readbug
1283 (can be passed to avoid rereading the bug information)
1285 =item bug_index -- optional tied index of bug status infomration;
1286 currently not correctly implemented.
1288 =item version -- optional version to check package status at
1290 =item dist -- optional distribution to check package status at
1292 =item arch -- optional architecture to check package status at
1294 =item sourceversion -- optional arrayref of source/version; overrides
1295 dist, arch, and version. [The entries in this array must be in the
1296 "source/version" format.] Eventually this can be used to for caching.
1303 my %param = validate_with(params => \@_,
1304 spec => {bug => {type => SCALAR,
1307 status => {type => HASHREF,
1310 version => {type => SCALAR|ARRAYREF,
1313 dist => {type => SCALAR|ARRAYREF,
1316 arch => {type => SCALAR|ARRAYREF,
1319 sourceversions => {type => ARRAYREF,
1325 if (defined $param{status}) {
1326 %status = %{$param{status}};
1329 my $location = getbuglocation($param{bug}, 'summary');
1330 return {} if not length $location;
1331 %status = %{ readbug( $param{bug}, $location ) };
1335 my $pseudo_desc = getpseudodesc();
1336 if (not exists $param{sourceversions}) {
1338 # pseudopackages do not have source versions by definition.
1339 if (exists $pseudo_desc->{$status{package}}) {
1342 elsif (defined $param{version}) {
1343 foreach my $arch (make_list($param{arch})) {
1344 for my $package (split /\s*,\s*/, $status{package}) {
1345 my @temp = makesourceversions($package,
1347 make_list($param{version})
1349 @sourceversions{@temp} = (1) x @temp;
1352 } elsif (defined $param{dist}) {
1353 my %affects_distribution_tags;
1354 @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
1355 (1) x @{$config{affects_distribution_tags}};
1356 my $some_distributions_disallowed = 0;
1357 my %allowed_distributions;
1358 for my $tag (split ' ', ($status{keywords}||'')) {
1359 if (exists $config{distribution_aliases}{$tag} and
1360 exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
1361 $some_distributions_disallowed = 1;
1362 $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
1364 elsif (exists $affects_distribution_tags{$tag}) {
1365 $some_distributions_disallowed = 1;
1366 $allowed_distributions{$tag} = 1;
1369 my @archs = make_list(exists $param{arch}?$param{arch}:());
1370 GET_SOURCE_VERSIONS:
1371 foreach my $arch (@archs) {
1372 for my $package (split /\s*,\s*/, $status{package}) {
1375 if ($package =~ /^src:(.+)$/) {
1379 foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
1380 # if some distributions are disallowed,
1381 # and this isn't an allowed
1382 # distribution, then we ignore this
1383 # distribution for the purposees of
1385 if ($some_distributions_disallowed and
1386 not exists $allowed_distributions{$dist}) {
1389 push @versions, get_versions(package => $package,
1391 ($source?(arch => 'source'):
1392 (defined $arch?(arch => $arch):())),
1395 next unless @versions;
1396 my @temp = make_source_versions(package => $package,
1398 versions => \@versions,
1400 @sourceversions{@temp} = (1) x @temp;
1403 # this should really be split out into a subroutine,
1404 # but it'd touch so many things currently, that we fake
1405 # it; it's needed to properly handle bugs which are
1406 # erroneously assigned to the binary package, and we'll
1407 # probably have it go away eventually.
1408 if (not keys %sourceversions and (not @archs or defined $archs[0])) {
1410 goto GET_SOURCE_VERSIONS;
1414 # TODO: This should probably be handled further out for efficiency and
1415 # for more ease of distinguishing between pkg= and src= queries.
1416 # DLA: src= queries should just pass arch=source, and they'll be happy.
1417 @sourceversions = keys %sourceversions;
1420 @sourceversions = @{$param{sourceversions}};
1422 my $maxbuggy = 'undef';
1423 if (@sourceversions) {
1424 $maxbuggy = max_buggy(bug => $param{bug},
1425 sourceversions => \@sourceversions,
1426 found => $status{found_versions},
1427 fixed => $status{fixed_versions},
1428 package => $status{package},
1429 version_cache => $version_cache,
1432 elsif (defined $param{dist} and
1433 not exists $pseudo_desc->{$status{package}}) {
1436 if (length($status{done}) and
1437 (not @sourceversions or not @{$status{fixed_versions}})) {
1452 =item bug -- scalar bug number
1454 =item sourceversion -- optional arrayref of source/version; overrides
1455 dist, arch, and version. [The entries in this array must be in the
1456 "source/version" format.] Eventually this can be used to for caching.
1460 Note: Currently the version information is cached; this needs to be
1461 changed before using this function in long lived programs.
1466 my %param = validate_with(params => \@_,
1467 spec => {bug => {type => SCALAR,
1470 sourceversions => {type => ARRAYREF,
1473 found => {type => ARRAYREF,
1476 fixed => {type => ARRAYREF,
1479 package => {type => SCALAR,
1481 version_cache => {type => HASHREF,
1486 # Resolve bugginess states (we might be looking at multiple
1487 # architectures, say). Found wins, then fixed, then absent.
1488 my $maxbuggy = 'absent';
1489 for my $package (split /\s*,\s*/, $param{package}) {
1490 for my $version (@{$param{sourceversions}}) {
1491 my $buggy = buggy(bug => $param{bug},
1492 version => $version,
1493 found => $param{found},
1494 fixed => $param{fixed},
1495 version_cache => $param{version_cache},
1496 package => $package,
1498 if ($buggy eq 'found') {
1500 } elsif ($buggy eq 'fixed') {
1501 $maxbuggy = 'fixed';
1518 Returns the output of Debbugs::Versions::buggy for a particular
1519 package, version and found/fixed set. Automatically turns found, fixed
1520 and version into source/version strings.
1522 Caching can be had by using the version_cache, but no attempt to check
1523 to see if the on disk information is more recent than the cache is
1524 made. [This will need to be fixed for long-lived processes.]
1529 my %param = validate_with(params => \@_,
1530 spec => {bug => {type => SCALAR,
1533 found => {type => ARRAYREF,
1536 fixed => {type => ARRAYREF,
1539 version_cache => {type => HASHREF,
1542 package => {type => SCALAR,
1544 version => {type => SCALAR,
1548 my @found = @{$param{found}};
1549 my @fixed = @{$param{fixed}};
1550 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1551 # We have non-source version versions
1552 @found = makesourceversions($param{package},undef,
1555 @fixed = makesourceversions($param{package},undef,
1559 if ($param{version} !~ m{/}) {
1560 my ($version) = makesourceversions($param{package},undef,
1563 $param{version} = $version if defined $version;
1565 # Figure out which source packages we need
1567 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1568 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1569 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1570 $param{version} =~ m{/};
1572 if (not defined $param{version_cache} or
1573 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1574 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1575 foreach my $source (keys %sources) {
1576 my $srchash = substr $source, 0, 1;
1577 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1578 if (not defined $version_fh) {
1579 # We only want to warn if it's a package which actually has a maintainer
1580 my $maints = getmaintainers();
1581 next if not exists $maints->{$source};
1582 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1585 $version->load($version_fh);
1587 if (defined $param{version_cache}) {
1588 $param{version_cache}{join(',',sort keys %sources)} = $version;
1592 $version = $param{version_cache}{join(',',sort keys %sources)};
1594 return $version->buggy($param{version},\@found,\@fixed);
1597 sub isstrongseverity {
1598 my $severity = shift;
1599 $severity = $config{default_severity} if
1600 not defined $severity or $severity eq '';
1601 return grep { $_ eq $severity } @{$config{strong_severities}};
1606 =head2 generate_index_db_line
1608 my $data = read_bug(bug => $bug,
1609 location => $initialdir);
1610 # generate_index_db_line hasn't been written yet at all.
1611 my $line = generate_index_db_line($data);
1613 Returns a line for a bug suitable to be written out to index.db.
1617 sub generate_index_db_line {
1618 my ($data,$bug) = @_;
1620 # just in case someone has given us a split out data
1621 $data = join_status_fields($data);
1623 my $whendone = "open";
1624 my $severity = $config{default_severity};
1625 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1626 $pkglist =~ s/^,+//;
1627 $pkglist =~ s/,+$//;
1628 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1629 $whendone = "done" if defined $data->{done} and length $data->{done};
1630 $severity = $data->{severity} if length $data->{severity};
1631 return sprintf "%s %d %d %s [%s] %s %s\n",
1632 $pkglist, $data->{bug_num}//$bug, $data->{date}, $whendone,
1633 $data->{originator}, $severity, $data->{keywords};
1638 =head1 PRIVATE FUNCTIONS
1642 sub update_realtime {
1643 my ($file, %bugs) = @_;
1645 # update realtime index.db
1647 return () unless keys %bugs;
1648 my $idx_old = IO::File->new($file,'r')
1649 or die "Couldn't open ${file}: $!";
1650 my $idx_new = IO::File->new($file.'.new','w')
1651 or die "Couldn't open ${file}.new: $!";
1653 binmode($idx_old,':raw:utf8');
1654 binmode($idx_new,':raw:encoding(UTF-8)');
1655 my $min_bug = min(keys %bugs);
1659 while($line = <$idx_old>) {
1660 @line = split /\s/, $line;
1661 # Two cases; replacing existing line or adding new line
1662 if (exists $bugs{$line[1]}) {
1663 my $new = $bugs{$line[1]};
1664 delete $bugs{$line[1]};
1665 $min_bug = min(keys %bugs);
1666 if ($new eq "NOCHANGE") {
1667 print {$idx_new} $line;
1668 $changed_bugs{$line[1]} = $line;
1669 } elsif ($new eq "REMOVE") {
1670 $changed_bugs{$line[1]} = $line;
1672 print {$idx_new} $new;
1673 $changed_bugs{$line[1]} = $line;
1677 while ($line[1] > $min_bug) {
1678 print {$idx_new} $bugs{$min_bug};
1679 delete $bugs{$min_bug};
1680 last unless keys %bugs;
1681 $min_bug = min(keys %bugs);
1683 print {$idx_new} $line;
1685 last unless keys %bugs;
1687 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1689 print {$idx_new} <$idx_old>;
1694 rename("$file.new", $file);
1696 return %changed_bugs;
1699 sub bughook_archive {
1701 filelock("$config{spool_dir}/debbugs.trace.lock");
1702 appendfile("$config{spool_dir}/debbugs.trace","archive ".join(',',@refs)."\n");
1703 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1704 map{($_,'REMOVE')} @refs);
1705 update_realtime("$config{spool_dir}/index.archive.realtime",
1711 my ( $type, %bugs_temp ) = @_;
1712 filelock("$config{spool_dir}/debbugs.trace.lock");
1715 for my $bug (keys %bugs_temp) {
1716 my $data = $bugs_temp{$bug};
1717 appendfile("$config{spool_dir}/debbugs.trace","$type $bug\n",makestatus($data, 1));
1719 $bugs{$bug} = generate_index_db_line($data,$bug);
1721 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);