1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
12 Debbugs::Bugs -- Bug selection routines for debbugs
16 use Debbugs::Bugs qw(get_bugs);
21 This module is a replacement for all of the various methods of
22 selecting different types of bugs.
24 It implements a single function, get_bugs, which defines the master
25 interface for selecting bugs.
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).]
40 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
41 use base qw(Exporter);
45 $DEBUG = 0 unless defined $DEBUG;
49 @EXPORT_OK = (qw(get_bugs count_bugs newest_bug bug_filter));
50 $EXPORT_TAGS{all} = [@EXPORT_OK];
53 use Debbugs::Config qw(:config);
54 use Params::Validate qw(validate_with :types);
56 use Debbugs::Status qw(splitpackages get_bug_status);
57 use Debbugs::Packages qw(getsrcpkgs);
58 use Debbugs::Common qw(getparsedaddrs getmaintainers getmaintainers_reverse make_list);
59 use Fcntl qw(O_RDONLY);
60 use MLDBM qw(DB_File Storable);
61 use List::Util qw(first);
70 The following parameters can either be a single scalar or a reference
71 to an array. The parameters are ANDed together, and the elements of
72 arrayrefs are a parameter are ORed. Future versions of this may allow
73 for limited regular expressions, and/or more complex expressions.
77 =item package -- name of the binary package
79 =item src -- name of the source package
81 =item maint -- address of the maintainer
83 =item submitter -- address of the submitter
85 =item severity -- severity of the bug
87 =item status -- status of the bug
91 =item owner -- owner of the bug
93 =item correspondent -- address of someone who sent mail to the log
95 =item dist -- distribution (I don't know about this one yet)
97 =item bugs -- list of bugs to search within
99 =item function -- see description below
103 =head3 Special options
105 The following options are special options used to modulate how the
106 searches are performed.
110 =item archive -- whether to search archived bugs or normal bugs;
111 defaults to false. As a special case, if archive is 'both', but
112 archived and unarchived bugs are returned.
114 =item usertags -- set of usertags and the bugs they are applied to
119 =head3 Subsidiary routines
121 All subsidiary routines get passed exactly the same set of options as
122 get_bugs. If for some reason they are unable to handle the options
123 passed (for example, they don't have the right type of index for the
124 type of selection) they should die as early as possible. [Using
125 Params::Validate and/or die when files don't exist makes this fairly
128 This function will then immediately move on to the next subroutine,
129 giving it the same arguments.
133 This option allows you to provide an arbitrary function which will be
134 given the information in the index.db file. This will be super, super
135 slow, so only do this if there's no other way to write the search.
137 You'll be given a list (which you can turn into a hash) like the
140 (pkg => ['a','b'], # may be a scalar (most common)
143 submitter => 'boo@baz.com',
144 severity => 'serious',
145 tags => ['a','b','c'], # may be an empty arrayref
148 The function should return 1 if the bug should be included; 0 if the
154 my %param = validate_with(params => \@_,
155 spec => {package => {type => SCALAR|ARRAYREF,
158 src => {type => SCALAR|ARRAYREF,
161 maint => {type => SCALAR|ARRAYREF,
164 submitter => {type => SCALAR|ARRAYREF,
167 severity => {type => SCALAR|ARRAYREF,
170 status => {type => SCALAR|ARRAYREF,
173 tag => {type => SCALAR|ARRAYREF,
176 owner => {type => SCALAR|ARRAYREF,
179 dist => {type => SCALAR|ARRAYREF,
182 correspondent => {type => SCALAR|ARRAYREF,
185 function => {type => CODEREF,
188 bugs => {type => SCALAR|ARRAYREF,
191 archive => {type => BOOLEAN|SCALAR,
194 usertags => {type => HASHREF,
201 my %options = %param;
203 if ($options{archive} eq 'both') {
204 push @bugs, get_bugs(%options,archive=>0);
205 push @bugs, get_bugs(%options,archive=>1);
207 @bugs{@bugs} = @bugs;
210 # A configuration option will set an array that we'll use here instead.
211 for my $routine (qw(Debbugs::Bugs::get_bugs_by_idx Debbugs::Bugs::get_bugs_flatfile)) {
212 my ($package) = $routine =~ m/^(.+)\:\:/;
213 eval "use $package;";
215 # We output errors here because using an invalid function
216 # in the configuration file isn't something that should
218 warn "use $package failed with $@";
221 @bugs = eval "${routine}(\%options)";
224 # We don't output errors here, because failure here
225 # via die may be a perfectly normal thing.
226 print STDERR "$@" if $DEBUG;
231 # If no one succeeded, die
240 count_bugs(function => sub {...})
242 Uses a subroutine to classify bugs into categories and return the
243 number of bugs which fall into those categories
248 my %param = validate_with(params => \@_,
249 spec => {function => {type => CODEREF,
251 archive => {type => BOOLEAN,
257 if ($param{archive}) {
258 $flatfile = IO::File->new("$config{spool_dir}/index.archive", 'r')
259 or die "Unable to open $config{spool_dir}/index.archive for reading: $!";
262 $flatfile = IO::File->new("$config{spool_dir}/index.db", 'r')
263 or die "Unable to open $config{spool_dir}/index.db for reading: $!";
267 if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
268 my @x = $param{function}->(pkg => $1,
276 $count{$_}++ foreach @x;
285 my $bug = newest_bug();
287 Returns the bug number of the newest bug, which is nextnumber-1.
292 my $nn_fh = IO::File->new("$config{spool_dir}/nextnumber",'r')
293 or die "Unable to open $config{spool_dir}nextnumber for reading: $!";
295 my $next_number = <$nn_fh>;
298 return $next_number-1;
305 Allows filtering bugs on commonly used criteria
310 my %param = validate_with(params => \@_,
311 spec => {bug => {type => SCALAR,
314 status => {type => HASHREF,
317 seen_merged => {type => HASHREF,
320 repeat_merged => {type => BOOLEAN,
323 include => {type => HASHREF,
326 exclude => {type => HASHREF,
329 min_days => {type => SCALAR,
332 max_days => {type => SCALAR,
337 if (exists $param{repeat_merged} and
338 not $param{repeat_merged} and
339 not defined $param{seen_merged}) {
340 croak "repeat_merged false requires seen_merged to be passed";
343 if (not exists $param{status}) {
344 my $location = getbuglocation($param{bug}, 'summary');
345 return 0 if not defined $location or not length $location;
346 $param{status} = readbug( $param{bug}, $location );
347 return 0 if not defined $param{status};
350 if (exists $param{include}) {
351 return 1 if (!__bug_matches($param{include}, $param{status}));
353 if (exists $param{exclude}) {
354 return 1 if (__bug_matches($param{exclude}, $param{status}));
356 if (exists $param{repeat_merged} and not $param{repeat_merged}) {
357 my @merged = sort {$a<=>$b} $param{bug}, split(/ /, $param{status}{mergedwith});
358 return 1 if first {defined $_} @{$param{seen_merged}}{@merged};
359 @{$param{seen_merged}}{@merged} = (1) x @merged;
361 my $daysold = int((time - $param{status}{date}) / 86400); # seconds to days
362 if (exists $param{min_days}) {
363 return 1 unless $param{min_days} <= $daysold;
365 if (exists $param{max_days}) {
366 return 1 unless $param{max_days} == -1 or
367 $param{max_days} >= $daysold;
373 =head2 get_bugs_by_idx
375 This routine uses the by-$index.idx indicies to try to speed up
382 my %param = validate_with(params => \@_,
383 spec => {package => {type => SCALAR|ARRAYREF,
386 submitter => {type => SCALAR|ARRAYREF,
389 severity => {type => SCALAR|ARRAYREF,
392 tag => {type => SCALAR|ARRAYREF,
395 archive => {type => BOOLEAN,
398 owner => {type => SCALAR|ARRAYREF,
401 src => {type => SCALAR|ARRAYREF,
404 maint => {type => SCALAR|ARRAYREF,
407 bugs => {type => SCALAR|ARRAYREF,
410 correspondent => {type => SCALAR|ARRAYREF,
413 usertags => {type => HASHREF,
420 # We handle src packages, maint and maintenc by mapping to the
421 # appropriate binary packages, then removing all packages which
422 # don't match all queries
423 my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
424 qw(package src maint)
426 if (exists $param{package} or
427 exists $param{src} or
428 exists $param{maint}) {
429 delete @param{qw(maint src)};
430 $param{package} = [@packages];
432 my $keys = grep {$_ !~ /^(archive|usertags|bugs)$/} keys(%param);
433 die "Need at least 1 key to search by" unless $keys;
434 my $arc = $param{archive} ? '-arc':'';
436 for my $key (grep {$_ !~ /^(archive|usertags|bugs)$/} keys %param) {
438 $index = 'submitter-email' if $key eq 'submitter';
439 $index = "$config{spool_dir}/by-${index}${arc}.idx";
440 tie(%idx, MLDBM => $index, O_RDONLY)
441 or die "Unable to open $index: $!";
442 my %bug_matching = ();
443 for my $search (make_list($param{$key})) {
444 for my $bug (keys %{$idx{$search}||{}}) {
445 next if $bug_matching{$bug};
446 # increment the number of searches that this bug matched
448 $bug_matching{$bug}=1;
450 if ($search ne lc($search)) {
451 for my $bug (keys %{$idx{lc($search)}||{}}) {
452 next if $bug_matching{$bug};
453 # increment the number of searches that this bug matched
455 $bug_matching{$bug}=1;
459 if ($key eq 'tag' and exists $param{usertags}) {
460 for my $bug (make_list(grep {defined $_ } @{$param{usertags}}{make_list($param{tag})})) {
461 next if $bug_matching{$bug};
463 $bug_matching{$bug}=1;
466 untie %idx or die 'Unable to untie %idx';
470 for my $bug (make_list($param{bugs})) {
474 # Throw out results that do not match all of the search specifications
475 return map {$keys <= $bugs{$_}?($_):()} keys %bugs;
479 =head2 get_bugs_flatfile
481 This is the fallback search routine. It should be able to complete all
482 searches. [Or at least, that's the idea.]
486 sub get_bugs_flatfile{
487 my %param = validate_with(params => \@_,
488 spec => {package => {type => SCALAR|ARRAYREF,
491 src => {type => SCALAR|ARRAYREF,
494 maint => {type => SCALAR|ARRAYREF,
497 submitter => {type => SCALAR|ARRAYREF,
500 severity => {type => SCALAR|ARRAYREF,
503 status => {type => SCALAR|ARRAYREF,
506 tag => {type => SCALAR|ARRAYREF,
509 owner => {type => SCALAR|ARRAYREF,
512 correspondent => {type => SCALAR|ARRAYREF,
516 # dist => {type => SCALAR|ARRAYREF,
519 archive => {type => BOOLEAN,
522 usertags => {type => HASHREF,
525 function => {type => CODEREF,
531 if ($param{archive}) {
532 $flatfile = IO::File->new("$config{spool_dir}/index.archive", 'r')
533 or die "Unable to open $config{spool_dir}/index.archive for reading: $!";
536 $flatfile = IO::File->new("$config{spool_dir}/index.db", 'r')
537 or die "Unable to open $config{spool_dir}/index.db for reading: $!";
540 if (exists $param{tag} and exists $param{usertags}) {
541 # This complex slice makes a hash with the bugs which have the
542 # usertags passed in $param{tag} set.
543 @usertag_bugs{make_list(@{$param{usertags}}{make_list($param{tag})})
544 } = (1) x make_list(@{$param{usertags}}{make_list($param{tag})});
546 # We handle src packages, maint and maintenc by mapping to the
547 # appropriate binary packages, then removing all packages which
548 # don't match all queries
549 my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
550 qw(package src maint)
552 if (exists $param{package} or
553 exists $param{src} or
554 exists $param{maint}) {
555 delete @param{qw(maint src)};
556 $param{package} = [@packages];
560 if (exists $param{bugs}) {
561 $bugs{$_} = 1 for make_list($param{bugs});
564 if (exists $param{owner} or exists $param{correspondent}) {
565 $bugs{$_} = 1 for get_bugs_by_idx(exists $param{correspondent}?(correspondent => $param{correspondent}):(),
566 exists $param{owner}?(owner => $param{owner}):(),
571 while (<$flatfile>) {
572 next unless m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/;
573 my ($pkg,$bug,$time,$status,$submitter,$severity,$tags) = ($1,$2,$3,$4,$5,$6,$7);
574 next if $grep_bugs and not exists $bugs{$bug};
575 if (exists $param{package}) {
576 my @packages = splitpackages($pkg);
577 next unless grep { my $pkg_list = $_;
578 grep {$pkg_list eq $_} make_list($param{package})
581 if (exists $param{src}) {
582 my @src_packages = map { getsrcpkgs($_)} make_list($param{src});
583 my @packages = splitpackages($pkg);
584 next unless grep { my $pkg_list = $_;
585 grep {$pkg_list eq $_} @packages
588 if (exists $param{submitter}) {
589 my @p_addrs = map {lc($_->address)}
590 map {getparsedaddrs($_)}
591 make_list($param{submitter});
592 my @f_addrs = map {$_->address}
593 getparsedaddrs($submitter||'');
594 next unless grep { my $f_addr = $_;
595 grep {$f_addr eq $_} @p_addrs
598 next if exists $param{severity} and not grep {$severity eq $_} make_list($param{severity});
599 next if exists $param{status} and not grep {$status eq $_} make_list($param{status});
600 if (exists $param{tag}) {
602 # either a normal tag, or a usertag must be set
603 $bug_ok = 1 if exists $param{usertags} and $usertag_bugs{$bug};
604 my @bug_tags = split ' ', $tags;
605 $bug_ok = 1 if grep {my $bug_tag = $_;
606 grep {$bug_tag eq $_} make_list($param{tag});
610 # We do this last, because a function may be slow...
611 if (exists $param{function}) {
612 my @bug_tags = split ' ', $tags;
613 my @packages = splitpackages($pkg);
614 my $package = (@packages > 1)?\@packages:$packages[0];
616 $param{function}->(pkg => $package,
619 submitter => $submitter,
620 severity => $severity,
629 =head1 PRIVATE FUNCTIONS
631 =head2 __handle_pkg_src_and_maint
633 my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
634 qw(package src maint)
637 Turn package/src/maint into a list of packages
641 sub __handle_pkg_src_and_maint{
642 my %param = validate_with(params => \@_,
643 spec => {package => {type => SCALAR|ARRAYREF,
646 src => {type => SCALAR|ARRAYREF,
649 maint => {type => SCALAR|ARRAYREF,
657 @packages = make_list($param{package}) if exists $param{package};
658 my $package_keys = @packages?1:0;
660 @packages{@packages} = (1) x @packages;
661 if (exists $param{src}) {
662 # We only want to increment the number of keys if there is
665 for my $package ((map { getsrcpkgs($_)} make_list($param{src})),make_list($param{src})) {
666 $packages{$package}++;
669 $package_keys += $key_inc;
671 if (exists $param{maint}) {
673 my $maint_rev = getmaintainers_reverse();
674 for my $package (map { exists $maint_rev->{$_}?@{$maint_rev->{$_}}:()}
675 make_list($param{maint})) {
676 $packages{$package}++;
679 $package_keys += $key_inc;
681 return grep {$packages{$_} >= $package_keys} keys %packages;
685 'subject' => \&__contains_field_match,
687 my ($field, $values, $status) = @_;
688 my %values = map {$_=>1} @$values;
689 foreach my $t (split /\s+/, $status->{$field}) {
690 return 1 if (defined $values{$t});
694 'severity' => \&__exact_field_match,
695 'pending' => \&__exact_field_match,
696 'originator' => \&__contains_field_match,
697 'forwarded' => \&__contains_field_match,
698 'owner' => \&__contains_field_match,
702 my ($hash, $status) = @_;
703 foreach my $key( keys( %$hash ) ) {
704 my $value = $hash->{$key};
705 my $sub = $field_match{$key};
706 return 1 if ($sub->($key, $value, $status));
711 sub __exact_field_match {
712 my ($field, $values, $status) = @_;
713 my @values = @$values;
714 my @ret = grep {$_ eq $status->{$field} } @values;
718 sub __contains_field_match {
719 my ($field, $values, $status) = @_;
720 foreach my $data (@$values) {
721 return 1 if (index($status->{$field}, $data) > -1);