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 uniq);
55 use DateTime::Format::Pg;
61 $DEBUG = 0 unless defined $DEBUG;
64 %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
65 qw(isstrongseverity bug_presence split_status_fields),
68 read => [qw(readbug read_bug lockreadbug lockreadbugmerge),
69 qw(lock_read_all_merged_bugs),
71 write => [qw(writebug makestatus unlockwritebug)],
73 versions => [qw(addfoundversions addfixedversions),
74 qw(removefoundversions removefixedversions)
76 hook => [qw(bughook bughook_archive)],
77 indexdb => [qw(generate_index_db_line)],
78 fields => [qw(%fields)],
81 Exporter::export_ok_tags(keys %EXPORT_TAGS);
82 $EXPORT_TAGS{all} = [@EXPORT_OK];
88 readbug($bug_num,$location)
91 Reads a summary file from the archive given a bug number and a bug
92 location. Valid locations are those understood by L</getbugcomponent>
96 # these probably shouldn't be imported by most people, but
97 # Debbugs::Control needs them, so they're now exportable
98 our %fields = (originator => 'submitter',
100 subject => 'subject',
101 msgid => 'message-id',
102 'package' => 'package',
105 forwarded => 'forwarded-to',
106 mergedwith => 'merged-with',
107 severity => 'severity',
109 found_versions => 'found-in',
110 found_date => 'found-date',
111 fixed_versions => 'fixed-in',
112 fixed_date => 'fixed-date',
114 blockedby => 'blocked-by',
115 unarchived => 'unarchived',
116 summary => 'summary',
117 outlook => 'outlook',
118 affects => 'affects',
122 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
123 my @rfc1522_fields = qw(originator subject done forwarded owner);
126 return read_bug(bug => $_[0],
127 (@_ > 1)?(location => $_[1]):()
133 read_bug(bug => $bug_num,
134 location => 'archive',
136 read_bug(summary => 'path/to/bugnum.summary');
139 A more complete function than readbug; it enables you to pass a full
140 path to the summary file instead of the bug number and/or location.
146 =item bug -- the bug number
148 =item location -- optional location which is passed to getbugcomponent
150 =item summary -- complete path to the .summary file which will be read
152 =item lock -- whether to obtain a lock for the bug to prevent
153 something modifying it while the bug has been read. You B<must> call
154 C<unfilelock();> if something not undef is returned from read_bug.
156 =item locks -- hashref of already obtained locks; incremented as new
157 locks are needed, and decremented as locks are released on particular
162 One of C<bug> or C<summary> must be passed. This function will return
163 undef on failure, and will die if improper arguments are passed.
172 {bug => {type => SCALAR,
174 # something really stupid passes negative bugnumbers
177 location => {type => SCALAR|UNDEF,
180 summary => {type => SCALAR,
183 lock => {type => BOOLEAN,
186 locks => {type => HASHREF,
190 my %param = validate_with(params => \@_,
193 die "One of bug or summary must be passed to read_bug"
194 if not exists $param{bug} and not exists $param{summary};
199 if (not defined $param{summary}) {
201 ($lref,$location) = @param{qw(bug location)};
202 if (not defined $location) {
203 $location = getbuglocation($lref,'summary');
204 return undef if not defined $location;
206 $status = getbugcomponent($lref, 'summary', $location);
207 $log = getbugcomponent($lref, 'log' , $location);
208 $report = getbugcomponent($lref, 'report' , $location);
209 return undef unless defined $status;
210 return undef if not -e $status;
213 $status = $param{summary};
216 $log =~ s/\.summary$/.log/;
217 $report =~ s/\.summary$/.report/;
218 ($location) = $status =~ m/(db-h|db|archive)/;
219 ($param{bug}) = $status =~ m/(\d+)\.summary$/;
222 filelock("$config{spool_dir}/lock/$param{bug}",exists $param{locks}?$param{locks}:());
224 my $status_fh = IO::File->new($status, 'r');
225 if (not defined $status_fh) {
226 warn "Unable to open $status for reading: $!";
228 unfilelock(exists $param{locks}?$param{locks}:());
232 binmode($status_fh,':encoding(UTF-8)');
239 while (<$status_fh>) {
242 if (not defined $version and
243 /^Format-Version: ([0-9]+)/i
248 $version = 2 if not defined $version;
249 # Version 3 is the latest format version currently supported.
251 warn "Unsupported status version '$version'";
253 unfilelock(exists $param{locks}?$param{locks}:());
258 state $namemap = {reverse %fields};
259 for my $line (@lines) {
260 if ($line =~ /(\S+?): (.*)/) {
261 my ($name, $value) = (lc $1, $2);
262 # this is a bit of a hack; we should never, ever have \r
263 # or \n in the fields of status. Kill them off here.
264 # [Eventually, this should be superfluous.]
265 $value =~ s/[\r\n]//g;
266 $data{$namemap->{$name}} = $value if exists $namemap->{$name};
269 for my $field (keys %fields) {
270 $data{$field} = '' unless exists $data{$field};
273 for my $field (@rfc1522_fields) {
274 $data{$field} = decode_rfc1522($data{$field});
277 $data{severity} = $config{default_severity} if $data{severity} eq '';
278 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
279 $data{$field} = [split ' ', $data{$field}];
281 for my $field (qw(found fixed)) {
282 # create the found/fixed hashes which indicate when a
283 # particular version was marked found or marked fixed.
284 @{$data{$field}}{@{$data{"${field}_versions"}}} =
285 (('') x (@{$data{"${field}_versions"}} - @{$data{"${field}_date"}}),
286 @{$data{"${field}_date"}});
289 my $status_modified = (stat($status))[9];
290 # Add log last modified time
291 $data{log_modified} = (stat($log))[9] // (stat("${log}.gz"))[9];
292 my $report_modified = (stat($report))[9] // $data{log_modified};
293 $data{last_modified} = max($status_modified,$data{log_modified});
294 # if the date isn't set (ancient bug), use the smallest of any of the modified
295 if (not defined $data{date} or not length($data{date})) {
296 $data{date} = min($report_modified,$status_modified,$data{log_modified});
298 $data{location} = $location;
299 $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
300 $data{bug_num} = $param{bug};
302 # Sort blockedby numerically so that bugs with identical blockers have
304 if (defined $data{blockedby} and
309 split / /, $data{blockedby}
313 # mergedwith occasionally is sorted badly. Fix it to always be sorted by <=>
314 # and not include this bug
315 if (defined $data{mergedwith} and
319 grep { $_ != $data{bug_num}}
321 split / /, $data{mergedwith}
327 =head2 split_status_fields
329 my @data = split_status_fields(@data);
331 Splits splittable status fields (like package, tags, blocks,
332 blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
333 passed @data intact using dclone.
335 In scalar context, returns only the first element of @data.
339 our $ditch_empty = sub{
341 my $splitter = shift @t;
342 return grep {length $_} map {split $splitter} @t;
345 our $sort_and_unique = sub {
350 if ($all_numeric and $v =~ /\D/) {
353 next if exists $u{$v};
358 return sort {$a <=> $b} @v;
364 my $ditch_space_unique_and_sort = sub {return &{$sort_and_unique}(&{$ditch_empty}(' ',@_))};
366 (package => \&splitpackages,
367 affects => \&splitpackages,
368 # Ideally we won't have to split source, but because some consumers of
369 # get_bug_status cannot handle arrayref, we will split it here.
370 source => \&splitpackages,
371 blocks => $ditch_space_unique_and_sort,
372 blockedby => $ditch_space_unique_and_sort,
373 # this isn't strictly correct, but we'll split both of them for
374 # the time being until we ditch all use of keywords everywhere
376 keywords => $ditch_space_unique_and_sort,
377 tags => $ditch_space_unique_and_sort,
378 found_versions => $ditch_space_unique_and_sort,
379 fixed_versions => $ditch_space_unique_and_sort,
380 mergedwith => $ditch_space_unique_and_sort,
383 sub split_status_fields {
384 my @data = @{dclone(\@_)};
385 for my $data (@data) {
386 next if not defined $data;
387 croak "Passed an element which is not a hashref to split_status_field".ref($data) if
388 not (ref($data) and ref($data) eq 'HASH');
389 for my $field (keys %{$data}) {
390 next unless defined $data->{$field};
391 if (exists $split_fields{$field}) {
392 next if ref($data->{$field});
394 if (ref($split_fields{$field}) eq 'CODE') {
395 @elements = &{$split_fields{$field}}($data->{$field});
397 elsif (not ref($split_fields{$field}) or
398 UNIVERSAL::isa($split_fields{$field},'Regex')
400 @elements = split $split_fields{$field}, $data->{$field};
402 $data->{$field} = \@elements;
406 return wantarray?@data:$data[0];
409 =head2 join_status_fields
411 my @data = join_status_fields(@data);
413 Handles joining the splitable status fields. (Basically, the inverse
414 of split_status_fields.
416 Primarily called from makestatus, but may be useful for other
417 functions after calling split_status_fields (or for legacy functions
418 if we transition to split fields by default).
422 sub join_status_fields {
429 found_versions => ' ',
430 fixed_versions => ' ',
435 my @data = @{dclone(\@_)};
436 for my $data (@data) {
437 next if not defined $data;
438 croak "Passed an element which is not a hashref to split_status_field: ".
440 if ref($data) ne 'HASH';
441 for my $field (keys %{$data}) {
442 next unless defined $data->{$field};
443 next unless ref($data->{$field}) eq 'ARRAY';
444 next unless exists $join_fields{$field};
445 $data->{$field} = join($join_fields{$field},@{$data->{$field}});
448 return wantarray?@data:$data[0];
454 lockreadbug($bug_num,$location)
456 Performs a filelock, then reads the bug; the bug is unlocked if the
457 return is undefined, otherwise, you need to call unfilelock or
460 See readbug above for information on what this returns
465 my ($lref, $location) = @_;
466 return read_bug(bug => $lref, location => $location, lock => 1);
469 =head2 lockreadbugmerge
471 my ($locks, $data) = lockreadbugmerge($bug_num,$location);
473 Performs a filelock, then reads the bug. If the bug is merged, locks
474 the merge lock. Returns a list of the number of locks and the bug
479 sub lockreadbugmerge {
480 my $data = lockreadbug(@_);
481 if (not defined $data) {
484 if (not length $data->{mergedwith}) {
488 filelock("$config{spool_dir}/lock/merge");
489 $data = lockreadbug(@_);
490 if (not defined $data) {
497 =head2 lock_read_all_merged_bugs
499 my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location);
501 Performs a filelock, then reads the bug passed. If the bug is merged,
502 locks the merge lock, then reads and locks all of the other merged
503 bugs. Returns a list of the number of locks and the bug data for all
506 Will also return undef if any of the merged bugs failed to be read,
507 even if all of the others were read properly.
511 sub lock_read_all_merged_bugs {
512 my %param = validate_with(params => \@_,
513 spec => {bug => {type => SCALAR,
516 location => {type => SCALAR,
519 locks => {type => HASHREF,
525 my @data = read_bug(bug => $param{bug},
527 exists $param{location} ? (location => $param{location}):(),
528 exists $param{locks} ? (locks => $param{locks}):(),
530 if (not @data or not defined $data[0]) {
534 if (not length $data[0]->{mergedwith}) {
535 return ($locks,@data);
537 unfilelock(exists $param{locks}?$param{locks}:());
539 filelock("$config{spool_dir}/lock/merge",exists $param{locks}?$param{locks}:());
541 @data = read_bug(bug => $param{bug},
543 exists $param{location} ? (location => $param{location}):(),
544 exists $param{locks} ? (locks => $param{locks}):(),
546 if (not @data or not defined $data[0]) {
547 unfilelock(exists $param{locks}?$param{locks}:()); #for merge lock above
552 my @bugs = split / /, $data[0]->{mergedwith};
553 push @bugs, $param{bug};
554 for my $bug (@bugs) {
556 if ($bug != $param{bug}) {
558 read_bug(bug => $bug,
560 exists $param{location} ? (location => $param{location}):(),
561 exists $param{locks} ? (locks => $param{locks}):(),
563 if (not defined $newdata) {
565 unfilelock(exists $param{locks}?$param{locks}:());
568 warn "Unable to read bug: $bug while handling merged bug: $param{bug}";
573 # perform a sanity check to make sure that the merged bugs
574 # are all merged with eachother
575 # We do a cmp sort instead of an <=> sort here, because that's
578 join(' ',grep {$_ != $bug }
581 if ($newdata->{mergedwith} ne $expectmerge) {
583 unfilelock(exists $param{locks}?$param{locks}:());
585 die "Bug $param{bug} mergedwith differs from bug $bug: ($newdata->{bug_num}: '$newdata->{mergedwith}') vs. ('$expectmerge') (".join(' ',@bugs).")";
589 return ($locks,@data);
594 my $new_bug_num = new_bug(copy => $data->{bug_num});
596 Creates a new bug and returns the new bug number upon success.
604 validate_with(params => \@_,
605 spec => {copy => {type => SCALAR,
611 filelock("nextnumber.lock");
612 my $nn_fh = IO::File->new("nextnumber",'r') or
613 die "Unable to open nextnuber for reading: $!";
616 ($nn) = $nn =~ m/^(\d+)\n$/ or die "Bad format of nextnumber; is not exactly ".'^\d+\n$';
618 overwritefile("nextnumber",
621 my $nn_hash = get_hashname($nn);
623 my $c_hash = get_hashname($param{copy});
624 for my $file (qw(log status summary report)) {
625 copy("db-h/$c_hash/$param{copy}.$file",
626 "db-h/$nn_hash/${nn}.$file")
630 for my $file (qw(log status summary report)) {
631 overwritefile("db-h/$nn_hash/${nn}.$file",
636 # this probably needs to be munged to do something more elegant
637 # &bughook('new', $clone, $data);
644 my @v1fieldorder = qw(originator date subject msgid package
645 keywords done forwarded mergedwith severity);
649 my $content = makestatus($status,$version)
650 my $content = makestatus($status);
652 Creates the content for a status file based on the $status hashref
655 Really only useful for writebug
657 Currently defaults to version 2 (non-encoded rfc1522 names) but will
658 eventually default to version 3. If you care, you should specify a
664 my ($data,$version) = @_;
665 $version = 3 unless defined $version;
669 my %newdata = %$data;
670 for my $field (qw(found fixed)) {
671 if (exists $newdata{$field}) {
672 $newdata{"${field}_date"} =
673 [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
676 %newdata = %{join_status_fields(\%newdata)};
678 %newdata = encode_utf8_structure(%newdata);
681 for my $field (@rfc1522_fields) {
682 $newdata{$field} = encode_rfc1522($newdata{$field});
686 # this is a bit of a hack; we should never, ever have \r or \n in
687 # the fields of status. Kill them off here. [Eventually, this
688 # should be superfluous.]
689 for my $field (keys %newdata) {
690 $newdata{$field} =~ s/[\r\n]//g if defined $newdata{$field};
694 for my $field (@v1fieldorder) {
695 if (exists $newdata{$field} and defined $newdata{$field}) {
696 $contents .= "$newdata{$field}\n";
701 } elsif ($version == 2 or $version == 3) {
702 # Version 2 or 3. Add a file format version number for the sake of
703 # further extensibility in the future.
704 $contents .= "Format-Version: $version\n";
705 for my $field (keys %fields) {
706 if (exists $newdata{$field} and defined $newdata{$field}
707 and $newdata{$field} ne '') {
708 # Output field names in proper case, e.g. 'Merged-With'.
709 my $properfield = $fields{$field};
710 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
711 my $data = $newdata{$field};
712 $contents .= "$properfield: $data\n";
721 writebug($bug_num,$status,$location,$minversion,$disablebughook)
723 Writes the bug status and summary files out.
725 Skips writing out a status file if minversion is 2
727 Does not call bughook if disablebughook is true.
732 my ($ref, $data, $location, $minversion, $disablebughook) = @_;
735 my %outputs = (1 => 'status', 3 => 'summary');
736 for my $version (keys %outputs) {
737 next if defined $minversion and $version < $minversion;
738 my $status = getbugcomponent($ref, $outputs{$version}, $location);
739 die "can't find location for $ref" unless defined $status;
742 open $sfh,">","$status.new" or
743 die "opening $status.new: $!";
746 open $sfh,">","$status.new" or
747 die "opening $status.new: $!";
749 print {$sfh} makestatus($data, $version) or
750 die "writing $status.new: $!";
751 close($sfh) or die "closing $status.new: $!";
757 rename("$status.new",$status) || die "installing new $status: $!";
760 # $disablebughook is a bit of a hack to let format migration scripts use
761 # this function rather than having to duplicate it themselves.
762 &bughook($change,$ref,$data) unless $disablebughook;
765 =head2 unlockwritebug
767 unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
769 Writes a bug, then calls unfilelock; see writebug for what these
781 The following functions are exported with the :versions tag
783 =head2 addfoundversions
785 addfoundversions($status,$package,$version,$isbinary);
787 All use of this should be phased out in favor of Debbugs::Control::fixed/found
792 sub addfoundversions {
796 my $isbinary = shift;
797 return unless defined $version;
798 undef $package if defined $package and $package =~ m[(?:\s|/)];
799 my $source = $package;
800 if (defined $package and $package =~ s/^src://) {
805 if (defined $package and $isbinary) {
806 my @srcinfo = binary_to_source(binary => $package,
807 version => $version);
809 # We know the source package(s). Use a fully-qualified version.
810 addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
813 # Otherwise, an unqualified version will have to do.
817 # Strip off various kinds of brain-damage.
819 $version =~ s/ *\(.*\)//;
820 $version =~ s/ +[A-Za-z].*//;
822 foreach my $ver (split /[,\s]+/, $version) {
823 my $sver = defined($source) ? "$source/$ver" : '';
824 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
825 push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
827 @{$data->{fixed_versions}} =
828 grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
832 =head2 removefoundversions
834 removefoundversions($data,$package,$versiontoremove)
836 Removes found versions from $data
838 If a version is fully qualified (contains /) only versions matching
839 exactly are removed. Otherwise, all versions matching the version
842 Currently $package and $isbinary are entirely ignored, but accepted
843 for backwards compatibility.
847 sub removefoundversions {
851 my $isbinary = shift;
852 return unless defined $version;
854 foreach my $ver (split /[,\s]+/, $version) {
856 # fully qualified version
857 @{$data->{found_versions}} =
859 @{$data->{found_versions}};
862 # non qualified version; delete all matchers
863 @{$data->{found_versions}} =
864 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
865 @{$data->{found_versions}};
871 sub addfixedversions {
875 my $isbinary = shift;
876 return unless defined $version;
877 undef $package if defined $package and $package =~ m[(?:\s|/)];
878 my $source = $package;
880 if (defined $package and $isbinary) {
881 my @srcinfo = binary_to_source(binary => $package,
882 version => $version);
884 # We know the source package(s). Use a fully-qualified version.
885 addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
888 # Otherwise, an unqualified version will have to do.
892 # Strip off various kinds of brain-damage.
894 $version =~ s/ *\(.*\)//;
895 $version =~ s/ +[A-Za-z].*//;
897 foreach my $ver (split /[,\s]+/, $version) {
898 my $sver = defined($source) ? "$source/$ver" : '';
899 unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
900 push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
902 @{$data->{found_versions}} =
903 grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
907 sub removefixedversions {
911 my $isbinary = shift;
912 return unless defined $version;
914 foreach my $ver (split /[,\s]+/, $version) {
916 # fully qualified version
917 @{$data->{fixed_versions}} =
919 @{$data->{fixed_versions}};
922 # non qualified version; delete all matchers
923 @{$data->{fixed_versions}} =
924 grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
925 @{$data->{fixed_versions}};
936 Split a package string from the status file into a list of package names.
942 return unless defined $pkgs;
943 return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
947 =head2 bug_archiveable
949 bug_archiveable(bug => $bug_num);
955 =item bug -- bug number (required)
957 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
959 =item version -- Debbugs::Version information (optional)
961 =item days_until -- return days until the bug can be archived
965 Returns 1 if the bug can be archived
966 Returns 0 if the bug cannot be archived
968 If days_until is true, returns the number of days until the bug can be
969 archived, -1 if it cannot be archived. 0 means that the bug can be
970 archived the next time the archiver runs.
972 Returns undef on failure.
976 # This will eventually need to be fixed before we start using mod_perl
977 our $version_cache = {};
979 state $spec = {bug => {type => SCALAR,
982 status => {type => HASHREF,
985 days_until => {type => BOOLEAN,
988 ignore_time => {type => BOOLEAN,
991 schema => {type => OBJECT,
995 my %param = validate_with(params => \@_,
998 # This is what we return if the bug cannot be archived.
999 my $cannot_archive = $param{days_until}?-1:0;
1000 # read the status information
1001 my $status = $param{status};
1002 if (not exists $param{status} or not defined $status) {
1003 $status = read_bug(bug=>$param{bug});
1004 if (not defined $status) {
1005 print STDERR "Cannot archive $param{bug} because it does not exist\n" if $DEBUG;
1009 # Bugs can be archived if they are
1011 if (not defined $status->{done} or not length $status->{done}) {
1012 print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG;
1013 return $cannot_archive
1015 # Check to make sure that the bug has none of the unremovable tags set
1016 if (@{$config{removal_unremovable_tags}}) {
1017 for my $tag (split ' ', ($status->{keywords}||'')) {
1018 if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) {
1019 print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG;
1020 return $cannot_archive;
1025 # If we just are checking if the bug can be archived, we'll not even bother
1026 # checking the versioning information if the bug has been -done for less than 28 days.
1027 my $log_file = getbugcomponent($param{bug},'log');
1028 if (not defined $log_file or not -e $log_file) {
1029 print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
1030 return $cannot_archive;
1032 my @log_files = $log_file, (map {my $log = getbugcomponent($_,'log');
1033 defined $log ? ($log) : ();
1035 split / /, $status->{mergedwith});
1036 my $max_log_age = max(map {-e $_?($config{remove_age} - -M _):0}
1038 if (not $param{days_until} and not $param{ignore_time}
1039 and $max_log_age > 0
1041 print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
1042 return $cannot_archive;
1044 # At this point, we have to get the versioning information for this bug.
1045 # We examine the set of distribution tags. If a bug has no distribution
1046 # tags set, we assume a default set, otherwise we use the tags the bug
1049 # In cases where we are assuming a default set, if the severity
1050 # is strong, we use the strong severity default; otherwise, we
1051 # use the normal default.
1053 # There must be fixed_versions for us to look at the versioning
1055 my $min_fixed_time = time;
1056 my $min_archive_days = 0;
1057 if (@{$status->{fixed_versions}}) {
1059 @dist_tags{@{$config{removal_distribution_tags}}} =
1060 (1) x @{$config{removal_distribution_tags}};
1062 for my $tag (split ' ', ($status->{keywords}||'')) {
1063 next unless exists $config{distribution_aliases}{$tag};
1064 next unless $dist_tags{$config{distribution_aliases}{$tag}};
1065 $dists{$config{distribution_aliases}{$tag}} = 1;
1067 if (not keys %dists) {
1068 if (isstrongseverity($status->{severity})) {
1069 @dists{@{$config{removal_strong_severity_default_distribution_tags}}} =
1070 (1) x @{$config{removal_strong_severity_default_distribution_tags}};
1073 @dists{@{$config{removal_default_distribution_tags}}} =
1074 (1) x @{$config{removal_default_distribution_tags}};
1077 my %source_versions;
1078 my @sourceversions = get_versions(package => $status->{package},
1079 dist => [keys %dists],
1081 hash_slice(%param,'schema'),
1083 @source_versions{@sourceversions} = (1) x @sourceversions;
1084 # If the bug has not been fixed in the versions actually
1085 # distributed, then it cannot be archived.
1086 if ('found' eq max_buggy(bug => $param{bug},
1087 sourceversions => [keys %source_versions],
1088 found => $status->{found_versions},
1089 fixed => $status->{fixed_versions},
1090 version_cache => $version_cache,
1091 package => $status->{package},
1092 hash_slice(%param,'schema'),
1094 print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
1095 return $cannot_archive;
1097 # Since the bug has at least been fixed in the architectures
1098 # that matters, we check to see how long it has been fixed.
1100 # If $param{ignore_time}, then we should ignore time.
1101 if ($param{ignore_time}) {
1102 return $param{days_until}?0:1;
1105 # To do this, we order the times from most recent to oldest;
1106 # when we come to the first found version, we stop.
1107 # If we run out of versions, we only report the time of the
1109 my %time_versions = get_versions(package => $status->{package},
1110 dist => [keys %dists],
1113 hash_slice(%param,'schema'),
1115 for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
1116 my $buggy = buggy(bug => $param{bug},
1117 version => $version,
1118 found => $status->{found_versions},
1119 fixed => $status->{fixed_versions},
1120 version_cache => $version_cache,
1121 package => $status->{package},
1122 hash_slice(%param,'schema'),
1124 last if $buggy eq 'found';
1125 $min_fixed_time = min($time_versions{$version},$min_fixed_time);
1127 $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)))
1128 # if there are no versions in the archive at all, then
1129 # we can archive if enough days have passed
1132 # If $param{ignore_time}, then we should ignore time.
1133 if ($param{ignore_time}) {
1134 return $param{days_until}?0:1;
1136 # 6. at least 28 days have passed since the last action has occured or the bug was closed
1137 my $age = ceil($max_log_age);
1138 if ($age > 0 or $min_archive_days > 0) {
1139 print STDERR "Cannot archive $param{bug} because not enough days have passed\n" if $DEBUG;
1140 return $param{days_until}?max($age,$min_archive_days):0;
1143 return $param{days_until}?0:1;
1148 =head2 get_bug_status
1150 my $status = get_bug_status(bug => $nnn);
1152 my $status = get_bug_status($bug_num)
1158 =item bug -- scalar bug number
1160 =item status -- optional hashref of bug status as returned by readbug
1161 (can be passed to avoid rereading the bug information)
1163 =item bug_index -- optional tied index of bug status infomration;
1164 currently not correctly implemented.
1166 =item version -- optional version(s) to check package status at
1168 =item dist -- optional distribution(s) to check package status at
1170 =item arch -- optional architecture(s) to check package status at
1172 =item bugusertags -- optional hashref of bugusertags
1174 =item sourceversion -- optional arrayref of source/version; overrides
1175 dist, arch, and version. [The entries in this array must be in the
1176 "source/version" format.] Eventually this can be used to for caching.
1178 =item indicatesource -- if true, indicate which source packages this
1179 bug could belong to (or does belong to in the case of bugs assigned to
1180 a source package). Defaults to true.
1184 Note: Currently the version information is cached; this needs to be
1185 changed before using this function in long lived programs.
1189 Currently returns a hashref of status with the following keys.
1193 =item id -- bug number
1195 =item bug_num -- duplicate of id
1197 =item keywords -- tags set on the bug, including usertags if bugusertags passed.
1199 =item tags -- duplicate of keywords
1201 =item package -- name of package that the bug is assigned to
1203 =item severity -- severity of the bug
1205 =item pending -- pending state of the bug; one of following possible
1206 values; values listed later have precedence if multiple conditions are
1211 =item pending -- default state
1213 =item forwarded -- bug has been forwarded
1215 =item pending-fixed -- bug is tagged pending
1217 =item fixed -- bug is tagged fixed
1219 =item absent -- bug does not apply to this distribution/architecture
1221 =item done -- bug is resolved in this distribution/architecture
1225 =item location -- db-h or archive; the location in the filesystem
1227 =item subject -- title of the bug
1229 =item last_modified -- epoch that the bug was last modified
1231 =item date -- epoch that the bug was filed
1233 =item originator -- bug reporter
1235 =item log_modified -- epoch that the log file was last modified
1237 =item msgid -- Message id of the original bug report
1242 Other key/value pairs are returned but are not currently documented here.
1246 sub get_bug_status {
1251 {bug => {type => SCALAR,
1254 status => {type => HASHREF,
1257 bug_index => {type => OBJECT,
1260 version => {type => SCALAR|ARRAYREF,
1263 dist => {type => SCALAR|ARRAYREF,
1266 arch => {type => SCALAR|ARRAYREF,
1269 bugusertags => {type => HASHREF,
1272 sourceversions => {type => ARRAYREF,
1275 indicatesource => {type => BOOLEAN,
1278 binary_to_source_cache => {type => HASHREF,
1281 schema => {type => OBJECT,
1285 my %param = validate_with(params => \@_,
1290 if (defined $param{bug_index} and
1291 exists $param{bug_index}{$param{bug}}) {
1292 %status = %{ $param{bug_index}{$param{bug}} };
1293 $status{pending} = $status{ status };
1294 $status{id} = $param{bug};
1297 my $statuses = get_bug_statuses(@_);
1298 if (exists $statuses->{$param{bug}}) {
1299 return $statuses->{$param{bug}};
1305 sub get_bug_statuses {
1307 {bug => {type => SCALAR|ARRAYREF,
1309 status => {type => HASHREF,
1312 bug_index => {type => OBJECT,
1315 version => {type => SCALAR|ARRAYREF,
1318 dist => {type => SCALAR|ARRAYREF,
1321 arch => {type => SCALAR|ARRAYREF,
1324 bugusertags => {type => HASHREF,
1327 sourceversions => {type => ARRAYREF,
1330 indicatesource => {type => BOOLEAN,
1333 binary_to_source_cache => {type => HASHREF,
1336 schema => {type => OBJECT,
1340 my %param = validate_with(params => \@_,
1343 my $bin_to_src_cache = {};
1344 if (defined $param{binary_to_source_cache}) {
1345 $bin_to_src_cache = $param{binary_to_source_cache};
1349 if (defined $param{schema}) {
1351 $param{schema}->resultset('BugStatus')->
1352 search_rs({id => [make_list($param{bug})]},
1353 {result_class => 'DBIx::Class::ResultClass::HashRefInflator'})->
1355 for my $bug_status (@bug_statuses) {
1356 $statuses{$bug_status->{bug_num}} =
1358 for my $field (qw(blocks blockedby done),
1359 qw(tags mergedwith affects)
1361 $bug_status->{$field} //='';
1363 $bug_status->{keywords} =
1364 $bug_status->{tags};
1365 $bug_status->{location} = $bug_status->{archived}?'archive':'db-h';
1366 for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
1367 $bug_status->{$field} = [split ' ', $bug_status->{$field} // ''];
1369 for my $field (qw(found fixed)) {
1370 # create the found/fixed hashes which indicate when a
1371 # particular version was marked found or marked fixed.
1372 @{$bug_status->{$field}}{@{$bug_status->{"${field}_versions"}}} =
1373 (('') x (@{$bug_status->{"${field}_versions"}} -
1374 @{$bug_status->{"${field}_date"}}),
1375 @{$bug_status->{"${field}_date"}});
1377 $bug_status->{id} = $bug_status->{bug_num};
1380 for my $bug (make_list($param{bug})) {
1381 if (defined $param{bug_index} and
1382 exists $param{bug_index}{$bug}) {
1383 my %status = %{$param{bug_index}{$bug}};
1384 $status{pending} = $status{status};
1386 $statuses{$bug} = \%status;
1388 elsif (defined $param{status} and
1389 $param{status}{bug_num} == $bug
1391 $statuses{$bug} = {%{$param{status}}};
1393 my $location = getbuglocation($bug, 'summary');
1394 next if not defined $location or not length $location;
1395 my %status = %{ readbug( $bug, $location ) };
1397 $statuses{$bug} = \%status;
1401 for my $bug (keys %statuses) {
1402 my $status = $statuses{$bug};
1404 if (defined $param{bugusertags}{$param{bug}}) {
1405 $status->{keywords} = "" unless defined $status->{keywords};
1406 $status->{keywords} .= " " unless $status->{keywords} eq "";
1407 $status->{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
1409 $status->{tags} = $status->{keywords};
1410 my %tags = map { $_ => 1 } split ' ', $status->{tags};
1412 $status->{package} = '' if not defined $status->{package};
1413 $status->{"package"} =~ s/\s*$//;
1415 $status->{"package"} = 'unknown' if ($status->{"package"} eq '');
1416 $status->{"severity"} = 'normal' if (not defined $status->{severity} or $status->{"severity"} eq '');
1418 $status->{"pending"} = 'pending';
1419 $status->{"pending"} = 'forwarded' if (length($status->{"forwarded"}));
1420 $status->{"pending"} = 'pending-fixed' if ($tags{pending});
1421 $status->{"pending"} = 'fixed' if ($tags{fixed});
1424 my $presence = bug_presence(status => $status,
1426 map{(exists $param{$_})?($_,$param{$_}):()}
1427 qw(sourceversions arch dist version found fixed package)
1429 if (defined $presence) {
1430 if ($presence eq 'fixed') {
1431 $status->{pending} = 'done';
1432 } elsif ($presence eq 'absent') {
1433 $status->{pending} = 'absent';
1442 my $precence = bug_presence(bug => nnn,
1446 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
1447 is found, absent, fixed, or no information is available in the
1448 distribution (dist) and/or architecture (arch) specified.
1455 =item bug -- scalar bug number
1457 =item status -- optional hashref of bug status as returned by readbug
1458 (can be passed to avoid rereading the bug information)
1460 =item bug_index -- optional tied index of bug status infomration;
1461 currently not correctly implemented.
1463 =item version -- optional version to check package status at
1465 =item dist -- optional distribution to check package status at
1467 =item arch -- optional architecture to check package status at
1469 =item sourceversion -- optional arrayref of source/version; overrides
1470 dist, arch, and version. [The entries in this array must be in the
1471 "source/version" format.] Eventually this can be used to for caching.
1478 my %param = validate_with(params => \@_,
1479 spec => {bug => {type => SCALAR,
1482 status => {type => HASHREF,
1485 version => {type => SCALAR|ARRAYREF,
1488 dist => {type => SCALAR|ARRAYREF,
1491 arch => {type => SCALAR|ARRAYREF,
1494 sourceversions => {type => ARRAYREF,
1500 if (defined $param{status}) {
1501 %status = %{$param{status}};
1504 my $location = getbuglocation($param{bug}, 'summary');
1505 return {} if not length $location;
1506 %status = %{ readbug( $param{bug}, $location ) };
1510 my $pseudo_desc = getpseudodesc();
1511 if (not exists $param{sourceversions}) {
1513 # pseudopackages do not have source versions by definition.
1514 if (exists $pseudo_desc->{$status{package}}) {
1517 elsif (defined $param{version}) {
1518 foreach my $arch (make_list($param{arch})) {
1519 for my $package (split /\s*,\s*/, $status{package}) {
1520 my @temp = makesourceversions($package,
1522 make_list($param{version})
1524 @sourceversions{@temp} = (1) x @temp;
1527 } elsif (defined $param{dist}) {
1528 my %affects_distribution_tags;
1529 @affects_distribution_tags{@{$config{affects_distribution_tags}}} =
1530 (1) x @{$config{affects_distribution_tags}};
1531 my $some_distributions_disallowed = 0;
1532 my %allowed_distributions;
1533 for my $tag (split ' ', ($status{keywords}||'')) {
1534 if (exists $config{distribution_aliases}{$tag} and
1535 exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) {
1536 $some_distributions_disallowed = 1;
1537 $allowed_distributions{$config{distribution_aliases}{$tag}} = 1;
1539 elsif (exists $affects_distribution_tags{$tag}) {
1540 $some_distributions_disallowed = 1;
1541 $allowed_distributions{$tag} = 1;
1544 my @archs = make_list(exists $param{arch}?$param{arch}:());
1545 GET_SOURCE_VERSIONS:
1546 foreach my $arch (@archs) {
1547 for my $package (split /\s*,\s*/, $status{package}) {
1550 if ($package =~ /^src:(.+)$/) {
1554 foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
1555 # if some distributions are disallowed,
1556 # and this isn't an allowed
1557 # distribution, then we ignore this
1558 # distribution for the purposees of
1560 if ($some_distributions_disallowed and
1561 not exists $allowed_distributions{$dist}) {
1564 push @versions, get_versions(package => $package,
1566 ($source?(arch => 'source'):
1567 (defined $arch?(arch => $arch):())),
1570 next unless @versions;
1571 my @temp = make_source_versions(package => $package,
1573 versions => \@versions,
1575 @sourceversions{@temp} = (1) x @temp;
1578 # this should really be split out into a subroutine,
1579 # but it'd touch so many things currently, that we fake
1580 # it; it's needed to properly handle bugs which are
1581 # erroneously assigned to the binary package, and we'll
1582 # probably have it go away eventually.
1583 if (not keys %sourceversions and (not @archs or defined $archs[0])) {
1585 goto GET_SOURCE_VERSIONS;
1589 # TODO: This should probably be handled further out for efficiency and
1590 # for more ease of distinguishing between pkg= and src= queries.
1591 # DLA: src= queries should just pass arch=source, and they'll be happy.
1592 @sourceversions = keys %sourceversions;
1595 @sourceversions = @{$param{sourceversions}};
1597 my $maxbuggy = 'undef';
1598 if (@sourceversions) {
1599 $maxbuggy = max_buggy(bug => $param{bug},
1600 sourceversions => \@sourceversions,
1601 found => $status{found_versions},
1602 fixed => $status{fixed_versions},
1603 package => $status{package},
1604 version_cache => $version_cache,
1607 elsif (defined $param{dist} and
1608 not exists $pseudo_desc->{$status{package}}) {
1611 if (length($status{done}) and
1612 (not @sourceversions or not @{$status{fixed_versions}})) {
1627 =item bug -- scalar bug number
1629 =item sourceversion -- optional arrayref of source/version; overrides
1630 dist, arch, and version. [The entries in this array must be in the
1631 "source/version" format.] Eventually this can be used to for caching.
1635 Note: Currently the version information is cached; this needs to be
1636 changed before using this function in long lived programs.
1641 my %param = validate_with(params => \@_,
1642 spec => {bug => {type => SCALAR,
1645 sourceversions => {type => ARRAYREF,
1648 found => {type => ARRAYREF,
1651 fixed => {type => ARRAYREF,
1654 package => {type => SCALAR,
1656 version_cache => {type => HASHREF,
1659 schema => {type => OBJECT,
1664 # Resolve bugginess states (we might be looking at multiple
1665 # architectures, say). Found wins, then fixed, then absent.
1666 my $maxbuggy = 'absent';
1667 for my $package (split /\s*,\s*/, $param{package}) {
1668 for my $version (@{$param{sourceversions}}) {
1669 my $buggy = buggy(bug => $param{bug},
1670 version => $version,
1671 found => $param{found},
1672 fixed => $param{fixed},
1673 version_cache => $param{version_cache},
1674 package => $package,
1676 if ($buggy eq 'found') {
1678 } elsif ($buggy eq 'fixed') {
1679 $maxbuggy = 'fixed';
1696 Returns the output of Debbugs::Versions::buggy for a particular
1697 package, version and found/fixed set. Automatically turns found, fixed
1698 and version into source/version strings.
1700 Caching can be had by using the version_cache, but no attempt to check
1701 to see if the on disk information is more recent than the cache is
1702 made. [This will need to be fixed for long-lived processes.]
1707 my %param = validate_with(params => \@_,
1708 spec => {bug => {type => SCALAR,
1711 found => {type => ARRAYREF,
1714 fixed => {type => ARRAYREF,
1717 version_cache => {type => HASHREF,
1720 package => {type => SCALAR,
1722 version => {type => SCALAR,
1724 schema => {type => OBJECT,
1729 my @found = @{$param{found}};
1730 my @fixed = @{$param{fixed}};
1731 if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1732 # We have non-source version versions
1733 @found = makesourceversions($param{package},undef,
1736 @fixed = makesourceversions($param{package},undef,
1740 if ($param{version} !~ m{/}) {
1741 my ($version) = makesourceversions($param{package},undef,
1744 $param{version} = $version if defined $version;
1746 # Figure out which source packages we need
1748 @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1749 @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1750 @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1751 $param{version} =~ m{/};
1753 if (not defined $param{version_cache} or
1754 not exists $param{version_cache}{join(',',sort keys %sources)}) {
1755 $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1756 foreach my $source (keys %sources) {
1757 my $srchash = substr $source, 0, 1;
1758 my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1759 if (not defined $version_fh) {
1760 # We only want to warn if it's a package which actually has a maintainer
1761 my @maint = package_maintainer(source => $source,
1762 hash_slice(%param,'schema'),
1765 warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1768 $version->load($version_fh);
1770 if (defined $param{version_cache}) {
1771 $param{version_cache}{join(',',sort keys %sources)} = $version;
1775 $version = $param{version_cache}{join(',',sort keys %sources)};
1777 return $version->buggy($param{version},\@found,\@fixed);
1780 sub isstrongseverity {
1781 my $severity = shift;
1782 $severity = $config{default_severity} if
1783 not defined $severity or $severity eq '';
1784 return grep { $_ eq $severity } @{$config{strong_severities}};
1789 =head2 generate_index_db_line
1791 my $data = read_bug(bug => $bug,
1792 location => $initialdir);
1793 # generate_index_db_line hasn't been written yet at all.
1794 my $line = generate_index_db_line($data);
1796 Returns a line for a bug suitable to be written out to index.db.
1800 sub generate_index_db_line {
1801 my ($data,$bug) = @_;
1803 # just in case someone has given us a split out data
1804 $data = join_status_fields($data);
1806 my $whendone = "open";
1807 my $severity = $config{default_severity};
1808 (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1809 $pkglist =~ s/^,+//;
1810 $pkglist =~ s/,+$//;
1811 $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1812 $whendone = "done" if defined $data->{done} and length $data->{done};
1813 $severity = $data->{severity} if length $data->{severity};
1814 return sprintf "%s %d %d %s [%s] %s %s\n",
1815 $pkglist, $data->{bug_num}//$bug, $data->{date}, $whendone,
1816 $data->{originator}, $severity, $data->{keywords};
1821 =head1 PRIVATE FUNCTIONS
1825 sub update_realtime {
1826 my ($file, %bugs) = @_;
1828 # update realtime index.db
1830 return () unless keys %bugs;
1831 my $idx_old = IO::File->new($file,'r')
1832 or die "Couldn't open ${file}: $!";
1833 my $idx_new = IO::File->new($file.'.new','w')
1834 or die "Couldn't open ${file}.new: $!";
1836 binmode($idx_old,':raw:utf8');
1837 binmode($idx_new,':raw:encoding(UTF-8)');
1838 my $min_bug = min(keys %bugs);
1842 while($line = <$idx_old>) {
1843 @line = split /\s/, $line;
1844 # Two cases; replacing existing line or adding new line
1845 if (exists $bugs{$line[1]}) {
1846 my $new = $bugs{$line[1]};
1847 delete $bugs{$line[1]};
1848 $min_bug = min(keys %bugs);
1849 if ($new eq "NOCHANGE") {
1850 print {$idx_new} $line;
1851 $changed_bugs{$line[1]} = $line;
1852 } elsif ($new eq "REMOVE") {
1853 $changed_bugs{$line[1]} = $line;
1855 print {$idx_new} $new;
1856 $changed_bugs{$line[1]} = $line;
1860 while ($line[1] > $min_bug) {
1861 print {$idx_new} $bugs{$min_bug};
1862 delete $bugs{$min_bug};
1863 last unless keys %bugs;
1864 $min_bug = min(keys %bugs);
1866 print {$idx_new} $line;
1868 last unless keys %bugs;
1870 print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1872 print {$idx_new} <$idx_old>;
1877 rename("$file.new", $file);
1879 return %changed_bugs;
1882 sub bughook_archive {
1884 filelock("$config{spool_dir}/debbugs.trace.lock");
1885 appendfile("$config{spool_dir}/debbugs.trace","archive ".join(',',@refs)."\n");
1886 my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1887 map{($_,'REMOVE')} @refs);
1888 update_realtime("$config{spool_dir}/index.archive.realtime",
1894 my ( $type, %bugs_temp ) = @_;
1895 filelock("$config{spool_dir}/debbugs.trace.lock");
1898 for my $bug (keys %bugs_temp) {
1899 my $data = $bugs_temp{$bug};
1900 appendfile("$config{spool_dir}/debbugs.trace","$type $bug\n",makestatus($data, 1));
1902 $bugs{$bug} = generate_index_db_line($data,$bug);
1904 update_realtime("$config{spool_dir}/index.db.realtime", %bugs);