]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Status.pm
* Examine the age of all merged bugs logs and use in calculation to
[debbugs.git] / Debbugs / Status.pm
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.
5 #
6 # [Other people have contributed to this file; their copyrights should
7 # go here too.]
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
9
10 package Debbugs::Status;
11
12 =head1 NAME
13
14 Debbugs::Status -- Routines for dealing with summary and status files
15
16 =head1 SYNOPSIS
17
18 use Debbugs::Status;
19
20
21 =head1 DESCRIPTION
22
23 This module is a replacement for the parts of errorlib.pl which write
24 and read status and summary files.
25
26 It also contains generic routines for returning information about the
27 status of a particular bug
28
29 =head1 FUNCTIONS
30
31 =cut
32
33 use warnings;
34 use strict;
35 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
36 use base qw(Exporter);
37
38 use Params::Validate qw(validate_with :types);
39 use Debbugs::Common qw(:util :lock :quit :misc);
40 use Debbugs::Config qw(:config);
41 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
42 use Debbugs::Packages qw(makesourceversions getversions get_versions binarytosource);
43 use Debbugs::Versions;
44 use Debbugs::Versions::Dpkg;
45 use POSIX qw(ceil);
46
47 use List::Util qw(min max);
48
49
50 BEGIN{
51      $VERSION = 1.00;
52      $DEBUG = 0 unless defined $DEBUG;
53
54      @EXPORT = ();
55      %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
56                                 qw(isstrongseverity bug_presence),
57                                ],
58                      read   => [qw(readbug read_bug lockreadbug lockreadbugmerge)],
59                      write  => [qw(writebug makestatus unlockwritebug)],
60                      versions => [qw(addfoundversions addfixedversions),
61                                   qw(removefoundversions removefixedversions)
62                                  ],
63                      hook     => [qw(bughook bughook_archive)],
64                     );
65      @EXPORT_OK = ();
66      Exporter::export_ok_tags(qw(status read write versions hook));
67      $EXPORT_TAGS{all} = [@EXPORT_OK];
68 }
69
70
71 =head2 readbug
72
73      readbug($bug_num,$location)
74      readbug($bug_num)
75
76 Reads a summary file from the archive given a bug number and a bug
77 location. Valid locations are those understood by L</getbugcomponent>
78
79 =cut
80
81
82 my %fields = (originator     => 'submitter',
83               date           => 'date',
84               subject        => 'subject',
85               msgid          => 'message-id',
86               'package'      => 'package',
87               keywords       => 'tags',
88               done           => 'done',
89               forwarded      => 'forwarded-to',
90               mergedwith     => 'merged-with',
91               severity       => 'severity',
92               owner          => 'owner',
93               found_versions => 'found-in',
94               found_date     => 'found-date',
95               fixed_versions => 'fixed-in',
96               fixed_date     => 'fixed-date',
97               blocks         => 'blocks',
98               blockedby      => 'blocked-by',
99               unarchived     => 'unarchived',
100              );
101
102 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
103 my @rfc1522_fields = qw(originator subject done forwarded owner);
104
105 sub readbug {
106      return read_bug(bug => $_[0],
107                      (@_ > 1)?(location => $_[1]):()
108                     );
109 }
110
111 =head2 read_bug
112
113      read_bug(bug => $bug_num,
114               location => 'archive',
115              );
116      read_bug(summary => 'path/to/bugnum.summary');
117      read_bug($bug_num);
118
119 A more complete function than readbug; it enables you to pass a full
120 path to the summary file instead of the bug number and/or location.
121
122 =head3 Options
123
124 =over
125
126 =item bug -- the bug number
127
128 =item location -- optional location which is passed to getbugcomponent
129
130 =item summary -- complete path to the .summary file which will be read
131
132 =back
133
134 One of C<bug> or C<summary> must be passed. This function will return
135 undef on failure, and will die if improper arguments are passed.
136
137 =cut
138
139 sub read_bug{
140     if (@_ == 1) {
141          unshift @_, 'bug';
142     }
143     my %param = validate_with(params => \@_,
144                               spec   => {bug => {type => SCALAR,
145                                                  optional => 1,
146                                                  # something really
147                                                  # stupid passes
148                                                  # negative bugnumbers
149                                                  regex    => qr/^-?\d+/,
150                                                 },
151                                          location => {type => SCALAR|UNDEF,
152                                                       optional => 1,
153                                                      },
154                                          summary  => {type => SCALAR,
155                                                       optional => 1,
156                                                      },
157                                         },
158                              );
159     die "One of bug or summary must be passed to read_bug"
160          if not exists $param{bug} and not exists $param{summary};
161     my $status;
162     my $log;
163     my $location;
164     if (not defined $param{summary}) {
165          my $lref;
166          ($lref,$location) = @param{qw(bug location)};
167          if (not defined $location) {
168               $location = getbuglocation($lref,'summary');
169               return undef if not defined $location;
170          }
171          $status = getbugcomponent($lref, 'summary', $location);
172          $log    = getbugcomponent($lref, 'log'    , $location);
173          return undef unless defined $status;
174     }
175     else {
176          $status = $param{summary};
177          $log = $status;
178          $log =~ s/\.summary$/.log/;
179          ($location) = $status =~ m/(db-h|db|archive)/;
180     }
181     my $status_fh = new IO::File $status, 'r' or
182          warn "Unable to open $status for reading: $!" and return undef;
183
184     my %data;
185     my @lines;
186     my $version = 2;
187     local $_;
188
189     while (<$status_fh>) {
190         chomp;
191         push @lines, $_;
192         $version = $1 if /^Format-Version: ([0-9]+)/i;
193     }
194
195     # Version 3 is the latest format version currently supported.
196     return undef if $version > 3;
197
198     my %namemap = reverse %fields;
199     for my $line (@lines) {
200         if ($line =~ /(\S+?): (.*)/) {
201             my ($name, $value) = (lc $1, $2);
202             $data{$namemap{$name}} = $value if exists $namemap{$name};
203         }
204     }
205     for my $field (keys %fields) {
206         $data{$field} = '' unless exists $data{$field};
207     }
208
209     $data{severity} = $config{default_severity} if $data{severity} eq '';
210     for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
211          $data{$field} = [split ' ', $data{$field}];
212     }
213     for my $field (qw(found fixed)) {
214          # create the found/fixed hashes which indicate when a
215          # particular version was marked found or marked fixed.
216          @{$data{$field}}{@{$data{"${field}_versions"}}} =
217               (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
218                @{$data{"${field}_date"}});
219     }
220
221     if ($version < 3) {
222         for my $field (@rfc1522_fields) {
223             $data{$field} = decode_rfc1522($data{$field});
224         }
225     }
226     # Add log last modified time
227     $data{log_modified} = (stat($log))[9];
228     $data{location} = $location;
229
230     return \%data;
231 }
232
233 =head2 lockreadbug
234
235      lockreadbug($bug_num,$location)
236
237 Performs a filelock, then reads the bug; the bug is unlocked if the
238 return is undefined, otherwise, you need to call unfilelock or
239 unlockwritebug.
240
241 See readbug above for information on what this returns
242
243 =cut
244
245 sub lockreadbug {
246     my ($lref, $location) = @_;
247     &filelock("lock/$lref");
248     my $data = read_bug(bug => $lref, location => $location);
249     &unfilelock unless defined $data;
250     return $data;
251 }
252
253 =head2 lockreadbugmerge
254
255      my ($locks, $data) = lockreadbugmerge($bug_num,$location);
256
257 Performs a filelock, then reads the bug. If the bug is merged, locks
258 the merge lock. Returns a list of the number of locks and the bug
259 data.
260
261 =cut
262
263 sub lockreadbugmerge {
264      my ($bug_num,$location) = @_;
265      my $data = lockreadbug(@_);
266      if (not defined $data) {
267           return (0,undef);
268      }
269      if (not length $data->{mergedwith}) {
270           return (1,$data);
271      }
272      unfilelock();
273      filelock('lock/merge');
274      $data = lockreadbug(@_);
275      if (not defined $data) {
276           unfilelock();
277           return (0,undef);
278      }
279      return (2,$data);
280 }
281
282
283 my @v1fieldorder = qw(originator date subject msgid package
284                       keywords done forwarded mergedwith severity);
285
286 =head2 makestatus
287
288      my $content = makestatus($status,$version)
289      my $content = makestatus($status);
290
291 Creates the content for a status file based on the $status hashref
292 passed.
293
294 Really only useful for writebug
295
296 Currently defaults to version 2 (non-encoded rfc1522 names) but will
297 eventually default to version 3. If you care, you should specify a
298 version.
299
300 =cut
301
302 sub makestatus {
303     my ($data,$version) = @_;
304     $version = 2 unless defined $version;
305
306     my $contents = '';
307
308     my %newdata = %$data;
309     for my $field (qw(found fixed)) {
310          if (exists $newdata{$field}) {
311               $newdata{"${field}_date"} =
312                    [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
313          }
314     }
315
316     for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
317          $newdata{$field} = join ' ', @{$newdata{$field}||[]};
318     }
319
320     if ($version < 3) {
321         for my $field (@rfc1522_fields) {
322             $newdata{$field} = encode_rfc1522($newdata{$field});
323         }
324     }
325
326     if ($version == 1) {
327         for my $field (@v1fieldorder) {
328             if (exists $newdata{$field} and defined $newdata{$field}) {
329                 $contents .= "$newdata{$field}\n";
330             } else {
331                 $contents .= "\n";
332             }
333         }
334     } elsif ($version == 2 or $version == 3) {
335         # Version 2 or 3. Add a file format version number for the sake of
336         # further extensibility in the future.
337         $contents .= "Format-Version: $version\n";
338         for my $field (keys %fields) {
339             if (exists $newdata{$field} and defined $newdata{$field}
340                 and $newdata{$field} ne '') {
341                 # Output field names in proper case, e.g. 'Merged-With'.
342                 my $properfield = $fields{$field};
343                 $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
344                 $contents .= "$properfield: $newdata{$field}\n";
345             }
346         }
347     }
348
349     return $contents;
350 }
351
352 =head2 writebug
353
354      writebug($bug_num,$status,$location,$minversion,$disablebughook)
355
356 Writes the bug status and summary files out.
357
358 Skips writting out a status file if minversion is 2
359
360 Does not call bughook if disablebughook is true.
361
362 =cut
363
364 sub writebug {
365     my ($ref, $data, $location, $minversion, $disablebughook) = @_;
366     my $change;
367
368     my %outputs = (1 => 'status', 2 => 'summary');
369     for my $version (keys %outputs) {
370         next if defined $minversion and $version < $minversion;
371         my $status = getbugcomponent($ref, $outputs{$version}, $location);
372         &quit("can't find location for $ref") unless defined $status;
373         open(S,"> $status.new") || &quit("opening $status.new: $!");
374         print(S makestatus($data, $version)) ||
375             &quit("writing $status.new: $!");
376         close(S) || &quit("closing $status.new: $!");
377         if (-e $status) {
378             $change = 'change';
379         } else {
380             $change = 'new';
381         }
382         rename("$status.new",$status) || &quit("installing new $status: $!");
383     }
384
385     # $disablebughook is a bit of a hack to let format migration scripts use
386     # this function rather than having to duplicate it themselves.
387     &bughook($change,$ref,$data) unless $disablebughook;
388 }
389
390 =head2 unlockwritebug
391
392      unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
393
394 Writes a bug, then calls unfilelock; see writebug for what these
395 options mean.
396
397 =cut
398
399 sub unlockwritebug {
400     writebug(@_);
401     &unfilelock;
402 }
403
404 =head1 VERSIONS
405
406 The following functions are exported with the :versions tag
407
408 =head2 addfoundversions
409
410      addfoundversions($status,$package,$version,$isbinary);
411
412
413
414 =cut
415
416
417 sub addfoundversions {
418     my $data = shift;
419     my $package = shift;
420     my $version = shift;
421     my $isbinary = shift;
422     return unless defined $version;
423     undef $package if $package =~ m[(?:\s|/)];
424     my $source = $package;
425
426     if (defined $package and $isbinary) {
427         my @srcinfo = binarytosource($package, $version, undef);
428         if (@srcinfo) {
429             # We know the source package(s). Use a fully-qualified version.
430             addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
431             return;
432         }
433         # Otherwise, an unqualified version will have to do.
434         undef $source;
435     }
436
437     # Strip off various kinds of brain-damage.
438     $version =~ s/;.*//;
439     $version =~ s/ *\(.*\)//;
440     $version =~ s/ +[A-Za-z].*//;
441
442     foreach my $ver (split /[,\s]+/, $version) {
443         my $sver = defined($source) ? "$source/$ver" : '';
444         unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
445             push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
446         }
447         @{$data->{fixed_versions}} =
448             grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
449     }
450 }
451
452 =head2 removefoundversions
453
454      removefoundversions($data,$package,$versiontoremove)
455
456 Removes found versions from $data
457
458 If a version is fully qualified (contains /) only versions matching
459 exactly are removed. Otherwise, all versions matching the version
460 number are removed.
461
462 Currently $package and $isbinary are entirely ignored, but accepted
463 for backwards compatibilty.
464
465 =cut
466
467 sub removefoundversions {
468     my $data = shift;
469     my $package = shift;
470     my $version = shift;
471     my $isbinary = shift;
472     return unless defined $version;
473
474     foreach my $ver (split /[,\s]+/, $version) {
475          if ($ver =~ m{/}) {
476               # fully qualified version
477               @{$data->{found_versions}} =
478                    grep {$_ ne $ver}
479                         @{$data->{found_versions}};
480          }
481          else {
482               # non qualified version; delete all matchers
483               @{$data->{found_versions}} =
484                    grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
485                         @{$data->{found_versions}};
486          }
487     }
488 }
489
490
491 sub addfixedversions {
492     my $data = shift;
493     my $package = shift;
494     my $version = shift;
495     my $isbinary = shift;
496     return unless defined $version;
497     undef $package if defined $package and $package =~ m[(?:\s|/)];
498     my $source = $package;
499
500     if (defined $package and $isbinary) {
501         my @srcinfo = binarytosource($package, $version, undef);
502         if (@srcinfo) {
503             # We know the source package(s). Use a fully-qualified version.
504             addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
505             return;
506         }
507         # Otherwise, an unqualified version will have to do.
508         undef $source;
509     }
510
511     # Strip off various kinds of brain-damage.
512     $version =~ s/;.*//;
513     $version =~ s/ *\(.*\)//;
514     $version =~ s/ +[A-Za-z].*//;
515
516     foreach my $ver (split /[,\s]+/, $version) {
517         my $sver = defined($source) ? "$source/$ver" : '';
518         unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
519             push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
520         }
521         @{$data->{found_versions}} =
522             grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
523     }
524 }
525
526 sub removefixedversions {
527     my $data = shift;
528     my $package = shift;
529     my $version = shift;
530     my $isbinary = shift;
531     return unless defined $version;
532     undef $package if $package =~ m[(?:\s|/)];
533     my $source = $package;
534
535     if (defined $package and $isbinary) {
536         my @srcinfo = binarytosource($package, $version, undef);
537         if (@srcinfo) {
538             # We know the source package(s). Use a fully-qualified version.
539             removefixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
540             return;
541         }
542         # Otherwise, an unqualified version will have to do.
543         undef $source;
544     }
545
546     foreach my $ver (split /[,\s]+/, $version) {
547         my $sver = defined($source) ? "$source/$ver" : '';
548         @{$data->{fixed_versions}} =
549             grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
550     }
551 }
552
553
554
555 =head2 splitpackages
556
557      splitpackages($pkgs)
558
559 Split a package string from the status file into a list of package names.
560
561 =cut
562
563 sub splitpackages {
564     my $pkgs = shift;
565     return unless defined $pkgs;
566     return map lc, split /[ \t?,()]+/, $pkgs;
567 }
568
569
570 =head2 bug_archiveable
571
572      bug_archiveable(bug => $bug_num);
573
574 Options
575
576 =over
577
578 =item bug -- bug number (required)
579
580 =item status -- Status hashref returned by read_bug or get_bug_status (optional)
581
582 =item version -- Debbugs::Version information (optional)
583
584 =item days_until -- return days until the bug can be archived
585
586 =back
587
588 Returns 1 if the bug can be archived
589 Returns 0 if the bug cannot be archived
590
591 If days_until is true, returns the number of days until the bug can be
592 archived, -1 if it cannot be archived. 0 means that the bug can be
593 archived the next time the archiver runs.
594
595 Returns undef on failure.
596
597 =cut
598
599 # This will eventually need to be fixed before we start using mod_perl
600 our $version_cache = {};
601 sub bug_archiveable{
602      my %param = validate_with(params => \@_,
603                                spec   => {bug => {type => SCALAR,
604                                                   regex => qr/^\d+$/,
605                                                  },
606                                           status => {type => HASHREF,
607                                                      optional => 1,
608                                                     },
609                                           days_until => {type => BOOLEAN,
610                                                          default => 0,
611                                                         },
612                                           ignore_time => {type => BOOLEAN,
613                                                           default => 0,
614                                                          },
615                                          },
616                               );
617      # This is what we return if the bug cannot be archived.
618      my $cannot_archive = $param{days_until}?-1:0;
619      # read the status information
620      my $status = $param{status};
621      if (not exists $param{status} or not defined $status) {
622           $status = read_bug(bug=>$param{bug});
623           return undef if not defined $status;
624      }
625      # Bugs can be archived if they are
626      # 1. Closed
627      return $cannot_archive if not defined $status->{done} or not length $status->{done};
628      # If we just are checking if the bug can be archived, we'll not even bother
629      # checking the versioning information if the bug has been -done for less than 28 days.
630      my $log_file = getbugcomponent($param{bug},'log');
631      if (not defined $log_file) {
632           print STDERR "Cannot archive $param{bug} because the log doesn't exist\n" if $DEBUG;
633           return $cannot_archive;
634      }
635      my $max_log_age = max(map {$config{remove_age} - -M $_}
636                            $log_file, map {my $log = getbugcomponent($_,'log');
637                                            defined $log ? ($log) : ();
638                                       }
639                            split / /, $status->{mergedwith}
640                        );
641      if (not $param{days_until} and not $param{ignore_time}
642          and $max_log_age > 0
643         ) {
644           print STDERR "Cannot archive $param{bug} because of time\n" if $DEBUG;
645           return $cannot_archive;
646      }
647      # At this point, we have to get the versioning information for this bug.
648      # We examine the set of distribution tags. If a bug has no distribution
649      # tags set, we assume a default set, otherwise we use the tags the bug
650      # has set.
651
652      # There must be fixed_versions for us to look at the versioning
653      # information
654      my $min_fixed_time = time;
655      my $min_archive_days = 0;
656      if (@{$status->{fixed_versions}}) {
657           my %dist_tags;
658           @dist_tags{@{$config{removal_distribution_tags}}} =
659                (1) x @{$config{removal_distribution_tags}};
660           my %dists;
661           @dists{@{$config{removal_default_distribution_tags}}} =
662                (1) x @{$config{removal_default_distribution_tags}};
663           for my $tag (split ' ', ($status->{tags}||'')) {
664                next unless $dist_tags{$tag};
665                $dists{$tag} = 1;
666           }
667           my %source_versions;
668           my @sourceversions = get_versions(package => $status->{package},
669                                             dist => [keys %dists],
670                                             source => 1,
671                                            );
672           @source_versions{@sourceversions} = (1) x @sourceversions;
673           # If the bug has not been fixed in the versions actually
674           # distributed, then it cannot be archived.
675           if ('found' eq max_buggy(bug => $param{bug},
676                                    sourceversions => [keys %source_versions],
677                                    found          => $status->{found_versions},
678                                    fixed          => $status->{fixed_versions},
679                                    version_cache  => $version_cache,
680                                    package        => $status->{package},
681                                   )) {
682                print STDERR "Cannot archive $param{bug} because it's found\n" if $DEBUG;
683                return $cannot_archive;
684           }
685           # Since the bug has at least been fixed in the architectures
686           # that matters, we check to see how long it has been fixed.
687
688           # If $param{ignore_time}, then we should ignore time.
689           if ($param{ignore_time}) {
690                return $param{days_until}?0:1;
691           }
692
693           # To do this, we order the times from most recent to oldest;
694           # when we come to the first found version, we stop.
695           # If we run out of versions, we only report the time of the
696           # last one.
697           my %time_versions = get_versions(package => $status->{package},
698                                            dist    => [keys %dists],
699                                            source  => 1,
700                                            time    => 1,
701                                           );
702           for my $version (sort {$time_versions{$b} <=> $time_versions{$a}} keys %time_versions) {
703                my $buggy = buggy(bug => $param{bug},
704                                  version        => $version,
705                                  found          => $status->{found_versions},
706                                  fixed          => $status->{fixed_versions},
707                                  version_cache  => $version_cache,
708                                  package        => $status->{package},
709                                 );
710                last if $buggy eq 'found';
711                $min_fixed_time = min($time_versions{$version},$min_fixed_time);
712           }
713           $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24)));
714      }
715      # If $param{ignore_time}, then we should ignore time.
716      if ($param{ignore_time}) {
717           return $param{days_until}?0:1;
718      }
719      # 6. at least 28 days have passed since the last action has occured or the bug was closed
720      my $age = ceil($max_log_age);
721      if ($age > 0 or $min_archive_days > 0) {
722           return $param{days_until}?max($age,$min_archive_days):0;
723      }
724      else {
725           return $param{days_until}?0:1;
726      }
727 }
728
729
730 =head2 get_bug_status
731
732      my $status = get_bug_status(bug => $nnn);
733
734      my $status = get_bug_status($bug_num)
735
736 =head3 Options
737
738 =over
739
740 =item bug -- scalar bug number
741
742 =item status -- optional hashref of bug status as returned by readbug
743 (can be passed to avoid rereading the bug information)
744
745 =item bug_index -- optional tied index of bug status infomration;
746 currently not correctly implemented.
747
748 =item version -- optional version(s) to check package status at
749
750 =item dist -- optional distribution(s) to check package status at
751
752 =item arch -- optional architecture(s) to check package status at
753
754 =item bugusertags -- optional hashref of bugusertags
755
756 =item sourceversion -- optional arrayref of source/version; overrides
757 dist, arch, and version. [The entries in this array must be in the
758 "source/version" format.] Eventually this can be used to for caching.
759
760 =back
761
762 Note: Currently the version information is cached; this needs to be
763 changed before using this function in long lived programs.
764
765 =cut
766
767 sub get_bug_status {
768      if (@_ == 1) {
769           unshift @_, 'bug';
770      }
771      my %param = validate_with(params => \@_,
772                                spec   => {bug       => {type => SCALAR,
773                                                         regex => qr/^\d+$/,
774                                                        },
775                                           status    => {type => HASHREF,
776                                                         optional => 1,
777                                                        },
778                                           bug_index => {type => OBJECT,
779                                                         optional => 1,
780                                                        },
781                                           version   => {type => SCALAR|ARRAYREF,
782                                                         optional => 1,
783                                                        },
784                                           dist       => {type => SCALAR|ARRAYREF,
785                                                          optional => 1,
786                                                         },
787                                           arch       => {type => SCALAR|ARRAYREF,
788                                                          optional => 1,
789                                                         },
790                                           bugusertags   => {type => HASHREF,
791                                                             optional => 1,
792                                                            },
793                                           sourceversions => {type => ARRAYREF,
794                                                              optional => 1,
795                                                             },
796                                          },
797                               );
798      my %status;
799
800      if (defined $param{bug_index} and
801          exists $param{bug_index}{$param{bug}}) {
802           %status = %{ $param{bug_index}{$param{bug}} };
803           $status{pending} = $status{ status };
804           $status{id} = $param{bug};
805           return \%status;
806      }
807      if (defined $param{status}) {
808           %status = %{$param{status}};
809      }
810      else {
811           my $location = getbuglocation($param{bug}, 'summary');
812           return {} if not defined $location or not length $location;
813           %status = %{ readbug( $param{bug}, $location ) };
814      }
815      $status{id} = $param{bug};
816
817      if (defined $param{bugusertags}{$param{bug}}) {
818           $status{keywords} = "" unless defined $status{keywords};
819           $status{keywords} .= " " unless $status{keywords} eq "";
820           $status{keywords} .= join(" ", @{$param{bugusertags}{$param{bug}}});
821      }
822      $status{tags} = $status{keywords};
823      my %tags = map { $_ => 1 } split ' ', $status{tags};
824
825      $status{"package"} =~ s/\s*$//;
826      $status{"package"} = 'unknown' if ($status{"package"} eq '');
827      $status{"severity"} = 'normal' if ($status{"severity"} eq '');
828
829      $status{"pending"} = 'pending';
830      $status{"pending"} = 'forwarded'       if (length($status{"forwarded"}));
831      $status{"pending"} = 'pending-fixed'    if ($tags{pending});
832      $status{"pending"} = 'fixed'           if ($tags{fixed});
833
834
835      my $presence = bug_presence(map{(exists $param{$_})?($_,$param{$_}):()}
836                                  qw(bug sourceversions arch dist version found fixed package)
837                                 );
838      if (defined $presence) {
839           if ($presence eq 'fixed') {
840                $status{pending} = 'done';
841           }
842           elsif ($presence eq 'absent') {
843                $status{pending} = 'absent';
844           }
845      }
846      return \%status;
847 }
848
849 =head2 bug_presence
850
851      my $precence = bug_presence(bug => nnn,
852                                  ...
853                                 );
854
855 Returns 'found', 'absent', 'fixed' or undef based on whether the bug
856 is found, absent, fixed, or no information is available in the
857 distribution (dist) and/or architecture (arch) specified.
858
859
860 =head3 Options
861
862 =over
863
864 =item bug -- scalar bug number
865
866 =item status -- optional hashref of bug status as returned by readbug
867 (can be passed to avoid rereading the bug information)
868
869 =item bug_index -- optional tied index of bug status infomration;
870 currently not correctly implemented.
871
872 =item version -- optional version to check package status at
873
874 =item dist -- optional distribution to check package status at
875
876 =item arch -- optional architecture to check package status at
877
878 =item sourceversion -- optional arrayref of source/version; overrides
879 dist, arch, and version. [The entries in this array must be in the
880 "source/version" format.] Eventually this can be used to for caching.
881
882 =back
883
884 =cut
885
886 sub bug_presence {
887      my %param = validate_with(params => \@_,
888                                spec   => {bug       => {type => SCALAR,
889                                                         regex => qr/^\d+$/,
890                                                        },
891                                           status    => {type => HASHREF,
892                                                         optional => 1,
893                                                        },
894                                           version   => {type => SCALAR|ARRAYREF,
895                                                         optional => 1,
896                                                        },
897                                           dist       => {type => SCALAR|ARRAYREF,
898                                                          optional => 1,
899                                                         },
900                                           arch       => {type => SCALAR|ARRAYREF,
901                                                          optional => 1,
902                                                         },
903                                           sourceversions => {type => ARRAYREF,
904                                                              optional => 1,
905                                                             },
906                                          },
907                               );
908      my %status;
909      if (defined $param{status}) {
910          %status = %{$param{status}};
911      }
912      else {
913           my $location = getbuglocation($param{bug}, 'summary');
914           return {} if not length $location;
915           %status = %{ readbug( $param{bug}, $location ) };
916      }
917
918      my @sourceversions;
919      my $pseudo_desc = getpseudodesc();
920      if (not exists $param{sourceversions}) {
921           my %sourceversions;
922           # pseudopackages do not have source versions by definition.
923           if (exists $pseudo_desc->{$status{package}}) {
924                # do nothing.
925           }
926           elsif (defined $param{version}) {
927                foreach my $arch (make_list($param{arch})) {
928                     for my $package (split /\s*,\s*/, $status{package}) {
929                          my @temp = makesourceversions($package,
930                                                        $arch,
931                                                        make_list($param{version})
932                                                       );
933                          @sourceversions{@temp} = (1) x @temp;
934                     }
935                }
936           } elsif (defined $param{dist}) {
937                foreach my $arch (make_list($param{arch})) {
938                     my @versions;
939                     for my $package (split /\s*,\s*/, $status{package}) {
940                          foreach my $dist (make_list($param{dist})) {
941                               push @versions, getversions($package, $dist, $arch);
942                          }
943                          my @temp = makesourceversions($package,
944                                                        $arch,
945                                                        @versions
946                                                       );
947                          @sourceversions{@temp} = (1) x @temp;
948                     }
949                }
950           }
951
952           # TODO: This should probably be handled further out for efficiency and
953           # for more ease of distinguishing between pkg= and src= queries.
954           # DLA: src= queries should just pass arch=source, and they'll be happy.
955           @sourceversions = keys %sourceversions;
956      }
957      else {
958           @sourceversions = @{$param{sourceversions}};
959      }
960      my $maxbuggy = 'undef';
961      if (@sourceversions) {
962           $maxbuggy = max_buggy(bug => $param{bug},
963                                    sourceversions => \@sourceversions,
964                                    found => $status{found_versions},
965                                    fixed => $status{fixed_versions},
966                                    package => $status{package},
967                                    version_cache => $version_cache,
968                                   );
969      }
970      elsif (defined $param{dist}) {
971           return 'absent';
972      }
973      if (length($status{done}) and
974          (not @sourceversions or not @{$status{fixed_versions}})) {
975           return 'fixed';
976      }
977      return $maxbuggy;
978 }
979
980
981 =head2 max_buggy
982
983      max_buggy()
984
985 =head3 Options
986
987 =over
988
989 =item bug -- scalar bug number
990
991 =item sourceversion -- optional arrayref of source/version; overrides
992 dist, arch, and version. [The entries in this array must be in the
993 "source/version" format.] Eventually this can be used to for caching.
994
995 =back
996
997 Note: Currently the version information is cached; this needs to be
998 changed before using this function in long lived programs.
999
1000
1001 =cut
1002 sub max_buggy{
1003      my %param = validate_with(params => \@_,
1004                                spec   => {bug       => {type => SCALAR,
1005                                                         regex => qr/^\d+$/,
1006                                                        },
1007                                           sourceversions => {type => ARRAYREF,
1008                                                              default => [],
1009                                                             },
1010                                           found          => {type => ARRAYREF,
1011                                                              default => [],
1012                                                             },
1013                                           fixed          => {type => ARRAYREF,
1014                                                              default => [],
1015                                                             },
1016                                           package        => {type => SCALAR,
1017                                                             },
1018                                           version_cache  => {type => HASHREF,
1019                                                              default => {},
1020                                                             },
1021                                          },
1022                               );
1023      # Resolve bugginess states (we might be looking at multiple
1024      # architectures, say). Found wins, then fixed, then absent.
1025      my $maxbuggy = 'absent';
1026      for my $package (split /\s*,\s*/, $param{package}) {
1027           for my $version (@{$param{sourceversions}}) {
1028                my $buggy = buggy(bug => $param{bug},
1029                                  version => $version,
1030                                  found => $param{found},
1031                                  fixed => $param{fixed},
1032                                  version_cache => $param{version_cache},
1033                                  package => $package,
1034                                 );
1035                if ($buggy eq 'found') {
1036                     return 'found';
1037                } elsif ($buggy eq 'fixed') {
1038                     $maxbuggy = 'fixed';
1039                }
1040           }
1041      }
1042      return $maxbuggy;
1043 }
1044
1045
1046 =head2 buggy
1047
1048      buggy(bug => nnn,
1049            found => \@found,
1050            fixed => \@fixed,
1051            package => 'foo',
1052            version => '1.0',
1053           );
1054
1055 Returns the output of Debbugs::Versions::buggy for a particular
1056 package, version and found/fixed set. Automatically turns found, fixed
1057 and version into source/version strings.
1058
1059 Caching can be had by using the version_cache, but no attempt to check
1060 to see if the on disk information is more recent than the cache is
1061 made. [This will need to be fixed for long-lived processes.]
1062
1063 =cut
1064
1065 sub buggy {
1066      my %param = validate_with(params => \@_,
1067                                spec   => {bug => {type => SCALAR,
1068                                                   regex => qr/^\d+$/,
1069                                                  },
1070                                           found => {type => ARRAYREF,
1071                                                     default => [],
1072                                                    },
1073                                           fixed => {type => ARRAYREF,
1074                                                     default => [],
1075                                                    },
1076                                           version_cache => {type => HASHREF,
1077                                                             optional => 1,
1078                                                            },
1079                                           package => {type => SCALAR,
1080                                                      },
1081                                           version => {type => SCALAR,
1082                                                      },
1083                                          },
1084                               );
1085      my @found = @{$param{found}};
1086      my @fixed = @{$param{fixed}};
1087      if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
1088           # We have non-source version versions
1089           @found = makesourceversions($param{package},undef,
1090                                       @found
1091                                      );
1092           @fixed = makesourceversions($param{package},undef,
1093                                       @fixed
1094                                      );
1095      }
1096      if ($param{version} !~ m{/}) {
1097           my ($version) = makesourceversions($param{package},undef,
1098                                              $param{version}
1099                                             );
1100           $param{version} = $version if defined $version;
1101      }
1102      # Figure out which source packages we need
1103      my %sources;
1104      @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
1105      @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
1106      @sources{map {m{(.+)/}; $1} $param{version}} = 1 if
1107           $param{version} =~ m{/};
1108      my $version;
1109      if (not defined $param{version_cache} or
1110          not exists $param{version_cache}{join(',',sort keys %sources)}) {
1111           $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
1112           foreach my $source (keys %sources) {
1113                my $srchash = substr $source, 0, 1;
1114                my $version_fh = IO::File->new("$config{version_packages_dir}/$srchash/$source", 'r');
1115                if (not defined $version_fh) {
1116                     # We only want to warn if it's a package which actually has a maintainer
1117                     my $maints = getmaintainers();
1118                     next if not exists $maints->{$source};
1119                     warn "Bug $param{bug}: unable to open $config{version_packages_dir}/$srchash/$source: $!";
1120                     next;
1121                }
1122                $version->load($version_fh);
1123           }
1124           if (defined $param{version_cache}) {
1125                $param{version_cache}{join(',',sort keys %sources)} = $version;
1126           }
1127      }
1128      else {
1129           $version = $param{version_cache}{join(',',sort keys %sources)};
1130      }
1131      return $version->buggy($param{version},\@found,\@fixed);
1132 }
1133
1134 sub isstrongseverity {
1135     my $severity = shift;
1136     $severity = $config{default_severity} if
1137          not defined $severity or $severity eq '';
1138     return grep { $_ eq $severity } @{$config{strong_severities}};
1139 }
1140
1141
1142 =head1 PRIVATE FUNCTIONS
1143
1144 =cut
1145
1146 sub update_realtime {
1147         my ($file, %bugs) = @_;
1148
1149         # update realtime index.db
1150
1151         return () unless keys %bugs;
1152         my $idx_old = IO::File->new($file,'r')
1153              or die "Couldn't open ${file}: $!";
1154         my $idx_new = IO::File->new($file.'.new','w')
1155              or die "Couldn't open ${file}.new: $!";
1156
1157         my $min_bug = min(keys %bugs);
1158         my $line;
1159         my @line;
1160         my %changed_bugs;
1161         while($line = <$idx_old>) {
1162              @line = split /\s/, $line;
1163              # Two cases; replacing existing line or adding new line
1164              if (exists $bugs{$line[1]}) {
1165                   my $new = $bugs{$line[1]};
1166                   delete $bugs{$line[1]};
1167                   $min_bug = min(keys %bugs);
1168                   if ($new eq "NOCHANGE") {
1169                        print {$idx_new} $line;
1170                        $changed_bugs{$line[1]} = $line;
1171                   } elsif ($new eq "REMOVE") {
1172                        $changed_bugs{$line[1]} = $line;
1173                   } else {
1174                        print {$idx_new} $new;
1175                        $changed_bugs{$line[1]} = $line;
1176                   }
1177              }
1178              else {
1179                   while ($line[1] > $min_bug) {
1180                        print {$idx_new} $bugs{$min_bug};
1181                        delete $bugs{$min_bug};
1182                        last unless keys %bugs;
1183                        $min_bug = min(keys %bugs);
1184                   }
1185                   print {$idx_new} $line;
1186              }
1187              last unless keys %bugs;
1188         }
1189         print {$idx_new} map {$bugs{$_}} sort keys %bugs;
1190
1191         print {$idx_new} <$idx_old>;
1192
1193         close($idx_new);
1194         close($idx_old);
1195
1196         rename("$file.new", $file);
1197
1198         return %changed_bugs;
1199 }
1200
1201 sub bughook_archive {
1202         my @refs = @_;
1203         &filelock("debbugs.trace.lock");
1204         &appendfile("debbugs.trace","archive ".join(',',@refs)."\n");
1205         my %bugs = update_realtime("$config{spool_dir}/index.db.realtime",
1206                                    map{($_,'REMOVE')} @refs);
1207         update_realtime("$config{spool_dir}/index.archive.realtime",
1208                         %bugs);
1209         &unfilelock;
1210 }
1211
1212 sub bughook {
1213         my ( $type, %bugs_temp ) = @_;
1214         &filelock("debbugs.trace.lock");
1215
1216         my %bugs;
1217         for my $bug (keys %bugs_temp) {
1218              my $data = $bugs_temp{$bug};
1219              &appendfile("debbugs.trace","$type $bug\n",makestatus($data, 1));
1220
1221              my $whendone = "open";
1222              my $severity = $config{default_severity};
1223              (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
1224              $pkglist =~ s/^,+//;
1225              $pkglist =~ s/,+$//;
1226              $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
1227              $whendone = "done" if defined $data->{done} and length $data->{done};
1228              $severity = $data->{severity} if length $data->{severity};
1229
1230              my $k = sprintf "%s %d %d %s [%s] %s %s\n",
1231                   $pkglist, $bug, $data->{date}, $whendone,
1232                        $data->{originator}, $severity, $data->{keywords};
1233              $bugs{$bug} = $k;
1234         }
1235         update_realtime("$config{spool_dir}/index.db.realtime", %bugs);
1236
1237         &unfilelock;
1238 }
1239
1240
1241 1;
1242
1243 __END__