]> git.donarmstrong.com Git - debbugs.git/blob - lib/Debbugs/Bug.pm
21a26e39af2c443e34c0b260887d29146f1fc225
[debbugs.git] / lib / 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     return $self->packages->sources->clone;
238 }
239
240 sub is_owned {
241     my $self = shift;
242     return defined $self->owner;
243 }
244
245 sub is_blocking {
246     my $self = shift;
247     return $self->blocks->count > 0;
248 }
249
250 sub _build_blocks {
251     my $self = shift;
252     return $self->bug_collection->
253         limit($self->status->blocks);
254 }
255
256 sub is_blocked {
257     my $self = shift;
258     return $self->blocked_by->count > 0;
259 }
260
261 sub _build_blocked_by {
262     my $self = shift;
263     return $self->bug_collection->
264         limit($self->status->blocked_by);
265 }
266
267 sub is_forwarded {
268     length($_[0]->forwarded) > 0;
269 }
270
271 for my $attr (qw(fixed found)) {
272     has $attr =>
273         (is => 'ro',
274          isa => 'Debbugs::Collection::Version',
275          clearer => '_clear_'.$attr,
276          builder => '_build_'.$attr,
277          handles => {},
278          lazy => 1,
279         );
280 }
281
282 sub has_found {
283     my $self = shift;
284     return any {1} $self->status->found;
285 }
286
287 sub _build_found {
288     my $self = shift;
289     return $self->packages->
290         get_source_versions($self->status->found);
291 }
292
293 sub has_fixed {
294     my $self = shift;
295     return any {1} $self->status->fixed;
296 }
297
298 sub _build_fixed {
299     my $self = shift;
300     return $self->packages->
301         get_source_versions($self->status->fixed);
302 }
303
304 sub is_merged {
305     my $self = shift;
306     return any {1} $self->status->mergedwith;
307 }
308
309 sub _build_mergedwith {
310     my $self = shift;
311     return $self->bug_collection->
312         limit($self->status->mergedwith);
313 }
314
315 for my $attr (qw(created modified)) {
316     has $attr => (is => 'rw', isa => 'Object',
317                 clearer => '_clear_'.$attr,
318                 builder => '_build_'.$attr,
319                 lazy => 1);
320 }
321 sub _build_created {
322     return DateTime->
323         from_epoch(epoch => $_[0]->status->date);
324 }
325 sub _build_modified {
326     return DateTime->
327         from_epoch(epoch => max($_[0]->status->log_modified,
328                                 $_[0]->status->last_modified
329                                ));
330 }
331
332 has tags => (is => 'ro',
333              isa => 'Debbugs::Bug::Tag',
334              clearer => '_clear_tags',
335              builder => '_build_tags',
336              lazy => 1,
337             );
338 sub _build_tags {
339     my $self = shift;
340     return Debbugs::Bug::Tag->new(keywords => join(' ',$self->status->tags),
341                                   bug => $self,
342                                   users => $self->bug_collection->users,
343                                  );
344 }
345
346 has pending => (is => 'ro',
347                 isa => 'Str',
348                 clearer => '_clear_pending',
349                 builder => '_build_pending',
350                 lazy => 1,
351                );
352
353 sub _build_pending {
354     my $self = shift;
355
356     my $pending = 'pending';
357     if (length($self->status->forwarded)) {
358         $pending = 'forwarded';
359     }
360     if ($self->tags->tag_is_set('pending')) {
361         $pending = 'pending-fixed';
362     }
363     if ($self->tags->tag_is_set('pending')) {
364         $pending = 'fixed';
365     }
366     # XXX This isn't quite right
367     return $pending;
368 }
369
370 =head2 buggy
371
372      $bug->buggy('debbugs/2.6.0-1','debbugs/2.6.0-2');
373      $bug->buggy(Debbugs::Version->new('debbugs/2.6.0-1'),
374                  Debbugs::Version->new('debbugs/2.6.0-2'),
375                 );
376
377 Returns the output of Debbugs::Versions::buggy for a particular
378 package, version and found/fixed set. Automatically turns found, fixed
379 and version into source/version strings.
380
381 =cut
382
383 sub buggy {
384     my $self = shift;
385     my $vertree =
386         $self->package_collection->
387         universe->versiontree;
388     my $max_buggy = 'absent';
389     for my $ver (@_) {
390         if (not ref($ver)) {
391             my @ver_opts = (version => $ver,
392                             package => $self->status->package,
393                             package_collection => $self->package_collection,
394                             $self->schema_arg
395                            );
396             if ($ver =~ m{/}) {
397                 $ver = Debbugs::Version::Source->(@ver_opts);
398             } else {
399                 $ver = Debbugs::Version::Binary->(@ver_opts);
400             }
401         }
402         $vertree->load($ver->source);
403         my $buggy =
404             $vertree->buggy($ver,
405                             [$self->found],
406                             [$self->fixed]);
407         if ($buggy eq 'found') {
408             return 'found'
409         }
410         if ($buggy eq 'fixed') {
411             $max_buggy = 'fixed';
412         }
413     }
414     return $max_buggy;
415 }
416
417 has archiveable =>
418     (is => 'ro', isa => 'Bool',
419      writer => '_set_archiveable',
420      builder => '_build_archiveable',
421      clearer => '_clear_archiveable',
422      lazy => 1,
423     );
424 has when_archiveable =>
425     (is => 'ro', isa => 'Num',
426      writer => '_set_when_archiveable',
427      builder => '_build_when_archiveable',
428      clearer => '_clear_when_archiveable',
429      lazy => 1,
430     );
431
432 sub _build_archiveable {
433     my $self = shift;
434     $self->_populate_archiveable(0);
435     return $self->archiveable;
436 }
437 sub _build_when_archiveable {
438     my $self = shift;
439     $self->_populate_archiveable(1);
440     return $self->when_archiveable;
441 }
442
443 sub _populate_archiveable {
444     my $self = shift;
445     my ($need_time) = @_;
446     $need_time //= 0;
447     # Bugs can be archived if they are
448     # 1. Closed
449     if (not $self->done) {
450         $self->_set_archiveable(0);
451         $self->_set_when_archiveable(-1);
452         return;
453     }
454     # 2. Have no unremovable tags set
455     if (@{$config{removal_unremovable_tags}}) {
456         state $unrem_tags =
457            {map {($_=>1)} @{$config{removal_unremovable_tags}}};
458         for my $tag ($self->tags) {
459             if ($unrem_tags->{$tag}) {
460                 $self->_set_archiveable(0);
461                 $self->_set_when_archiveable(-1);
462                 return;
463             }
464         }
465     }
466     my $time = time;
467     state $remove_time = 24 * 60 * 60 * ($config{removal_age} // 30);
468     # 4. Have been modified more than removal_age ago
469     my $moded_ago =
470         $time - $self->modified->epoch;
471     # if we don't need to know when we can archive, we can stop here if it's
472     # been modified too recently
473     if ($moded_ago < $remove_time) {
474         $self->_set_archiveable(0);
475         return unless $need_time;
476     }
477     my @distributions =
478         @{$config{removal_default_distribution_tags}};
479     if ($self->strong_severity) {
480         @distributions =
481             @{$config{removal_strong_severity_default_distribution_tags}};
482     }
483     # 3. Have a maximum buggy of fixed
484     my $buggy = $self->buggy($self->packages->
485                              get_source_versions_distributions(@distributions));
486     if ('found' eq $buggy) {
487         $self->_set_archiveable(0);
488         $self->_set_when_archiveable(-1);
489         return;
490     }
491     my $fixed_ago = $moded_ago;
492     # $fixed_ago = $time - $self->when_fixed(@distributions);
493     # if ($fixed_ago < $remove_time) {
494     #     $self->_set_archiveable(0);
495     # }
496     $self->_set_when_archiveable(($remove_time - min($fixed_ago,$moded_ago)) / (24 * 60 * 60));
497     if ($fixed_ago > $remove_time and
498         $moded_ago > $remove_time) {
499         $self->_set_archiveable(1);
500         $self->_set_when_archiveable(0);
501     }
502     return;
503 }
504
505 sub filter {
506     my $self = shift;
507     my %param = validate_with(params => \@_,
508                               spec   => {seen_merged => {type => HASHREF,
509                                                          default => sub {return {}},
510                                                         },
511                                          repeat_merged => {type => BOOLEAN,
512                                                            default => 1,
513                                                           },
514                                          include => {type => HASHREF,
515                                                      optional => 1,
516                                                     },
517                                          exclude => {type => HASHREF,
518                                                      optional => 1,
519                                                     },
520                                          min_days => {type => SCALAR,
521                                                       optional => 1,
522                                                      },
523                                          max_days => {type => SCALAR,
524                                                       optional => 1,
525                                                      },
526                                          },
527                              );
528     if (exists $param{include}) {
529         return 1 if not $self->matches($param{include});
530     }
531     if (exists $param{exclude}) {
532         return 1 if $self->matches($param{exclude});
533     }
534     if (exists $param{repeat_merged} and not $param{repeat_merged}) {
535         my @merged = sort {$a<=>$b} $self->bug, $self->status->mergedwith;
536         return 1 if first {sub {defined $_}}
537             @{$param{seen_merged}}{@merged};
538         @{$param{seen_merged}}{@merged} = (1) x @merged;
539     }
540     if (exists $param{min_days}) {
541         return 1 unless $param{min_days} <=
542             (DateTime->now() - $self->created)->days();
543     }
544     if (exists $param{max_days}) {
545         return 1 unless $param{max_days} >=
546             (DateTime->now() - $self->created)->days();
547     }
548     return 0;
549
550 }
551
552 sub __exact_match {
553     my ($field, $values) = @_;
554     my @ret = first {sub {$_ eq $field}} @{$values};
555     return @ret != 0;
556 }
557
558 sub __contains_match {
559     my ($field, $values) = @_;
560     foreach my $value (@{$values}) {
561         return 1 if (index($field, $value) > -1);
562     }
563     return 0;
564 }
565
566 state $field_match =
567    {subject => sub {__contains_match($_[0]->subject,@_)},
568     tags => sub {
569         for my $value (@{$_[1]}) {
570             if ($_[0]->tags->is_set($value)) {
571                 return 1;
572             }
573         }
574         return 0;
575         },
576     severity => sub {__exact_match($_[0]->severity,@_)},
577     pending => sub {__exact_match($_[0]->pending,@_)},
578     originator => sub {__exact_match($_[0]->submitter,@_)},
579     submitter => sub {__exact_match($_[0]->submitter,@_)},
580     forwarded => sub {__exact_match($_[0]->forwarded,@_)},
581     owner => sub {__exact_match($_[0]->owner,@_)},
582    };
583
584 sub matches {
585     my ($self,$hash) = @_;
586     for my $key (keys %{$hash}) {
587         my $sub = $field_match->{$key};
588         if (not defined $sub) {
589             carp "No subroutine for key: $key";
590             next;
591         }
592         return 1 if $sub->($self,$hash->{$key});
593     }
594     return 0;
595 }
596
597 sub email {
598     my $self = shift;
599     return $self->id.'@'.$config{email_domain};
600 }
601
602 sub subscribe_email {
603     my $self = shift;
604     return $self->id.'-subscribe@'.$config{email_domain};
605 }
606
607 sub url {
608     my $self = shift;
609     return $config{web_domain}.'/'.$self->id;
610 }
611
612 sub mbox_url {
613     my $self = shift;
614     return $config{web_domain}.'/mbox:'.$self->id;
615 }
616
617 sub mbox_status_url {
618     my $self = shift;
619     return $self->mbox_url.'?mboxstatus=yes';
620 }
621
622 sub mbox_maint_url {
623     my $self = shift;
624     $self->mbox_url.'?mboxmaint=yes';
625 }
626
627 sub version_url {
628     my $self = shift;
629     my $url = Debbugs::URI->new('version.cgi?');
630     $url->query_form(package => $self->status->package(),
631                        found => [$self->status->found],
632                        fixed => [$self->status->fixed],
633                      @_,
634                     );
635     return $url->as_string;
636 }
637
638 sub related_packages_and_versions {
639     my $self = shift;
640     my @packages = $self->status->package;
641     my @versions = ($self->status->found,
642                     $self->status->fixed);
643     my @unqualified_versions;
644     my @return;
645     for my $ver (@versions) {
646         if ($ver =~ m{(<src>.+)/(<ver>.+)}) { # It's a src_pkg_ver
647             push @return, ['src:'.$+{src}, $+{ver}];
648         } else {
649            push @unqualified_versions,$ver;
650         }
651     }
652     for my $pkg (@packages) {
653         if (@unqualified_versions) {
654             push @return,
655                 [$pkg,@unqualified_versions];
656         } else {
657            push @return,$pkg;
658         }
659     }
660     return @return;
661 }
662
663 sub CARP_TRACE {
664     my $self = shift;
665     return 'Debbugs::Bug={bug='.$self->bug.'}';
666 }
667
668 __PACKAGE__->meta->make_immutable;
669
670 no Mouse;
671 1;
672
673
674 __END__
675 # Local Variables:
676 # indent-tabs-mode: nil
677 # cperl-indent-level: 4
678 # End: