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