1 # This module is part of debbugs, and
2 # is released under the terms of the GPL version 2, or any later
3 # version (at your option). See the file README and COPYING for more
5 # Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
11 Debbugs::Bug -- OO interface to bugs
16 Debbugs::Bug->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
27 use v5.10; # for state
30 use List::AllUtils qw(max first min any);
32 use Params::Validate qw(validate_with :types);
33 use Debbugs::Config qw(:config);
34 use Debbugs::Status qw(read_bug);
35 use Debbugs::Common qw(bug_status);
36 use Debbugs::Bug::Tag;
37 use Debbugs::Bug::Status;
38 use Debbugs::Collection::Package;
39 use Debbugs::Collection::Bug;
40 use Debbugs::Collection::Correspondent;
46 extends 'Debbugs::OOBase';
48 my $meta = __PACKAGE__->meta;
50 state $strong_severities =
51 {map {($_,1)} @{$config{strong_severities}}};
53 has bug => (is => 'ro', isa => 'Int',
61 has exists => (is => 'ro',
63 builder => '_build_exists',
68 if ($self->has_schema) { # check database to see if the bug exists
69 my $count = $self->schema->resultset('Bug')->
70 search({id => $self->bug})->
73 } else { # check filesystem to see if the bug exists
74 return defined bug_status($self->bug);
79 has saved => (is => 'ro', isa => 'Bool',
81 writer => '_set_saved',
84 has status => (is => 'ro', isa => 'Debbugs::Bug::Status',
86 builder => '_build_status',
87 handles => {date => 'date',
89 message_id => 'message_id',
90 severity => 'severity',
91 archived => 'archived',
94 forwarded => 'forwarded',
100 return Debbugs::Bug::Status->new(bug=>$self->bug,
101 $self->schema_argument,
105 has log => (is => 'bare', isa => 'Debbugs::Log',
107 builder => '_build_log',
108 handles => {_read_record => 'read_record',
109 log_records => 'read_all_records',
115 return Debbugs::Log->new(bug_num => $self->id,
120 has spam => (is => 'bare', isa => 'Debbugs::Log::Spam',
122 builder => '_build_spam',
123 handles => ['is_spam'],
127 return Debbugs::Log::Spam->new(bug_num => $self->id);
130 has 'package_collection' => (is => 'ro',
131 isa => 'Debbugs::Collection::Package',
132 builder => '_build_package_collection',
136 sub _build_package_collection {
138 if ($self->has_schema) {
139 return Debbugs::Collection::Package->new(schema => $self->schema);
141 carp "No schema when building package collection";
142 return Debbugs::Collection::Package->new();
145 has bug_collection => (is => 'ro',
146 isa => 'Debbugs::Collection::Bug',
147 builder => '_build_bug_collection',
149 sub _build_bug_collection {
151 if ($self->has_schema) {
152 return Debbugs::Collection::Bug->new(schema => $self->schema);
154 return Debbugs::Collection::Bug->new();
157 has correspondent_collection =>
159 isa => 'Debbugs::Collection::Correspondent',
160 builder => '_build_correspondent_collection',
163 sub _build_correspondent_collection {
165 return Debbugs::Collection::Correspondent->new($self->schema_argument);
169 for my $attr (qw(packages affects sources)) {
172 isa => 'Debbugs::Collection::Package',
173 clearer => '_clear_'.$attr,
174 builder => '_build_'.$attr,
180 for my $attr (qw(blocks blocked_by mergedwith)) {
183 isa => 'Debbugs::Collection::Bug',
184 clearer => '_clear_'.$attr,
185 builder => '_build_'.$attr,
192 for my $attr (qw(owner submitter done)) {
195 isa => 'Maybe[Debbugs::Correspondent]',
197 builder => '_build_'.$attr.'_corr',
198 clearer => '_clear_'.$attr.'_corr',
199 handles => {$attr.'_url' => $attr.'_url',
200 $attr.'_email' => 'email',
201 $attr.'_phrase' => 'phrase',
204 $meta->add_method('has_'.$attr,
205 sub {my $self = shift;
206 my $m = $meta->find_method_by_name($attr);
207 return defined $m->($self);
209 $meta->add_method('_build_'.$attr.'_corr',
210 sub {my $self = shift;
211 my $m = $self->status->meta->find_method_by_name($attr);
212 my $v = $m->($self->status);
213 if (defined $v and length($v)) {
214 return $self->correspondent_collection->
215 get_or_add_by_key($v);
225 return $self->has_done;
228 sub strong_severity {
230 return exists $strong_severities->{$self->severity};
234 $_[0]->severity =~ m/^(.)/;
238 sub _build_packages {
240 return $self->package_collection->
241 limit($self->status->package);
246 return $self->affects->count > 0;
251 return $self->package_collection->
252 limit($self->status->affects);
256 return $self->packages->sources->clone;
261 return defined $self->owner;
266 return $self->blocks->count > 0;
271 return $self->bug_collection->
272 limit($self->status->blocks);
277 return $self->blocked_by->count > 0;
280 sub _build_blocked_by {
282 return $self->bug_collection->
283 limit($self->status->blocked_by);
287 length($_[0]->forwarded) > 0;
290 for my $attr (qw(fixed found)) {
293 isa => 'Debbugs::Collection::Version',
294 clearer => '_clear_'.$attr,
295 builder => '_build_'.$attr,
303 return any {1} $self->status->found;
308 return $self->packages->
309 get_source_versions($self->status->found);
314 return any {1} $self->status->fixed;
319 return $self->packages->
320 get_source_versions($self->status->fixed);
325 return any {1} $self->status->mergedwith;
328 sub _build_mergedwith {
330 return $self->bug_collection->
331 limit($self->status->mergedwith);
334 for my $attr (qw(created modified)) {
335 has $attr => (is => 'rw', isa => 'Object',
336 clearer => '_clear_'.$attr,
337 builder => '_build_'.$attr,
342 from_epoch(epoch => $_[0]->status->date);
344 sub _build_modified {
346 from_epoch(epoch => max($_[0]->status->log_modified,
347 $_[0]->status->last_modified
351 has tags => (is => 'ro',
352 isa => 'Debbugs::Bug::Tag',
353 clearer => '_clear_tags',
354 builder => '_build_tags',
359 return Debbugs::Bug::Tag->new(keywords => join(' ',$self->status->tags),
361 users => $self->bug_collection->users,
365 has pending => (is => 'ro',
367 clearer => '_clear_pending',
368 builder => '_build_pending',
375 my $pending = 'pending';
376 if (length($self->status->forwarded)) {
377 $pending = 'forwarded';
379 if ($self->tags->tag_is_set('pending')) {
380 $pending = 'pending-fixed';
382 if ($self->tags->tag_is_set('pending')) {
385 # XXX This isn't quite right
391 $bug->buggy('debbugs/2.6.0-1','debbugs/2.6.0-2');
392 $bug->buggy(Debbugs::Version->new('debbugs/2.6.0-1'),
393 Debbugs::Version->new('debbugs/2.6.0-2'),
396 Returns the output of Debbugs::Versions::buggy for a particular
397 package, version and found/fixed set. Automatically turns found, fixed
398 and version into source/version strings.
405 $self->package_collection->
406 universe->versiontree;
407 my $max_buggy = 'absent';
410 my @ver_opts = (version => $ver,
411 package => $self->status->package,
412 package_collection => $self->package_collection,
416 $ver = Debbugs::Version::Source->(@ver_opts);
418 $ver = Debbugs::Version::Binary->(@ver_opts);
421 $vertree->load($ver->source);
423 $vertree->buggy($ver,
426 if ($buggy eq 'found') {
429 if ($buggy eq 'fixed') {
430 $max_buggy = 'fixed';
437 (is => 'ro', isa => 'Bool',
438 writer => '_set_archiveable',
439 builder => '_build_archiveable',
440 clearer => '_clear_archiveable',
443 has when_archiveable =>
444 (is => 'ro', isa => 'Num',
445 writer => '_set_when_archiveable',
446 builder => '_build_when_archiveable',
447 clearer => '_clear_when_archiveable',
451 sub _build_archiveable {
453 $self->_populate_archiveable(0);
454 return $self->archiveable;
456 sub _build_when_archiveable {
458 $self->_populate_archiveable(1);
459 return $self->when_archiveable;
462 sub _populate_archiveable {
464 my ($need_time) = @_;
466 # Bugs can be archived if they are
468 if (not $self->done) {
469 $self->_set_archiveable(0);
470 $self->_set_when_archiveable(-1);
473 # 2. Have no unremovable tags set
474 if (@{$config{removal_unremovable_tags}}) {
476 {map {($_=>1)} @{$config{removal_unremovable_tags}}};
477 for my $tag ($self->tags) {
478 if ($unrem_tags->{$tag}) {
479 $self->_set_archiveable(0);
480 $self->_set_when_archiveable(-1);
486 state $remove_time = 24 * 60 * 60 * ($config{removal_age} // 30);
487 # 4. Have been modified more than removal_age ago
489 $time - $self->modified->epoch;
490 # if we don't need to know when we can archive, we can stop here if it's
491 # been modified too recently
492 if ($moded_ago < $remove_time) {
493 $self->_set_archiveable(0);
494 return unless $need_time;
497 @{$config{removal_default_distribution_tags}};
498 if ($self->strong_severity) {
500 @{$config{removal_strong_severity_default_distribution_tags}};
502 # 3. Have a maximum buggy of fixed
503 my $buggy = $self->buggy($self->packages->
504 get_source_versions_distributions(@distributions));
505 if ('found' eq $buggy) {
506 $self->_set_archiveable(0);
507 $self->_set_when_archiveable(-1);
510 my $fixed_ago = $moded_ago;
511 # $fixed_ago = $time - $self->when_fixed(@distributions);
512 # if ($fixed_ago < $remove_time) {
513 # $self->_set_archiveable(0);
515 $self->_set_when_archiveable(($remove_time - min($fixed_ago,$moded_ago)) / (24 * 60 * 60));
516 if ($fixed_ago > $remove_time and
517 $moded_ago > $remove_time) {
518 $self->_set_archiveable(1);
519 $self->_set_when_archiveable(0);
526 my %param = validate_with(params => \@_,
527 spec => {seen_merged => {type => HASHREF,
528 default => sub {return {}},
530 repeat_merged => {type => BOOLEAN,
533 include => {type => HASHREF,
536 exclude => {type => HASHREF,
539 min_days => {type => SCALAR,
542 max_days => {type => SCALAR,
547 if (exists $param{include}) {
548 return 1 if not $self->matches($param{include});
550 if (exists $param{exclude}) {
551 return 1 if $self->matches($param{exclude});
553 if (exists $param{repeat_merged} and not $param{repeat_merged}) {
554 my @merged = sort {$a<=>$b} $self->bug, $self->status->mergedwith;
555 return 1 if first {sub {defined $_}}
556 @{$param{seen_merged}}{@merged};
557 @{$param{seen_merged}}{@merged} = (1) x @merged;
559 if (exists $param{min_days}) {
560 return 1 unless $param{min_days} <=
561 (DateTime->now() - $self->created)->days();
563 if (exists $param{max_days}) {
564 return 1 unless $param{max_days} >=
565 (DateTime->now() - $self->created)->days();
572 my ($field, $values) = @_;
573 my @ret = first {sub {$_ eq $field}} @{$values};
577 sub __contains_match {
578 my ($field, $values) = @_;
579 foreach my $value (@{$values}) {
580 return 1 if (index($field, $value) > -1);
586 {subject => sub {__contains_match($_[0]->subject,@_)},
588 for my $value (@{$_[1]}) {
589 if ($_[0]->tags->is_set($value)) {
595 severity => sub {__exact_match($_[0]->severity,@_)},
596 pending => sub {__exact_match($_[0]->pending,@_)},
597 originator => sub {__exact_match($_[0]->submitter,@_)},
598 submitter => sub {__exact_match($_[0]->submitter,@_)},
599 forwarded => sub {__exact_match($_[0]->forwarded,@_)},
600 owner => sub {__exact_match($_[0]->owner,@_)},
604 my ($self,$hash) = @_;
605 for my $key (keys %{$hash}) {
606 my $sub = $field_match->{$key};
607 if (not defined $sub) {
608 carp "No subroutine for key: $key";
611 return 1 if $sub->($self,$hash->{$key});
618 return $self->id.'@'.$config{email_domain};
621 sub subscribe_email {
623 return $self->id.'-subscribe@'.$config{email_domain};
628 return $config{web_domain}.'/'.$self->id;
633 return $config{web_domain}.'/mbox:'.$self->id;
636 sub mbox_status_url {
638 return $self->mbox_url.'?mboxstatus=yes';
643 $self->mbox_url.'?mboxmaint=yes';
648 my $url = Debbugs::URI->new('version.cgi?');
649 $url->query_form(package => $self->status->package(),
650 found => [$self->status->found],
651 fixed => [$self->status->fixed],
654 return $url->as_string;
657 sub related_packages_and_versions {
659 my @packages = $self->status->package;
660 my @versions = ($self->status->found,
661 $self->status->fixed);
662 my @unqualified_versions;
664 for my $ver (@versions) {
665 if ($ver =~ m{(<src>.+)/(<ver>.+)}) { # It's a src_pkg_ver
666 push @return, ['src:'.$+{src}, $+{ver}];
668 push @unqualified_versions,$ver;
671 for my $pkg (@packages) {
672 if (@unqualified_versions) {
674 [$pkg,@unqualified_versions];
684 return 'Debbugs::Bug={bug='.$self->bug.'}';
687 __PACKAGE__->meta->make_immutable;
695 # indent-tabs-mode: nil
696 # cperl-indent-level: 4