]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Bugs.pm
use state for Params::Validate options
[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 $keys = grep {$_ !~ $_non_search_key_regex} keys(%param);
507      die "Need at least 1 key to search by" unless $keys;
508      my $rs = $param{schema}->resultset('Bug');
509      if (exists $param{package}) {
510          $rs = $rs->search({-or => {map 'bin_package.'}})
511      }
512      if (exists $param{severity}) {
513          $rs = $rs->search({-or => {map {('severity.severity' => $_)}
514                                     make_list($param{severity})},
515                            },
516                           {join => 'severity'},
517                           );
518      }
519      for my $key (qw(owner submitter done)) {
520          if (exists $param{$key}) {
521              $rs = $rs->search({-or => {map {("${key}.addr" => $_)}
522                                         make_list($param{$key})},
523                                },
524                               {join => $key},
525                               );
526          }
527      }
528      if (exists $param{correspondent}) {
529          $rs = $rs->search({-or => {map {('message_correspondents.addr' => $_)}
530                                     make_list($param{correspondent})},
531                            },
532                           {join => {correspondent =>
533                                    {bug_messages =>
534                                    {message => 'message_correspondents'}}}},
535                           );
536      }
537      if (exists $param{affects}) {
538          $rs = $rs->search({-or => {map {('bin_pkg.pkg' => $_,
539                                           'src_pkg.pkg' => $_,
540                                          )}
541                                     make_list($param{affects}),
542                                    },
543                            },
544                           {join => [{bug_affects_binpackages => 'bin_pkg'},
545                                    {bug_affects_srcpackages => 'src_pkg'},
546                                    ],
547                           },
548                           );
549      }
550      if (exists $param{package}) {
551          $rs = $rs->search({-or => {map {('bin_pkg.pkg' => $_)}
552                                     make_list($param{package})},
553                            },
554                           {join => {bug_binpackages => 'bin_pkg'}});
555      }
556      if (exists $param{maintainer}) {
557          $rs = $rs->search({-or => {map {(correspondent => $_ eq '' ? undef : $_,
558                                           correspondent2 => $_ eq '' ? undef : $_,
559                                          )}
560                                     make_list($param{maintainer})
561                                    }
562                            },
563                           {join => {bug_affects_binpackage =>
564                                    {bin_pkg =>
565                                    {bin_ver =>
566                                    {src_ver =>
567                                    {maintainer => 'correspondent'}
568                                    }}},
569                                    {bug_affects_srcpackage =>
570                                    {src_pkg =>
571                                    {src_ver =>
572                                    {maintainer => 'correspondent'}
573                                    }}}}
574                           }
575                           );
576      }
577      if (exists $param{src}) {
578          $rs = $rs->search({-or => {map {('src_pkg.pkg' => $_)}
579                                     make_list($param{src})},
580                            },
581                           {join => {bug_srcpackages => 'src_pkg'}});
582      }
583      # tags are very odd, because we must handle usertags.
584      if (exists $param{tag}) {
585          # bugs from usertags which matter
586          my %bugs_matching_usertags;
587          for my $bug (make_list(grep {defined $_ }
588                                 @{$param{usertags}}{make_list($param{tag})})) {
589              $bugs_matching_usertags{$bug} = 1;
590          }
591          # we want all bugs which either match the tag name given in
592          # param, or have a usertag set which matches one of the tag
593          # names given in param.
594          $rs = $rs->search({-or => {map {('tag.tag' => $_)}
595                                     make_list($param{tag}),
596                                     map {('me.id' => $_)}
597                                     keys %bugs_matching_usertags
598                                    },
599                            },
600                           {join => {bug_tags => 'tag'}});
601      }
602      if (exists $param{bugs}) {
603          $rs = $rs->search({-or => {map {('me.id' => $_)}
604                                     make_list($param{bugs})}
605                            });
606      }
607      # handle archive
608      if (defined $param{archive} and $param{archive} ne 'both') {
609          $rs = $rs->search({'me.archived' => $param{archive}});
610      }
611      return $rs->get_column('id')->all();
612 }
613
614
615 =head2 get_bugs_flatfile
616
617 This is the fallback search routine. It should be able to complete all
618 searches. [Or at least, that's the idea.]
619
620 =cut
621
622 state $_get_bugs_flatfile_options =
623    {hash_slice(%_get_bugs_common_options,
624                map {$_ eq 'dist'?():($_)} keys %_get_bugs_common_options
625               )
626    };
627
628 sub get_bugs_flatfile{
629      my %param = validate_with(params => \@_,
630                                spec   => $_get_bugs_flatfile_options
631                               );
632      my $flatfile;
633      if ($param{archive}) {
634           $flatfile = IO::File->new("$config{spool_dir}/index.archive", 'r')
635                or die "Unable to open $config{spool_dir}/index.archive for reading: $!";
636      }
637      else {
638           $flatfile = IO::File->new("$config{spool_dir}/index.db", 'r')
639                or die "Unable to open $config{spool_dir}/index.db for reading: $!";
640      }
641      my %usertag_bugs;
642      if (exists $param{tag} and exists $param{usertags}) {
643           # This complex slice makes a hash with the bugs which have the
644           # usertags passed in $param{tag} set.
645           @usertag_bugs{make_list(@{$param{usertags}}{make_list($param{tag})})
646                         } = (1) x make_list(@{$param{usertags}}{make_list($param{tag})});
647      }
648      my $unmaintained_packages = 0;
649      # unmaintained packages is a special case
650      my @maints = make_list(exists $param{maint}?$param{maint}:[]);
651      $param{maint} = [];
652      for my $maint (@maints) {
653           if (defined $maint and $maint eq '' and not $unmaintained_packages) {
654                $unmaintained_packages = 1;
655                our %maintainers = %{getmaintainers()};
656                $param{function} = [(exists $param{function}?
657                                     (ref $param{function}?@{$param{function}}:$param{function}):()),
658                                    sub {my %d=@_;
659                                         foreach my $try (make_list($d{"pkg"})) {
660                                              next unless length $try;
661                                              ($try) = $try =~ m/^(?:src:)?(.+)/;
662                                              return 1 if not exists $maintainers{$try};
663                                         }
664                                         return 0;
665                                    }
666                                   ];
667           }
668           elsif (defined $maint and $maint ne '') {
669                push @{$param{maint}},$maint;
670           }
671      }
672      # We handle src packages, maint and maintenc by mapping to the
673      # appropriate binary packages, then removing all packages which
674      # don't match all queries
675      my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
676                                                qw(package src maint)
677                                               );
678      if (exists $param{package} or
679          exists $param{src} or
680          exists $param{maint}) {
681           delete @param{qw(maint src)};
682           $param{package} = [@packages] if @packages;
683      }
684      my $grep_bugs = 0;
685      my %bugs;
686      if (exists $param{bugs}) {
687           $bugs{$_} = 1 for make_list($param{bugs});
688           $grep_bugs = 1;
689      }
690      # These queries have to be handled by get_bugs_by_idx
691      if (exists $param{owner}
692          or exists $param{correspondent}
693          or exists $param{affects}) {
694           $bugs{$_} = 1 for get_bugs_by_idx(map {exists $param{$_}?($_,$param{$_}):()}
695                                             qw(owner correspondent affects),
696                                            );
697           $grep_bugs = 1;
698      }
699      my @bugs;
700      BUG: while (<$flatfile>) {
701           next unless m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*(.*)\s*\]\s+(\w+)\s+(.*)$/;
702           my ($pkg,$bug,$time,$status,$submitter,$severity,$tags) = ($1,$2,$3,$4,$5,$6,$7);
703           next if $grep_bugs and not exists $bugs{$bug};
704           if (exists $param{package}) {
705                my @packages = splitpackages($pkg);
706                next unless grep { my $pkg_list = $_;
707                                   grep {$pkg_list eq $_} make_list($param{package})
708                              } @packages;
709           }
710           if (exists $param{src}) {
711                my @src_packages = map { getsrcpkgs($_)} make_list($param{src});
712                my @packages = splitpackages($pkg);
713                next unless grep { my $pkg_list = $_;
714                                   grep {$pkg_list eq $_} @packages
715                              } @src_packages;
716           }
717           if (exists $param{submitter}) {
718                my @p_addrs = map {lc($_->address)}
719                     map {getparsedaddrs($_)}
720                          make_list($param{submitter});
721                my @f_addrs = map {$_->address}
722                     getparsedaddrs($submitter||'');
723                next unless grep { my $f_addr = $_; 
724                                   grep {$f_addr eq $_} @p_addrs
725                              } @f_addrs;
726           }
727           next if exists $param{severity} and not grep {$severity eq $_} make_list($param{severity});
728           next if exists $param{status} and not grep {$status eq $_} make_list($param{status});
729           if (exists $param{tag}) {
730                my $bug_ok = 0;
731                # either a normal tag, or a usertag must be set
732                $bug_ok = 1 if exists $param{usertags} and $usertag_bugs{$bug};
733                my @bug_tags = split ' ', $tags;
734                $bug_ok = 1 if grep {my $bug_tag = $_;
735                                     grep {$bug_tag eq $_} make_list($param{tag});
736                                } @bug_tags;
737                next unless $bug_ok;
738           }
739           # We do this last, because a function may be slow...
740           if (exists $param{function}) {
741                my @bug_tags = split ' ', $tags;
742                my @packages = splitpackages($pkg);
743                my $package = (@packages > 1)?\@packages:$packages[0];
744                for my $function (make_list($param{function})) {
745                     next BUG unless
746                          $function->(pkg       => $package,
747                                      bug       => $bug,
748                                      status    => $status,
749                                      submitter => $submitter,
750                                      severity  => $severity,
751                                      tags      => \@bug_tags,
752                                     );
753                }
754           }
755           push @bugs, $bug;
756      }
757      return @bugs;
758 }
759
760 =head1 PRIVATE FUNCTIONS
761
762 =head2 __handle_pkg_src_and_maint
763
764      my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
765                                                qw(package src maint)
766                                               );
767
768 Turn package/src/maint into a list of packages
769
770 =cut
771
772 sub __handle_pkg_src_and_maint{
773      my %param = validate_with(params => \@_,
774                                spec   => {package   => {type => SCALAR|ARRAYREF,
775                                                         optional => 1,
776                                                        },
777                                           src       => {type => SCALAR|ARRAYREF,
778                                                         optional => 1,
779                                                        },
780                                           maint     => {type => SCALAR|ARRAYREF,
781                                                         optional => 1,
782                                                        },
783                                          },
784                                allow_extra => 1,
785                               );
786
787      my @packages;
788      @packages = make_list($param{package}) if exists $param{package};
789      my $package_keys = @packages?1:0;
790      my %packages;
791      @packages{@packages} = (1) x @packages;
792      if (exists $param{src}) {
793           # We only want to increment the number of keys if there is
794           # something to match
795           my $key_inc = 0;
796           # in case there are binaries with the same name as the
797           # source
798           my %_temp_p = ();
799           for my $package ((map {getsrcpkgs($_)} make_list($param{src}))) {
800                $packages{$package}++ unless exists $_temp_p{$package};
801                $_temp_p{$package} = 1;
802                $key_inc=1;
803           }
804           for my $package (make_list($param{src})) {
805                $packages{"src:$package"}++ unless exists $_temp_p{"src:$package"};
806                $_temp_p{"src:$package"} = 1;
807                $key_inc=1;
808                # As a temporary hack, we will also include $param{src}
809                # in this list for packages passed which do not have a
810                # corresponding binary package
811                if (not exists getpkgsrc()->{$package}) {
812                    $packages{$package}++ unless exists $_temp_p{$package};
813                    $_temp_p{$package} = 1;
814                }
815           }
816           $package_keys += $key_inc;
817      }
818      if (exists $param{maint}) {
819           my $key_inc = 0;
820           my %_temp_p = ();
821           for my $package (package_maintainer(maintainer=>$param{maint})) {
822                $packages{$package}++ unless exists $_temp_p{$package};
823                $_temp_p{$package} = 1;
824                $key_inc = 1;
825           }
826           $package_keys += $key_inc;
827      }
828      return grep {$packages{$_} >= $package_keys} keys %packages;
829 }
830
831 state %field_match = (
832     'subject' => \&__contains_field_match,
833     'tags' => sub {
834         my ($field, $values, $status) = @_; 
835         my %values = map {$_=>1} @$values;
836         foreach my $t (split /\s+/, $status->{$field}) {
837             return 1 if (defined $values{$t});
838         }
839         return 0;
840     },
841     'severity' => \&__exact_field_match,
842     'pending' => \&__exact_field_match,
843     'package' => \&__exact_field_match,
844     'originator' => \&__contains_field_match,
845     'forwarded' => \&__contains_field_match,
846     'owner' => \&__contains_field_match,
847 );
848
849 sub __bug_matches {
850     my ($hash, $status) = @_;
851     foreach my $key( keys( %$hash ) ) {
852         my $value = $hash->{$key};
853         next unless exists $field_match{$key};
854         my $sub = $field_match{$key};
855         if (not defined $sub) {
856             die "No defined subroutine for key: $key";
857         }
858         return 1 if ($sub->($key, $value, $status));
859     }
860     return 0;
861 }
862
863 sub __exact_field_match {
864     my ($field, $values, $status) = @_; 
865     my @values = @$values;
866     my @ret = grep {$_ eq $status->{$field} } @values;
867     $#ret != -1;
868 }
869
870 sub __contains_field_match {
871     my ($field, $values, $status) = @_; 
872     foreach my $data (@$values) {
873         return 1 if (index($status->{$field}, $data) > -1);
874     }
875     return 0;
876 }
877
878
879
880
881
882 1;
883
884 __END__