]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Bug.pm
78bd5b73b62a6ea104b5eed8cc763af531487c32
[debbugs.git] / Debbugs / Bug.pm
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
4 # information.
5 # Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
6
7 package Debbugs::Bug;
8
9 =head1 NAME
10
11 Debbugs::Bug -- OO interface to bugs
12
13 =head1 SYNOPSIS
14
15    use Debbugs::Bug;
16    Debbugs::Bug->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
17
18 =head1 DESCRIPTION
19
20
21
22 =cut
23
24 use Mouse;
25 use strictures 2;
26 use namespace::clean;
27 use v5.10; # for state
28
29 use DateTime;
30 use List::AllUtils qw(max first min any);
31
32 use Params::Validate qw(validate_with :types);
33 use Debbugs::Config qw(:config);
34 use Debbugs::Status qw(read_bug);
35 use Debbugs::Bug::Tag;
36 use Debbugs::Bug::Status;
37 use Debbugs::Collection::Package;
38 use Debbugs::Collection::Bug;
39 use Debbugs::Collection::Correspondent;
40
41 use Debbugs::OOTypes;
42
43 use Carp;
44
45 extends 'Debbugs::OOBase';
46
47 my $meta = __PACKAGE__->meta;
48
49 state $strong_severities =
50    {map {($_,1)} @{$config{strong_severities}}};
51
52 has bug => (is => 'ro', isa => 'Int',
53             required => 1,
54            );
55
56 sub id {
57     return $_[0]->bug;
58 }
59
60 has saved => (is => 'ro', isa => 'Bool',
61               default => 0,
62               writer => '_set_saved',
63              );
64
65 has status => (is => 'ro', isa => 'Debbugs::Bug::Status',
66                lazy => 1,
67                builder => '_build_status',
68                handles => {date => 'date',
69                            subject => 'subject',
70                            message_id => 'message_id',
71                            severity => 'severity',
72                            archived => 'archived',
73                            summary => 'summary',
74                            outlook => 'outlook',
75                            forwarded => 'forwarded',
76                           },
77               );
78
79 sub _build_status {
80     my $self = shift;
81     return Debbugs::Bug::Status->new(bug=>$self->bug,
82                                      $self->schema_argument,
83                                     );
84 }
85
86 has log => (is => 'bare', isa => 'Debbugs::Log',
87             lazy => 1,
88             builder => '_build_log',
89             handles => {_read_record => 'read_record',
90                         log_records => 'read_all_records',
91                        },
92            );
93
94 sub _build_log {
95     my $self = shift;
96     return Debbugs::Log->new(bug_num => $self->id,
97                              inner_file => 1,
98                             );
99 }
100
101 has spam => (is => 'bare', isa => 'Debbugs::Log::Spam',
102              lazy => 1,
103              builder => '_build_spam',
104              handles => ['is_spam'],
105             );
106 sub _build_spam {
107     my $self = shift;
108     return Debbugs::Log::Spam->new(bug_num => $self->id);
109 }
110
111 has 'package_collection' => (is => 'ro',
112                              isa => 'Debbugs::Collection::Package',
113                              builder => '_build_package_collection',
114                              lazy => 1,
115                             );
116
117 sub _build_package_collection {
118     my $self = shift;
119     if ($self->has_schema) {
120         return Debbugs::Collection::Package->new(schema => $self->schema);
121     }
122     carp "No schema when building package collection";
123     return Debbugs::Collection::Package->new();
124 }
125
126 has bug_collection => (is => 'ro',
127                        isa => 'Debbugs::Collection::Bug',
128                        builder => '_build_bug_collection',
129                       );
130 sub _build_bug_collection {
131     my $self = shift;
132     if ($self->has_schema) {
133         return Debbugs::Collection::Bug->new(schema => $self->schema);
134     }
135     return Debbugs::Collection::Bug->new();
136 }
137
138 has correspondent_collection =>
139     (is => 'ro',
140      isa => 'Debbugs::Collection::Correspondent',
141      builder => '_build_correspondent_collection',
142      lazy => 1,
143     );
144 sub _build_correspondent_collection   {
145     my $self = shift;
146     return Debbugs::Collection::Correspondent->new($self->schema_argument);
147 }
148
149 # package attributes
150 for my $attr (qw(packages affects sources)) {
151     has $attr =>
152         (is => 'rw',
153          isa => 'Debbugs::Collection::Package',
154          clearer => '_clear_'.$attr,
155          builder => '_build_'.$attr,
156          lazy => 1,
157         );
158 }
159
160 # bugs
161 for my $attr (qw(blocks blocked_by mergedwith)) {
162     has $attr =>
163         (is => 'ro',
164          isa => 'Debbugs::Collection::Bug',
165          clearer => '_clear_'.$attr,
166          builder => '_build_'.$attr,
167          handles => {},
168          lazy => 1,
169         );
170 }
171
172
173 for my $attr (qw(owner submitter done)) {
174     has $attr,
175         (is => 'ro',
176          isa => 'Maybe[Debbugs::Correspondent]',
177          lazy => 1,
178          builder => '_build_'.$attr.'_corr',
179          clearer => '_clear_'.$attr.'_corr',
180          handles => {$attr.'_url' => $attr.'_url',
181                      $attr.'_email' => 'email',
182                      $attr.'_phrase' => 'phrase',
183                     },
184         );
185     $meta->add_method('has_'.$attr,
186                       sub {my $self = shift;
187                            my $m = $meta->find_method_by_name($attr);
188                            return defined $m->($self);
189                        });
190     $meta->add_method('_build_'.$attr.'_corr',
191                       sub {my $self = shift;
192                            my $m = $self->status->meta->find_method_by_name($attr);
193                            my $v = $m->($self->status);
194                            if (defined $v and length($v)) {
195                                return $self->correspondent_collection->
196                                    get_or_add_by_key($v);
197                            } else {
198                                return undef;
199                            }
200                        }
201                      );
202 }
203
204 sub is_done {
205     my $self = shift;
206     return $self->has_done;
207 }
208
209 sub strong_severity {
210     my $self = shift;
211     return exists $strong_severities->{$self->severity};
212 }
213
214 sub short_severity {
215     $_[0]->severity =~ m/^(.)/;
216     return $1;
217 }
218
219 sub _build_packages {
220     my $self = shift;
221     return $self->package_collection->
222             limit($self->status->package);
223 }
224
225 sub is_affecting {
226     my $self = shift;
227     return $self->affects->count > 0;
228 }
229
230 sub _build_affects {
231     my $self = shift;
232     return $self->package_collection->
233             limit($self->status->affects);
234 }
235 sub _build_sources {
236     my $self = shift;
237     local $_;
238     my @sources = map {$_->sources} $self->packages->members;
239     return @sources;
240 }
241
242 sub is_owned {
243     my $self = shift;
244     return defined $self->owner;
245 }
246
247 sub is_blocking {
248     my $self = shift;
249     return $self->blocks->count > 0;
250 }
251
252 sub _build_blocks {
253     my $self = shift;
254     return $self->bug_collection->
255         limit($self->status->blocks);
256 }
257
258 sub is_blocked {
259     my $self = shift;
260     return $self->blocked_by->count > 0;
261 }
262
263 sub _build_blocked_by {
264     my $self = shift;
265     return $self->bug_collection->
266         limit($self->status->blocked_by);
267 }
268
269 sub is_forwarded {
270     length($_[0]->forwarded) > 0;
271 }
272
273 for my $attr (qw(fixed found)) {
274     has $attr =>
275         (is => 'ro',
276          isa => 'Debbugs::Collection::Version',
277          clearer => '_clear_'.$attr,
278          builder => '_build_'.$attr,
279          handles => {},
280          lazy => 1,
281         );
282 }
283
284 sub has_found {
285     my $self = shift;
286     return any {1} $self->status->found;
287 }
288
289 sub _build_found {
290     my $self = shift;
291     return $self->packages->
292         get_source_versions($self->status->found);
293 }
294
295 sub has_fixed {
296     my $self = shift;
297     return any {1} $self->status->fixed;
298 }
299
300 sub _build_fixed {
301     my $self = shift;
302     return $self->packages->
303         get_source_versions($self->status->fixed);
304 }
305
306 sub is_merged {
307     my $self = shift;
308     return any {1} $self->status->mergedwith;
309 }
310
311 sub _build_mergedwith {
312     my $self = shift;
313     return $self->bug_collection->
314         limit($self->status->mergedwith);
315 }
316
317 for my $attr (qw(created modified)) {
318     has $attr => (is => 'rw', isa => 'Object',
319                 clearer => '_clear_'.$attr,
320                 builder => '_build_'.$attr,
321                 lazy => 1);
322 }
323 sub _build_created {
324     return DateTime->
325         from_epoch(epoch => $_[0]->status->date);
326 }
327 sub _build_modified {
328     return DateTime->
329         from_epoch(epoch => max($_[0]->status->log_modified,
330                                 $_[0]->status->last_modified
331                                ));
332 }
333
334 has tags => (is => 'ro',
335              isa => 'Debbugs::Bug::Tag',
336              clearer => '_clear_tags',
337              builder => '_build_tags',
338              lazy => 1,
339             );
340 sub _build_tags {
341     my $self = shift;
342     return Debbugs::Bug::Tag->new(keywords => join(' ',$self->status->tags),
343                                   bug => $self,
344                                   users => $self->bug_collection->users,
345                                  );
346 }
347
348 has pending => (is => 'ro',
349                 isa => 'Str',
350                 clearer => '_clear_pending',
351                 builder => '_build_pending',
352                 lazy => 1,
353                );
354
355 sub _build_pending {
356     my $self = shift;
357
358     my $pending = 'pending';
359     if (length($self->status->forwarded)) {
360         $pending = 'forwarded';
361     }
362     if ($self->tags->tag_is_set('pending')) {
363         $pending = 'pending-fixed';
364     }
365     if ($self->tags->tag_is_set('pending')) {
366         $pending = 'fixed';
367     }
368     # XXX This isn't quite right
369     return $pending;
370 }
371
372 =item buggy
373
374      $bug->buggy('debbugs/2.6.0-1','debbugs/2.6.0-2');
375      $bug->buggy(Debbugs::Version->new('debbugs/2.6.0-1'),
376                  Debbugs::Version->new('debbugs/2.6.0-2'),
377                 );
378
379 Returns the output of Debbugs::Versions::buggy for a particular
380 package, version and found/fixed set. Automatically turns found, fixed
381 and version into source/version strings.
382
383 =cut
384
385 sub buggy {
386     my $self = shift;
387     my $vertree =
388         $self->package_collection->
389         universe->versiontree;
390     my $max_buggy = 'absent';
391     for my $ver (@_) {
392         if (not ref($ver)) {
393             $ver = Debbugs::Version->
394                 new(version => $ver,
395                     package => $self,
396                     package_collection => $self->package_collection,
397                    );
398         }
399         $vertree->load($ver->source);
400         my $buggy =
401             $vertree->buggy($ver,
402                             [$self->found],
403                             [$self->fixed]);
404         if ($buggy eq 'found') {
405             return 'found'
406         }
407         if ($buggy eq 'fixed') {
408             $max_buggy = 'fixed';
409         }
410     }
411     return $max_buggy;
412 }
413
414 has archiveable =>
415     (is => 'ro', isa => 'Bool',
416      writer => '_set_archiveable',
417      builder => '_build_archiveable',
418      clearer => '_clear_archiveable',
419      lazy => 1,
420     );
421 has when_archiveable =>
422     (is => 'ro', isa => 'Num',
423      writer => '_set_when_archiveable',
424      builder => '_build_when_archiveable',
425      clearer => '_clear_when_archiveable',
426      lazy => 1,
427     );
428
429 sub _build_archiveable {
430     my $self = shift;
431     $self->_populate_archiveable(0);
432     return $self->archiveable;
433 }
434 sub _build_when_archiveable {
435     my $self = shift;
436     $self->_populate_archiveable(1);
437     return $self->when_archiveable;
438 }
439
440 sub _populate_archiveable {
441     my $self = shift;
442     my ($need_time) = @_;
443     $need_time //= 0;
444     # Bugs can be archived if they are
445     # 1. Closed
446     if (not $self->done) {
447         $self->_set_archiveable(0);
448         $self->_set_when_archiveable(-1);
449         return;
450     }
451     # 2. Have no unremovable tags set
452     if (@{$config{removal_unremovable_tags}}) {
453         state $unrem_tags =
454            {map {($_=>1)} @{$config{removal_unremovable_tags}}};
455         for my $tag ($self->tags) {
456             if ($unrem_tags->{$tag}) {
457                 $self->_set_archiveable(0);
458                 $self->_set_when_archiveable(-1);
459                 return;
460             }
461         }
462     }
463     my $time = time;
464     state $remove_time = 24 * 60 * 60 * ($config{removal_age} // 30);
465     # 4. Have been modified more than removal_age ago
466     my $moded_ago =
467         $time - $self->modified->epoch;
468     # if we don't need to know when we can archive, we can stop here if it's
469     # been modified too recently
470     if ($moded_ago < $remove_time) {
471         $self->_set_archiveable(0);
472         return unless $need_time;
473     }
474     my @distributions =
475         @{$config{removal_default_distribution_tags}};
476     if ($self->strong_severity) {
477         @distributions =
478             @{$config{removal_strong_severity_default_distribution_tags}};
479     }
480     # 3. Have a maximum buggy of fixed
481     my $buggy = $self->buggy($self->packages->
482                              get_source_versions_distributions(@distributions));
483     if ('found' eq $buggy) {
484         $self->_set_archiveable(0);
485         $self->_set_when_archiveable(-1);
486         return;
487     }
488     my $fixed_ago = $moded_ago;
489     # $fixed_ago = $time - $self->when_fixed(@distributions);
490     # if ($fixed_ago < $remove_time) {
491     #     $self->_set_archiveable(0);
492     # }
493     $self->_set_when_archiveable(($remove_time - min($fixed_ago,$moded_ago)) / (24 * 60 * 60));
494     if ($fixed_ago > $remove_time and
495         $moded_ago > $remove_time) {
496         $self->_set_archiveable(1);
497         $self->_set_when_archiveable(0);
498     }
499     return;
500 }
501
502 sub filter {
503     my $self = shift;
504     my %param = validate_with(params => \@_,
505                               spec   => {seen_merged => {type => HASHREF,
506                                                          default => sub {return {}},
507                                                         },
508                                          repeat_merged => {type => BOOLEAN,
509                                                            default => 1,
510                                                           },
511                                          include => {type => HASHREF,
512                                                      optional => 1,
513                                                     },
514                                          exclude => {type => HASHREF,
515                                                      optional => 1,
516                                                     },
517                                          min_days => {type => SCALAR,
518                                                       optional => 1,
519                                                      },
520                                          max_days => {type => SCALAR,
521                                                       optional => 1,
522                                                      },
523                                          },
524                              );
525     if (exists $param{include}) {
526         return 1 if not $self->matches($param{include});
527     }
528     if (exists $param{exclude}) {
529         return 1 if $self->matches($param{exclude});
530     }
531     if (exists $param{repeat_merged} and not $param{repeat_merged}) {
532         my @merged = sort {$a<=>$b} $self->bug, $self->status->mergedwith;
533         return 1 if first {sub {defined $_}}
534             @{$param{seen_merged}}{@merged};
535         @{$param{seen_merged}}{@merged} = (1) x @merged;
536     }
537     if (exists $param{min_days}) {
538         return 1 unless $param{min_days} <=
539             (DateTime->now() - $self->created)->days();
540     }
541     if (exists $param{max_days}) {
542         return 1 unless $param{max_days} >=
543             (DateTime->now() - $self->created)->days();
544     }
545     return 0;
546
547 }
548
549 sub __exact_match {
550     my ($field, $values) = @_;
551     my @ret = first {sub {$_ eq $field}} @{$values};
552     return @ret != 0;
553 }
554
555 sub __contains_match {
556     my ($field, $values) = @_;
557     foreach my $value (@{$values}) {
558         return 1 if (index($field, $value) > -1);
559     }
560     return 0;
561 }
562
563 state $field_match =
564    {subject => sub {__contains_match($_[0]->subject,@_)},
565     tags => sub {
566         for my $value (@{$_[1]}) {
567             if ($_[0]->tags->is_set($value)) {
568                 return 1;
569             }
570         }
571         return 0;
572         },
573     severity => sub {__exact_match($_[0]->severity,@_)},
574     pending => sub {__exact_match($_[0]->pending,@_)},
575     originator => sub {__exact_match($_[0]->submitter,@_)},
576     submitter => sub {__exact_match($_[0]->submitter,@_)},
577     forwarded => sub {__exact_match($_[0]->forwarded,@_)},
578     owner => sub {__exact_match($_[0]->owner,@_)},
579    };
580
581 sub matches {
582     my ($self,$hash) = @_;
583     for my $key (keys %{$hash}) {
584         my $sub = $field_match->{$key};
585         if (not defined $sub) {
586             carp "No subroutine for key: $key";
587             next;
588         }
589         return 1 if $sub->($self,$hash->{$key});
590     }
591     return 0;
592 }
593
594 sub email {
595     my $self = shift;
596     return $self->id.'@'.$config{email_domain};
597 }
598
599 sub subscribe_email {
600     my $self = shift;
601     return $self->id.'-subscribe@'.$config{email_domain};
602 }
603
604 sub url {
605     my $self = shift;
606     return $config{web_domain}.'/'.$self->id;
607 }
608
609 sub mbox_url {
610     my $self = shift;
611     return $config{web_domain}.'/mbox:'.$self->id;
612 }
613
614 sub mbox_status_url {
615     my $self = shift;
616     return $self->mbox_url.'?mboxstatus=yes';
617 }
618
619 sub mbox_maint_url {
620     my $self = shift;
621     $self->mbox_url.'?mboxmaint=yes';
622 }
623
624 sub version_url {
625     my $self = shift;
626     my $url = Debbugs::URI->new('version.cgi?');
627     $url->query_form(package => $self->status->package(),
628                        found => [$self->status->found],
629                        fixed => [$self->status->fixed],
630                      @_,
631                     );
632     return $url->as_string;
633 }
634
635 sub related_packages_and_versions {
636     my $self = shift;
637     my @packages;
638     if (length($self->status->{package}//'')) {
639         @packages = split /,/,$self->status->{package}//'';
640     }
641     if (length($self->status->{affects}//'')) {
642         push @packages,
643             split /,/,$self->status->{affects}//'';
644     }
645     my @versions =
646         (@{$self->status->{found_versions}//[]},
647          @{$self->status->{fixed_versions}//[]});
648     my @unqualified_versions;
649     my @return;
650     for my $ver (@versions) {
651         if ($ver =~ m{(<src>.+)/(<ver>.+)}) { # It's a src_pkg_ver
652             push @return, ['src:'.$+{src}, $+{ver}];
653         } else {
654            push @unqualified_versions,$ver;
655         }
656     }
657     for my $pkg (@packages) {
658         if (@unqualified_versions) {
659             push @return,
660                 [$pkg,@unqualified_versions];
661         } else {
662            push @return,$pkg;
663         }
664     }
665     return @return;
666 }
667
668 sub CARP_TRACE {
669     my $self = shift;
670     return 'Debbugs::Bug={bug='.$self->bug.'}';
671 }
672
673 __PACKAGE__->meta->make_immutable;
674
675 no Mouse;
676 1;
677
678
679 __END__
680 # Local Variables:
681 # indent-tabs-mode: nil
682 # cperl-indent-level: 4
683 # End: