]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Bugs.pm
speed up src and maint searches by using subselects
[debbugs.git] / Debbugs / Bugs.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 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
7
8 package Debbugs::Bugs;
9
10 =head1 NAME
11
12 Debbugs::Bugs -- Bug selection routines for debbugs
13
14 =head1 SYNOPSIS
15
16 use Debbugs::Bugs qw(get_bugs);
17
18
19 =head1 DESCRIPTION
20
21 This module is a replacement for all of the various methods of
22 selecting different types of bugs.
23
24 It implements a single function, get_bugs, which defines the master
25 interface for selecting bugs.
26
27 It attempts to use subsidiary functions to actually do the selection,
28 in the order specified in the configuration files. [Unless you're
29 insane, they should be in order from fastest (and often most
30 incomplete) to slowest (and most complete).]
31
32 =head1 BUGS
33
34 =head1 FUNCTIONS
35
36 =cut
37
38 use warnings;
39 use strict;
40 use feature 'state';
41 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
42 use Exporter qw(import);
43
44 BEGIN{
45      $VERSION = 1.00;
46      $DEBUG = 0 unless defined $DEBUG;
47
48      @EXPORT = ();
49      %EXPORT_TAGS = ();
50      @EXPORT_OK = (qw(get_bugs count_bugs newest_bug bug_filter));
51      $EXPORT_TAGS{all} = [@EXPORT_OK];
52 }
53
54 use Debbugs::Config qw(:config);
55 use Params::Validate qw(validate_with :types);
56 use IO::File;
57 use Debbugs::Status qw(splitpackages get_bug_status);
58 use Debbugs::Packages qw(getsrcpkgs getpkgsrc);
59 use Debbugs::Common qw(getparsedaddrs package_maintainer getmaintainers make_list hash_slice);
60 use Fcntl qw(O_RDONLY);
61 use MLDBM qw(DB_File Storable);
62 use List::AllUtils qw(first);
63 use Carp;
64
65 =head2 get_bugs
66
67      get_bugs()
68
69 =head3 Parameters
70
71 The following parameters can either be a single scalar or a reference
72 to an array. The parameters are ANDed together, and the elements of
73 arrayrefs are a parameter are ORed. Future versions of this may allow
74 for limited regular expressions, and/or more complex expressions.
75
76 =over
77
78 =item package -- name of the binary package
79
80 =item src -- name of the source package
81
82 =item maint -- address of the maintainer
83
84 =item submitter -- address of the submitter
85
86 =item severity -- severity of the bug
87
88 =item status -- status of the bug
89
90 =item tag -- bug tags
91
92 =item owner -- owner of the bug
93
94 =item correspondent -- address of someone who sent mail to the log
95
96 =item affects -- bugs which affect this package
97
98 =item dist -- distribution (I don't know about this one yet)
99
100 =item bugs -- list of bugs to search within
101
102 =item function -- see description below
103
104 =back
105
106 =head3 Special options
107
108 The following options are special options used to modulate how the
109 searches are performed.
110
111 =over
112
113 =item archive -- whether to search archived bugs or normal bugs;
114 defaults to false. As a special case, if archive is 'both', but
115 archived and unarchived bugs are returned.
116
117 =item usertags -- set of usertags and the bugs they are applied to
118
119 =back
120
121
122 =head3 Subsidiary routines
123
124 All subsidiary routines get passed exactly the same set of options as
125 get_bugs. If for some reason they are unable to handle the options
126 passed (for example, they don't have the right type of index for the
127 type of selection) they should die as early as possible. [Using
128 Params::Validate and/or die when files don't exist makes this fairly
129 trivial.]
130
131 This function will then immediately move on to the next subroutine,
132 giving it the same arguments.
133
134 =head3 function
135
136 This option allows you to provide an arbitrary function which will be
137 given the information in the index.db file. This will be super, super
138 slow, so only do this if there's no other way to write the search.
139
140 You'll be given a list (which you can turn into a hash) like the
141 following:
142
143  (pkg => ['a','b'], # may be a scalar (most common)
144   bug => 1234,
145   status => 'pending',
146   submitter => 'boo@baz.com',
147   severity => 'serious',
148   tags => ['a','b','c'], # may be an empty arrayref
149  )
150
151 The function should return 1 if the bug should be included; 0 if the
152 bug should not.
153
154 =cut
155
156 state $_non_search_key_regex = qr/^(bugs|archive|usertags|schema)$/;
157
158 my %_get_bugs_common_options =
159     (package   => {type => SCALAR|ARRAYREF,
160                    optional => 1,
161                   },
162      src       => {type => SCALAR|ARRAYREF,
163                    optional => 1,
164                   },
165      maint     => {type => SCALAR|ARRAYREF,
166                    optional => 1,
167                   },
168      submitter => {type => SCALAR|ARRAYREF,
169                    optional => 1,
170                   },
171      severity  => {type => SCALAR|ARRAYREF,
172                    optional => 1,
173                   },
174      status    => {type => SCALAR|ARRAYREF,
175                    optional => 1,
176                   },
177      tag       => {type => SCALAR|ARRAYREF,
178                    optional => 1,
179                   },
180      owner     => {type => SCALAR|ARRAYREF,
181                    optional => 1,
182                   },
183      dist      => {type => SCALAR|ARRAYREF,
184                    optional => 1,
185                   },
186      correspondent => {type => SCALAR|ARRAYREF,
187                        optional => 1,
188                       },
189      affects   => {type => SCALAR|ARRAYREF,
190                    optional => 1,
191                   },
192      function  => {type => CODEREF,
193                    optional => 1,
194                   },
195      bugs      => {type => SCALAR|ARRAYREF,
196                    optional => 1,
197                   },
198      archive   => {type => BOOLEAN|SCALAR,
199                    default => 0,
200                   },
201      usertags  => {type => HASHREF,
202                    optional => 1,
203                   },
204      schema => {type     => OBJECT,
205                 optional => 1,
206                },
207     );
208
209
210 state $_get_bugs_options = {%_get_bugs_common_options};
211 sub get_bugs{
212      my %param = validate_with(params => \@_,
213                                spec   => $_get_bugs_options,
214                               );
215
216      # Normalize options
217      my %options = %param;
218      my @bugs;
219      if ($options{archive} eq 'both') {
220           push @bugs, get_bugs(%options,archive=>0);
221           push @bugs, get_bugs(%options,archive=>1);
222           my %bugs;
223           @bugs{@bugs} = @bugs;
224           return keys %bugs;
225      }
226      # A configuration option will set an array that we'll use here instead.
227      for my $routine (qw(Debbugs::Bugs::get_bugs_by_db Debbugs::Bugs::get_bugs_by_idx Debbugs::Bugs::get_bugs_flatfile)) {
228           my ($package) = $routine =~ m/^(.+)\:\:/;
229           eval "use $package;";
230           if ($@) {
231                # We output errors here because using an invalid function
232                # in the configuration file isn't something that should
233                # be done.
234                warn "use $package failed with $@";
235                next;
236           }
237           @bugs = eval "${routine}(\%options)";
238           if ($@) {
239
240                # We don't output errors here, because failure here
241                # via die may be a perfectly normal thing.
242                print STDERR "$@" if $DEBUG;
243                next;
244           }
245           last;
246      }
247      # If no one succeeded, die
248      if ($@) {
249           die "$@";
250      }
251      return @bugs;
252 }
253
254 =head2 count_bugs
255
256      count_bugs(function => sub {...})
257
258 Uses a subroutine to classify bugs into categories and return the
259 number of bugs which fall into those categories
260
261 =cut
262
263 sub count_bugs {
264      my %param = validate_with(params => \@_,
265                                spec   => {function => {type => CODEREF,
266                                                       },
267                                           archive  => {type => BOOLEAN,
268                                                        default => 0,
269                                                       },
270                                          },
271                               );
272      my $flatfile;
273      if ($param{archive}) {
274           $flatfile = IO::File->new("$config{spool_dir}/index.archive", 'r')
275                or die "Unable to open $config{spool_dir}/index.archive for reading: $!";
276      }
277      else {
278           $flatfile = IO::File->new("$config{spool_dir}/index.db", 'r')
279                or die "Unable to open $config{spool_dir}/index.db for reading: $!";
280      }
281      my %count = ();
282      while(<$flatfile>) {
283           if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
284                my @x = $param{function}->(pkg       => $1,
285                                           bug       => $2,
286                                           status    => $4,
287                                           submitter => $5,
288                                           severity  => $6,
289                                           tags      => $7,
290                                          );
291                local $_;
292                $count{$_}++ foreach @x;
293           }
294      }
295      close $flatfile;
296      return %count;
297 }
298
299 =head2 newest_bug
300
301      my $bug = newest_bug();
302
303 Returns the bug number of the newest bug, which is nextnumber-1.
304
305 =cut
306
307 sub newest_bug {
308      my $nn_fh = IO::File->new("$config{spool_dir}/nextnumber",'r')
309           or die "Unable to open $config{spool_dir}nextnumber for reading: $!";
310      local $/;
311      my $next_number = <$nn_fh>;
312      close $nn_fh;
313      chomp $next_number;
314      return $next_number-1;
315 }
316
317 =head2 bug_filter
318
319      bug_filter
320
321 Allows filtering bugs on commonly used criteria
322
323
324
325 =cut
326
327 sub bug_filter {
328      my %param = validate_with(params => \@_,
329                                spec   => {bug    => {type => ARRAYREF|SCALAR,
330                                                      optional => 1,
331                                                     },
332                                           status => {type => HASHREF|ARRAYREF,
333                                                      optional => 1,
334                                                     },
335                                           seen_merged => {type => HASHREF,
336                                                           optional => 1,
337                                                          },
338                                           repeat_merged => {type => BOOLEAN,
339                                                             default => 1,
340                                                            },
341                                           include => {type => HASHREF,
342                                                       optional => 1,
343                                                      },
344                                           exclude => {type => HASHREF,
345                                                       optional => 1,
346                                                      },
347                                           min_days => {type => SCALAR,
348                                                        optional => 1,
349                                                       },
350                                           max_days => {type => SCALAR,
351                                                        optional => 1,
352                                                       },
353                                          },
354                               );
355      if (exists $param{repeat_merged} and
356          not $param{repeat_merged} and
357          not defined $param{seen_merged}) {
358           croak "repeat_merged false requires seen_merged to be passed";
359      }
360      if (not exists $param{bug} and not exists $param{status}) {
361          croak "one of bug or status must be passed";
362      }
363
364      if (not exists $param{status}) {
365           my $location = getbuglocation($param{bug}, 'summary');
366           return 0 if not defined $location or not length $location;
367           $param{status} = readbug( $param{bug}, $location );
368           return 0 if not defined $param{status};
369      }
370
371      if (exists $param{include}) {
372           return 1 if (!__bug_matches($param{include}, $param{status}));
373      }
374      if (exists $param{exclude}) {
375           return 1 if (__bug_matches($param{exclude}, $param{status}));
376      }
377      if (exists $param{repeat_merged} and not $param{repeat_merged}) {
378           my @merged = sort {$a<=>$b} $param{bug}, split(/ /, $param{status}{mergedwith});
379           return 1 if first {defined $_} @{$param{seen_merged}}{@merged};
380           @{$param{seen_merged}}{@merged} = (1) x @merged;
381      }
382      my $daysold = int((time - $param{status}{date}) / 86400);   # seconds to days
383      if (exists $param{min_days}) {
384           return 1 unless $param{min_days} <= $daysold;
385      }
386      if (exists $param{max_days}) {
387           return 1 unless $param{max_days} == -1 or
388                $param{max_days} >= $daysold;
389      }
390      return 0;
391 }
392
393
394 =head2 get_bugs_by_idx
395
396 This routine uses the by-$index.idx indicies to try to speed up
397 searches.
398
399
400 =cut
401
402
403 state $_get_bugs_by_idx_options =
404    {hash_slice(%_get_bugs_common_options,
405                (qw(package submitter severity tag archive),
406                 qw(owner src maint bugs correspondent),
407                 qw(affects usertags))
408               )
409    };
410 sub get_bugs_by_idx{
411      my %param = validate_with(params => \@_,
412                                spec   => $_get_bugs_by_idx_options
413                               );
414      my %bugs = ();
415
416      # If we're given an empty maint (unmaintained packages), we can't
417      # handle it, so bail out here
418      for my $maint (make_list(exists $param{maint}?$param{maint}:[])) {
419           if (defined $maint and $maint eq '') {
420                die "Can't handle empty maint (unmaintained packages) in get_bugs_by_idx";
421           }
422      }
423
424      # We handle src packages, maint and maintenc by mapping to the
425      # appropriate binary packages, then removing all packages which
426      # don't match all queries
427      my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
428                                                qw(package src maint)
429                                               );
430      if (exists $param{package} or
431          exists $param{src} or
432          exists $param{maint}) {
433           delete @param{qw(maint src)};
434           $param{package} = [@packages];
435      }
436      my $keys = grep {$_ !~ $_non_search_key_regex} keys(%param);
437      die "Need at least 1 key to search by" unless $keys;
438      my $arc = $param{archive} ? '-arc':'';
439      my %idx;
440      for my $key (grep {$_ !~ $_non_search_key_regex} keys %param) {
441           my $index = $key;
442           $index = 'submitter-email' if $key eq 'submitter';
443           $index = "$config{spool_dir}/by-${index}${arc}.idx";
444           tie(%idx, MLDBM => $index, O_RDONLY)
445                or die "Unable to open $index: $!";
446           my %bug_matching = ();
447           for my $search (make_list($param{$key})) {
448                for my $bug (keys %{$idx{$search}||{}}) {
449                     next if $bug_matching{$bug};
450                     # increment the number of searches that this bug matched
451                     $bugs{$bug}++;
452                     $bug_matching{$bug}=1;
453                }
454                if ($search ne lc($search)) {
455                     for my $bug (keys %{$idx{lc($search)}||{}}) {
456                          next if $bug_matching{$bug};
457                          # increment the number of searches that this bug matched
458                          $bugs{$bug}++;
459                          $bug_matching{$bug}=1;
460                     }
461                }
462           }
463           if ($key eq 'tag' and exists $param{usertags}) {
464                for my $bug (make_list(grep {defined $_ } @{$param{usertags}}{make_list($param{tag})})) {
465                     next if $bug_matching{$bug};
466                     $bugs{$bug}++;
467                     $bug_matching{$bug}=1;
468                }
469           }
470           untie %idx or die 'Unable to untie %idx';
471      }
472      if ($param{bugs}) {
473           $keys++;
474           for my $bug (make_list($param{bugs})) {
475                $bugs{$bug}++;
476           }
477      }
478      # Throw out results that do not match all of the search specifications
479      return map {$keys <= $bugs{$_}?($_):()} keys %bugs;
480 }
481
482
483 =head2 get_bugs_by_db
484
485 This routine uses the database to try to speed up
486 searches.
487
488
489 =cut
490
491 state $_get_bugs_by_db_options =
492    {hash_slice(%_get_bugs_common_options,
493                (qw(package submitter severity tag archive),
494                 qw(owner src maint bugs correspondent),
495                 qw(affects usertags))
496               ),
497     schema => {type     => OBJECT,
498               },
499    };
500 sub get_bugs_by_db{
501      my %param = validate_with(params => \@_,
502                                spec   => $_get_bugs_by_db_options,
503                               );
504      my %bugs = ();
505
506      my $s = $param{schema};
507      my $keys = grep {$_ !~ $_non_search_key_regex} keys(%param);
508      die "Need at least 1 key to search by" unless $keys;
509      my $rs = $param{schema}->resultset('Bug');
510      if (exists $param{package}) {
511          $rs = $rs->search({-or => {map 'bin_package.'}})
512      }
513      if (exists $param{severity}) {
514          $rs = $rs->search({'severity.severity' =>
515                             [make_list($param{severity})],
516                            },
517                           {join => 'severity'},
518                           );
519      }
520      for my $key (qw(owner submitter done)) {
521          if (exists $param{$key}) {
522              $rs = $rs->search({"${key}.addr" =>
523                                 [make_list($param{$key})],
524                                },
525                               {join => $key},
526                               );
527          }
528      }
529      if (exists $param{correspondent}) {
530          $rs = $rs->search({'message_correspondents.addr' =>
531                             [make_list($param{correspondent})],
532                            },
533                           {join => {correspondent =>
534                                    {bug_messages =>
535                                    {message => 'message_correspondents'}}}},
536                           );
537      }
538      if (exists $param{affects}) {
539          my @aff_list = make_list($param{affects});
540          $rs = $rs->search({-or => {'bin_pkg.pkg' =>
541                                     [@aff_list],
542                                     'src_pkg.pkg' =>
543                                     [@aff_list],
544                                     'me.unknown_affects' =>
545                                     [@aff_list]
546                                    },
547                            },
548                           {join => [{bug_affects_binpackages => 'bin_pkg'},
549                                    {bug_affects_srcpackages => 'src_pkg'},
550                                    ],
551                           },
552                           );
553      }
554      if (exists $param{package}) {
555          $rs = $rs->search({'bin_pkg.pkg' =>
556                             [make_list($param{package})],
557                            },
558                           {join => {bug_binpackages => 'bin_pkg'}});
559      }
560      if (exists $param{maint}) {
561          my @maint_list =
562              map {$_ eq '' ? undef : $_}
563              make_list($param{maint});
564          my $bin_pkgs_rs =
565              $s->resultset('BinPkg')->
566              search({'correspondent.addr' => [@maint_list]},
567                    {join => {bin_vers =>
568                             {src_ver =>
569                             {maintainer => 'correspondent'}}},
570                     columns => ['id'],
571                     group_by => ['me.id'],
572                    },
573                    );
574          my $src_pkgs_rs =
575              $s->resultset('SrcPkg')->
576              search({'correspondent.addr' => [@maint_list]},
577                    {join => {src_vers =>
578                             {maintainer => 'correspondent'}},
579                     columns => ['id'],
580                     group_by => ['me.id'],
581                    },
582                    );
583          $rs = $rs->search({-or => {'bug_binpackages.bin_pkg' =>
584                                    { -in => $bin_pkgs_rs->get_column('id')->as_query},
585                                     'bug_srcpackages.src_pkg' => 
586                                    { -in => $bin_pkgs_rs->get_column('id')->as_query},
587                                    },
588                            },
589                           {join => ['bug_binpackages',
590                                     'bug_srcpackages',
591                                    ]}
592                           );
593      }
594      if (exists $param{src}) {
595          # identify all of the srcpackages and binpackages that match first
596          my $src_pkgs_rs =
597          $s->resultset('SrcPkg')->
598              search({-or => [map {('me.pkg' => $_,
599                                   )}
600                              make_list($param{src})],
601                      columns => ['id'],
602                      group_by => ['me.id'],
603                     },
604                    );
605          my $bin_pkgs_rs =
606          $s->resultset('BinPkg')->
607              search({-or => [map {('src_pkg.pkg' => $_,
608                                   )}
609                              make_list($param{src})],
610                      },
611                    {join => {bin_vers => {src_ver => 'src_pkg'}},
612                     columns => ['id'],
613                     group_by => ['me.id'],
614                    });
615          $rs = $rs->search({-or => {'bug_binpackages.bin_pkg' =>
616                                    { -in => $bin_pkgs_rs->get_column('id')->as_query},
617                                     'bug_srcpackages.src_pkg' => 
618                                    { -in => $bin_pkgs_rs->get_column('id')->as_query},
619                                     'me.unknown_package' =>
620                                     [make_list($param{src})],
621                                    },
622                            },
623                           {join => ['bug_binpackages',
624                                     'bug_srcpackages',
625                                    ]}
626                           );
627      }
628      # tags are very odd, because we must handle usertags.
629      if (exists $param{tag}) {
630          # bugs from usertags which matter
631          my %bugs_matching_usertags;
632          for my $bug (make_list(grep {defined $_ }
633                                 @{$param{usertags}}{make_list($param{tag})})) {
634              $bugs_matching_usertags{$bug} = 1;
635          }
636          # we want all bugs which either match the tag name given in
637          # param, or have a usertag set which matches one of the tag
638          # names given in param.
639          $rs = $rs->search({-or => {map {('tag.tag' => $_)}
640                                     make_list($param{tag}),
641                                     map {('me.id' => $_)}
642                                     keys %bugs_matching_usertags
643                                    },
644                            },
645                           {join => {bug_tags => 'tag'}});
646      }
647      if (exists $param{bugs}) {
648          $rs = $rs->search({-or => {map {('me.id' => $_)}
649                                     make_list($param{bugs})}
650                            });
651      }
652      # handle archive
653      if (defined $param{archive} and $param{archive} ne 'both') {
654          $rs = $rs->search({'me.archived' => $param{archive}});
655      }
656      return $rs->get_column('id')->all();
657 }
658
659
660 =head2 get_bugs_flatfile
661
662 This is the fallback search routine. It should be able to complete all
663 searches. [Or at least, that's the idea.]
664
665 =cut
666
667 state $_get_bugs_flatfile_options =
668    {hash_slice(%_get_bugs_common_options,
669                map {$_ eq 'dist'?():($_)} keys %_get_bugs_common_options
670               )
671    };
672
673 sub get_bugs_flatfile{
674      my %param = validate_with(params => \@_,
675                                spec   => $_get_bugs_flatfile_options
676                               );
677      my $flatfile;
678      if ($param{archive}) {
679           $flatfile = IO::File->new("$config{spool_dir}/index.archive", 'r')
680                or die "Unable to open $config{spool_dir}/index.archive for reading: $!";
681      }
682      else {
683           $flatfile = IO::File->new("$config{spool_dir}/index.db", 'r')
684                or die "Unable to open $config{spool_dir}/index.db for reading: $!";
685      }
686      my %usertag_bugs;
687      if (exists $param{tag} and exists $param{usertags}) {
688           # This complex slice makes a hash with the bugs which have the
689           # usertags passed in $param{tag} set.
690           @usertag_bugs{make_list(@{$param{usertags}}{make_list($param{tag})})
691                         } = (1) x make_list(@{$param{usertags}}{make_list($param{tag})});
692      }
693      my $unmaintained_packages = 0;
694      # unmaintained packages is a special case
695      my @maints = make_list(exists $param{maint}?$param{maint}:[]);
696      $param{maint} = [];
697      for my $maint (@maints) {
698           if (defined $maint and $maint eq '' and not $unmaintained_packages) {
699                $unmaintained_packages = 1;
700                our %maintainers = %{getmaintainers()};
701                $param{function} = [(exists $param{function}?
702                                     (ref $param{function}?@{$param{function}}:$param{function}):()),
703                                    sub {my %d=@_;
704                                         foreach my $try (make_list($d{"pkg"})) {
705                                              next unless length $try;
706                                              ($try) = $try =~ m/^(?:src:)?(.+)/;
707                                              return 1 if not exists $maintainers{$try};
708                                         }
709                                         return 0;
710                                    }
711                                   ];
712           }
713           elsif (defined $maint and $maint ne '') {
714                push @{$param{maint}},$maint;
715           }
716      }
717      # We handle src packages, maint and maintenc by mapping to the
718      # appropriate binary packages, then removing all packages which
719      # don't match all queries
720      my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
721                                                qw(package src maint)
722                                               );
723      if (exists $param{package} or
724          exists $param{src} or
725          exists $param{maint}) {
726           delete @param{qw(maint src)};
727           $param{package} = [@packages] if @packages;
728      }
729      my $grep_bugs = 0;
730      my %bugs;
731      if (exists $param{bugs}) {
732           $bugs{$_} = 1 for make_list($param{bugs});
733           $grep_bugs = 1;
734      }
735      # These queries have to be handled by get_bugs_by_idx
736      if (exists $param{owner}
737          or exists $param{correspondent}
738          or exists $param{affects}) {
739           $bugs{$_} = 1 for get_bugs_by_idx(map {exists $param{$_}?($_,$param{$_}):()}
740                                             qw(owner correspondent affects),
741                                            );
742           $grep_bugs = 1;
743      }
744      my @bugs;
745      BUG: while (<$flatfile>) {
746           next unless m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*(.*)\s*\]\s+(\w+)\s+(.*)$/;
747           my ($pkg,$bug,$time,$status,$submitter,$severity,$tags) = ($1,$2,$3,$4,$5,$6,$7);
748           next if $grep_bugs and not exists $bugs{$bug};
749           if (exists $param{package}) {
750                my @packages = splitpackages($pkg);
751                next unless grep { my $pkg_list = $_;
752                                   grep {$pkg_list eq $_} make_list($param{package})
753                              } @packages;
754           }
755           if (exists $param{src}) {
756                my @src_packages = map { getsrcpkgs($_)} make_list($param{src});
757                my @packages = splitpackages($pkg);
758                next unless grep { my $pkg_list = $_;
759                                   grep {$pkg_list eq $_} @packages
760                              } @src_packages;
761           }
762           if (exists $param{submitter}) {
763                my @p_addrs = map {lc($_->address)}
764                     map {getparsedaddrs($_)}
765                          make_list($param{submitter});
766                my @f_addrs = map {$_->address}
767                     getparsedaddrs($submitter||'');
768                next unless grep { my $f_addr = $_; 
769                                   grep {$f_addr eq $_} @p_addrs
770                              } @f_addrs;
771           }
772           next if exists $param{severity} and not grep {$severity eq $_} make_list($param{severity});
773           next if exists $param{status} and not grep {$status eq $_} make_list($param{status});
774           if (exists $param{tag}) {
775                my $bug_ok = 0;
776                # either a normal tag, or a usertag must be set
777                $bug_ok = 1 if exists $param{usertags} and $usertag_bugs{$bug};
778                my @bug_tags = split ' ', $tags;
779                $bug_ok = 1 if grep {my $bug_tag = $_;
780                                     grep {$bug_tag eq $_} make_list($param{tag});
781                                } @bug_tags;
782                next unless $bug_ok;
783           }
784           # We do this last, because a function may be slow...
785           if (exists $param{function}) {
786                my @bug_tags = split ' ', $tags;
787                my @packages = splitpackages($pkg);
788                my $package = (@packages > 1)?\@packages:$packages[0];
789                for my $function (make_list($param{function})) {
790                     next BUG unless
791                          $function->(pkg       => $package,
792                                      bug       => $bug,
793                                      status    => $status,
794                                      submitter => $submitter,
795                                      severity  => $severity,
796                                      tags      => \@bug_tags,
797                                     );
798                }
799           }
800           push @bugs, $bug;
801      }
802      return @bugs;
803 }
804
805 =head1 PRIVATE FUNCTIONS
806
807 =head2 __handle_pkg_src_and_maint
808
809      my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
810                                                qw(package src maint)
811                                               );
812
813 Turn package/src/maint into a list of packages
814
815 =cut
816
817 sub __handle_pkg_src_and_maint{
818      my %param = validate_with(params => \@_,
819                                spec   => {package   => {type => SCALAR|ARRAYREF,
820                                                         optional => 1,
821                                                        },
822                                           src       => {type => SCALAR|ARRAYREF,
823                                                         optional => 1,
824                                                        },
825                                           maint     => {type => SCALAR|ARRAYREF,
826                                                         optional => 1,
827                                                        },
828                                          },
829                                allow_extra => 1,
830                               );
831
832      my @packages;
833      @packages = make_list($param{package}) if exists $param{package};
834      my $package_keys = @packages?1:0;
835      my %packages;
836      @packages{@packages} = (1) x @packages;
837      if (exists $param{src}) {
838           # We only want to increment the number of keys if there is
839           # something to match
840           my $key_inc = 0;
841           # in case there are binaries with the same name as the
842           # source
843           my %_temp_p = ();
844           for my $package ((map {getsrcpkgs($_)} make_list($param{src}))) {
845                $packages{$package}++ unless exists $_temp_p{$package};
846                $_temp_p{$package} = 1;
847                $key_inc=1;
848           }
849           for my $package (make_list($param{src})) {
850                $packages{"src:$package"}++ unless exists $_temp_p{"src:$package"};
851                $_temp_p{"src:$package"} = 1;
852                $key_inc=1;
853                # As a temporary hack, we will also include $param{src}
854                # in this list for packages passed which do not have a
855                # corresponding binary package
856                if (not exists getpkgsrc()->{$package}) {
857                    $packages{$package}++ unless exists $_temp_p{$package};
858                    $_temp_p{$package} = 1;
859                }
860           }
861           $package_keys += $key_inc;
862      }
863      if (exists $param{maint}) {
864           my $key_inc = 0;
865           my %_temp_p = ();
866           for my $package (package_maintainer(maintainer=>$param{maint})) {
867                $packages{$package}++ unless exists $_temp_p{$package};
868                $_temp_p{$package} = 1;
869                $key_inc = 1;
870           }
871           $package_keys += $key_inc;
872      }
873      return grep {$packages{$_} >= $package_keys} keys %packages;
874 }
875
876 state $field_match = {
877     'subject' => \&__contains_field_match,
878     'tags' => sub {
879         my ($field, $values, $status) = @_; 
880         my %values = map {$_=>1} @$values;
881         foreach my $t (split /\s+/, $status->{$field}) {
882             return 1 if (defined $values{$t});
883         }
884         return 0;
885     },
886     'severity' => \&__exact_field_match,
887     'pending' => \&__exact_field_match,
888     'package' => \&__exact_field_match,
889     'originator' => \&__contains_field_match,
890     'forwarded' => \&__contains_field_match,
891     'owner' => \&__contains_field_match,
892 };
893
894 sub __bug_matches {
895     my ($hash, $status) = @_;
896     foreach my $key( keys( %$hash ) ) {
897         my $value = $hash->{$key};
898         next unless exists $field_match->{$key};
899         my $sub = $field_match->{$key};
900         if (not defined $sub) {
901             die "No defined subroutine for key: $key";
902         }
903         return 1 if ($sub->($key, $value, $status));
904     }
905     return 0;
906 }
907
908 sub __exact_field_match {
909     my ($field, $values, $status) = @_; 
910     my @values = @$values;
911     my @ret = grep {$_ eq $status->{$field} } @values;
912     $#ret != -1;
913 }
914
915 sub __contains_field_match {
916     my ($field, $values, $status) = @_; 
917     foreach my $data (@$values) {
918         return 1 if (index($status->{$field}, $data) > -1);
919     }
920     return 0;
921 }
922
923
924
925
926
927 1;
928
929 __END__