]> git.donarmstrong.com Git - debbugs.git/commitdiff
Merge branch 'mouseify'
authorDon Armstrong <don@donarmstrong.com>
Sat, 6 Jul 2019 04:01:33 +0000 (21:01 -0700)
committerDon Armstrong <don@donarmstrong.com>
Sat, 6 Jul 2019 04:01:33 +0000 (21:01 -0700)
39 files changed:
Debbugs/Bug.pm [new file with mode: 0644]
Debbugs/Bug/Status.pm [new file with mode: 0644]
Debbugs/Bug/Tag.pm [new file with mode: 0644]
Debbugs/Bugs.pm
Debbugs/CGI.pm
Debbugs/CGI/Pkgreport.pm
Debbugs/Collection.pm [new file with mode: 0644]
Debbugs/Collection/Bug.pm [new file with mode: 0644]
Debbugs/Collection/Correspondent.pm [new file with mode: 0644]
Debbugs/Collection/Package.pm [new file with mode: 0644]
Debbugs/Collection/Version.pm [new file with mode: 0644]
Debbugs/Config.pm
Debbugs/Correspondent.pm [new file with mode: 0644]
Debbugs/DB/Result/BinPkg.pm
Debbugs/DB/Result/BinPkgSrcPkg.pm [new file with mode: 0644]
Debbugs/DB/Result/BugBinpackage.pm
Debbugs/DB/Result/BugStatus.pm
Debbugs/DB/Result/SrcPkg.pm
Debbugs/Log.pm
Debbugs/OOBase.pm [new file with mode: 0644]
Debbugs/OOTypes.pm [new file with mode: 0644]
Debbugs/Package.pm [new file with mode: 0644]
Debbugs/User.pm
Debbugs/Version.pm [new file with mode: 0644]
Debbugs/Version/Binary.pm [new file with mode: 0644]
Debbugs/Version/Source.pm [new file with mode: 0644]
Debbugs/VersionTree.pm [new file with mode: 0644]
cgi/bugreport.cgi
cgi/pkgreport.cgi
debian/control
sql/debbugs_schema.sql
t/16_usertags.t [deleted file]
t/22_oo_interface.t [new file with mode: 0644]
t/fake_ftpdist
t/lib/DebbugsTest.pm
templates/en_US/cgi/bugreport.tx
templates/en_US/cgi/bugreport_buginfo.tx
templates/en_US/cgi/bugreport_pkginfo.tx
templates/en_US/cgi/short_bug_status.tx

diff --git a/Debbugs/Bug.pm b/Debbugs/Bug.pm
new file mode 100644 (file)
index 0000000..21a26e3
--- /dev/null
@@ -0,0 +1,678 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Bug;
+
+=head1 NAME
+
+Debbugs::Bug -- OO interface to bugs
+
+=head1 SYNOPSIS
+
+   use Debbugs::Bug;
+   Debbugs::Bug->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::clean;
+use v5.10; # for state
+
+use DateTime;
+use List::AllUtils qw(max first min any);
+
+use Params::Validate qw(validate_with :types);
+use Debbugs::Config qw(:config);
+use Debbugs::Status qw(read_bug);
+use Debbugs::Bug::Tag;
+use Debbugs::Bug::Status;
+use Debbugs::Collection::Package;
+use Debbugs::Collection::Bug;
+use Debbugs::Collection::Correspondent;
+
+use Debbugs::OOTypes;
+
+use Carp;
+
+extends 'Debbugs::OOBase';
+
+my $meta = __PACKAGE__->meta;
+
+state $strong_severities =
+   {map {($_,1)} @{$config{strong_severities}}};
+
+has bug => (is => 'ro', isa => 'Int',
+           required => 1,
+          );
+
+sub id {
+    return $_[0]->bug;
+}
+
+has saved => (is => 'ro', isa => 'Bool',
+             default => 0,
+             writer => '_set_saved',
+            );
+
+has status => (is => 'ro', isa => 'Debbugs::Bug::Status',
+              lazy => 1,
+              builder => '_build_status',
+               handles => {date => 'date',
+                           subject => 'subject',
+                           message_id => 'message_id',
+                           severity => 'severity',
+                           archived => 'archived',
+                           summary => 'summary',
+                           outlook => 'outlook',
+                           forwarded => 'forwarded',
+                          },
+             );
+
+sub _build_status {
+    my $self = shift;
+    return Debbugs::Bug::Status->new(bug=>$self->bug,
+                                     $self->schema_argument,
+                                    );
+}
+
+has log => (is => 'bare', isa => 'Debbugs::Log',
+            lazy => 1,
+            builder => '_build_log',
+            handles => {_read_record => 'read_record',
+                        log_records => 'read_all_records',
+                       },
+           );
+
+sub _build_log {
+    my $self = shift;
+    return Debbugs::Log->new(bug_num => $self->id,
+                             inner_file => 1,
+                            );
+}
+
+has spam => (is => 'bare', isa => 'Debbugs::Log::Spam',
+             lazy => 1,
+             builder => '_build_spam',
+             handles => ['is_spam'],
+            );
+sub _build_spam {
+    my $self = shift;
+    return Debbugs::Log::Spam->new(bug_num => $self->id);
+}
+
+has 'package_collection' => (is => 'ro',
+                            isa => 'Debbugs::Collection::Package',
+                            builder => '_build_package_collection',
+                            lazy => 1,
+                           );
+
+sub _build_package_collection {
+    my $self = shift;
+    if ($self->has_schema) {
+        return Debbugs::Collection::Package->new(schema => $self->schema);
+    }
+    carp "No schema when building package collection";
+    return Debbugs::Collection::Package->new();
+}
+
+has bug_collection => (is => 'ro',
+                      isa => 'Debbugs::Collection::Bug',
+                      builder => '_build_bug_collection',
+                     );
+sub _build_bug_collection {
+    my $self = shift;
+    if ($self->has_schema) {
+        return Debbugs::Collection::Bug->new(schema => $self->schema);
+    }
+    return Debbugs::Collection::Bug->new();
+}
+
+has correspondent_collection =>
+    (is => 'ro',
+     isa => 'Debbugs::Collection::Correspondent',
+     builder => '_build_correspondent_collection',
+     lazy => 1,
+    );
+sub _build_correspondent_collection   {
+    my $self = shift;
+    return Debbugs::Collection::Correspondent->new($self->schema_argument);
+}
+
+# package attributes
+for my $attr (qw(packages affects sources)) {
+    has $attr =>
+       (is => 'rw',
+        isa => 'Debbugs::Collection::Package',
+        clearer => '_clear_'.$attr,
+        builder => '_build_'.$attr,
+        lazy => 1,
+       );
+}
+
+# bugs
+for my $attr (qw(blocks blocked_by mergedwith)) {
+    has $attr =>
+       (is => 'ro',
+        isa => 'Debbugs::Collection::Bug',
+        clearer => '_clear_'.$attr,
+        builder => '_build_'.$attr,
+        handles => {},
+        lazy => 1,
+       );
+}
+
+
+for my $attr (qw(owner submitter done)) {
+    has $attr,
+        (is => 'ro',
+         isa => 'Maybe[Debbugs::Correspondent]',
+         lazy => 1,
+         builder => '_build_'.$attr.'_corr',
+         clearer => '_clear_'.$attr.'_corr',
+         handles => {$attr.'_url' => $attr.'_url',
+                     $attr.'_email' => 'email',
+                     $attr.'_phrase' => 'phrase',
+                    },
+        );
+    $meta->add_method('has_'.$attr,
+                     sub {my $self = shift;
+                           my $m = $meta->find_method_by_name($attr);
+                           return defined $m->($self);
+                      });
+    $meta->add_method('_build_'.$attr.'_corr',
+                      sub {my $self = shift;
+                           my $m = $self->status->meta->find_method_by_name($attr);
+                           my $v = $m->($self->status);
+                           if (defined $v and length($v)) {
+                               return $self->correspondent_collection->
+                                   get_or_add_by_key($v);
+                           } else {
+                               return undef;
+                           }
+                       }
+                     );
+}
+
+sub is_done {
+    my $self = shift;
+    return $self->has_done;
+}
+
+sub strong_severity {
+    my $self = shift;
+    return exists $strong_severities->{$self->severity};
+}
+
+sub short_severity {
+    $_[0]->severity =~ m/^(.)/;
+    return $1;
+}
+
+sub _build_packages {
+    my $self = shift;
+    return $self->package_collection->
+           limit($self->status->package);
+}
+
+sub is_affecting {
+    my $self = shift;
+    return $self->affects->count > 0;
+}
+
+sub _build_affects {
+    my $self = shift;
+    return $self->package_collection->
+           limit($self->status->affects);
+}
+sub _build_sources {
+    my $self = shift;
+    return $self->packages->sources->clone;
+}
+
+sub is_owned {
+    my $self = shift;
+    return defined $self->owner;
+}
+
+sub is_blocking {
+    my $self = shift;
+    return $self->blocks->count > 0;
+}
+
+sub _build_blocks {
+    my $self = shift;
+    return $self->bug_collection->
+       limit($self->status->blocks);
+}
+
+sub is_blocked {
+    my $self = shift;
+    return $self->blocked_by->count > 0;
+}
+
+sub _build_blocked_by {
+    my $self = shift;
+    return $self->bug_collection->
+       limit($self->status->blocked_by);
+}
+
+sub is_forwarded {
+    length($_[0]->forwarded) > 0;
+}
+
+for my $attr (qw(fixed found)) {
+    has $attr =>
+       (is => 'ro',
+        isa => 'Debbugs::Collection::Version',
+        clearer => '_clear_'.$attr,
+        builder => '_build_'.$attr,
+        handles => {},
+        lazy => 1,
+       );
+}
+
+sub has_found {
+    my $self = shift;
+    return any {1} $self->status->found;
+}
+
+sub _build_found {
+    my $self = shift;
+    return $self->packages->
+       get_source_versions($self->status->found);
+}
+
+sub has_fixed {
+    my $self = shift;
+    return any {1} $self->status->fixed;
+}
+
+sub _build_fixed {
+    my $self = shift;
+    return $self->packages->
+        get_source_versions($self->status->fixed);
+}
+
+sub is_merged {
+    my $self = shift;
+    return any {1} $self->status->mergedwith;
+}
+
+sub _build_mergedwith {
+    my $self = shift;
+    return $self->bug_collection->
+       limit($self->status->mergedwith);
+}
+
+for my $attr (qw(created modified)) {
+    has $attr => (is => 'rw', isa => 'Object',
+               clearer => '_clear_'.$attr,
+               builder => '_build_'.$attr,
+               lazy => 1);
+}
+sub _build_created {
+    return DateTime->
+       from_epoch(epoch => $_[0]->status->date);
+}
+sub _build_modified {
+    return DateTime->
+       from_epoch(epoch => max($_[0]->status->log_modified,
+                               $_[0]->status->last_modified
+                              ));
+}
+
+has tags => (is => 'ro',
+             isa => 'Debbugs::Bug::Tag',
+            clearer => '_clear_tags',
+            builder => '_build_tags',
+            lazy => 1,
+           );
+sub _build_tags {
+    my $self = shift;
+    return Debbugs::Bug::Tag->new(keywords => join(' ',$self->status->tags),
+                                  bug => $self,
+                                  users => $self->bug_collection->users,
+                                 );
+}
+
+has pending => (is => 'ro',
+                isa => 'Str',
+                clearer => '_clear_pending',
+                builder => '_build_pending',
+                lazy => 1,
+               );
+
+sub _build_pending {
+    my $self = shift;
+
+    my $pending = 'pending';
+    if (length($self->status->forwarded)) {
+        $pending = 'forwarded';
+    }
+    if ($self->tags->tag_is_set('pending')) {
+        $pending = 'pending-fixed';
+    }
+    if ($self->tags->tag_is_set('pending')) {
+        $pending = 'fixed';
+    }
+    # XXX This isn't quite right
+    return $pending;
+}
+
+=head2 buggy
+
+     $bug->buggy('debbugs/2.6.0-1','debbugs/2.6.0-2');
+     $bug->buggy(Debbugs::Version->new('debbugs/2.6.0-1'),
+                 Debbugs::Version->new('debbugs/2.6.0-2'),
+                );
+
+Returns the output of Debbugs::Versions::buggy for a particular
+package, version and found/fixed set. Automatically turns found, fixed
+and version into source/version strings.
+
+=cut
+
+sub buggy {
+    my $self = shift;
+    my $vertree =
+       $self->package_collection->
+       universe->versiontree;
+    my $max_buggy = 'absent';
+    for my $ver (@_) {
+       if (not ref($ver)) {
+            my @ver_opts = (version => $ver,
+                            package => $self->status->package,
+                            package_collection => $self->package_collection,
+                            $self->schema_arg
+                           );
+            if ($ver =~ m{/}) {
+                $ver = Debbugs::Version::Source->(@ver_opts);
+            } else {
+                $ver = Debbugs::Version::Binary->(@ver_opts);
+            }
+       }
+       $vertree->load($ver->source);
+       my $buggy =
+           $vertree->buggy($ver,
+                            [$self->found],
+                            [$self->fixed]);
+       if ($buggy eq 'found') {
+           return 'found'
+       }
+       if ($buggy eq 'fixed') {
+           $max_buggy = 'fixed';
+       }
+    }
+    return $max_buggy;
+}
+
+has archiveable =>
+    (is => 'ro', isa => 'Bool',
+     writer => '_set_archiveable',
+     builder => '_build_archiveable',
+     clearer => '_clear_archiveable',
+     lazy => 1,
+    );
+has when_archiveable =>
+    (is => 'ro', isa => 'Num',
+     writer => '_set_when_archiveable',
+     builder => '_build_when_archiveable',
+     clearer => '_clear_when_archiveable',
+     lazy => 1,
+    );
+
+sub _build_archiveable {
+    my $self = shift;
+    $self->_populate_archiveable(0);
+    return $self->archiveable;
+}
+sub _build_when_archiveable {
+    my $self = shift;
+    $self->_populate_archiveable(1);
+    return $self->when_archiveable;
+}
+
+sub _populate_archiveable {
+    my $self = shift;
+    my ($need_time) = @_;
+    $need_time //= 0;
+    # Bugs can be archived if they are
+    # 1. Closed
+    if (not $self->done) {
+       $self->_set_archiveable(0);
+       $self->_set_when_archiveable(-1);
+       return;
+    }
+    # 2. Have no unremovable tags set
+    if (@{$config{removal_unremovable_tags}}) {
+       state $unrem_tags =
+          {map {($_=>1)} @{$config{removal_unremovable_tags}}};
+       for my $tag ($self->tags) {
+           if ($unrem_tags->{$tag}) {
+               $self->_set_archiveable(0);
+               $self->_set_when_archiveable(-1);
+               return;
+           }
+       }
+    }
+    my $time = time;
+    state $remove_time = 24 * 60 * 60 * ($config{removal_age} // 30);
+    # 4. Have been modified more than removal_age ago
+    my $moded_ago =
+       $time - $self->modified->epoch;
+    # if we don't need to know when we can archive, we can stop here if it's
+    # been modified too recently
+    if ($moded_ago < $remove_time) {
+       $self->_set_archiveable(0);
+       return unless $need_time;
+    }
+    my @distributions =
+       @{$config{removal_default_distribution_tags}};
+    if ($self->strong_severity) {
+       @distributions =
+           @{$config{removal_strong_severity_default_distribution_tags}};
+    }
+    # 3. Have a maximum buggy of fixed
+    my $buggy = $self->buggy($self->packages->
+                            get_source_versions_distributions(@distributions));
+    if ('found' eq $buggy) {
+       $self->_set_archiveable(0);
+       $self->_set_when_archiveable(-1);
+       return;
+    }
+    my $fixed_ago = $moded_ago;
+    # $fixed_ago = $time - $self->when_fixed(@distributions);
+    # if ($fixed_ago < $remove_time) {
+    #     $self->_set_archiveable(0);
+    # }
+    $self->_set_when_archiveable(($remove_time - min($fixed_ago,$moded_ago)) / (24 * 60 * 60));
+    if ($fixed_ago > $remove_time and
+       $moded_ago > $remove_time) {
+       $self->_set_archiveable(1);
+       $self->_set_when_archiveable(0);
+    }
+    return;
+}
+
+sub filter {
+    my $self = shift;
+    my %param = validate_with(params => \@_,
+                             spec   => {seen_merged => {type => HASHREF,
+                                                        default => sub {return {}},
+                                                       },
+                                        repeat_merged => {type => BOOLEAN,
+                                                          default => 1,
+                                                         },
+                                        include => {type => HASHREF,
+                                                    optional => 1,
+                                                   },
+                                        exclude => {type => HASHREF,
+                                                    optional => 1,
+                                                   },
+                                        min_days => {type => SCALAR,
+                                                     optional => 1,
+                                                    },
+                                        max_days => {type => SCALAR,
+                                                     optional => 1,
+                                                    },
+                                        },
+                            );
+    if (exists $param{include}) {
+       return 1 if not $self->matches($param{include});
+    }
+    if (exists $param{exclude}) {
+       return 1 if $self->matches($param{exclude});
+    }
+    if (exists $param{repeat_merged} and not $param{repeat_merged}) {
+       my @merged = sort {$a<=>$b} $self->bug, $self->status->mergedwith;
+       return 1 if first {sub {defined $_}}
+            @{$param{seen_merged}}{@merged};
+       @{$param{seen_merged}}{@merged} = (1) x @merged;
+    }
+    if (exists $param{min_days}) {
+       return 1 unless $param{min_days} <=
+           (DateTime->now() - $self->created)->days();
+    }
+    if (exists $param{max_days}) {
+       return 1 unless $param{max_days} >=
+           (DateTime->now() - $self->created)->days();
+    }
+    return 0;
+
+}
+
+sub __exact_match {
+    my ($field, $values) = @_;
+    my @ret = first {sub {$_ eq $field}} @{$values};
+    return @ret != 0;
+}
+
+sub __contains_match {
+    my ($field, $values) = @_;
+    foreach my $value (@{$values}) {
+        return 1 if (index($field, $value) > -1);
+    }
+    return 0;
+}
+
+state $field_match =
+   {subject => sub {__contains_match($_[0]->subject,@_)},
+    tags => sub {
+       for my $value (@{$_[1]}) {
+           if ($_[0]->tags->is_set($value)) {
+               return 1;
+           }
+       }
+       return 0;
+       },
+    severity => sub {__exact_match($_[0]->severity,@_)},
+    pending => sub {__exact_match($_[0]->pending,@_)},
+    originator => sub {__exact_match($_[0]->submitter,@_)},
+    submitter => sub {__exact_match($_[0]->submitter,@_)},
+    forwarded => sub {__exact_match($_[0]->forwarded,@_)},
+    owner => sub {__exact_match($_[0]->owner,@_)},
+   };
+
+sub matches {
+    my ($self,$hash) = @_;
+    for my $key (keys %{$hash}) {
+       my $sub = $field_match->{$key};
+       if (not defined $sub) {
+           carp "No subroutine for key: $key";
+           next;
+       }
+       return 1 if $sub->($self,$hash->{$key});
+    }
+    return 0;
+}
+
+sub email {
+    my $self = shift;
+    return $self->id.'@'.$config{email_domain};
+}
+
+sub subscribe_email {
+    my $self = shift;
+    return $self->id.'-subscribe@'.$config{email_domain};
+}
+
+sub url {
+    my $self = shift;
+    return $config{web_domain}.'/'.$self->id;
+}
+
+sub mbox_url {
+    my $self = shift;
+    return $config{web_domain}.'/mbox:'.$self->id;
+}
+
+sub mbox_status_url {
+    my $self = shift;
+    return $self->mbox_url.'?mboxstatus=yes';
+}
+
+sub mbox_maint_url {
+    my $self = shift;
+    $self->mbox_url.'?mboxmaint=yes';
+}
+
+sub version_url {
+    my $self = shift;
+    my $url = Debbugs::URI->new('version.cgi?');
+    $url->query_form(package => $self->status->package(),
+                       found => [$self->status->found],
+                       fixed => [$self->status->fixed],
+                     @_,
+                    );
+    return $url->as_string;
+}
+
+sub related_packages_and_versions {
+    my $self = shift;
+    my @packages = $self->status->package;
+    my @versions = ($self->status->found,
+                    $self->status->fixed);
+    my @unqualified_versions;
+    my @return;
+    for my $ver (@versions) {
+        if ($ver =~ m{(<src>.+)/(<ver>.+)}) { # It's a src_pkg_ver
+            push @return, ['src:'.$+{src}, $+{ver}];
+        } else {
+           push @unqualified_versions,$ver;
+        }
+    }
+    for my $pkg (@packages) {
+        if (@unqualified_versions) {
+            push @return,
+                [$pkg,@unqualified_versions];
+        } else {
+           push @return,$pkg;
+        }
+    }
+    return @return;
+}
+
+sub CARP_TRACE {
+    my $self = shift;
+    return 'Debbugs::Bug={bug='.$self->bug.'}';
+}
+
+__PACKAGE__->meta->make_immutable;
+
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Bug/Status.pm b/Debbugs/Bug/Status.pm
new file mode 100644 (file)
index 0000000..9209485
--- /dev/null
@@ -0,0 +1,576 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Bug::Status;
+
+=head1 NAME
+
+Debbugs::Bug::Status -- OO interface to status files
+
+=head1 SYNOPSIS
+
+   use Debbugs::Bug;
+   Debbugs::Bug->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::clean;
+use v5.10; # for state
+use Mouse::Util::TypeConstraints qw(enum);
+
+use DateTime;
+use List::AllUtils qw(max first min);
+
+use Params::Validate qw(validate_with :types);
+use Debbugs::Common qw(make_list);
+use Debbugs::Config qw(:config);
+use Debbugs::Status qw(get_bug_status);
+
+use Debbugs::OOTypes;
+
+use Carp;
+
+extends 'Debbugs::OOBase';
+
+my $meta = __PACKAGE__->meta;
+
+has bug => (is => 'ro', isa => 'Int',
+          );
+
+# status obtained from DB, filesystem, or hashref
+has status_source => (is => 'ro',
+                     isa => enum([qw(db filesystem hashref)]),
+                     default => 'filesystem',
+                     writer => '_set_status_source',
+                    );
+
+has _status => (is => 'bare',
+                writer => '_set_status',
+                reader => '_status',
+                predicate => '_has__status',
+               );
+
+my %field_methods;
+
+sub BUILD {
+    my $self = shift;
+    my $args = shift;
+    state $field_mapping =
+       {originator => 'submitter',
+        keywords => 'tags',
+        msgid => 'message_id',
+        blockedby => 'blocked_by',
+        found_versions => 'found',
+        fixed_versions => 'fixed',
+       };
+    if (not exists $args->{status} and exists $args->{bug}) {
+       if ($self->has_schema) {
+           ($args->{status}) =
+               $self->schema->resultset('BugStatus')->
+               search_rs({id => [make_list($args->{bug})]},
+                        {result_class => 'DBIx::Class::ResultClass::HashRefInflator'})->
+                            all();
+           for my $field (keys %{$field_mapping}) {
+               $args->{status}{$field_mapping->{$field}} =
+                   $args->{status}{$field} if defined $args->{status}{$field};
+               delete $args->{status}{$field};
+           }
+           $self->_set_status_source('db');
+       } else {
+           $args->{status} = get_bug_status(bug=>$args->{bug});
+           for my $field (keys %{$field_mapping}) {
+               $args->{status}{$field_mapping->{$field}} =
+                   $args->{status}{$field} if defined $args->{status}{$field};
+           }
+           $self->_set_status_source('filesystem');
+       }
+    } elsif (exists $args->{status}) {
+        for my $field (keys %{$field_mapping}) {
+            $args->{status}{$field_mapping->{$field}} =
+                $args->{status}{$field} if defined $args->{status}{$field};
+        }
+       $self->_set_status_source('hashref');
+    }
+    if (exists $args->{status}) {
+       if (ref($args->{status}) ne 'HASH') {
+           croak "status must be a HASHREF (argument to __PACKAGE__)";
+       }
+        $self->_set_status($args->{status});
+       delete $args->{status};
+    }
+}
+
+has saved => (is => 'ro', isa => 'Bool',
+             default => 0,
+             writer => '_set_set_saved',
+            );
+
+sub __field_or_def {
+    my ($self,$field,$default) = @_;
+    if ($self->_has__status) {
+        my $s = $self->_status()->{$field};
+        return $s if defined $s;
+    }
+    return $default;
+}
+
+=head2 Status Fields
+
+=cut
+
+=head3 Single-value Fields
+
+=over
+
+=item submitter (single)
+
+=cut
+
+has submitter =>
+    (is => 'ro',
+     isa => 'Str',
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('submitter',
+                               $config{maintainer_email});
+      },
+     lazy => 1,
+     writer => '_set_submitter',
+    );
+
+=item date (single)
+
+=cut
+
+has date =>
+    (is => 'ro',
+     isa => 'Str',
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('date',
+                               time);
+      },
+     lazy => 1,
+     writer => '_set_date',
+    );
+
+=item last_modified (single)
+
+=cut
+
+has last_modified =>
+    (is => 'ro',
+     isa => 'Str',
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('last_modified',
+                               time);
+      },
+     lazy => 1,
+     writer => '_set_last_modified',
+    );
+
+=item log_modified (single)
+
+=cut
+
+has log_modified =>
+    (is => 'ro',
+     isa => 'Str',
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('log_modified',
+                                time);
+      },
+     lazy => 1,
+     writer => '_set_log_modified',
+    );
+
+
+=item subject
+
+=cut
+
+has subject =>
+    (is => 'ro',
+     isa => 'Str',
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('subject',
+                               'No subject');
+     },
+     lazy => 1,
+     writer => '_set_subject',
+    );
+
+=item message_id
+
+=cut
+
+has message_id =>
+    (is => 'ro',
+     isa => 'Str',
+     lazy => 1,
+     builder =>
+     sub {
+        my $self = shift;
+         $self->__field_or_def('message_id',
+                               'nomessageid.'.$self->date.'_'.
+                               md5_hex($self->subject.$self->submitter).
+                               '@'.$config{email_domain},
+                              );
+     },
+     writer => '_set_message_id',
+    );
+
+
+=item done
+
+=item severity
+
+=cut
+
+has severity =>
+    (is => 'ro',
+     isa => 'Str',
+     lazy => 1,
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('severity',
+                               $config{default_severity});
+     },
+     writer => '_set_severity',
+    );
+
+=item unarchived
+
+Unix epoch the bug was last unarchived. Zero if the bug has never been
+unarchived.
+
+=cut
+
+has unarchived =>
+    (is => 'ro',
+     isa => 'Int',
+     lazy => 1,
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('unarchived',
+                               0);
+     },
+     writer => '_set_unarchived',
+    );
+
+=item archived
+
+True if the bug is archived, false otherwise.
+
+=cut
+
+has archived =>
+    (is => 'ro',
+     isa => 'Int',
+     lazy => 1,
+     builder =>
+     sub {
+         my $self = shift;
+         $self->__field_or_def('archived',
+                               0);
+     },
+     writer => '_set_archived',
+    );
+
+=item owner
+
+=item summary
+
+=item outlook
+
+=item done
+
+=item forwarded
+
+=cut
+
+for my $field (qw(owner unarchived summary outlook done forwarded)) {
+    has $field =>
+       (is => 'ro',
+        isa => 'Str',
+         builder =>
+         sub {
+             my $self = shift;
+             $self->__field_or_def($field,
+                                   '');
+         },
+        writer => '_set_'.$field,
+         lazy => 1,
+       );
+    my $field_method = $meta->find_method_by_name($field);
+    die "No field method for $field" unless defined $field_method;
+    $meta->add_method('has_'.$field =>
+                     sub {my $self = shift;
+                          return length($field_method->($self));
+                      });
+}
+
+=back
+
+=head3 Multi-value Fields
+
+=over
+
+=item affects
+
+=item package
+
+=item tags
+
+=cut
+
+for my $field (qw(affects package tags)) {
+    has '_'.$field =>
+       (is => 'ro',
+        traits => [qw(Array)],
+        isa => 'ArrayRef[Str]',
+         builder =>
+         sub {
+             my $self = shift;
+             if ($self->_has__status) {
+                 my $s = $self->_status()->{$field};
+                 if (!ref($s)) {
+                     $s = _build_split_field($s,
+                                             $field);
+                 }
+                 return $s;
+             }
+             return [];
+         },
+        writer => '_set_'.$field,
+        handles => {$field => 'elements',
+                     $field.'_count' => 'count',
+                     $field.'_join' => 'join',
+                   },
+        lazy => 1,
+       );
+    my $field_method = $meta->find_method_by_name($field);
+    if (defined $field_method) {
+       $meta->add_method($field.'_ref'=>
+                         sub {my $self = shift;
+                              return [$field_method->($self)]
+                          });
+    }
+}
+
+=item found
+
+=item fixed
+
+=cut
+
+sub __hashref_field {
+    my ($self,$field) = @_;
+
+    if ($self->_has__status) {
+        my $s = $self->_status()->{$field};
+        if (!ref($s)) {
+            $s = _build_split_field($s,
+                                    $field);
+        }
+        return $s;
+    }
+    return [];
+}
+
+for my $field (qw(found fixed)) {
+    has '_'.$field =>
+       (is => 'ro',
+        traits => ['Hash'],
+        isa => 'HashRef[Str]',
+         builder =>
+         sub {
+             my $self = shift;
+             if ($self->_has__status) {
+                 my $s = $self->_status()->{$field};
+                 if (!ref($s)) {
+                     $s = _build_split_field($s,
+                                             $field);
+                 }
+                 if (ref($s) ne 'HASH') {
+                     $s = {map {$_,'1'} @{$s}};
+                 }
+                 return $s;
+             }
+             return {};
+         },
+        default => sub {return {}},
+        writer => '_set_'.$field,
+        handles => {$field => 'keys',
+                     $field.'_count' => 'count',
+                   },
+        lazy => 1,
+       );
+    my $field_method = $meta->find_method_by_name($field);
+    if (defined $field_method) {
+       $meta->add_method('_'.$field.'_ref'=>
+                         sub {my $self = shift;
+                              return [$field_method->($self)]
+                          });
+       $meta->add_method($field.'_join'=>
+                         sub {my ($self,$joiner) = @_;
+                              return join($joiner,$field_method->($self));
+                          });
+    }
+}
+
+
+for (qw(found fixed)) {
+    around '_set_'.$_ => sub {
+       my $orig = shift;
+       my $self = shift;
+       if (defined ref($_[0]) and
+           ref($_[0]) eq 'ARRAY'
+          ) {
+           @_ = {map {$_,'1'} @{$_[0]}};
+       } elsif (@_ > 1) {
+           @_ = {map {$_,'1'} @_};
+       }
+       $self->$orig(@_);
+    };
+}
+
+
+
+=item mergedwith
+
+=item blocks
+
+=item blocked_by
+
+=cut
+
+for my $field (qw(blocks blocked_by mergedwith)) {
+    has '_'.$field =>
+       (is => 'ro',
+        traits => ['Hash'],
+        isa => 'HashRef[Int]',
+         builder =>
+         sub {
+             my $self = shift;
+             if ($self->_has__status) {
+                 my $s = $self->_status()->{$field};
+                 if (!ref($s)) {
+                     $s = _build_split_field($s,
+                                             $field);
+                 }
+                 if (ref($s) ne 'HASH') {
+                     $s = {map {$_,'1'} @{$s}};
+                 }
+                 return $s;
+             }
+             return {};
+         },
+        handles => {$field.'_count' => 'count',
+                   },
+        writer => '_set_'.$field,
+        lazy => 1,
+       );
+    my $internal_field_method = $meta->find_method_by_name('_'.$field);
+    die "No field method for _$field" unless defined $internal_field_method;
+    $meta->add_method($field =>
+                     sub {my $self = shift;
+                          return sort {$a <=> $b}
+                              keys %{$internal_field_method->($self)};
+                      });
+    my $field_method = $meta->find_method_by_name($field);
+    die "No field method for _$field" unless defined $field_method;
+    $meta->add_method('_'.$field.'_ref'=>
+                     sub {my $self = shift;
+                          return [$field_method->($self)]
+                      });
+    $meta->add_method($field.'_join'=>
+                      sub {my ($self,$joiner) = @_;
+                           return join($joiner,$field_method->($self));
+                       });
+}
+
+for (qw(blocks blocked_by mergedwith)) {
+    around '_set_'.$_ => sub {
+       my $orig = shift;
+       my $self = shift;
+       if (defined ref($_[0]) and
+           ref($_[0]) eq 'ARRAY'
+          ) {
+           $_[0] = {map {$_,'1'} @{$_[0]}};
+       } elsif (@_ > 1) {
+           @_ = {map {$_,'1'} @{$_[0]}};
+       }
+       $self->$orig(@_);
+    };
+}
+
+=back
+
+=cut
+
+sub _build_split_field {
+    sub sort_and_unique {
+       my @v;
+       my %u;
+       my $all_numeric = 1;
+       for my $v (@_) {
+           if ($all_numeric and $v =~ /\D/) {
+               $all_numeric = 0;
+           }
+           next if exists $u{$v};
+           $u{$v} = 1;
+           push @v, $v;
+       }
+       if ($all_numeric) {
+           return sort {$a <=> $b} @v;
+       } else {
+           return sort @v;
+       }
+    }
+    sub split_ditch_empty {
+       return grep {length $_} map {split ' '} @_;
+
+    }
+    my ($val,$field) = @_;
+    $val //= '';
+
+    if ($field =~ /^(package|affects|source)$/) {
+       return [grep {length $_} map lc, split /[\s,()?]+/, $val];
+    } else {
+       return [sort_and_unique(split_ditch_empty($val))];
+    }
+}
+
+
+__PACKAGE__->meta->make_immutable;
+
+no Mouse;
+no Mouse::Util::TypeConstraints;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Bug/Tag.pm b/Debbugs/Bug/Tag.pm
new file mode 100644 (file)
index 0000000..06dfb3f
--- /dev/null
@@ -0,0 +1,212 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Bug::Tag;
+
+=head1 NAME
+
+Debbugs::Bug::Tag -- OO interface to bug tags
+
+=head1 SYNOPSIS
+
+   use Debbugs::Bug::Tag;
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::clean;
+use v5.10; # for state
+
+use Debbugs::User;
+use List::AllUtils qw(uniq);
+use Debbugs::Config qw(:config);
+use Carp qw(croak);
+
+state $valid_tags =
+    {map {($_,1)} @{$config{tags}}};
+
+state $short_tags =
+   {%{$config{tags_single_letter}}};
+
+extends 'Debbugs::OOBase';
+
+around BUILDARGS => sub {
+    my $orig = shift;
+    my $class = shift;
+    if (@_ == 1 && !ref $_[0]) {
+       return $class->$orig(keywords => $_[0]);
+    } else {
+       return $class->$orig(@_);
+    }
+};
+
+sub BUILD {
+    my $self = shift;
+    my $args = shift;
+    if (exists $args->{keywords}) {
+        my @tags;
+        if (ref($args->{keywords})) {
+            @tags = @{$args->{keywords}}
+        } else {
+            @tags = split /[, ]/,$args->{keywords};
+        }
+        return unless @tags;
+        $self->_set_tag(map {($_,1)} @tags);
+        delete $args->{keywords};
+    }
+}
+
+has tags => (is => 'ro',
+            isa => 'HashRef[Str]',
+            traits => ['Hash'],
+            lazy => 1,
+            reader => '_tags',
+            builder => '_build_tags',
+            handles => {has_tags => 'count',
+                         _set_tag => 'set',
+                         unset_tag => 'delete',
+                        },
+           );
+has usertags => (is => 'ro',
+                isa => 'HashRef[Str]',
+                lazy => 1,
+                 traits => ['Hash'],
+                 handles => {unset_usertag => 'delete',
+                             has_usertags => 'count',
+                            },
+                reader => '_usertags',
+                builder => '_build_usertags',
+               );
+
+sub has_any_tags {
+    my $self = shift;
+    return ($self->has_tags || $self->has_usertags);
+}
+
+has bug => (is => 'ro',
+            isa => 'Debbugs::Bug',
+            required => 1,
+           );
+
+has users => (is => 'ro',
+              isa => 'ArrayRef[Debbugs::User]',
+              default => sub {[]},
+             );
+
+sub _build_tags {
+    return {};
+}
+
+sub _build_usertags {
+    my $self = shift;
+    local $_;
+    my $t = {};
+    my $id = $self->bug->id;
+    for my $user (@{$self->users}) {
+        for my $tag ($user->tags_on_bug($id)) {
+            $t->{$tag} = $user->email;
+        }
+    }
+    return $t;
+}
+
+sub is_set {
+    return ($_[0]->tag_is_set($_[1]) or
+        $_[0]->usertag_is_set($_[1]));
+}
+
+sub tag_is_set {
+    return exists $_[0]->_tags->{$_[1]} ? 1 : 0;
+}
+
+sub usertag_is_set {
+    return exists $_[0]->_usertags->{$_[1]} ? 1 : 0;
+}
+
+sub set_tag {
+    my $self = shift;
+    for my $tag (@_) {
+       if (not $self->valid_tag($tag)) {
+           confess("Invalid tag $tag");
+       }
+       $self->_tags->{$tag} = 1;
+    }
+    return $self;
+}
+
+sub valid_tag {
+    return exists $valid_tags->{$_[1]}?1:0;
+}
+
+sub as_string {
+    my $self = shift;
+    return $self->join_all(' ');
+}
+
+sub join_all {
+    my $self = shift;
+    my $joiner = shift;
+    $joiner //= ', ';
+    return join($joiner,$self->all_tags);
+}
+
+sub join_usertags {
+    my $self = shift;
+    my $joiner = shift;
+    $joiner //= ', ';
+    return join($joiner,$self->usertags);
+}
+
+sub join_tags {
+    my $self = shift;
+    my $joiner = shift;
+    $joiner //= ', ';
+    return join($joiner,$self->tags);
+}
+
+sub all_tags {
+    return uniq sort $_[0]->tags,$_[0]->usertags;
+}
+
+sub tags {
+    return sort keys %{$_[0]->_tags}
+}
+
+sub short_tags {
+    my $self = shift;
+    my @r;
+    for my $tag ($self->tags) {
+       next unless exists $short_tags->{$tag};
+       push @r,
+          {long => $tag,
+           short => $short_tags->{$tag},
+          };
+    }
+    if (wantarray) {
+       return @r;
+    } else {
+       return [@r];
+    }
+}
+
+sub usertags {
+    return sort keys %{$_[0]->_usertags}
+}
+
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
index 5879f724b5564cbb4e99ad9a6a70c9e939f992c6..127e4727248b11457a021c987cb74b83377e6a2e 100644 (file)
@@ -621,26 +621,22 @@ sub get_bugs_by_db{
         # identify all of the srcpackages and binpackages that match first
         my $src_pkgs_rs =
         $s->resultset('SrcPkg')->
-            search({-or => [map {('me.pkg' => $_,
-                                 )}
-                            make_list($param{src})],
+            search({'pkg' => [make_list($param{src})],
                    },
                   { columns => ['id'],
                     group_by => ['me.id'],
                    },
                   );
         my $bin_pkgs_rs =
-        $s->resultset('BinPkg')->
-            search({-or => [map {('src_pkg.pkg' => $_,
-                                 )}
-                            make_list($param{src})],
-                    },
-                  {join => {bin_vers => {src_ver => 'src_pkg'}},
-                   columns => ['id'],
-                   group_by => ['me.id'],
+            $s->resultset('BinPkgSrcPkg')->
+            search({'src_pkg.pkg' => [make_list($param{src})],
+                   },
+                  {columns => ['bin_pkg'],
+                   join => ['src_pkg'],
+                   group_by => ['bin_pkg'],
                   });
          $rs = $rs->search({-or => {'bug_binpackages.bin_pkg' =>
-                                  { -in => $bin_pkgs_rs->get_column('id')->as_query},
+                                  { -in => $bin_pkgs_rs->get_column('bin_pkg')->as_query},
                                    'bug_srcpackages.src_pkg' =>
                                   { -in => $src_pkgs_rs->get_column('id')->as_query},
                                    'me.unknown_packages' =>
index 7cc7f4166481335a7d9a6c6fda95b0f2fe694602..dffa8ec1e021a896f1d42c782f76807dadda395c 100644 (file)
@@ -78,6 +78,7 @@ use Debbugs::User qw();
 use Mail::Address;
 use POSIX qw(ceil);
 use Storable qw(dclone);
+use Scalar::Util qw(looks_like_number);
 
 use List::AllUtils qw(max);
 use File::stat;
@@ -468,23 +469,24 @@ returning htmlized links.
 =cut
 
 sub bug_links {
+    state $spec = {bug => {type => SCALAR|ARRAYREF,
+                          optional => 1,
+                         },
+                  links_only => {type => BOOLEAN,
+                                 default => 0,
+                                },
+                  class => {type => SCALAR,
+                            default => '',
+                           },
+                  separator => {type => SCALAR,
+                                default => ', ',
+                               },
+                  options => {type => HASHREF,
+                              default => {},
+                             },
+                 };
      my %param = validate_with(params => \@_,
-                              spec   => {bug => {type => SCALAR|ARRAYREF,
-                                                 optional => 1,
-                                                },
-                                         links_only => {type => BOOLEAN,
-                                                        default => 0,
-                                                       },
-                                         class => {type => SCALAR,
-                                                   default => '',
-                                                  },
-                                         separator => {type => SCALAR,
-                                                       default => ', ',
-                                                      },
-                                         options => {type => HASHREF,
-                                                     default => {},
-                                                    },
-                                        },
+                              spec   => $spec,
                              );
      my %options = %{$param{options}};
 
@@ -501,8 +503,11 @@ sub bug_links {
                            $_);
                       } make_list($param{bug}) if exists $param{bug};
      } else {
-        push @links, map {('bugreport.cgi?bug='.uri_escape_utf8($_),
-                           $_)}
+        push @links,
+            map {my $b = ceil($_);
+                 ('bugreport.cgi?bug='.$b,
+                  $b)}
+            grep {looks_like_number($_)}
             make_list($param{bug}) if exists $param{bug};
      }
      my @return;
index 331073e54164a7b232c12f439e06e636a13aceea..e3dcc1235bc0d4a3f119d6d5593673e0448f33b8 100644 (file)
@@ -33,6 +33,11 @@ use Exporter qw(import);
 use IO::Scalar;
 use Params::Validate qw(validate_with :types);
 
+use Debbugs::Collection::Bug;
+
+use Carp;
+use List::AllUtils qw(apply);
+
 use Debbugs::Config qw(:config :globals);
 use Debbugs::CGI qw(:url :html :util);
 use Debbugs::Common qw(:misc :util :date);
@@ -128,7 +133,7 @@ sub generate_package_info{
                                 # distribution.
                                 @{$config{distributions}//[]} ?
                                 (dist => [@{$config{distributions}}]) : (),
-                               );
+                               ) if defined $srcforpkg;
      @pkgs = grep( !/^\Q$package\E$/, @pkgs );
      if ( @pkgs ) {
          @pkgs = sort @pkgs;
@@ -210,52 +215,14 @@ display below
 
 sub short_bug_status_html {
      my %param = validate_with(params => \@_,
-                              spec   => {status => {type => HASHREF,
-                                                   },
-                                         options => {type => HASHREF,
-                                                     default => {},
-                                                    },
-                                         bug_options => {type => HASHREF,
-                                                         default => {},
-                                                        },
-                                         snippet => {type => SCALAR,
-                                                     default => '',
-                                                    },
+                              spec   => {bug => {type => OBJECT,
+                                                 isa => 'Debbugs::Bug',
+                                                },
                                         },
                              );
 
-     my %status = %{$param{status}};
-
-     $status{tags_array} = [sort(split(/\s+/, $status{tags}))];
-     $status{date_text} = strftime('%a, %e %b %Y %T UTC', gmtime($status{date}));
-     $status{mergedwith_array} = [split(/ /,$status{mergedwith})];
-
-     my @blockedby= split(/ /, $status{blockedby});
-     $status{blockedby_array} = [];
-     if (@blockedby && $status{"pending"} ne 'fixed' && ! length($status{done})) {
-         for my $b (@blockedby) {
-              my %s = %{get_bug_status($b)};
-              next if (defined $s{pending} and $s{pending} eq 'fixed') or (defined $s{done} and length $s{done});
-              push @{$status{blockedby_array}},{bug_num => $b, subject => $s{subject}, status => \%s};
-         }
-     }
-
-     my @blocks= split(/ /, $status{blocks});
-     $status{blocks_array} = [];
-     if (@blocks && $status{"pending"} ne 'fixed' && ! length($status{done})) {
-         for my $b (@blocks) {
-              my %s = %{get_bug_status($b)};
-              next if (defined $s{pending} and $s{pending} eq 'fixed') or (defined $s{done} and length $s{done});
-              push @{$status{blocks_array}}, {bug_num => $b, subject => $s{subject}, status => \%s};
-         }
-     }
-     my $days = bug_archiveable(bug => $status{id},
-                               status => \%status,
-                               days_until => 1,
-                              );
-     $status{archive_days} = $days;
      return fill_in_template(template => 'cgi/short_bug_status',
-                            variables => {status => \%status,
+                            variables => {bug => $param{bug},
                                           isstrongseverity => \&Debbugs::Status::isstrongseverity,
                                           html_escape   => \&Debbugs::CGI::html_escape,
                                           looks_like_number => \&Scalar::Util::looks_like_number,
@@ -273,7 +240,7 @@ sub short_bug_status_html {
 
 sub pkg_htmlizebugs {
      my %param = validate_with(params => \@_,
-                              spec   => {bugs => {type => ARRAYREF,
+                              spec   => {bugs => {type => OBJECT,
                                                  },
                                          names => {type => ARRAYREF,
                                                   },
@@ -316,23 +283,15 @@ sub pkg_htmlizebugs {
                                                      },
                                         }
                              );
-     my @bugs = @{$param{bugs}};
-
-     my @status = ();
+     my $bugs = $param{bugs};
      my %count;
      my $header = '';
      my $footer = "<h2 class=\"outstanding\">Summary</h2>\n";
 
-     if (@bugs == 0) {
+     if ($bugs->count == 0) {
          return "<HR><H2>No reports found!</H2></HR>\n";
      }
 
-     if ( $param{bug_rev} ) {
-         @bugs = sort {$b<=>$a} @bugs;
-     }
-     else {
-         @bugs = sort {$a<=>$b} @bugs;
-     }
      my %seenmerged;
 
      my %common = (
@@ -363,52 +322,50 @@ sub pkg_htmlizebugs {
          push @{$exclude{$key}}, split /\s*,\s*/, $value;
      }
 
-     my $binary_to_source_cache = {};
-     my $statuses =
-        get_bug_statuses(bug => \@bugs,
-                         hash_slice(%param,
-                          qw(dist version schema bugusertags),
-                         ),
-                         (exists $param{arch}?(arch => $param{arch}):(arch => $config{default_architectures})),
-                         binary_to_source_cache => $binary_to_source_cache,
+     my $sorter = sub {$_[0]->id <=> $_[1]->id};
+     if ($param{bug_rev}) {
+        $sorter = sub {$_[1]->id <=> $_[0]->id}
+     }
+     elsif ($param{bug_order} eq 'age') {
+        $sorter = sub {$_[0]->modified->epoch <=> $_[1]->modified->epoch};
+     }
+     elsif ($param{bug_order} eq 'agerev') {
+        $sorter = sub {$_[1]->modified->epoch <=> $_[0]->modified->epoch};
+     }
+     my @status;
+     for my $bug ($bugs->sort($sorter)) {
+        next if
+            $bug->filter(repeat_merged => $param{repeatmerged},
+                         seen_merged => \%seenmerged,
+                         (keys %include ? (include => \%include):()),
+                         (keys %exclude ? (exclude => \%exclude):()),
                         );
-     for my $bug (sort {$a <=> $b} keys %{$statuses}) {
-        next unless %{$statuses->{$bug}};
-        next if bug_filter(bug => $bug,
-                           status => $statuses->{$bug},
-                           repeat_merged => $param{repeatmerged},
-                           seen_merged => \%seenmerged,
-                           (keys %include ? (include => \%include):()),
-                           (keys %exclude ? (exclude => \%exclude):()),
-                          );
 
         my $html = "<li>";     #<a href=\"%s\">#%d: %s</a>\n<br>",
-        $html .= short_bug_status_html(status  => $statuses->{$bug},
-                                       options => $param{options},
+        $html .= short_bug_status_html(bug => $bug,
                                       ) . "\n";
-        push @status, [ $bug, $statuses->{$bug}, $html ];
-     }
-     if ($param{bug_order} eq 'age') {
-         # MWHAHAHAHA
-         @status = sort {$a->[1]{log_modified} <=> $b->[1]{log_modified}} @status;
-     }
-     elsif ($param{bug_order} eq 'agerev') {
-         @status = sort {$b->[1]{log_modified} <=> $a->[1]{log_modified}} @status;
+        push @status, [ $bug, $html ];
      }
+     # parse bug order indexes into subroutines
+     my @order_subs =
+        map {
+            my $a = $_;
+            [map {parse_order_statement_to_subroutine($_)} @{$a}];
+        } @{$param{prior}};
      for my $entry (@status) {
          my $key = "";
-         for my $i (0..$#{$param{prior}}) {
-              my $v = get_bug_order_index($param{prior}[$i], $entry->[1]);
+         for my $i (0..$#order_subs) {
+              my $v = get_bug_order_index($order_subs[$i], $entry->[0]);
               $count{"g_${i}_${v}"}++;
               $key .= "_$v";
          }
-         $section{$key} .= $entry->[2];
+         $section{$key} .= $entry->[1];
          $count{"_$key"}++;
      }
 
      my $result = "";
      if ($param{ordering} eq "raw") {
-         $result .= "<UL class=\"bugs\">\n" . join("", map( { $_->[ 2 ] } @status ) ) . "</UL>\n";
+         $result .= "<UL class=\"bugs\">\n" . join("", map( { $_->[ 1 ] } @status ) ) . "</UL>\n";
      }
      else {
          $header .= "<div class=\"msgreceived\">\n<ul>\n";
@@ -474,6 +431,61 @@ sub pkg_htmlizebugs {
      return $result;
 }
 
+sub parse_order_statement_to_subroutine {
+    my ($statement) = @_;
+    if (not defined $statement or not length $statement) {
+       return sub {return 1};
+    }
+    croak "invalid statement '$statement'" unless
+       $statement =~ /^(?:(package|tag|pending|severity) # field
+                          = # equals
+                          ([^=|\&,\+]+(?:,[^=|\&,+])*) #value
+                          (\+|,|$) # joiner or end
+                      )+ # one or more of these statements
+                     /x;
+    my @sub_bits;
+    while ($statement =~ /(?<joiner>^|,|\+) # joiner
+                         (?<field>package|tag|pending|severity) # field
+                          = # equals
+                          (?<value>[^=|\&,\+]+(?:,[^=|\&,\+])*) #value
+                        /xg) {
+       my $field = $+{field};
+       my $value = $+{value};
+       my $joiner = $+{joiner} // '';
+       my @vals = apply {quotemeta($_)} split /,/,$value;
+       if (length $joiner) {
+           if ($joiner eq '+') {
+               push @sub_bits, ' and ';
+           }
+           else {
+               push @sub_bits, ' or ';
+           }
+       }
+       my @vals_bits;
+       for my $val (@vals) {
+           if ($field =~ /package|severity/o) {
+               push @vals_bits, '$_[0]->status->'.$field.
+                   ' eq q('.$val.')';
+           } elsif ($field eq 'tag') {
+               push @vals_bits, '$_[0]->tags->is_set('.
+                   'q('.$val.'))';
+           } elsif ($field eq 'pending') {
+               push @vals_bits, '$_[0]->'.$field.
+                   ' eq q('.$val.')';
+           }
+       }
+       push @sub_bits ,' ('.join(' or ',@vals_bits).') ';
+    }
+    # return a subroutine reference which determines whether an order statement
+    # matches this bug
+    my $sub = 'sub { return ('.join ("\n",@sub_bits).');};';
+    my $subref = eval $sub;
+    if ($@) {
+       croak "Unable to generate subroutine: $@; $sub";
+    }
+    return $subref;
+}
+
 sub parse_order_statement_into_boolean {
     my ($statement,$status,$tags) = @_;
 
@@ -510,19 +522,13 @@ sub parse_order_statement_into_boolean {
 }
 
 sub get_bug_order_index {
-     my $order = shift;
-     my $status = shift;
-     my $pos = 0;
-     my $tags = {map { $_, 1 } split / /, $status->{"tags"}
-                }
-         if defined $status->{"tags"};
-     for my $el (@${order}) {
-         if (not length $el or
-             parse_order_statement_into_boolean($el,$status,$tags)
-            ) {
-             return $pos;
-         }
-         $pos++;
+    my ($order,$bug) = @_;
+    my $pos = 0;
+    for my $el (@{$order}) {
+       if ($el->($bug)) {
+           return $pos;
+        }
+        $pos++;
      }
      return $pos;
 }
diff --git a/Debbugs/Collection.pm b/Debbugs/Collection.pm
new file mode 100644 (file)
index 0000000..6e3d49d
--- /dev/null
@@ -0,0 +1,390 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Collection;
+
+=head1 NAME
+
+Debbugs::Collection -- Collection base class which can generate lots of objects
+
+=head1 SYNOPSIS
+
+This base class is designed for holding collections of objects which can be
+uniquely identified by a key and added/generated by that same key.
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::autoclean;
+use List::AllUtils qw(pairmap);
+use Carp qw(croak);
+
+extends 'Debbugs::OOBase';
+
+=head1 METHODS
+
+=head2 Debbugs::Collection->new(%params|$params)
+
+Creates a new Debbugs::Collection object.
+
+Parameters:
+
+=over
+
+=item universe
+
+To avoid unnecessarily constructing new members, collections have a universe to
+which existing members can be obtained from. By default the universe is this
+collection. Generally, you should create exactly one universe for each
+collection type.
+
+=item schema
+
+Optional Debbugs::Schema object
+
+
+=back
+
+=head2 $collection->members()
+
+Returns list of members of this collection
+
+=head2 $collection->members_ref()
+
+Returns an ARRAYREF of members of this collection
+
+=head2 $collection->keys_of_members()
+
+Returns a list of the keys of all members of this collection
+
+=head2 $collection->member_key($member)
+
+Given a member, returns the key of that member
+
+=head2 $collection->exists($member_key)
+
+Returns true if a member with $member_key exists in the collection
+
+=head2 $collection->clone()
+
+Returns a clone of this collection with the same universe as this collection
+
+=head2 $collection->limit(@member_keys)
+
+Returns a new collection limited to the list of member keys passed. Will add new
+members to the universe if they do not currently exist.
+
+=head2 $collection->add($member)
+
+Add a member to this collection
+
+=head2 $collection->add_by_key($member_key)
+
+Add a member to this collection by key
+
+=head2 $collection->combine($collection2) or $collection + $collection2
+
+Combines the members of both collections together and returns the new collection
+
+=head2 $collection->get($member_key)
+
+Get member(s) by key, returning undef for keys which do not exist in the
+collection
+
+=head2 $collection->get_or_add_by_key($member_key)
+
+Get or add a member by the member key.
+
+=head2 $collection->count()
+
+Return the number of members in this collection
+
+=head2 $collection->grep({$_ eq 5})
+
+Return the members in this collection which satisfy the condition, setting $_
+locally to each member object
+
+=head2 $collection->join(', ')
+
+Returns the keys of the members of this collection joined
+
+=head2 $collection->apply({$_*2})
+
+Return the list of applying BLOCK to each member; each member can return 0 or
+more results
+
+=head2 $collection->map({$_*2})
+
+Returns the list of applying BLOCK to each member; each member should return
+exactly one result
+
+=head2 $collection->sort({$a <=> $b})
+
+Return the list of members sorted by BLOCK
+
+=cut
+
+has 'members' => (is => 'bare',
+                 isa => 'ArrayRef',
+                 traits => ['Array'],
+                 default => sub {[]},
+                  writer => '_set_members',
+                  predicate => '_has_members',
+                 handles => {_add => 'push',
+                             members => 'elements',
+                             count => 'count',
+                             _get_member => 'get',
+                              grep => 'grep',
+                              map => 'map',
+                              sort => 'sort',
+                            },
+                );
+
+sub apply {
+    my $self = shift;
+    my $block = shift;
+    my @r;
+    for ($self->members) {
+        push @r,$block->();
+    }
+    return @r;
+}
+
+sub members_ref {
+    my $self = shift;
+    return [$self->members];
+}
+
+has 'member_hash' => (traits => ['Hash'],
+                     is => 'bare',
+                      # really a HashRef[Int], but type checking is too slow
+                     isa => 'HashRef',
+                     lazy => 1,
+                     reader => '_member_hash',
+                     builder => '_build_member_hash',
+                      clearer => '_clear_member_hash',
+                      predicate => '_has_member_hash',
+                      writer => '_set_member_hash',
+                     handles => {# _add_member_hash => 'set',
+                                 _member_key_exists => 'exists',
+                                 _get_member_hash => 'get',
+                                },
+                    );
+
+# because _add_member_hash needs to be fast, we are overriding the default set
+# method which is very safe but slow, because it makes copies.
+sub _add_member_hash {
+    my ($self,@kv) = @_;
+    pairmap {
+        defined($a)
+            or $self->meta->
+            throw_error("Hash keys passed to _add_member_hash must be defined" );
+        ($b eq int($b)) or
+            $self->meta->
+            throw_error("Values passed to _add_member_hash must be integer");
+    } @kv;
+    my @return;
+    while (my ($key, $value) = splice @kv, 0, 2 ) {
+        push @return,
+            $self->{member_hash}{$key} = $value
+    }
+    wantarray ? return @return: return $return[0];
+}
+
+=head2 $collection->universe
+
+
+=cut
+
+has 'universe' => (is => 'ro',
+                   isa => 'Debbugs::Collection',
+                   required => 1,
+                   builder => '_build_universe',
+                   writer => '_set_universe',
+                   predicate => 'has_universe',
+                  );
+
+sub _build_universe {
+    # By default, the universe is myself
+    return $_[0];
+}
+
+sub clone {
+    my $self = shift;
+    my $new = bless { %{$self} }, ref $self;
+    if ($self->_has_members) {
+        $new->_set_members([$self->members]);
+    }
+    if ($self->_has_member_hash) {
+        $new->_set_member_hash({%{$self->_member_hash}})
+    }
+    return $new;
+}
+
+sub _shallow_clone {
+    my $self = shift;
+    return bless { %{$self} }, ref $self;
+}
+
+sub limit {
+    my $self = shift;
+    my $limit = $self->_shallow_clone();
+    # Set the universe to whatever my universe is (potentially myself)
+    # $limit->_set_universe($self->universe);
+    $limit->_set_members([]);
+    $limit->_clear_member_hash();
+    $limit->add($self->universe->get_or_add_by_key(@_)) if @_;
+    return $limit;
+}
+
+sub get_or_add_by_key {
+    my $self = shift;
+    return () unless @_;
+    my @return;
+    my @exists;
+    my @need_to_add;
+    for my $i (0..$#_) {
+        # we assume that if it's already a blessed reference, that it's the
+        # right object to return
+        if (ref $_[$i]) {
+            croak "Passed a reference instead of a key to get_or_add_by_key";
+        }
+        elsif ($self->_member_key_exists($_[$i])) {
+            push @exists,$i;
+        } else {
+            push @need_to_add,$i;
+        }
+    }
+    # create and add by key
+    if (@need_to_add) {
+        @return[@need_to_add] =
+            $self->add_by_key(@_[@need_to_add]);
+    }
+    if (@exists) {
+        @return[@exists] =
+            $self->get(@_[@exists]);
+    }
+    # if we've only been asked to get or create one thing, then it's expected
+    # that we are returning only one thing
+    if (@_ == 1) {
+        return $return[0];
+    }
+    return @return;
+}
+
+has 'constructor_args' => (is => 'rw',
+                          isa => 'ArrayRef',
+                          lazy => 1,
+                           builder => '_build_constructor_args',
+                         );
+
+sub _build_constructor_args {
+    return [];
+}
+
+sub add_by_key {
+    my $self = shift;
+    # we'll assume that add does the right thing. around this in subclasses
+    return $self->add(@_);
+}
+
+sub add {
+    my $self = shift;
+    my @members_added;
+    for my $member (@_) {
+        if (not defined $member) {
+            confess("Undefined member to add");
+        }
+        push @members_added,$member;
+       if ($self->exists($member)) {
+           next;
+       }
+       $self->_add($member);
+       $self->_add_member_hash($self->member_key($member),
+                               $self->count()-1,
+                              );
+    }
+    return @members_added;
+}
+
+use overload '+' => "combine",
+    '""' => "CARP_TRACE";
+
+sub combine {
+    my $self = shift;
+    my $return = $self->clone;
+    $return->add($_->members) for @_;
+    return $return;
+}
+
+sub get {
+    my $self = shift;
+    my @res = map {$self->_get_member($_)}
+        $self->_get_member_hash(@_);
+    wantarray?@res:$res[0];
+}
+
+
+sub member_key {
+    return $_[1];
+}
+
+sub keys_of_members {
+    my $self = shift;
+    return $self->map(sub {$self->member_key($_)});
+}
+
+sub exists {
+    my $self = shift;
+    return $self->_member_key_exists($self->member_key($_[0]));
+}
+
+sub join {
+    my $self = shift;
+    my $joiner = shift;
+    return CORE::join($joiner,$self->keys_of_members);
+}
+
+sub _build_member_hash {
+    my $self = shift;
+    my $hash = {};
+    my $i = 0;
+    for my $member ($self->members) {
+       $hash->{$self->member_key($member)} =
+           $i++;
+    }
+    return $hash;
+}
+
+sub CARP_TRACE {
+    my $self = shift;
+    my @members = $self->members;
+    if (@members > 5) {
+        @members = map {$self->member_key($_)}
+            @members[0..4];
+        push @members,'...';
+    } else {
+        @members = map {$self->member_key($_)} @members;
+    }
+    return __PACKAGE__.'={n_members='.$self->count().
+        ',members=('.CORE::join(',',@members).')}';
+}
+
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Collection/Bug.pm b/Debbugs/Collection/Bug.pm
new file mode 100644 (file)
index 0000000..3f40b0c
--- /dev/null
@@ -0,0 +1,216 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Collection::Bug;
+
+=head1 NAME
+
+Debbugs::Collection::Bug -- Bug generation factory
+
+=head1 SYNOPSIS
+
+This collection extends L<Debbugs::Collection> and contains members of
+L<Debbugs::Bug>. Useful for any field which contains one or more bug or tracking
+lists of packages
+
+=head1 DESCRIPTION
+
+
+
+=head1 METHODS
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::autoclean;
+use Debbugs::Common qw(make_list hash_slice);
+use Debbugs::OOTypes;
+use Debbugs::Status qw(get_bug_statuses);
+use Debbugs::Collection::Package;
+use Debbugs::Collection::Correspondent;
+
+use Debbugs::Bug;
+
+extends 'Debbugs::Collection';
+
+=head2 my $bugs = Debbugs::Collection::Bug->new(%params|$param)
+
+Parameters in addition to those defined by L<Debbugs::Collection>
+
+=over
+
+=item package_collection
+
+Optional L<Debbugs::Collection::Package> which is used to look up packages
+
+
+=item correspondent_collection
+
+Optional L<Debbugs::Collection::Correspondent> which is used to look up correspondents
+
+
+=item users
+
+Optional arrayref of L<Debbugs::User> which set usertags for bugs in this collection
+
+=back
+
+=head2 $bugs->package_collection()
+
+Returns the package collection that this bug collection is using
+
+=head2 $bugs->correspondent_collection()
+
+Returns the correspondent collection that this bug collection is using
+
+=head2 $bugs->users()
+
+Returns the arrayref of users that this bug collection is using
+
+=head2 $bugs->add_user($user)
+
+Add a user to the set of users that this bug collection is using
+
+=head2 $bugs->load_related_packages_and_versions()
+
+Preload all of the related packages and versions for the bugs in this bug
+collection. You should call this if you plan on calculating whether the bugs in
+this collection are present/absent.
+
+=cut
+
+has '+members' => (isa => 'ArrayRef[Bug]');
+has 'package_collection' =>
+    (is => 'ro',
+     isa => 'Debbugs::Collection::Package',
+     builder => '_build_package_collection',
+     lazy => 1,
+    );
+
+sub _build_package_collection {
+    my $self = shift;
+    return Debbugs::Collection::Package->new($self->has_schema?(schema => $self->schema):());
+}
+
+has 'correspondent_collection' =>
+    (is => 'ro',
+     isa => 'Debbugs::Collection::Correspondent',
+     builder => '_build_correspondent_collection',
+     lazy => 1,
+    );
+
+sub _build_correspondent_collection {
+    my $self = shift;
+    return Debbugs::Collection::Correspondent->new($self->has_schema?(schema => $self->schema):());
+}
+
+has 'users' =>
+    (is => 'ro',
+     isa => 'ArrayRef[Debbugs::User]',
+     traits => ['Array'],
+     default => sub {[]},
+     handles => {'add_user' => 'push'},
+    );
+
+sub BUILD {
+    my $self = shift;
+    my $args = shift;
+    if (exists $args->{bugs}) {
+        $self->add(
+            $self->_member_constructor(bugs => $args->{bugs}
+                                      ));
+    }
+}
+
+sub _member_constructor {
+    # handle being called $self->_member_constructor;
+    my $self = shift;
+    my %args = @_;
+    my @return;
+    my $schema;
+    $schema = $self->schema if $self->has_schema;
+
+    if (defined $schema) {
+        my $statuses = get_bug_statuses(bug => [make_list($args{bugs})],
+                                        schema => $schema,
+                                       );
+        # preload as many of the packages as we need
+        my %packages;
+        while (my ($bug, $status) = each %{$statuses}) {
+            if (defined $status->{package}) {
+                $packages{$_} = 1 for split /,/, $status->{package};
+            }
+            if (defined $status->{source}) {
+                $packages{$_} = 1 for split /,/, $status->{source};
+            }
+        }
+        $self->package_collection->universe->add_by_key(keys %packages);
+        while (my ($bug, $status) = each %{$statuses}) {
+            push @return,
+                Debbugs::Bug->new(bug => $bug,
+                                  status =>
+                                  Debbugs::Bug::Status->new(status => $status,
+                                                            bug => $bug,
+                                                            status_source => 'db',
+                                                           ),
+                                  schema => $schema,
+                                  package_collection =>
+                                  $self->package_collection->universe,
+                                  bug_collection =>
+                                  $self->universe,
+                                  correspondent_collection =>
+                                  $self->correspondent_collection->universe,
+                                  @{$args{constructor_args}//[]},
+                                 );
+        }
+    } else {
+        for my $bug (make_list($args{bugs})) {
+            push @return,
+                Debbugs::Bug->new(bug => $bug,
+                                  package_collection =>
+                                  $self->package_collection->universe,
+                                  bug_collection =>
+                                  $self->universe,
+                                  correspondent_collection =>
+                                  $self->correspondent_collection->universe,
+                                  @{$args{constructor_args}//[]},
+                                 );
+        }
+    }
+    return @return;
+}
+
+around add_by_key => sub {
+    my $orig = shift;
+    my $self = shift;
+    my @members =
+        $self->_member_constructor(bugs => [@_],
+                                  );
+    return $self->$orig(@members);
+};
+
+sub member_key {
+    return $_[1]->bug;
+}
+
+sub load_related_packages_and_versions {
+    my $self = shift;
+    my @related_packages_and_versions =
+        $self->apply(sub {$_->related_packages_and_versions});
+    $self->package_collection->
+        add_packages_and_versions(@related_packages_and_versions);
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Collection/Correspondent.pm b/Debbugs/Collection/Correspondent.pm
new file mode 100644 (file)
index 0000000..43ac8c0
--- /dev/null
@@ -0,0 +1,83 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Collection::Correspondent;
+
+=head1 NAME
+
+Debbugs::Collection::Correspondent -- Bug generation factory
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::autoclean;
+use Debbugs::Common qw(make_list hash_slice);
+use Debbugs::OOTypes;
+use Debbugs::Status qw(get_bug_statuses);
+
+use Debbugs::Correspondent;
+
+extends 'Debbugs::Collection';
+
+has '+members' => (isa => 'ArrayRef[Debbugs::Correspondent]');
+
+sub BUILD {
+    my $self = shift;
+    my $args = shift;
+    if (exists $args->{correspondent}) {
+        $self->
+            add($self->_member_constructor(correspondent =>
+                                           $args->{correspondent}));
+    }
+}
+
+
+sub _member_constructor {
+    # handle being called $self->_member_constructor;
+    my $self = shift;
+    my %args = @_;
+    my @return;
+    for my $corr (make_list($args{correspondent})) {
+       push @return,
+           Debbugs::Correspondent->new(name => $corr,
+                                       $self->schema_argument,
+                                      );
+    }
+    return @return;
+}
+
+around add_by_key => sub {
+    my $orig = shift;
+    my $self = shift;
+    my @members =
+        $self->_member_constructor(correspondent => [@_],
+                                  $self->schema_argument,
+                                 );
+    return $self->$orig(@members);
+};
+
+sub member_key {
+    return $_[1]->name;
+}
+
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Collection/Package.pm b/Debbugs/Collection/Package.pm
new file mode 100644 (file)
index 0000000..055cbae
--- /dev/null
@@ -0,0 +1,293 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Collection::Package;
+
+=head1 NAME
+
+Debbugs::Collection::Package -- Package generation factory
+
+=head1 SYNOPSIS
+
+This collection extends L<Debbugs::Collection> and contains members of
+L<Debbugs::Package>. Useful for any field which contains one or more package or
+tracking lists of packages
+
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use v5.10; # for state
+use namespace::autoclean;
+
+use Carp;
+use Debbugs::Common qw(make_list hash_slice);
+use Debbugs::Config qw(:config);
+use Debbugs::OOTypes;
+use Debbugs::Package;
+
+use List::AllUtils qw(part);
+
+use Debbugs::Version::Binary;
+use Debbugs::Collection::Version;
+use Debbugs::Collection::Correspondent;
+use Debbugs::VersionTree;
+
+extends 'Debbugs::Collection';
+
+=head1 Object Creation
+
+=head2 my $packages = Debbugs::Collection::Package->new(%params|$param)
+
+Parameters in addition to those defined by L<Debbugs::Collection>
+
+=over
+
+=item correspondent_collection
+
+Optional L<Debbugs::Collection::Correspondent> which is used to look up correspondents
+
+
+=item versiontree
+
+Optional L<Debbugs::VersionTree> which contains known package source versions
+
+=back
+
+=head1 Methods
+
+=head2 correspondent_collection
+
+     $packages->correspondent_collection
+
+Returns the L<Debbugs::Collection::Correspondent> for this package collection
+
+=head2 versiontree
+
+Returns the L<Debbugs::VersionTree> for this package collection
+
+=cut
+
+has '+members' => (isa => 'ArrayRef[Debbugs::Package]');
+
+sub BUILD {
+    my $self = shift;
+    my $args = shift;
+    if (exists $args->{packages}) {
+        $self->
+            add($self->_member_constructor(packages =>
+                                           $args->{packages}));
+    }
+}
+
+around add_by_key => sub {
+    my $orig = shift;
+    my $self = shift;
+    my @members =
+        $self->_member_constructor(packages => [@_]);
+    return $self->$orig(@members);
+};
+
+sub _member_constructor {
+    # handle being called $self->_member_constructor;
+    my $self = shift;
+    my %args = @_;
+    my $schema;
+    if ($self->has_schema) {
+        $schema = $self->schema;
+    }
+    my @return;
+    if (defined $schema) {
+        if (not ref($args{packages}) or @{$args{packages}} == 1 and
+            $self->universe->count() > 0
+           ) {
+            carp("Likely inefficiency; member_constructor called with one argument");
+        }
+        my $packages =
+            Debbugs::Package::_get_valid_version_info_from_db(packages => $args{packages},
+                                                              schema => $schema,
+                                                             );
+        for my $package (keys %{$packages}) {
+            push @return,
+                Debbugs::Package->new(%{$packages->{$package}},
+                                      schema => $schema,
+                                      package_collection => $self->universe,
+                                      correspondent_collection =>
+                                      $self->correspondent_collection->universe,
+                                     );
+        }
+    } else {
+        for my $package (make_list($args{packages})) {
+            push @return,
+                Debbugs::Package->new(name => $package,
+                                      package_collection => $self->universe,
+                                      correspondent_collection =>
+                                      $self->correspondent_collection->universe,
+                                     );
+        }
+    }
+    return @return;
+}
+
+sub add_packages_and_versions {
+    my $self = shift;
+    $self->add($self->_member_constructor(packages => \@_));
+}
+
+
+sub member_key {
+    return $_[1]->qualified_name;
+}
+
+has 'correspondent_collection' =>
+    (is => 'ro',
+     isa => 'Debbugs::Collection::Correspondent',
+     default => sub {Debbugs::Collection::Correspondent->new()},
+    );
+
+has 'versiontree' =>
+    (is => 'ro',
+     isa => 'Debbugs::VersionTree',
+     lazy => 1,
+     builder => '_build_versiontree',
+    );
+
+sub _build_versiontree {
+    my $self = shift;
+    return Debbugs::VersionTree->new($self->has_schema?(schema => $self->schema):());
+}
+
+=head2 get_source_versions_distributions
+
+     $packages->get_source_versions_distributions('unstable')
+
+Given a list of distributions or suites, returns a
+L<Debbugs::Collection::Version> of all of the versions in this package
+collection which are known to match.
+
+Effectively, this calls L<Debbugs::Package/get_source_version_distribution> for
+each package in the collection and merges the results and returns them
+
+=cut
+
+sub get_source_versions_distributions {
+    my $self = shift;
+    my @return;
+    push @return,
+        $self->map(sub {$_->get_source_version_distribution(@_)});
+    if (@return > 1) {
+        return $return[0]->combine($return[1..$#return]);
+    }
+    return @return;
+}
+
+
+=head2 get_source_versions
+
+    $packages->get_source_versions('1.2.3-1','foo/1.2.3-5')
+
+Given a list of binary versions or src/versions, returns a
+L<Debbugs::Collection::Version> of all of the versions in this package
+collection which are known to match.
+
+If you give a binary version ('1.2.3-1'), you must have already loaded source
+packages into this package collection for it to find an appropriate match.
+
+If no package is known to match, an version which is invalid will be returned
+
+For fully qualified versions this loads the appropriate source package into the
+universe of this collection and calls L<Debbugs::Package/get_source_version>.
+For unqualified versions, calls L<Debbugs::Package/get_source_version>; if no
+valid versions are returned, creates an invalid version.
+
+=cut
+
+sub get_source_versions {
+    my $self = shift;
+    my @return;
+    for my $ver (@_) {
+        my $sv;
+        if ($ver =~ m{(?<src>.+?)/(?<ver>.+)$}) {
+            my $sp = $self->universe->
+                get_or_add_by_key('src:'.$+{src});
+            push @return,
+                $sp->get_source_version($+{ver});
+           next;
+        } else {
+            my $found_valid = 0;
+            for my $p ($self->members) {
+                local $_;
+                my @vs =
+                    grep {$_->is_valid}
+                    $p->get_source_version($ver);
+                if (@vs) {
+                    $found_valid = 1;
+                    push @return,@vs;
+                    next;
+                }
+            }
+            if (not $found_valid) {
+                push @return,
+                    Debbugs::Version::Binary->new(version => $ver,
+                                                  package_collection => $self->universe,
+                                                  valid => 0,
+                                                  $self->schema_argument,
+                                                 );
+            }
+        }
+    }
+    return
+        Debbugs::Collection::Version->new(members => \@return,
+                                          $self->schema_argument,
+                                          package_collection => $self->universe,
+                                         );
+}
+
+=head2 source_names
+
+     $packages->source_names()
+
+Returns a unique list of source names from all members of this collection by
+calling L<Debbugs::Package/source_names> on each member.
+
+=cut
+
+sub source_names {
+    my $self = shift;
+    local $_;
+    return uniq map {$_->source_names} $self->members;
+}
+
+=head2 sources
+
+     $packages->sources()
+
+Returns a L<Debbugs::Collection::Package> limited to source packages
+corresponding to all packages in this collection
+
+=cut
+
+sub sources {
+    my $self = shift;
+    return $self->universe->limit($self->source_names);
+}
+
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Collection/Version.pm b/Debbugs/Collection/Version.pm
new file mode 100644 (file)
index 0000000..f461afe
--- /dev/null
@@ -0,0 +1,148 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Collection::Version;
+
+=head1 NAME
+
+Debbugs::Collection::Version -- Version generation factory
+
+=head1 SYNOPSIS
+
+This collection extends L<Debbugs::Collection> and contains members of
+L<Debbugs::Version>. Useful for any field which contains package versions.
+
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use v5.10; # for state
+use namespace::autoclean;
+use Debbugs::Common qw(make_list hash_slice);
+use Debbugs::Config qw(:config);
+use Debbugs::OOTypes;
+use Debbugs::Version;
+
+use List::AllUtils qw(part);
+
+extends 'Debbugs::Collection';
+
+=head2 my $bugs = Debbugs::Collection::version->new(%params|$param)
+
+Parameters in addition to those defined by L<Debbugs::Collection>
+
+=over
+
+=item package_collection
+
+Optional L<Debbugs::Collection::Package> which is used to look up packages
+
+=item versions
+
+Optional arrayref of C<package/version/arch> string triples
+
+=back
+
+=cut
+
+has '+members' => (isa => 'ArrayRef[Debbugs::Version]');
+
+has 'package_collection' =>
+    (is => 'ro',
+     isa => 'Debbugs::Collection::Package',
+     builder => '_build_package_collection',
+     lazy => 1,
+    );
+
+sub _build_package_collection {
+    my $self = shift;
+    return Debbugs::Collection::Package->new($self->schema_argument);
+}
+
+sub member_key {
+    my ($self,$v) = @_;
+    confess("v not defined") unless defined $v;
+    return $v->package.'/'.$v->version.'/'.$v->arch;
+}
+
+
+around add_by_key => sub {
+    my $orig = shift;
+    my $self = shift;
+    my @members =
+        $self->_member_constructor(versions => [@_]);
+    return $self->$orig(@members);
+};
+
+sub _member_constructor {
+    my $self = shift;
+    my %args = @_;
+    my @return;
+    for my $pkg_ver_arch (make_list($args{versions})) {
+        my ($pkg,$ver,$arch) = $pkg_ver_arch =~ m{^([^/]+)/([^/]+)/?([^/]*)$} or
+            confess("Invalid version key: $pkg_ver_arch");
+        if ($pkg =~ s/^src://) {
+            $arch = 'source';
+        }
+        if (not length $arch) {
+            $arch = 'any';
+        }
+        if ($arch eq 'source') {
+            push @return,
+                Debbugs::Version::Source->
+                    new($self->schema_argument,
+                        package => $pkg,
+                        version => $ver,
+                       );
+        } else {
+            push @return,
+                Debbugs::Version::Binary->
+                    new($self->schema_argument,
+                        package => $pkg,
+                        version => $ver,
+                        arch => [$arch],
+                       );
+        }
+    }
+    return @return;
+}
+
+=head2 $versions->universe
+
+Unlike most collections, Debbugs::Collection::Version do not have a universe.
+
+=cut
+
+sub universe {
+    return $_[0];
+}
+
+=head2 $versions->source
+
+Returns a (potentially duplicated) list of source packages which are part of
+this version collection
+
+=cut
+
+sub source {
+    my $self = shift;
+    return $self->map(sub{$_->source});
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
index d85261a5b35599d194c24088427a70a893813a19..0d0abae37ff6c489f80cd4b7a413f11ba279e92c 100644 (file)
@@ -1151,7 +1151,7 @@ sub read_config{
         return;
      }
      # first, figure out what type of file we're reading in.
-     my $fh = new IO::File $conf_file,'r'
+     my $fh = IO::File->new($conf_file,'r')
          or die "Unable to open configuration file $conf_file for reading: $!";
      # A new version configuration file must have a comment as its first line
      my $first_line = <$fh>;
diff --git a/Debbugs/Correspondent.pm b/Debbugs/Correspondent.pm
new file mode 100644 (file)
index 0000000..0044347
--- /dev/null
@@ -0,0 +1,99 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Correspondent;
+
+=head1 NAME
+
+Debbugs::Correspondent -- OO interface to bugs
+
+=head1 SYNOPSIS
+
+   use Debbugs::Correspondent;
+   Debbugs::Correspondent->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::clean;
+use v5.10; # for state
+
+use Mail::Address;
+use Debbugs::OOTypes;
+use Debbugs::Config qw(:config);
+
+use Carp;
+
+extends 'Debbugs::OOBase';
+
+has name => (is => 'ro', isa => 'Str',
+            required => 1,
+            writer => '_set_name',
+           );
+
+has _mail_address => (is => 'bare', isa => 'Mail::Address',
+                     lazy => 1,
+                     handles => [qw(address phrase comment)],
+                     builder => '_build_mail_address',
+                    );
+
+sub _build_mail_address {
+    my @addr = Mail::Address->parse($_[0]->name) or
+       confess("unable to parse mail address");
+    if (@addr > 1) {
+       warn("Multiple addresses to Debbugs::Correspondent");
+    }
+    return $addr[0];
+}
+
+sub email {
+    my $email = $_[0]->address;
+    warn "No email" unless defined $email;
+    return $email;
+}
+
+sub url {
+    my $self = shift;
+    return $config{web_domain}.'/correspondent:'.$self->email;
+}
+
+sub maintainer_url {
+    my $self = shift;
+    return $config{web_domain}.'/maintainer:'.$self->email;
+}
+
+sub owner_url {
+    my $self = shift;
+    return $config{web_domain}.'/owner:'.$self->email;
+}
+
+sub submitter_url {
+    my $self = shift;
+    return $config{web_domain}.'/submitter:'.$self->email;
+}
+
+sub CARP_TRACE {
+    my $self = shift;
+    return 'Debbugs::Correspondent={name='.$self->name.'}';
+}
+
+
+__PACKAGE__->meta->make_immutable;
+
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
index eb4002ff3b28880e0f97db5787c58e4915d3838b..0e0c554773b7d211f24e82e82befe1a07f38adf0 100644 (file)
@@ -156,8 +156,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-04-18 16:55:56
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Uoaf3KzTvRYIf33q7tBnZw
+# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-07-05 20:56:47
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VH/9QrwjZx0r7FLaEWGYMg
 
 
 # You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/Debbugs/DB/Result/BinPkgSrcPkg.pm b/Debbugs/DB/Result/BinPkgSrcPkg.pm
new file mode 100644 (file)
index 0000000..4836b05
--- /dev/null
@@ -0,0 +1,198 @@
+use utf8;
+package Debbugs::DB::Result::BinPkgSrcPkg;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+Debbugs::DB::Result::BinPkgSrcPkg - Binary package <-> source package mapping sumpmary table
+
+=cut
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 COMPONENTS LOADED
+
+=over 4
+
+=item * L<DBIx::Class::InflateColumn::DateTime>
+
+=item * L<DBIx::Class::TimeStamp>
+
+=back
+
+=cut
+
+__PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp");
+
+=head1 TABLE: C<bin_pkg_src_pkg>
+
+=cut
+
+__PACKAGE__->table("bin_pkg_src_pkg");
+
+=head1 ACCESSORS
+
+=head2 bin_pkg
+
+  data_type: 'integer'
+  is_foreign_key: 1
+  is_nullable: 0
+
+Binary package id (matches bin_pkg)
+
+=head2 src_pkg
+
+  data_type: 'integer'
+  is_foreign_key: 1
+  is_nullable: 0
+
+Source package id (matches src_pkg)
+
+=cut
+
+__PACKAGE__->add_columns(
+  "bin_pkg",
+  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+  "src_pkg",
+  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+);
+
+=head1 UNIQUE CONSTRAINTS
+
+=head2 C<bin_pkg_src_pkg_bin_pkg_src_pkg>
+
+=over 4
+
+=item * L</bin_pkg>
+
+=item * L</src_pkg>
+
+=back
+
+=cut
+
+__PACKAGE__->add_unique_constraint("bin_pkg_src_pkg_bin_pkg_src_pkg", ["bin_pkg", "src_pkg"]);
+
+=head2 C<bin_pkg_src_pkg_src_pkg_bin_pkg>
+
+=over 4
+
+=item * L</src_pkg>
+
+=item * L</bin_pkg>
+
+=back
+
+=cut
+
+__PACKAGE__->add_unique_constraint("bin_pkg_src_pkg_src_pkg_bin_pkg", ["src_pkg", "bin_pkg"]);
+
+=head1 RELATIONS
+
+=head2 bin_pkg
+
+Type: belongs_to
+
+Related object: L<Debbugs::DB::Result::BinPkg>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "bin_pkg",
+  "Debbugs::DB::Result::BinPkg",
+  { id => "bin_pkg" },
+  { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
+);
+
+=head2 src_pkg
+
+Type: belongs_to
+
+Related object: L<Debbugs::DB::Result::SrcPkg>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "src_pkg",
+  "Debbugs::DB::Result::SrcPkg",
+  { id => "src_pkg" },
+  { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-04-18 16:55:56
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:O/v5RtjJF9SgxXEy76U/xw
+
+sub sqlt_deploy_hook {
+    my ($self, $sqlt_table) = @_;
+    $sqlt_table->schema->
+       add_procedure(name => 'bin_ver_to_src_pkg',
+                     sql => <<'EOF',
+CREATE OR REPLACE FUNCTION bin_ver_to_src_pkg(bin_ver INT) RETURNS INT
+  AS $src_pkg_from_bin_ver$
+  DECLARE
+  src_pkg int;
+  BEGIN
+       SELECT sv.src_pkg INTO STRICT src_pkg
+              FROM bin_ver bv JOIN src_ver sv ON bv.src_ver=sv.id
+              WHERE bv.id=bin_ver;
+       RETURN src_pkg;
+  END
+  $src_pkg_from_bin_ver$ LANGUAGE plpgsql;
+EOF
+                     );
+    $sqlt_table->schema->
+       add_procedure(name => 'src_ver_to_src_pkg',
+                     sql => <<'EOF',
+CREATE OR REPLACE FUNCTION src_ver_to_src_pkg(src_ver INT) RETURNS INT
+  AS $src_ver_to_src_pkg$
+  DECLARE
+  src_pkg int;
+  BEGIN
+       SELECT sv.src_pkg INTO STRICT src_pkg
+              FROM src_ver sv WHERE sv.id=src_ver;
+       RETURN src_pkg;
+  END
+  $src_ver_to_src_pkg$ LANGUAGE plpgsql;
+EOF
+                     );
+    $sqlt_table->schema->
+       add_procedure(name => 'update_bin_pkg_src_pkg_bin_ver',
+                     sql => <<'EOF',
+CREATE OR REPLACE FUNCTION update_bin_pkg_src_pkg_bin_ver () RETURNS TRIGGER
+  AS $update_bin_pkg_src_pkg_bin_ver$
+  DECLARE
+  src_ver_rows integer;
+  BEGIN
+  IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE' )  THEN
+     -- if there is still a bin_ver with this src_pkg, then do nothing
+     PERFORM * FROM bin_ver bv JOIN src_ver sv ON bv.src_ver = sv.id
+           WHERE sv.id = OLD.src_ver LIMIT 2;
+     GET DIAGNOSTICS src_ver_rows = ROW_COUNT;
+     IF (src_ver_rows <= 1) THEN
+        DELETE FROM bin_pkg_src_pkg
+              WHERE bin_pkg=OLD.bin_pkg AND
+                    src_pkg=src_ver_to_src_pkg(OLD.src_ver);
+     END IF;
+  END IF;
+  IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
+     BEGIN
+     INSERT INTO bin_pkg_src_pkg (bin_pkg,src_pkg)
+       VALUES (NEW.bin_pkg,src_ver_to_src_pkg(NEW.src_ver))
+       ON CONFLICT (bin_pkg,src_pkg) DO NOTHING;
+     END;
+  END IF;
+  RETURN NULL;
+  END
+  $update_bin_pkg_src_pkg_bin_ver$ LANGUAGE plpgsql;
+EOF
+                    );
+
+}
+
+1;
index f4a757c2e09681025922313c9d5226fe9feac9df..2f2a29d54b9bfc84ca39ae410cb036faaf17eb50 100644 (file)
@@ -125,8 +125,8 @@ __PACKAGE__->belongs_to(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-04-20 10:29:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PJ2U+jVEO2uKfwgCYtho1A
+# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-07-05 21:00:23
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:STaqCap5Dk4AORK6ghGnPg
 
 
 sub sqlt_deploy_hook {
index 0f33b4e47a04418872a99418d7e4302f0f76dc44..ee3efc8bad565b44fae8ef7b454b938b20b623ab 100644 (file)
@@ -35,7 +35,7 @@ __PACKAGE__->table_class("DBIx::Class::ResultSource::View");
 =cut
 
 __PACKAGE__->table("bug_status");
-__PACKAGE__->result_source_instance->view_definition(" SELECT b.id,\n    b.id AS bug_num,\n    string_agg(t.tag, ','::text) AS tags,\n    b.subject,\n    ( SELECT s.severity\n           FROM severity s\n          WHERE (s.id = b.severity)) AS severity,\n    ( SELECT string_agg(package.package, ','::text ORDER BY package.package) AS string_agg\n           FROM ( SELECT bp.pkg AS package\n                   FROM (bug_binpackage bbp\n                     JOIN bin_pkg bp ON ((bbp.bin_pkg = bp.id)))\n                  WHERE (bbp.bug = b.id)\n                UNION\n                 SELECT concat('src:', sp.pkg) AS package\n                   FROM (bug_srcpackage bsp\n                     JOIN src_pkg sp ON ((bsp.src_pkg = sp.id)))\n                  WHERE (bsp.bug = b.id)) package) AS package,\n    ( SELECT string_agg(affects.affects, ','::text ORDER BY affects.affects) AS string_agg\n           FROM ( SELECT bp.pkg AS affects\n                   FROM (bug_affects_binpackage bbp\n                     JOIN bin_pkg bp ON ((bbp.bin_pkg = bp.id)))\n                  WHERE (bbp.bug = b.id)\n                UNION\n                 SELECT concat('src:', sp.pkg) AS affects\n                   FROM (bug_affects_srcpackage bsp\n                     JOIN src_pkg sp ON ((bsp.src_pkg = sp.id)))\n                  WHERE (bsp.bug = b.id)) affects) AS affects,\n    b.submitter_full AS originator,\n    date_part('epoch'::text, b.log_modified) AS log_modified,\n    date_part('epoch'::text, b.creation) AS date,\n    date_part('epoch'::text, b.last_modified) AS last_modified,\n    b.done_full AS done,\n    string_agg((bb.blocks)::text, ' '::text ORDER BY bb.blocks) AS blocks,\n    string_agg((bbb.bug)::text, ' '::text ORDER BY bbb.bug) AS blockedby,\n    ( SELECT string_agg((bug.bug)::text, ' '::text ORDER BY bug.bug) AS string_agg\n           FROM ( SELECT bm.merged AS bug\n                   FROM bug_merged bm\n                  WHERE (bm.bug = b.id)\n                UNION\n                 SELECT bm.bug\n                   FROM bug_merged bm\n                  WHERE (bm.merged = b.id)) bug) AS mergedwith,\n    ( SELECT string_agg(bv.ver_string, ' '::text) AS string_agg\n           FROM bug_ver bv\n          WHERE ((bv.bug = b.id) AND (bv.found IS TRUE))) AS found_versions,\n    ( SELECT string_agg(bv.ver_string, ' '::text) AS string_agg\n           FROM bug_ver bv\n          WHERE ((bv.bug = b.id) AND (bv.found IS FALSE))) AS fixed_versions\n   FROM ((((bug b\n     LEFT JOIN bug_tag bt ON ((bt.bug = b.id)))\n     LEFT JOIN tag t ON ((bt.tag = t.id)))\n     LEFT JOIN bug_blocks bb ON ((bb.bug = b.id)))\n     LEFT JOIN bug_blocks bbb ON ((bbb.blocks = b.id)))\n  GROUP BY b.id");
+__PACKAGE__->result_source_instance->view_definition(" SELECT b.id,\n    b.id AS bug_num,\n    string_agg(t.tag, ','::text) AS tags,\n    b.subject,\n    ( SELECT s.severity\n           FROM severity s\n          WHERE (s.id = b.severity)) AS severity,\n    ( SELECT string_agg(package.package, ','::text ORDER BY package.package) AS string_agg\n           FROM ( SELECT bp.pkg AS package\n                   FROM (bug_binpackage bbp\n                     JOIN bin_pkg bp ON ((bbp.bin_pkg = bp.id)))\n                  WHERE (bbp.bug = b.id)\n                UNION\n                 SELECT concat('src:', sp.pkg) AS package\n                   FROM (bug_srcpackage bsp\n                     JOIN src_pkg sp ON ((bsp.src_pkg = sp.id)))\n                  WHERE (bsp.bug = b.id)) package) AS package,\n    ( SELECT string_agg(affects.affects, ','::text ORDER BY affects.affects) AS string_agg\n           FROM ( SELECT bp.pkg AS affects\n                   FROM (bug_affects_binpackage bbp\n                     JOIN bin_pkg bp ON ((bbp.bin_pkg = bp.id)))\n                  WHERE (bbp.bug = b.id)\n                UNION\n                 SELECT concat('src:', sp.pkg) AS affects\n                   FROM (bug_affects_srcpackage bsp\n                     JOIN src_pkg sp ON ((bsp.src_pkg = sp.id)))\n                  WHERE (bsp.bug = b.id)) affects) AS affects,\n    ( SELECT m.msgid\n           FROM (message m\n             LEFT JOIN bug_message bm ON ((bm.message = m.id)))\n          WHERE (bm.bug = b.id)\n          ORDER BY m.sent_date\n         LIMIT 1) AS message_id,\n    b.submitter_full AS originator,\n    date_part('epoch'::text, b.log_modified) AS log_modified,\n    date_part('epoch'::text, b.creation) AS date,\n    date_part('epoch'::text, b.last_modified) AS last_modified,\n    b.done_full AS done,\n    string_agg((bb.blocks)::text, ' '::text ORDER BY bb.blocks) AS blocks,\n    string_agg((bbb.bug)::text, ' '::text ORDER BY bbb.bug) AS blockedby,\n    ( SELECT string_agg((bug.bug)::text, ' '::text ORDER BY bug.bug) AS string_agg\n           FROM ( SELECT bm.merged AS bug\n                   FROM bug_merged bm\n                  WHERE (bm.bug = b.id)\n                UNION\n                 SELECT bm.bug\n                   FROM bug_merged bm\n                  WHERE (bm.merged = b.id)) bug) AS mergedwith,\n    ( SELECT string_agg(bv.ver_string, ' '::text) AS string_agg\n           FROM bug_ver bv\n          WHERE ((bv.bug = b.id) AND (bv.found IS TRUE))) AS found_versions,\n    ( SELECT string_agg(bv.ver_string, ' '::text) AS string_agg\n           FROM bug_ver bv\n          WHERE ((bv.bug = b.id) AND (bv.found IS FALSE))) AS fixed_versions\n   FROM ((((bug b\n     LEFT JOIN bug_tag bt ON ((bt.bug = b.id)))\n     LEFT JOIN tag t ON ((bt.tag = t.id)))\n     LEFT JOIN bug_blocks bb ON ((bb.bug = b.id)))\n     LEFT JOIN bug_blocks bbb ON ((bbb.blocks = b.id)))\n  GROUP BY b.id");
 
 =head1 ACCESSORS
 
@@ -74,6 +74,11 @@ __PACKAGE__->result_source_instance->view_definition(" SELECT b.id,\n    b.id AS
   data_type: 'text'
   is_nullable: 1
 
+=head2 message_id
+
+  data_type: 'text'
+  is_nullable: 1
+
 =head2 originator
 
   data_type: 'text'
@@ -141,6 +146,8 @@ __PACKAGE__->add_columns(
   { data_type => "text", is_nullable => 1 },
   "affects",
   { data_type => "text", is_nullable => 1 },
+  "message_id",
+  { data_type => "text", is_nullable => 1 },
   "originator",
   { data_type => "text", is_nullable => 1 },
   "log_modified",
@@ -164,8 +171,8 @@ __PACKAGE__->add_columns(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-04-20 10:29:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OPfPxXCqSaz2OeYsZqilAg
+# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-07-05 20:55:00
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xkAEshcLIPrG/6hoRbSsrw
 
 
 # You can replace this text with custom code or comments, and it will be preserved on regeneration
index 76b710d9d9c574533224fb225de90e108e6d644a..26e56a49006aa5219e80d8bf8a14e8497fb07611 100644 (file)
@@ -274,8 +274,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-04-18 16:55:56
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fMMA9wnkPIdT5eiUIkLxqg
+# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-07-05 20:56:47
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:G2uhLQ7coWRoAHFiDkF5cQ
 
 
 sub sqlt_deploy_hook {
index d824d9a996d1b3fdce8fae13954fb28d7614e55c..710a844164624c37a4e712c5f7830adc86784f71 100644 (file)
 
 package Debbugs::Log;
 
-
-use warnings;
-use strict;
+use Mouse;
+use strictures 2;
+use namespace::clean;
+use v5.10; # for state
 
 use vars qw($VERSION $DEBUG @EXPORT @EXPORT_OK %EXPORT_TAGS);
 use Exporter qw(import);
@@ -41,7 +42,6 @@ use Debbugs::Common qw(getbuglocation getbugcomponent make_list);
 use Params::Validate qw(:types validate_with);
 use Encode qw(encode encode_utf8 is_utf8);
 use IO::InnerFile;
-use feature 'state';
 
 =head1 NAME
 
@@ -165,71 +165,100 @@ One of the above options must be passed.
 
 =cut
 
-sub new
-{
-    my $this = shift;
-    my %param;
-    if (@_ == 1) {
-        ($param{logfh}) = @_;
-        $param{inner_file} = 0;
-    }
-    else {
-        state $spec =
-            {bug_num => {type => SCALAR,
-                         optional => 1,
-                        },
-             logfh   => {type => HANDLE,
-                         optional => 1,
-                        },
-             log_name => {type => SCALAR,
-                          optional => 1,
-                         },
-             inner_file => {type => BOOLEAN,
-                            default => 0,
-                           },
-            };
-        %param = validate_with(params => \@_,
-                               spec   => $spec,
-                              );
-    }
-    if (grep({exists $param{$_} and defined $param{$_}}
-             qw(bug_num logfh log_name)) ne 1) {
+sub BUILD {
+    my ($self,$args) = @_;
+    if (not ($self->_has_bug_num or
+             $self->_has_logfh or
+             $self->_has_log_name)) {
         croak "Exactly one of bug_num, logfh, or log_name ".
             "must be passed and must be defined";
     }
+}
 
-    my $class = ref($this) || $this;
-    my $self = {};
-    bless $self, $class;
-
-    if (exists $param{logfh}) {
-        $self->{logfh} = $param{logfh}
+has 'bug_num' =>
+    (is => 'ro',
+     isa => 'Int',
+     predicate => '_has_bug_num',
+    );
+
+has 'logfh' =>
+    (is => 'ro',
+     lazy => 1,
+     builder => '_build_logfh',
+     predicate => '_has_logfh',
+    );
+
+sub _build_logfh {
+    my $self = shift;
+    my $bug_log =
+        $self->log_name;
+    my $log_fh;
+    if ($bug_log =~ m/\.gz$/) {
+        my $oldpath = $ENV{'PATH'};
+        $ENV{'PATH'} = '/bin:/usr/bin';
+        open($log_fh,'-|','gzip','-dc',$bug_log) or
+            die "Unable to open $bug_log for reading: $!";
+        $ENV{'PATH'} = $oldpath;
     } else {
-       my $bug_log;
-       if (exists $param{bug_num}) {
-           my $location = getbuglocation($param{bug_num},'log');
-           $bug_log = getbugcomponent($param{bug_num},'log',$location);
-       } else {
-           $bug_log = $param{log_name};
-       }
-       if ($bug_log =~ m/\.gz$/) {
-           my $oldpath = $ENV{'PATH'};
-           $ENV{'PATH'} = '/bin:/usr/bin';
-           open($self->{logfh},'-|','gzip','-dc',$bug_log) or
-               die "Unable to open $bug_log for reading: $!";
-            $ENV{'PATH'} = $oldpath;
-       } else {
-            open($self->{logfh},'<',$bug_log) or
-                die "Unable to open $bug_log for reading: $!";
-       }
+        open($log_fh,'<',$bug_log) or
+            die "Unable to open $bug_log for reading: $!";
     }
+    return $log_fh;
+}
 
-    $self->{state} = 'kill-init';
-    $self->{linenum} = 0;
-    $self->{inner_file} = $param{inner_file};
-    return $self;
+has 'log_name' =>
+    (is => 'ro',
+     isa => 'Str',
+     lazy => 1,
+     builder => '_build_log_name',
+     predicate => '_has_log_name',
+    );
+
+sub _build_log_name {
+    my $self = shift;
+    my $location = getbuglocation($self->bug_num,'log');
+    return getbugcomponent($self->bug_num,'log',$location);
 }
 
+has 'inner_file' =>
+    (is => 'ro',
+     isa => 'Bool',
+     default => 0,
+    );
+
+has 'state' =>
+    (is => 'ro',
+     isa => 'Str',
+     default => 'kill-init',
+     writer => '_state',
+    );
+
+sub state_transition {
+    my $self = shift;
+    my $new_state = shift;
+    my $old_state = $self->state;
+    local $_ = "$old_state $new_state";
+    unless (/^(go|go-nox|html) kill-end$/ or
+            /^(kill-init|kill-end) (incoming-recv|autocheck|recips|html)$/ or
+            /^autocheck autowait$/ or
+            /^autowait go-nox$/ or
+            /^recips kill-body$/ or
+            /^(kill-body|incoming-recv) go$/) {
+        confess "transition from $old_state to $new_state at $self->linenum disallowed";
+    }
+    $self->_state($new_state);
+}
+
+sub increment_linenum {
+    my $self = shift;
+    $self->_linenum($self->_linenum+1);
+}
+has '_linenum' =>
+    (is => 'rw',
+     isa => 'Int',
+     default => 0,
+    );
+
 =item read_record
 
 Reads and returns a single record from a log reader object. At end of file,
@@ -241,7 +270,7 @@ in an eval().
 sub read_record
 {
     my $this = shift;
-    my $logfh = $this->{logfh};
+    my $logfh = $this->logfh;
 
     # This comes from bugreport.cgi, but is much simpler since it doesn't
     # worry about the details of output.
@@ -251,74 +280,66 @@ sub read_record
     while (defined (my $line = <$logfh>)) {
         $record->{start} = $logfh->tell() if not defined $record->{start};
        chomp $line;
-       ++$this->{linenum};
+       $this->increment_linenum;
        if (length($line) == 1 and exists $states{ord($line)}) {
            # state transitions
-           my $newstate = $states{ord($line)};
-
-           # disallowed transitions
-           $_ = "$this->{state} $newstate";
-           unless (/^(go|go-nox|html) kill-end$/ or
-                   /^(kill-init|kill-end) (incoming-recv|autocheck|recips|html)$/ or
-                   /^kill-body go$/) {
-               die "transition from $this->{state} to $newstate at $this->{linenum} disallowed";
-           }
-
-           $this->{state} = $newstate;
-           if ($this->{state} =~ /^(autocheck|recips|html|incoming-recv)$/) {
-            $record->{type} = $this->{state};
-            $record->{start} = $logfh->tell;
-            $record->{stop} = $logfh->tell;
-            $record->{inner_file} = $this->{inner_file};
-           } elsif ($this->{state} eq 'kill-end') {
-            if ($this->{inner_file}) {
-                $record->{fh} = IO::InnerFile->new($logfh,$record->{start},$record->{stop} - $record->{start})
-            }
+           $this->state_transition($states{ord($line)});
+           if ($this->state =~ /^(autocheck|recips|html|incoming-recv)$/) {
+                $record->{type} = $this->state;
+                $record->{start} = $logfh->tell;
+                $record->{stop} = $logfh->tell;
+                $record->{inner_file} = $this->inner_file;
+           } elsif ($this->state eq 'kill-end') {
+                if ($this->inner_file) {
+                    $record->{fh} =
+                        IO::InnerFile->new($logfh,$record->{start},
+                                           $record->{stop} - $record->{start})
+                        }
                return $record;
            }
 
            next;
        }
-    $record->{stop} = $logfh->tell;
+        $record->{stop} = $logfh->tell;
        $_ = $line;
-       if ($this->{state} eq 'incoming-recv') {
+       if ($this->state eq 'incoming-recv') {
            my $pl = $_;
            unless (/^Received: \(at \S+\) by \S+;/) {
                die "bad line '$pl' in state incoming-recv";
            }
-           $this->{state} = 'go';
-           $record->{text} .= "$_\n" unless $this->{inner_file};
-       } elsif ($this->{state} eq 'html') {
-           $record->{text} .= "$_\n"  unless $this->{inner_file};
-       } elsif ($this->{state} eq 'go') {
+           $this->state_transition('go');
+           $record->{text} .= "$_\n" unless $this->inner_file;
+       } elsif ($this->state eq 'html') {
+           $record->{text} .= "$_\n"  unless $this->inner_file;
+       } elsif ($this->state eq 'go') {
            s/^\030//;
-           $record->{text} .= "$_\n"  unless $this->{inner_file};
-       } elsif ($this->{state} eq 'go-nox') {
-           $record->{text} .= "$_\n"  unless $this->{inner_file};
-       } elsif ($this->{state} eq 'recips') {
+           $record->{text} .= "$_\n"  unless $this->inner_file;
+       } elsif ($this->state eq 'go-nox') {
+           $record->{text} .= "$_\n"  unless $this->inner_file;
+       } elsif ($this->state eq 'recips') {
            if (/^-t$/) {
                undef $record->{recips};
            } else {
                # preserve trailing null fields, e.g. #2298
                $record->{recips} = [split /\04/, $_, -1];
            }
-           $this->{state} = 'kill-body';
-        $record->{start} = $logfh->tell+2;
-        $record->{stop} = $logfh->tell+2;
-        $record->{inner_file} = $this->{inner_file};
-       } elsif ($this->{state} eq 'autocheck') {
-           $record->{text} .= "$_\n" unless $this->{inner_file};
+           $this->state_transition('kill-body');
+            $record->{start} = $logfh->tell+2;
+            $record->{stop} = $logfh->tell+2;
+            $record->{inner_file} = $this->inner_file;
+       } elsif ($this->state eq 'autocheck') {
+           $record->{text} .= "$_\n" unless $this->inner_file;
            next if !/^X-Debian-Bugs(-\w+)?: This is an autoforward from (\S+)/;
-           $this->{state} = 'autowait';
-       } elsif ($this->{state} eq 'autowait') {
-           $record->{text} .= "$_\n" unless $this->{inner_file};
+           $this->state_transition('autowait');
+       } elsif ($this->state eq 'autowait') {
+           $record->{text} .= "$_\n" unless $this->inner_file;
            next if !/^$/;
-           $this->{state} = 'go-nox';
+           $this->state_transition('go-nox');
        } else {
-           die "state $this->{state} at line $this->{linenum} ('$_')";
+           die "state $this->state at line $this->linenum ('$_')";
        }
     }
-    die "state $this->{state} at end" unless $this->{state} eq 'kill-end';
+    die "state $this->state at end" unless $this->state eq 'kill-end';
 
     if (keys %$record) {
        return $record;
@@ -327,6 +348,42 @@ sub read_record
     }
 }
 
+=item rewind
+
+Rewinds the Debbugs::Log to the beginning
+
+=cut
+
+sub rewind {
+    my $self = shift;
+    if ($self->_has_log_name) {
+        $self->_clear_log_fh;
+    } else {
+        $self->log_fh->seek(0);
+    }
+    $self->_state('kill-init');
+    $self->_linenum(0);
+}
+
+=item read_all_records
+
+Reads all of the Debbugs::Records
+
+=cut
+
+sub read_all_records {
+    my $self = shift;
+    if ($self->_linenum != 0) {
+        $self->rewind;
+    }
+    my @records;
+    while (defined(my $record = $self->read_record())) {
+       push @records, $record;
+    }
+    return @records;
+}
+
+
 =item read_log_records
 
 Takes a .log filehandle as input, and returns an array of all records in
diff --git a/Debbugs/OOBase.pm b/Debbugs/OOBase.pm
new file mode 100644 (file)
index 0000000..6600e02
--- /dev/null
@@ -0,0 +1,48 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::OOBase;
+
+=head1 NAME
+
+Debbugs::OOBase -- OO Base class for Debbugs
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use namespace::autoclean;
+
+has schema => (is => 'ro', isa => 'Object',
+              required => 0,
+              predicate => 'has_schema',
+             );
+
+sub schema_argument {
+    my $self = shift;
+    if ($self->has_schema) {
+        return (schema => $self->schema);
+    } else {
+       return ();
+    }
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/OOTypes.pm b/Debbugs/OOTypes.pm
new file mode 100644 (file)
index 0000000..37473d0
--- /dev/null
@@ -0,0 +1,58 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::OOTypes;
+
+=head1 NAME
+
+Debbugs::OOTypes -- OO Types for Debbugs
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse::Util::TypeConstraints;
+use strictures 2;
+use namespace::autoclean;
+
+# Bug Subtype
+subtype 'Bug' =>
+    as 'Debbugs::Bug';
+
+coerce 'Bug' =>
+    from 'Int' =>
+    via {Debbugs::Bug->new($_)};
+
+# Package Subtype
+subtype 'Package' =>
+    as 'Debbugs::Package';
+
+coerce 'Package' =>
+    from 'Str' =>
+    via {Debbugs::Package->new(package => $_)};
+
+
+# Version Subtype
+subtype 'Version' =>
+    as 'Debbugs::Version';
+
+coerce 'Version' =>
+    from 'Str' =>
+    via {Debbugs::Version->new(string=>$_)};
+
+no Mouse::Util::TypeConstraints;
+1;
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Package.pm b/Debbugs/Package.pm
new file mode 100644 (file)
index 0000000..70f0e35
--- /dev/null
@@ -0,0 +1,729 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 3, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Package;
+
+=head1 NAME
+
+Debbugs::Package -- OO interface to packages
+
+=head1 SYNOPSIS
+
+   use Debbugs::Package;
+   Debbugs::Package->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use strictures 2;
+use v5.10; # for state
+use namespace::autoclean;
+
+use List::AllUtils  qw(uniq pairmap);
+use Debbugs::Config qw(:config);
+use Debbugs::Version::Source;
+use Debbugs::Version::Binary;
+
+extends 'Debbugs::OOBase';
+
+=head2 name
+
+Name of the Package
+
+=head2 qualified_name
+
+name if binary, name prefixed with C<src:> if source
+
+=cut
+
+has name => (is => 'ro', isa => 'Str',
+            required => 1,
+           );
+
+sub qualified_name {
+    my $self = shift;
+    return
+       # src: if source, nothing if binary
+       ($self->_type eq 'source' ? 'src:':'') .
+       $self->name;
+}
+
+
+=head2 type
+
+Type of the package; either C<binary> or C<source>
+
+=cut
+
+has type => (is => 'bare', isa => 'Str',
+            lazy => 1,
+            builder => '_build_type',
+            clearer => '_clear_type',
+            reader => '_type',
+            writer => '_set_type',
+           );
+
+sub _build_type {
+    my $self = shift;
+    if ($self->name !~ /^src:/) {
+       return 'binary';
+    }
+}
+
+=head2 url
+
+url to the package
+
+=cut
+
+sub url {
+    my $self = shift;
+    return $config{web_domain}.'/'.$self->qualified_name;
+}
+
+around BUILDARGS => sub {
+    my $orig = shift;
+    my $class = shift;
+    my %args;
+    if (@_==1 and ref($_[0]) eq 'HASH') {
+       %args = %{$_[0]};
+    } else {
+        %args = @_;
+    }
+    $args{name} //= '(unknown)';
+    if ($args{name} =~ /src:(.+)/) {
+       $args{name} = $1;
+       $args{type} = 'source';
+    } else {
+       $args{type} = 'binary' unless
+           defined $args{type};
+    }
+    return $class->$orig(%args);
+};
+
+=head2 is_source
+
+true if the package is a source package
+
+=head2 is_binary
+
+true if the package is a binary package
+
+=cut
+
+sub is_source {
+    return $_[0]->_type eq 'source'
+}
+
+sub is_binary {
+    return $_[0]->_type eq 'binary'
+}
+
+=head2 valid -- true if the package has any valid versions
+
+=cut
+
+has valid => (is => 'ro', isa => 'Bool',
+             lazy => 1,
+             builder => '_build_valid',
+             writer => '_set_valid',
+            );
+
+sub _build_valid {
+    my $self = shift;
+    if ($self->valid_version_info_count> 0) {
+       return 1;
+    }
+    return 0;
+}
+
+# this contains source name, source version, binary name, binary version, arch,
+# and dist which have been selected from the database. It is used to build
+# versions and anything else which are known as required.
+has 'valid_version_info' =>
+    (is => 'bare', isa => 'ArrayRef',
+     traits => ['Array'],
+     lazy => 1,
+     builder => '_build_valid_version_info',
+     predicate => '_has_valid_version_info',
+     clearer => '_clear_valid_version_info',
+     handles => {'_get_valid_version_info' => 'get',
+                'valid_version_info_grep' => 'grep',
+                '_valid_version_info' => 'elements',
+                 'valid_version_info_count' => 'count',
+               },
+    );
+
+sub _build_valid_version_info {
+    my $self = shift;
+    my $pkgs = $self->_get_valid_version_info_from_db;
+    for my $invalid_version (@{$pkgs->{$self->qualified_name}->{invalid_versions}}) {
+        $self->_mark_invalid_version($invalid_version,1);
+    }
+    return $pkgs->{$self->qualified_name}->{valid_version_info} // [];
+}
+
+state $common_dists = [@{$config{distributions}}];
+sub _get_valid_version_info_from_db {
+    my $self;
+    if ((@_ % 2) == 1 and
+       blessed($_[0])) {
+       $self = shift;
+    }
+    my %args = @_;
+    my @packages;
+    my $s; # schema
+    if (defined $self) {
+       if ($self->has_schema) {
+           $s = $self->schema;
+       } else {
+           $s = $args{schema};
+       }
+       @packages = $self->qualified_name;
+    } else {
+       $s = $args{schema};
+       @packages = @{$args{packages}};
+    }
+    if (not defined $s) {
+       confess("get_info_from_db not implemented without schema");
+    }
+    my %src_packages;
+    my %src_ver_packages;
+    my %bin_packages;
+    my %bin_ver_packages;
+    # split packages into src/ver, bin/ver, src, and bin so we can select them
+    # from the database
+    local $_;
+    for my $pkg (@packages) {
+        if (ref($pkg)) {
+            if ($pkg->[0] =~ /^src:(.+)$/) {
+                for my $ver (@{$pkg}[1..$#{$pkg}]) {
+                    $src_ver_packages{$1}{$ver} = 0;
+                }
+            } else {
+                for my $ver (@{$pkg}[1..$#{$pkg}]) {
+                    $bin_ver_packages{$pkg->[0]}{$ver} = 0;
+                }
+            }
+        } elsif ($pkg =~ /^src:(.+)$/) {
+            $src_packages{$1} = 0;
+        } else {
+            $bin_packages{$pkg} = 0;
+        }
+    }
+    # calculate searches for packages where we want specific versions. We
+    # calculate this here so add_result_to_package can stomp over
+    # %src_ver_packages and %bin_ver_packages
+    my @src_ver_search;
+    for my $sp (keys %src_ver_packages) {
+        push @src_ver_search,
+            (-and => {'src_pkg.pkg' => $sp,
+                      'me.ver' => [keys %{$src_ver_packages{$sp}}],
+                     },
+             );
+    }
+    my @src_packages = keys %src_packages;
+
+    my @bin_ver_search;
+    for my $sp (keys %bin_ver_packages) {
+        push @bin_ver_search,
+            (-and => {'bin_pkg.pkg' => $sp,
+                      'me.ver' => [keys %{$bin_ver_packages{$sp}}],
+                     },
+             );
+    }
+    my @bin_packages = keys %bin_packages;
+    my $packages = {};
+    sub _default_pkg_info {
+        return {name => $_[0],
+                type => $_[1]//'source',
+                valid => $_[2]//1,
+                valid_version_info => [],
+                invalid_versions => {},
+               };
+    }
+    sub add_result_to_package {
+       my ($pkgs,$rs,$svp,$bvp,$sp,$bp) = @_;
+       while (my $pkg = $rs->next) {
+           my $n = 'src:'.$pkg->{src_pkg};
+           if (not exists $pkgs->{$n}) {
+                $pkgs->{$n} =
+                    _default_pkg_info($pkg->{src_pkg});
+            }
+            push @{$pkgs->{$n}{valid_version_info}},
+               {%$pkg};
+           $n = $pkg->{bin_pkg};
+            if (not exists $pkgs->{$n}) {
+                $pkgs->{$n} =
+                    _default_pkg_info($pkg->{bin_pkg},'binary');
+            }
+            push @{$pkgs->{$n}{valid_version_info}},
+                  {%$pkg};
+            # this is a package with a valid src_ver
+            $svp->{$pkg->{src_pkg}}{$pkg->{src_ver}}++;
+            $sp->{$pkg->{src_pkg}}++;
+            # this is a package with a valid bin_ver
+            $bvp->{$pkg->{bin_pkg}}{$pkg->{bin_ver}}++;
+            $bp->{$pkg->{bin_pkg}}++;
+       }
+    }
+    if (@src_packages) {
+        my $src_rs = $s->resultset('SrcVer')->
+            search({-or => [-and => {'src_pkg.pkg' => [@src_packages],
+                                     -or => {'suite.codename' => $common_dists,
+                                             'suite.suite_name' => $common_dists,
+                                            },
+                                    },
+                            @src_ver_search,
+                           ],
+                   },
+                  {join => ['src_pkg',
+                           {
+                            'src_associations' => 'suite'},
+                           {
+                            'bin_vers' => ['bin_pkg','arch']},
+                            'maintainer',
+                           ],
+                   'select' => [qw(src_pkg.pkg),
+                                qw(suite.codename),
+                                qw(suite.suite_name),
+                                qw(src_associations.modified),
+                                qw(me.ver),
+                                q(CONCAT(src_pkg.pkg,'/',me.ver)),
+                                qw(bin_vers.ver bin_pkg.pkg arch.arch),
+                                qw(maintainer.name),
+                               ],
+                   'as' => [qw(src_pkg codename suite_name),
+                            qw(modified_time src_ver src_pkg_ver),
+                            qw(bin_ver bin_pkg arch maintainer),
+                           ],
+                   result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+                  },
+                  );
+        add_result_to_package($packages,$src_rs,
+                              \%src_ver_packages,
+                              \%bin_ver_packages,
+                              \%src_packages,
+                              \%bin_packages,
+                             );
+    }
+    if (@bin_packages) {
+        my $bin_assoc_rs =
+            $s->resultset('BinAssociation')->
+            search({-and => {'bin_pkg.pkg' => [@bin_packages],
+                             -or => {'suite.codename' => $common_dists,
+                                     'suite.suite_name' => $common_dists,
+                                    },
+                            }},
+                  {join => [{'bin' =>
+                             [{'src_ver' => ['src_pkg',
+                                             'maintainer',
+                                            ]},
+                              'bin_pkg',
+                              'arch']},
+                            'suite',
+                           ],
+                   'select' => [qw(src_pkg.pkg),
+                                qw(suite.codename),
+                                qw(suite.suite_name),
+                                qw(me.modified),
+                                qw(src_ver.ver),
+                                q(CONCAT(src_pkg.pkg,'/',src_ver.ver)),
+                                qw(bin.ver bin_pkg.pkg arch.arch),
+                                qw(maintainer.name),
+                               ],
+                   'as' => [qw(src_pkg codename suite_name),
+                            qw(modified_time src_ver src_pkg_ver),
+                            qw(bin_ver bin_pkg arch maintainer),
+                           ],
+                   result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+                  },
+                  );
+        add_result_to_package($packages,$bin_assoc_rs,
+                              \%src_ver_packages,
+                              \%bin_ver_packages,
+                              \%src_packages,
+                              \%bin_packages,
+                             );
+    }
+    if (@bin_ver_search) {
+        my $bin_rs = $s->resultset('BinVer')->
+            search({-or => [@bin_ver_search,
+                           ],
+                   },
+                  {join => ['bin_pkg',
+                           {
+                            'bin_associations' => 'suite'},
+                           {'src_ver' => ['src_pkg',
+                                          'maintainer',
+                                         ]},
+                            'arch',
+                           ],
+                   'select' => [qw(src_pkg.pkg),
+                                qw(suite.codename),
+                                qw(suite.suite_name),
+                                qw(bin_associations.modified),
+                                qw(src_ver.ver),
+                                q(CONCAT(src_pkg.pkg,'/',src_ver.ver)),
+                                qw(me.ver bin_pkg.pkg arch.arch),
+                                qw(maintainer.name),
+                               ],
+                   'as' => [qw(src_pkg codename suite_name),
+                            qw(modified_time src_ver src_pkg_ver),
+                            qw(bin_ver bin_pkg arch maintainer),
+                           ],
+                   result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+                  },
+                  );
+        add_result_to_package($packages,$bin_rs,
+                              \%src_ver_packages,
+                              \%bin_ver_packages,
+                              \%src_packages,
+                              \%bin_packages,
+                             );
+    }
+    for my $sp (keys %src_ver_packages) {
+        if (not exists $packages->{'src:'.$sp}) {
+            $packages->{'src:'.$sp} =
+                _default_pkg_info($sp,'source',0);
+        }
+        for my $sv (keys %{$src_ver_packages{$sp}}) {
+            next if $src_ver_packages{$sp}{$sv} > 0;
+            $packages->{'src:'.$sp}{invalid_versions}{$sv} = 1;
+        }
+    }
+    for my $bp (keys %bin_ver_packages) {
+        if (not exists $packages->{$bp}) {
+            $packages->{$bp} =
+                _default_pkg_info($bp,'binary',0);
+        }
+        for my $bv (keys %{$bin_ver_packages{$bp}}) {
+            next if $bin_ver_packages{$bp}{$bv} > 0;
+            $packages->{$bp}{invalid_versions}{$bv} = 1;
+        }
+    }
+    for my $sp (keys %src_packages) {
+        next if $src_packages{$sp} > 0;
+        $packages->{'src:'.$sp} =
+            _default_pkg_info($sp,'source',0);
+    }
+    for my $bp (keys %bin_packages) {
+        next if $bin_packages{$bp} > 0;
+        $packages->{$bp} =
+            _default_pkg_info($bp,'binary',0);
+    }
+    return $packages;
+}
+
+has 'source_version_to_info' =>
+    (is => 'bare', isa => 'HashRef',
+     traits => ['Hash'],
+     lazy => 1,
+     builder => '_build_source_version_to_info',
+     handles => {_get_source_version_to_info => 'get',
+               },
+    );
+
+sub _build_source_version_to_info {
+    my $self = shift;
+    my $info = {};
+    my $i = 0;
+    for my $v ($self->_valid_version_info) {
+       push @{$info->{$v->{src_ver}}}, $i;
+       $i++;
+    }
+    return $info;
+}
+
+has 'binary_version_to_info' =>
+    (is => 'bare', isa => 'HashRef',
+     traits => ['Hash'],
+     lazy => 1,
+     builder => '_build_binary_version_to_info',
+     handles => {_get_binary_version_to_info => 'get',
+               },
+    );
+
+sub _build_binary_version_to_info {
+    my $self = shift;
+    my $info = {};
+    my $i = 0;
+    for my $v ($self->_valid_version_info) {
+       push @{$info->{$v->{bin_ver}}}, $i;
+       $i++;
+    }
+    return $info;
+}
+
+has 'dist_to_info' =>
+    (is => 'bare', isa => 'HashRef',
+     traits => ['Hash'],
+     lazy => 1,
+     builder => '_build_dist_to_info',
+     handles => {_get_dist_to_info => 'get',
+               },
+    );
+sub _build_dist_to_info {
+    my $self = shift;
+    my $info = {};
+    my $i = 0;
+    for my $v ($self->_valid_version_info) {
+        next unless defined $v->{suite_name} and length($v->{suite_name});
+       push @{$info->{$v->{suite_name}}}, $i;
+       $i++;
+    }
+    return $info;
+}
+
+# this is a hashref of versions that we know are invalid
+has 'invalid_versions' =>
+    (is => 'bare',isa => 'HashRef[Bool]',
+     lazy => 1,
+     default => sub {{}},
+     clearer => '_clear_invalid_versions',
+     traits => ['Hash'],
+     handles => {_invalid_version => 'exists',
+                 _mark_invalid_version => 'set',
+                },
+    );
+
+has 'binaries' => (is => 'ro',
+                  isa => 'Debbugs::Collection::Package',
+                  lazy => 1,
+                  builder => '_build_binaries',
+                  predicate => '_has_binaries',
+                 );
+
+sub _build_binaries {
+    my $self = shift;
+    if ($self->is_binary) {
+       return $self->package_collection->limit($self->name);
+    }
+    # OK, walk through the valid_versions for this package
+    my @binaries =
+       uniq map {$_->{bin_pkg}} $self->_valid_version_info;
+    return $self->package_collection->limit(@binaries);
+}
+
+has 'sources' => (is => 'ro',
+                 isa => 'Debbugs::Collection::Package',
+                 lazy => 1,
+                 builder => '_build_sources',
+                 predicate => '_has_sources',
+                );
+
+sub _build_sources {
+    my $self = shift;
+    return $self->package_collection->limit($self->source_names);
+}
+
+sub source_names {
+    my $self = shift;
+
+    if ($self->is_source) {
+        return $self->name
+    }
+    return uniq map {'src:'.$_->{src_pkg}} $self->_valid_version_info;
+}
+
+=head2 maintainers 
+
+L<Debbugs::Collection::Correspondent> of the maintainer(s) of the current package
+
+=cut
+
+has maintainers => (is => 'ro',
+                    isa => 'Debbugs::Collection::Correspondent',
+                    lazy => 1,
+                    builder => '_build_maintainers',
+                    predicate => '_has_maintainers',
+                   );
+
+sub _build_maintainers {
+    my $self = shift;
+    my @maintainers;
+    for my $v ($self->_valid_version_info) {
+        next unless length($v->{suite_name}) and length($v->{maintainer});
+        push @maintainers,$v->{maintainer};
+    }
+    @maintainers =
+        uniq @maintainers;
+    return $self->correspondent_collection->limit(@maintainers);
+}
+
+has 'versions' => (is => 'bare',
+                  isa => 'HashRef[Debbugs::Version]',
+                   traits => ['Hash'],
+                  handles => {_exists_version => 'exists',
+                              _get_version => 'get',
+                               _set_version => 'set',
+                             },
+                   lazy => 1,
+                   builder => '_build_versions',
+                 );
+
+sub _build_versions {
+    my $self = shift;
+    return {};
+}
+
+sub _add_version {
+    my $self = shift;
+    my @set;
+    for my $v (@_) {
+        push @set,
+            $v->version,$v;
+    }
+    $self->_set_version(@set);
+}
+
+sub get_source_version_distribution {
+    my $self = shift;
+
+    my %src_pkg_vers = @_;
+    for my $dist (@_) {
+        my @ver_loc =
+            grep {defined $_}
+            $self->_get_dist_to_info($dist);
+        for my $v ($self->
+                   _get_valid_version_info(@ver_loc)) {
+            $src_pkg_vers{$v->{src_pkg_ver}} = 1;
+        }
+    }
+    return $self->package_collection->
+        get_source_versions(keys %src_pkg_vers)->members;
+}
+
+# returns the source version(s) corresponding to the version of *this* package; the
+# version passed may be binary or source, depending.
+sub get_source_version {
+    my $self = shift;
+    if ($self->is_source) {
+        return $self->get_version(@_);
+    }
+    my %src_pkg_vers;
+    for my $ver (@_) {
+        my %archs;
+        if (ref $ver) {
+            my @archs;
+            ($ver,@archs) = @{$ver};
+            @archs{@archs} = (1) x @archs;
+        }
+        my @ver_loc =
+            @{$self->_get_binary_version_to_info($ver)//[]};
+        next unless @ver_loc;
+        my @vers = map {$self->
+                            _get_valid_version_info($_)}
+            @ver_loc;
+        for my $v (@vers) {
+            if (keys %archs) {
+                next unless exists $archs{$v->{arch}};
+            }
+            $src_pkg_vers{$v->{src_pkg_ver}} = 1;
+        }
+    }
+    return $self->package_collection->
+        get_source_versions(keys %src_pkg_vers)->members;
+}
+
+sub get_version {
+    my $self = shift;
+    my @ret;
+    for my $v (@_) {
+       if ($self->_exists_version($v)) {
+           push @ret,$self->_get_version($v);
+       } else {
+           push @ret,
+               $self->_create_version($v);
+       }
+    }
+    return @ret;
+}
+
+sub _create_version {
+    my $self = shift;
+    my @versions;
+    if ($self->is_source) {
+       for my $v (@_) {
+           push @versions,
+               $v,
+               Debbugs::Version::Source->
+                   new(pkg => $self,
+                       version => $v,
+                       package_collection => $self->package_collection,
+                        $self->schema_argument,
+                      );
+       }
+    } else {
+       for my $v (@_) {
+           push @versions,
+               $v,
+               Debbugs::Version::Binary->
+                   new(pkg => $self,
+                       version => $v,
+                       package_collection => $self->package_collection,
+                        $self->schema_argument,
+                      );
+       }
+    }
+    $self->_set_version(@versions);
+}
+
+=head2 package_collection
+
+L<Debbugs::Collection::Package> to get additional packages required
+
+=cut
+
+# gets used to retrieve packages
+has 'package_collection' => (is => 'ro',
+                            isa => 'Debbugs::Collection::Package',
+                            builder => '_build_package_collection',
+                            lazy => 1,
+                           );
+
+sub _build_package_collection {
+    my $self = shift;
+    return Debbugs::Collection::Package->new($self->schema_argument)
+}
+
+=head2 correspondent_collection
+
+L<Debbugs::Collection::Correspondent> to get additional maintainers required
+
+=cut
+
+has 'correspondent_collection' => (is => 'ro',
+                                   isa => 'Debbugs::Collection::Correspondent',
+                                   builder => '_build_correspondent_collection',
+                                   lazy => 1,
+                                  );
+
+sub _build_correspondent_collection {
+    my $self = shift;
+    return Debbugs::Collection::Correspondent->new($self->schema_argument)
+}
+
+sub CARP_TRACE {
+    my $self = shift;
+    return 'Debbugs::Package={package='.$self->qualified_name.'}';
+}
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
index 2457e54c4e5d51633794b909c2f18f0e2f9da004..50a09657233ae06e9951d4e7003559b66b445df5 100644 (file)
@@ -154,6 +154,7 @@ sub new {
                "visible_cats" => [],
                "unknown_stanzas" => [],
                values => {},
+               bug_tags => {},
                email => $email,
               };
     bless $self, $class;
@@ -176,7 +177,12 @@ sub new {
             my %tag = @stanza;
             my $t = $tag{"Tag"};
             $ut->{$t} = [] unless defined $ut->{$t};
-            push @{$ut->{$t}}, split /\s*,\s*/, $tag{Bugs};
+           my @bugs = split /\s*,\s*/, $tag{Bugs};
+            push @{$ut->{$t}}, @bugs;
+           for my $bug (@bugs) {
+               push @{$self->{bug_tags}{$bug}},
+                   $t;
+           }
         } elsif ($stanza[0] eq "Category") {
             my @cat = ();
             my %stanza = @stanza;
@@ -235,6 +241,27 @@ sub new {
     return $self;
 }
 
+sub email {
+    my $self = shift;
+    return $self->{email};
+}
+
+sub tags {
+    my $self = shift;
+
+    return $self->{"tags"};
+}
+
+sub tags_on_bug {
+    my $self = shift;
+    return map {@{$self->{"bug_tags"}{$_}//[]}} @_;
+}
+
+sub has_bug_tags {
+    my $self = shift;
+    return keys %{$self->{bug_tags}} > 0;
+}
+
 sub write {
     my $self = shift;
 
diff --git a/Debbugs/Version.pm b/Debbugs/Version.pm
new file mode 100644 (file)
index 0000000..71dc008
--- /dev/null
@@ -0,0 +1,220 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Version;
+
+=head1 NAME
+
+Debbugs::Version -- OO interface to Version
+
+=head1 SYNOPSIS
+
+This package provides a convenient interface to refer to package versions and
+potentially make calculations based upon them
+
+   use Debbugs::Version;
+   my $v = Debbugs::Version->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use v5.10;
+use strictures 2;
+use namespace::autoclean;
+
+use Debbugs::Config qw(:config);
+use Debbugs::Collection::Package;
+use Debbugs::OOTypes;
+use Carp;
+
+extends 'Debbugs::OOBase';
+
+=head1 Object Creation
+
+=head2 my $version = Debbugs::Version::Source->new(%params|$param)
+
+or C<Debbugs::Version::Binary->new(%params|$param)> for a binary version
+
+=over
+
+=item schema
+
+L<Debbugs::DB> schema which can be used to look up versions
+
+=item package
+
+String representation of the package
+
+=item pkg
+
+L<Debbugs::Package> which refers to the package given.
+
+Only one of C<package> or C<pkg> should be given
+
+=item package_collection
+
+L<Debbugs::Collection::Package> which is used to generate a L<Debbugs::Package>
+object from the package name
+
+=back
+
+=cut
+
+around BUILDARGS => sub {
+    my $orig = shift;
+    my $class = shift;
+    if ($class eq __PACKAGE__) {
+        confess("You should not be instantiating Debbugs::Version. ".
+                "Use Debbugs::Version::Source or ::Binary");
+    }
+    my %args;
+    if (@_==1 and ref($_[0]) eq 'HASH') {
+       %args = %{$_[0]};
+    } else {
+        %args = @_;
+    }
+    return $class->$orig(%args);
+};
+
+
+
+state $strong_severities =
+   {map {($_,1)} @{$config{strong_severities}}};
+
+=head1 Methods
+
+=head2 version
+
+     $version->version
+
+Returns the source or binary package version
+
+=cut
+
+has version => (is => 'ro', isa => 'Str',
+               required => 1,
+               builder => '_build_version',
+               predicate => '_has_version',
+              );
+
+=head2 type
+
+Returns 'source' if this is a source version, or 'binary' if this is a binary
+version.
+
+=cut
+
+=head2 source_version
+
+Returns the source version for this version; if this is a source version,
+returns itself.
+
+=cut
+
+=head2 src_pkg_ver
+
+Returns the fully qualified source_package/version string for this version.
+
+=cut
+
+=head2 package
+
+Returns the name of the package that this version is in
+
+=cut
+
+has package => (is => 'ro',
+                isa => 'Str',
+                builder => '_build_package',
+                predicate => '_has_package',
+                lazy => 1,
+               );
+
+sub _build_package {
+    my $self = shift;
+    if ($self->_has_pkg) {
+        return $self->pkg->name;
+    }
+    return '(unknown)';
+}
+
+=head2 pkg
+
+Returns a L<Debbugs::Package> object corresponding to C<package>.
+
+=cut
+
+
+has pkg => (is => 'ro',
+            isa => 'Debbugs::Package',
+            lazy => 1,
+            builder => '_build_pkg',
+            reader => 'pkg',
+            predicate => '_has_pkg',
+           );
+
+sub _build_pkg {
+    my $self = shift;
+    return Debbugs::Package->new(package => $self->package,
+                                 type => $self->type,
+                                 valid => 0,
+                                 package_collection => $self->package_collection,
+                                 $self->schema_argument,
+                                );
+}
+
+
+=head2 valid
+
+Returns 1 if this package is valid, 0 otherwise.
+
+=cut
+
+has valid => (is => 'ro',
+             isa => 'Bool',
+             reader => 'is_valid',
+              lazy => 1,
+              builder => '_build_valid',
+            );
+
+sub _build_valid {
+    my $self = shift;
+    return 0;
+}
+
+
+=head2 package_collection
+
+Returns the L<Debugs::Collection::Package> which is in use by this version
+object.
+
+=cut
+
+has 'package_collection' => (is => 'ro',
+                            isa => 'Debbugs::Collection::Package',
+                            builder => '_build_package_collection',
+                            lazy => 1,
+                           );
+sub _build_package_collection {
+    my $self = shift;
+    return Debbugs::Collection::Package->new($self->schema_arg)
+}
+
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Version/Binary.pm b/Debbugs/Version/Binary.pm
new file mode 100644 (file)
index 0000000..25d7020
--- /dev/null
@@ -0,0 +1,97 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Version::Binary;
+
+=head1 NAME
+
+Debbugs::Version::Binary -- OO interface to Version
+
+=head1 SYNOPSIS
+
+   use Debbugs::Version::Binary;
+   Debbugs::Version::Binary->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use v5.10;
+use strictures 2;
+use namespace::autoclean;
+
+use Debbugs::Config qw(:config);
+use Debbugs::Collection::Package;
+use Debbugs::OOTypes;
+
+extends 'Debbugs::Version';
+
+sub type {
+    return 'binary';
+}
+
+has source_version => (is => 'ro',
+                      isa => 'Debbugs::Version::Source',
+                      lazy => 1,
+                      builder => '_build_source_version',
+                     );
+
+sub _build_source_version {
+    my $self = shift;
+    my $source_version =
+       $self->pkg->
+       get_source_version(version => $self->version,
+                          $self->_count_archs?(archs => [$self->_archs]):(),
+                         );
+    if (defined $source_version) {
+       return $source_version;
+    }
+    return Debbugs::Version::Source->new(version => $self->version,
+                                        package => '(unknown)',
+                                        valid => 0,
+                                        package_collection => $self->package_collection,
+                                       );
+}
+
+sub src_pkg_ver {
+    my $self = shift;
+    return $self->source->src_pkg_ver;
+}
+
+has archs => (is => 'bare',
+             isa => 'ArrayRef[Str]',
+             builder => '_build_archs',
+             traits => ['Array'],
+             handles => {'_archs' => 'elements',
+                         '_count_archs' => 'count',
+                        },
+            );
+
+sub _build_archs {
+    my $self = shift;
+    # this is wrong, but we'll start like this for now
+    return ['any'];
+}
+
+sub arch {
+    my $self = shift;
+    return $self->_count_archs > 0?join(',',$self->_archs):'any';
+}
+
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/Version/Source.pm b/Debbugs/Version/Source.pm
new file mode 100644 (file)
index 0000000..a23959c
--- /dev/null
@@ -0,0 +1,71 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::Version::Source;
+
+=head1 NAME
+
+Debbugs::Version::Source -- OO interface to Version
+
+=head1 SYNOPSIS
+
+   use Debbugs::Version::Source;
+   Debbugs::Version::Source->new(schema => $s,binaries => [qw(foo)],sources => [qw(bar)]);
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use v5.10;
+use strictures 2;
+use namespace::autoclean;
+
+use Debbugs::Config qw(:config);
+use Debbugs::Collection::Package;
+use Debbugs::OOTypes;
+
+extends 'Debbugs::Version';
+
+sub type {
+    return 'source';
+}
+
+sub source_version {
+    return $_[0];
+}
+
+sub src_pkg_ver {
+    my $self = shift;
+    return $self->package.'/'.$self->version;
+}
+
+has maintainer => (is => 'ro',
+                   isa => 'Str',
+                  );
+
+sub source {
+    my $self = shift;
+    return $self->pkg;
+}
+
+sub arch {
+    return 'source';
+}
+
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
diff --git a/Debbugs/VersionTree.pm b/Debbugs/VersionTree.pm
new file mode 100644 (file)
index 0000000..1231bd8
--- /dev/null
@@ -0,0 +1,125 @@
+# This module is part of debbugs, and
+# is released under the terms of the GPL version 2, or any later
+# version (at your option). See the file README and COPYING for more
+# information.
+# Copyright 2018 by Don Armstrong <don@donarmstrong.com>.
+
+package Debbugs::VersionTree;
+
+=head1 NAME
+
+Debbugs::VersionTree -- OO interface to Debbugs::Versions
+
+=head1 SYNOPSIS
+
+   use Debbugs::VersionTree;
+   my $vt = Debbugs::VersionTree->new();
+
+=head1 DESCRIPTION
+
+
+
+=cut
+
+use Mouse;
+use v5.10;
+use strictures 2;
+use namespace::autoclean;
+
+use Debbugs::Config qw(:config);
+use Debbugs::Versions;
+use Carp;
+
+extends 'Debbugs::OOBase';
+
+has _versions => (is => 'bare',
+                 isa => 'Debbugs::Versions',
+                 default => sub {Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp)},
+                 handles => {_isancestor => 'isancestor',
+                             _load => 'load',
+                             _buggy => 'buggy',
+                             _allstates => 'allstates',
+                            },
+                );
+
+has loaded_src_pkg => (is => 'bare',
+                    isa => 'HashRef[Bool]',
+                    default => sub {{}},
+                    traits => ['Hash'],
+                    handles => {src_pkg_loaded => 'exists',
+                                _set_src_pkg_loaded => 'set',
+                               },
+                   );
+
+sub _srcify_version {
+    my @return;
+    for my $v (@_) {
+       if (ref($_)) {
+           push @return,
+               $v->source_version->src_pkg_ver;
+       } else {
+           push @return,
+               $v;
+       }
+    }
+    return @_ > 1?@return:$return[0];
+}
+
+sub isancestor {
+    my ($self,$ancestor,$descendant) = @_;
+    return $self->_isancestor(_srcify_version($ancestor),
+                             _srcify_version($descendant),
+                            );
+}
+
+sub buggy {
+    my $self = shift;
+    my ($version,$found,$fixed) = @_;
+    ($version) = _srcify_version($version);
+    $found = [_srcify_version(@{$found})];
+    $fixed = [_srcify_version(@{$fixed})];
+    return $self->_buggy($version,$found,$fixed);
+}
+
+sub allstates {
+    my $self = shift;
+    my $found = shift;
+    my $fixed = shift;
+    my $interested = shift;
+    return $self->_allstates([_srcify_version(@{$found})],
+                            [_srcify_version(@{$fixed})],
+                            [_srcify_version(@{$interested})],
+                           );
+}
+
+sub load {
+    my $self = shift;
+    for my $src_pkg (@_) {
+       my $is_valid = 0;
+       if (ref($src_pkg)) {
+           $is_valid = $src_pkg->valid;
+           $src_pkg = $src_pkg->name;
+       }
+       next if $self->src_pkg_loaded($src_pkg);
+       my $srchash = substr $src_pkg, 0, 1;
+       my $version_fh;
+       open($version_fh,'<',"$config{version_packages_dir}/$srchash/$src_pkg");
+       if (not defined $version_fh) {
+           carp "No version file for package $src_pkg" if $is_valid;
+           next;
+       }
+       $self->_load($version_fh);
+       $self->_set_src_pkg_loaded($src_pkg,1);
+    }
+}
+
+__PACKAGE__->meta->make_immutable;
+no Mouse;
+1;
+
+
+__END__
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
index 41884cf568c9c4de831def5ba28aae50363eed1d..088f43b874c24dc6fd9c8ce1599bac32ac4489ba 100755 (executable)
@@ -45,10 +45,11 @@ use Debbugs::Log qw(:read);
 use Debbugs::Log::Spam;
 use Debbugs::CGI qw(:url :html :util :cache :usertags);
 use Debbugs::CGI::Bugreport qw(:all);
-use Debbugs::Common qw(buglog getmaintainers make_list bug_status);
-use Debbugs::Packages qw(getpkgsrc);
+use Debbugs::Common qw(buglog getmaintainers make_list bug_status package_maintainer);
+use Debbugs::Packages qw(binary_to_source);
 use Debbugs::DB;
 use Debbugs::Status qw(splitpackages split_status_fields get_bug_status isstrongseverity);
+use Debbugs::Bug;
 
 use Scalar::Util qw(looks_like_number);
 
@@ -57,13 +58,15 @@ use URI::Escape qw(uri_escape_utf8);
 use List::AllUtils qw(max);
 
 my $s;
+my @schema_arg = ();
 if (defined $config{database}) {
     $s = Debbugs::DB->connect($config{database}) or
-        die "Unable to connect to database";
+        die "Unable to connect to DB";
+    @schema_arg = ('schema',$s);
 }
 
 use CGI::Simple;
-my $q = new CGI::Simple;
+my $q = CGI::Simple->new();
 # STDOUT should be using the utf8 io layer
 binmode(STDOUT,':raw:encoding(UTF-8)');
 
@@ -213,19 +216,21 @@ if (defined $param{usertag}) {
      }
 }
 
+my $bug = Debbugs::Bug->new(bug => $ref,
+                            @schema_arg,
+                           );
+
 my %status;
 if ($need_status) {
     %status = %{split_status_fields(get_bug_status(bug=>$ref,
                                                   bugusertags => \%bugusertags,
-                                                   defined $s?(schema => $s):(),
+                                                   @schema_arg,
                                                  ))}
 }
 
 my @records;
-my $spam;
 eval{
-    @records = read_log_records(bug_num => $ref,inner_file => 1);
-    $spam = Debbugs::Log::Spam->new(bug_num => $ref);
+    @records = $bug->log_records();
 };
 if ($@) {
      quitcgi("Bad bug log for $gBug $ref. Unable to read records: $@");
@@ -301,7 +306,7 @@ END
          next if not $boring and not $record->{type} eq $wanted_type and not $record_wanted_anyway and @records > 1;
          $seen_message_ids{$msg_id} = 1 if defined $msg_id;
           # skip spam messages if we're outputting more than one message
-          next if @records > 1 and $spam->is_spam($msg_id);
+          next if @records > 1 and $bug->is_spam($msg_id);
       my @lines;
       if ($record->{inner_file}) {
           push @lines, scalar $record->{fh}->getline;
@@ -359,7 +364,7 @@ else {
                                   terse => $terse,
                                    # if we're only looking at one record, allow
                                    # spam to be output
-                                   spam  => (@records > 1)?$spam:undef,
+                                   spam  => (@records > 1)?$bug:undef,
                                   );
      }
 }
@@ -370,116 +375,16 @@ $log = join("\n",@log);
 
 # All of the below should be turned into a template
 
-my %maintainer = %{getmaintainers()};
-my %pkgsrc = %{getpkgsrc()};
-
 my $indexentry;
 my $showseverity;
 
-my $tpack;
-my $tmain;
-
-my $dtime = strftime "%a, %e %b %Y %T UTC", gmtime;
-
 unless (%status) {
     no_such_bug($q,$ref);
 }
 
-#$|=1;
-
-
 my @packages = make_list($status{package});
 
 
-my %packages_affects;
-for my $p_a (qw(package affects)) {
-    foreach my $pkg (make_list($status{$p_a})) {
-        if ($pkg =~ /^src\:/) {
-            my ($srcpkg) = $pkg =~ /^src:(.*)/;
-            my @maint = package_maintainer(source => $srcpkg,
-                                           @schema_arg,
-                                          );
-            $packages_affects{$p_a}{$pkg} =
-               {maintainer => @maint?\@maint : ['(unknown)'],
-                source     => $srcpkg,
-                package    => $pkg,
-                is_source  => 1,
-               };
-        }
-        else {
-            my @maint = package_maintainer(binary => $pkg,
-                                           @schema_arg,
-                                          );
-            my $source =
-                binary_to_source(binary => $pkg,
-                                 source_only => 1,
-                                 scalar_only => 1,
-                                 @schema_arg,
-                                );
-            $packages_affects{$p_a}{$pkg} =
-               {maintainer => @maint?\@maint : '(unknown)',
-                length($source)?(source => $source):(),
-                package    => $pkg,
-               };
-        }
-    }
-}
-
-# fixup various bits of the status
-$status{tags_array} = [sort(make_list($status{tags}))];
-$status{date_text} = strftime('%a, %e %b %Y %T UTC', gmtime($status{date}));
-$status{mergedwith_array} = [make_list($status{mergedwith})];
-
-
-my $version_graph = '';
-if (@{$status{found_versions}} or @{$status{fixed_versions}}) {
-     $version_graph = q(<a href=").
-         html_escape(version_url(package => $status{package},
-                                 found => $status{found_versions},
-                                 fixed => $status{fixed_versions},
-                                )
-                    ).
-         q("><img alt="version graph" src=").
-         html_escape(version_url(package => $status{package},
-                                 found => $status{found_versions},
-                                 fixed => $status{fixed_versions},
-                                 width => 2,
-                                 height => 2,
-                                )
-                    ).
-         qq{"></a>};
-}
-
-
-
-my @blockedby= make_list($status{blockedby});
-$status{blockedby_array} = [];
-if (@blockedby && $status{"pending"} ne 'fixed' && ! length($status{done})) {
-    for my $b (@blockedby) {
-        my %s = %{get_bug_status($b)};
-        next if (defined $s{pending} and
-                 $s{"pending"} eq 'fixed') or
-                     length $s{done};
-       push @{$status{blockedby_array}},{bug_num => $b, subject => $s{subject}, status => \%s};
-   }
-}
-
-my @blocks= make_list($status{blocks});
-$status{blocks_array} = [];
-if (@blocks && $status{"pending"} ne 'fixed' && ! length($status{done})) {
-    for my $b (@blocks) {
-        my %s = %{get_bug_status($b)};
-        next if $s{"pending"} eq 'fixed' || length $s{done};
-       push @{$status{blocks_array}}, {bug_num => $b, subject => $s{subject}, status => \%s};
-    }
-}
-
-if ($buglog !~ m#^\Q$gSpoolDir/db#) {
-     $status{archived} = 1;
-}
-
-my $descriptivehead = $indexentry;
-
 print $q->header(-type => "text/html",
                 -charset => 'utf-8',
                 -cache_control => 'public, max-age=300',
@@ -487,12 +392,8 @@ print $q->header(-type => "text/html",
                );
 
 print fill_in_template(template => 'cgi/bugreport',
-                      variables => {status => \%status,
-                                    package => $packages_affects{'package'},
-                                    affects => $packages_affects{'affects'},
+                      variables => {bug => $bug,
                                     log           => $log,
-                                    bug_num       => $ref,
-                                    version_graph => $version_graph,
                                     msg           => $msg,
                                     isstrongseverity => \&Debbugs::Status::isstrongseverity,
                                     html_escape   => \&Debbugs::CGI::html_escape,
index 8733d9a3198adfb1c9defc2f7eadeccc766c3e91..3855928e042a941d388bf74bbd9b4045c912ca18 100755 (executable)
@@ -37,6 +37,7 @@ BEGIN {
     # if the first directory in @INC is not an absolute directory, assume that
     # someone has overridden us via -I.
     if ($INC[0] !~ /^\//) {
+       undef $debbugs_dir;
     }
 }
 use if defined $debbugs_dir, lib => $debbugs_dir;
@@ -53,6 +54,7 @@ use Debbugs::Common qw(getparsedaddrs make_list getmaintainers getpseudodesc);
 
 use Debbugs::Bugs qw(get_bugs bug_filter newest_bug);
 use Debbugs::Packages qw(source_to_binary binary_to_source get_versions);
+use Debbugs::Collection::Bug;
 
 use Debbugs::Status qw(splitpackages);
 
@@ -292,8 +294,10 @@ my %bugusertags;
 my %ut;
 my %seen_users;
 
+my @users;
 for my $user (map {split /[\s*,\s*]+/} make_list($param{users}||[])) {
     next unless length($user);
+    push @users, $user;
     add_user($user,\%ut,\%bugusertags,\%seen_users,\%cats,\%hidden);
 }
 
@@ -304,7 +308,8 @@ if (defined $param{usertag}) {
          Debbugs::User::read_usertags(\%select_ut, $u);
          unless (defined $t && $t ne "") {
               $t = join(",", keys(%select_ut));
-         }
+          }
+         push @users,$u;
          add_user($u,\%ut,\%bugusertags,\%seen_users,\%cats,\%hidden);
          push @{$param{tag}}, split /,/, $t;
      }
@@ -355,6 +360,8 @@ if (defined $config{usertag_package_domain}) {
     }
     for my $package (@possible_packages) {
        next unless defined $package and length $package;
+       push @users,
+           $package.'@'.$config{usertag_package_domain};
        add_user($package.'@'.$config{usertag_package_domain},
                 \%ut,\%bugusertags,\%seen_users,\%cats,\%hidden);
     }
@@ -464,7 +471,17 @@ my %bugs;
 @bugs{@bugs} = @bugs;
 @bugs = keys %bugs;
 
-my $result = pkg_htmlizebugs(bugs => \@bugs,
+my $bugs = Debbugs::Collection::Bug->
+    new(bugs => \@bugs,
+       @schema_arg,
+       users => [map {my $u = Debbugs::User->new($_);
+                      $u->has_bug_tags()?($u):()
+                  } @users],
+       );
+
+$bugs->load_related_packages_and_versions();
+
+my $result = pkg_htmlizebugs(bugs => $bugs,
                             names => \@names,
                             title => \@title,
                             order => \@order,
index a85af5b8c51f61d5166289871c9497e97c66978e..74b07491b56de15d566ef6eef96e16c891de0ec9 100644 (file)
@@ -18,10 +18,12 @@ Build-Depends-Indep: libparams-validate-perl,
  libdbix-class-timestamp-perl,
  libdbix-class-deploymenthandler-perl,
  libdatetime-format-mail-perl,
+ libdatetime-perl,
  libaliased-perl,
  postgresql,
  postgresql-9.6-debversion|postgresql-10-debversion,
  libtext-xslate-perl, graphviz, libtext-iconv-perl, libnet-server-perl,
+ libmouse-perl, libmousex-nativetraits-perl,
 # used to make the logo
  inkscape
 Homepage: https://salsa.debian.org/debbugs-team
@@ -56,11 +58,13 @@ Depends:
  libcgi-simple-perl, libparams-validate-perl, libtext-xslate-perl,
  libmail-rfc822-address-perl, liblist-allutils-perl,
  graphviz, libtext-iconv-perl, libuser-perl,
+ libmouse-perl, libmousex-nativetraits-perl,
 # used by Debbugs::Libravatar and libravatar.cgi
  libfile-libmagic-perl, libgravatar-url-perl, libwww-perl, imagemagick
  # used by the database
  libdbix-class-timestamp-perl,
  libdbix-class-deploymenthandler-perl,
+ libdatetime-perl,
  libaliased-perl,
  libdatetime-format-mail-perl, libdbix-class-perl, libdatetime-format-pg-perl
 Section: perl
index 3eaeee69fa70ab60568489733acf84e3fe96d086..3a75bac8b3b7af8767e3349461fd45c347535954 100644 (file)
@@ -406,6 +406,7 @@ CREATE TABLE bug_binpackage (
        bin_pkg INT NOT NULL REFERENCES bin_pkg ON UPDATE CASCADE ON DELETE CASCADE
 );
 CREATE UNIQUE INDEX bug_binpackage_id_pkg ON bug_binpackage(bug,bin_pkg);
+CREATE UNIQUE INDEX bug_binpackage_bin_pkg_bug_idx ON bug_binpackage(bin_pkg,bug);
 INSERT INTO table_comments VALUES ('bug_binpackage','Bug <-> binary package mapping');
 INSERT INTO column_comments VALUES ('bug_binpackage','bug','Bug id (matches bug)');
 INSERT INTO column_comments VALUES ('bug_binpackage','bin_pkg','Binary package id (matches bin_pkg)');
@@ -652,6 +653,8 @@ CREATE VIEW bug_status --(id,bug_num,tags,subject,
                     JOIN src_pkg sp ON bsp.src_pkg=sp.id
                      WHERE bsp.bug=b.id) AS affects
         ) AS affects,
+       (SELECT msgid FROM message m LEFT JOIN bug_message bm ON bm.message=m.id
+               WHERE bm.bug=b.id ORDER BY m.sent_date ASC limit 1) AS message_id,
        b.submitter_full AS originator,
        EXTRACT(EPOCH FROM b.log_modified) AS log_modified,
        EXTRACT(EPOCH FROM b.creation) AS date,
diff --git a/t/16_usertags.t b/t/16_usertags.t
deleted file mode 100644 (file)
index fc1a67e..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- mode: cperl;-*-
-
-use Test::More;
-
-use warnings;
-use strict;
-
-plan tests => 4;
-
-use_ok('Debbugs::CGI::Pkgreport');
-
-my @usertags = ('severity=serious,severity=grave,severity=critical',
-                'tag=second',
-                'tag=third',
-                '',
-               );
-
-my @bugs =
-    ({severity => 'normal',
-      tags => 'wrongtag',
-      order => 3,
-     },
-    {severity => 'critical',
-     tags => 'second',
-     order => 0,
-    },
-    {severity => 'normal',
-     tags => 'third',
-     order => 2,
-    },
-    );
-
-for my $bug (@bugs) {
-    my $order = Debbugs::CGI::Pkgreport::get_bug_order_index(\@usertags,$bug);
-    ok($order == $bug->{order},
-       "order $bug->{order} == $order",
-      );
-}
-
-
diff --git a/t/22_oo_interface.t b/t/22_oo_interface.t
new file mode 100644 (file)
index 0000000..f8262c3
--- /dev/null
@@ -0,0 +1,96 @@
+# -*- mode: cperl;-*-
+
+use Test::More;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+use Test::WWW::Mechanize;
+use HTTP::Status qw(RC_NOT_MODIFIED);
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+
+# This must happen before anything is used, otherwise Debbugs::Config will be
+# set to wrong values.
+my %config = create_debbugs_configuration();
+
+my $tests = 0;
+use_ok('Debbugs::Bug');
+$tests++;
+use_ok('Debbugs::Collection::Bug');
+$tests++;
+
+# create 4 bugs
+for (1..4) {
+    submit_bug(subject => 'Submitting a bug '.$_,
+              pseudoheaders => {Severity => 'normal',
+                                Tags => 'wontfix moreinfo',
+                               },
+             );
+}
+run_processall();
+
+my $bc = Debbugs::Collection::Bug->new(bugs => [1..4]);
+
+my $bug;
+ok($bug = $bc->get(1),
+   "Created a bug correctly"
+  );
+$tests++;
+
+ok(!$bug->archiveable,
+   "Newly created bugs are not archiveable"
+  );
+$tests++;
+
+is($bug->submitter->email,'foo@bugs.something',
+   "Submitter works"
+  );
+$tests++;
+
+ok($bug->tags->tag_is_set('wontfix'),
+   "Wontfix tag set"
+  );
+$tests++;
+
+is($bug->tags->as_string(),
+   'moreinfo wontfix',
+   "as_string works"
+  );
+$tests++;
+
+### run some tests with the database creation
+
+## create the database
+my $pgsql = create_postgresql_database();
+update_postgresql_database($pgsql);
+
+use_ok('Debbugs::DB');
+$tests++;
+my $s;
+ok($s = Debbugs::DB->connect($pgsql->dsn),
+   "Able to connect to database");
+$tests++;
+
+$bc = Debbugs::Collection::Bug->new(bugs => [1..4],
+                                 schema => $s);
+ok($bug = $bc->get(1),
+   "Created a bug correctly with DB"
+  );
+$tests++;
+
+done_testing($tests);
+
index a388aadac65afa6745fd11029d90d4834340db5a..792041c9ec3005454be1989a2e6bec5c0e81444d 100755 (executable)
@@ -112,8 +112,6 @@ for my $suite (keys %s_p) {
        }
 }
 $prog_bar->target($tot) if $prog_bar;
-use Data::Printer;
-p %s_di;
 my $i = 0;
 my $avg_pkgs = 0;
 my $tot_suites = scalar keys %s_p;
index c18e86eea278396b5ad6c006712281abe6466a8a..152bd5801fa3afcb80451422835d86f7a9cf477f 100644 (file)
@@ -24,6 +24,8 @@ use strict;
 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
 use base qw(Exporter);
 
+use v5.10;
+
 use IO::File;
 use File::Temp qw(tempdir);
 use Cwd qw(getcwd);
@@ -41,7 +43,8 @@ BEGIN{
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
-     %EXPORT_TAGS = (configuration => [qw(dirsize create_debbugs_configuration send_message)],
+     %EXPORT_TAGS = (configuration => [qw(dirsize create_debbugs_configuration send_message),
+                                      qw(submit_bug run_processall)],
                     mail          => [qw(num_messages_sent)],
                     control       => [qw(test_control_commands)],
                     database => [qw(create_postgresql_database update_postgresql_database)]
@@ -207,11 +210,15 @@ sub send_message{
      }
      # now we should run processall to see if the message gets processed
      if ($param{run_processall}) {
-         system('scripts/processall') == 0 or die "processall failed";
+        run_processall();
       }
      return 1;
 }
 
+sub run_processall {
+    system('scripts/processall') == 0 or die "processall failed";
+}
+
 =item test_control_commands
 
  test_control_commands(\%config,
@@ -285,6 +292,49 @@ EOF
     }
 }
 
+sub submit_bug {
+    state $spec =
+       {subject => {type => SCALAR,
+                   default => 'Submitting a bug',
+                  },
+       body => {type => SCALAR,
+                default => 'This is a silly bug',
+               },
+       submitter => {type => SCALAR,
+                     default => 'foo@bugs.something',
+                    },
+       pseudoheaders => {type => HASHREF,
+                         default => sub {{}},
+                        },
+       package => {type => SCALAR,
+                   default => 'foo',
+                  },
+       run_processall => {type => SCALAR,
+                          default => 0,
+                         },
+       };
+    my %param =
+       validate_with(params => \@_,
+                     spec => $spec);
+    my $body = 'Package: '.$param{package}."\n";
+    foreach my $key (keys %{$param{pseudoheaders}}) {
+       for my $val (ref($param{pseudoheaders}{$key}) ?
+                    @{$param{pseudoheaders}{$key}} :
+                    $param{pseudoheaders}{$key}) {
+           $body .= $key. ': '.$val."\n";
+       }
+    }
+    $body .="\n".$param{body};
+    send_message(to => 'submit@bugs.something',
+                headers => [To => 'submit@bugs.something',
+                            From => $param{submitter},
+                            Subject => $param{subject},
+                           ],
+                run_processall => $param{run_processall},
+                body => $body
+               );
+}
+
 
 {
      package DebbugsTest::HTTPServer;
index ce72b3274afc318af43d7e231ac850f709c49371..6e7fef4378fbc423de5285b2ed19d55c8174f320 100644 (file)
@@ -1,5 +1,5 @@
-<: include "html/pre_title.tx" :>#<: $bug_num :> - <: $status.subject :> - <: $config.project :> <: $config.bug :> report logs<: include "html/post_title.tx" :>
-<link rel="canonical" href="<: bug_links(bug=>$bug_num,links_only=>1) :>">
+<: include "html/pre_title.tx" :>#<: $bug.id :> - <: $bug.subject :> - <: $config.project :> <: $config.bug :> report logs<: include "html/post_title.tx" :>
+<link rel="canonical" href="<: $bug.url :>">
 <script type="text/javascript">
 <!--
 function toggle_infmessages()
@@ -18,25 +18,31 @@ function toggle_infmessages()
 </head>
 <body>
     <div class="debbugs_install"><: $config.project :> <: $config.bug :> report logs</div>
-    <h1 class="bug_header"><a class="bug_email" href="mailto:<: $bug_num :>@<: $config.email_domain :>">#<: $bug_num :></a>
-        <span class="bug_subject"><: $status.subject :></span>
+    <h1 class="bug_header"><a class="bug_email" href="mailto:<: $bug.id :>@<: $config.email_domain :>">#<: $bug.id :></a>
+        <span class="bug_subject"><: $bug.subject :></span>
     </h1>
-<div class="versiongraph"><: raw($version_graph) :></div>
+: if $bug.has_found or $bug.has_fixed {
+    <div class="versiongraph">
+        <a href="<: $bug.version_url :>">
+            <img alt="version graph"
+                 src="<: $bug.version_url("width",2,"height",2) :>">
+        </a></div>
+: }
 <: include "cgi/bugreport_pkginfo.tx" :>
 <: include "cgi/bugreport_buginfo.tx" :>
 <div class="bugreport_operations">
 : if looks_like_number($msg) {
-    <span><a href="<: bug_links(bug => $bug_num, links_only => 1) :>">Full log</a></span>
+    <span><a href="<: $bug.url :>">Full log</a></span>
 : } else {
 : if ! $status.archived {
-<span><a href="mailto:<: $bug_num :>@<: $config.email_domain :>">Reply</a>
-or <a href="mailto:<: $bug_num :>-subscribe@<: $config.email_domain :>">subscribe</a>
+<span><a href="mailto:<: $bug.email :>">Reply</a>
+or <a href="mailto:<: $bug.subscribe_email :>">subscribe</a>
 to this <: $config.bug :>.</span>
 : }
 <span>View this <: $config.bug :> as an
-<a href="<: bug_links("bug",$bug_num,links_only=>1,options=>{mbox=>"yes"}) :>">mbox</a>,
-<a href="<: bug_links("bug",$bug_num,links_only=>1,options=>{mbox=>"yes",mboxstatus => "yes"}) :>">status mbox</a>, or
-<a href="<: bug_links("bug",$bug_num,links_only=>1,options=>{mbox=>"yes",mboxmaint => "yes"}) :>">maintainer mbox</a>
+    <a href="<: $bug.mbox_url :>">mbox</a>,
+    <a href="<: $bug.mbox_status_url :>">status mbox</a>, or
+    <a href="<: $bug.mbox_maint_url :>">maintainer mbox</a>
 </span>
 </div>
 : }
index 9067959c7f62ebf9ad4f5b99f57e678439b47665..f7d60513a7e31874622f24c3b8457c12f00828b8 100644 (file)
@@ -1,88 +1,88 @@
 <div class="buginfo">
     <ul>
         <li><span class="key">Reported by</span>
-            <span class="value"><: package_links(submitter=>$status.originator) :></span>
+            <span class="value"><a href="<: $bug.submitter_url :>"><: $bug.submitter.name :></a></span>
         </li>
         <li><span class="key">Date</span>
-            <span class="value"><: $status.date_text :></span>
+            <span class="value"><: $bug.created :></span>
         </li>
-        : if defined($status.owner) && $status.owner.length() {
+        : if $bug.has_owner {
         <li><span class="key">Owned by</span>
-            <span class="value"><: package_links("owner",$status.owner) :></span>
+            <span class="value"><a href="<: $bug.owner_url :>"><: $bug.owner.name :></a></span>
         </li>
         : }
         <li><span class="key">Severity</span>
             <span class="value">
-                <:- if $status.severity { :><em class="severity"><: } -:>
-                <:- $status.severity -:>
-                <:- if $status.severity { :></em><: } -:>
+                <:- if $bug.severity { :><em class="severity"><: } -:>
+                <:- $bug.severity -:>
+                <:- if $bug.severity { :></em><: } -:>
             </span>
         </li>
-        : if $status.tags_array {
+        : if $bug.tags.has_any_tags {
         <li><span class="key">Tags</span>
-            <span class="value"><: $status.tags_array.join(' ') :></span>
+            <span class="value"><: $bug.tags.as_string :></span>
         </li>
         : }
-        : if $status.mergedwith_array.count > 0 {
+        : if $bug.mergedwith.count > 0 {
         <li><span class="key">Merged with</span>
-            <span class="value"><: bug_links(bug=>$status.mergedwith_array).join(",\n") :></span>
+            <span class="value"><: bug_links(bug=>$bug.mergedwith).join(",\n") :></span>
         </li>
         : }
-        : if $status.found_versions.count {
+        : if $bug.has_found {
         <li><span class="key">Found in
-            version<:- if $status.found_versions.count > 1 { -:>s<: } -:></span>
-            <span class="value"><: $status.found_versions.join(', ') :></span>
+            version<:- if $bug.found.count > 1 { -:>s<: } -:></span>
+            <span class="value"><: $bug.found.join(', ') :></span>
         </li>
         : }
-        : if $status.fixed_versions.count {
+        : if $bug.has_fixed {
         <li><span class="key">Fixed in 
-            version<: if $status.fixed_versions.count > 1 { :>s<: } :></span>
-            <span class="value"><: $status.fixed_versions.join(', ') :></span>
+            version<: if $bug.fixed.count > 1 { :>s<: } :></span>
+            <span class="value"><: $bug.fixed.join(', ') :></span>
         </li>
         : }
-        <: if $status.done.length() { :>
+        <: if $bug.is_done { :>
         <li><span class="key">Done</span>
-            <span class="value"><: $status.done :></span>
+            <span class="value"><a href="<: $bug.done.url :>"><: $bug.done.name :></a></span>
         </li>
         : }
-        : if $status.blockedby_array.count {
+        : if $bug.blocked_by.count {
         <li><span class="key">Fix blocked by</span>
             <span class="value">
-                : for $status.blockedby_array -> $bug {
+                : for $bug.blockedby_array -> $bug {
                 <: bug_links("bug",$bug.bug_num) :>: <: $bug.subject -:>
                 <:- if !$~bug.is_last { :>, <:  } else {  :>.<: } :>
                 : }
             </span>
         </li>
         : }
-        : if $status.blocks_array.count {
+        : if $bug.blocks.count {
         <li><span class="key">Blocking fix for</span>
             <span class="value" 
-                  : for $status.blocks_array -> $bug {
+                  : for $bug.blocks_array -> $bug {
                   <: bug_links("bug",$bug.bug_num) :>: <: $bug.subject -:>
                   <:- if ! $~bug.is_last { :>, <:  } else {  :>.<: } :>
                   : }
             </span>
         </li>
         : }
-        : if $status.archived {
+        : if $bug.archived {
         <li><span class="key">Bug is</span>
             <span class="value">Archived</span>
         </li>
         : }
-        : if defined $status.forwarded and $status.forwarded.length() {
+        : if defined $bug.forwarded and $bug.forwarded.length() {
         <li><span class="key">Forwarded to</span>
-            <span class="value"><: split($status.forwarded,',\s+').map(maybelink).join(', ') :></span>
+            <span class="value"><: split($bug.forwarded,',\s+').map(maybelink).join(', ') :></span>
         </li>
         : }
-        : if defined $status.summary and $status.summary.length() {
+        : if defined $bug.summary and $bug.summary.length() {
         <li><span class="key">Summary</span>
-            <span class="value"><: $status.summary :></span>
+            <span class="value"><: $bug.summary :></span>
         </li>
         : }
-        : if defined $status.outlook and $status.outlook.length() {
+        : if defined $bug.outlook and $bug.outlook.length() {
         <li><span class="key">Outlook</span>
-            <span class="value"><: $status.outlook :></span>
+            <span class="value"><: $bug.outlook :></span>
         </li>
         : }
     </ul>
index 18d259abedc5e98ec5cdb46b0f81e2c165d977dc..f5721624847858e0aaa2e2364bcb4b7337932be9 100644 (file)
 ,
 : } } }
 <div class="pkginfo">
-    <p>Package<: if ($package.keys.count > 1) {:>s<: } :>
-    <: link_to_package($package) :>
-: for $package.values() -> $pkg { 
-<p>Maintainer for <: package_links($pkg.is_source ? "source": "package",$pkg.is_source ? $pkg.source : $pkg.package ) :> is <: package_links(maintainer => $pkg.maintainer) :>;
-<: if defined($pkg.source) && not $pkg.is_source { :>
-Source for <: package_links(package => $pkg.package) :> is
-<: package_links(source => $pkg.source) :>.
-<: } :></p>
+    <table><th>Package</th><th>Source(s)</th><th></th><th>Maintainer(s)</th>
+: for $bug.packages.members_ref -> $pkg {
+        <tr>
+            <td><a href="<: $pkg.url :>"><: $pkg.name :></a></td>
+           <td>
+: for $pkg.sources.members_ref -> $src {
+               <a href="<: $src.url :>"><: $src.name :></a>
+               <:- if ! $~src.is_last { :>, <: } -:>
 : }
+            </td>
+           <td><a href="https://tracker.debian.org/<:$pkg.name:>">PTS</a>
+               <a href="https://buildd.debian.org/<:$pkg.name:>">Buildd</a>
+               <a href="https://qa.debian.org/popcon.php?package=<:$pkg.name:>">Popcon</a>
+           </td>
+           <td>
+: for $pkg.maintainers.members_ref -> $maint {
+                <a href="<: $maint.maintainer_url :>"><: $maint.name :></a>
+                <:- if ! $~maint.is_last { :>, <: } -:>
+: }
+            </td>
+        </tr>
+: }
+    </table>
 : if $affects.keys.size > 0 {
 <p>Affects: <: link_to_package($affects) :>
 </p>
index ccca13aea39b3a7683aa977d1fd43f1b4ea86aaa..73de0cd168cb57e9a30feb5f140aa9baf177568d 100644 (file)
+<:- macro bug_url_subject->($bug) {-:>
+<a href="<: $bug.url :>"<:$bug.is_done?' style="text-decoration:line-through"':'':>>#
+    <:- $bug.bug :>: <: $bug.subject :></a>
+<:- } -:>
 <div class="shortbugstatus">
-  <a href="<: bug_links(bug=>$status.bug_num,links_only=>1):>"<:length($status.done)?' style="text-decoration:line-through"':'':>>#<: $status.bug_num :></a>
-  [<font face="fixed"><span class="link" onclick="javascript:extra_status_visible(<: $status.bug_num :>)"><abbr title="<: $status.severity :>">
-      <:- my $short_sev = substr($status.severity,0,1) -:>
-      <:- if isstrongseverity($status.severity) { -:><em class="severity"><: uc($short_sev) :></em>
-      <:- } else { -:>
-      <:- $short_sev } -:></abbr>|
-      <:- for $status.tags_array -> $tag { -:>
-      <:- if defined($config.tags_single_letter[$tag]) { -:>
-      <abbr title="<: $tag :>"><: $config.tags_single_letter[$tag] :></abbr><:- } -:>
-      : }
-          <:- if $status.tags_array.size() == 0 { -:>&nbsp;&nbsp;<: } :>|
-          <:- if $status.mergedwith_array.size() > 0 { -:>
-          <abbr title="merged">=</abbr>
-          <:- } -:>
-          <:- if $status.fixed_versions.size() > 0 { -:>
-          <abbr title="fixed versions">☺</abbr>
-          <:- } -:>
-          <:- if $status.fixed_versions.size() > 0 { -:>
-          <abbr title="fixed versions">☺</abbr>
-          <:- } -:>
-          <:- if $status.blockedby_array.size() > 0 { -:>
-          <abbr title="blocked by">♙</abbr>
-          <:- } -:>
-          <:- if $status.blocks_array.size() > 0 { -:>
-          <abbr title="blocks">♔</abbr>
-          <:- } -:>
-          <:- if length($status.forwarded) { -:>
-          <abbr title="forwarded">↝</abbr>
-          <:- } -:>
-          <:- if $status.archived { -:>
-          <abbr title="archived">♲</abbr>
-          <:- } -:>
-          <:- if length($status.affects) { -:>
-          <abbr title="affects">☣</abbr>
-          <:- } -:></span></font>]
-  [<: raw(package_links(package=>$status.package.split(','),class=>"submitter")) :>]
-  <a href="<: bug_links(bug=>$status.bug_num,links_only=>1) :>"><: $status.subject :></a>
-  <div id="extra_status_<: $status.bug_num :>" class="shortbugstatusextra">
-      <span>Reported by: <: raw(package_links(submitter=>$status.originator)) :>;</span>
-      <span>Date: <: $status.date_text :>;</span>
-      <:- if (defined $status.owner and length($status.owner)) { -:>
-      <span>Owned by: <: raw(package_links(owner=>$status.owner)) :>;</span>
+    <a href="<: $bug.url :>"<:$bug.is_done?' style="text-decoration:line-through"':'':>>#<: $bug.bug :></a>
+    [<font face="fixed"><span class="link" onclick="javascript:extra_status_visible(<: $bug.bug :>)"><abbr title="<: $bug.severity :>">
+        <:- if $bug.strong_severity { -:><em class="severity"><: $bug.short_severity :></em>
+        <:- } else { -:>
+        <:- $bug.short_severity } -:></abbr>|
+        <:- for $bug.tags.short_tags -> $tag { -:>
+        <abbr title="<: $tag.long :>"><: $tag.short :></abbr>
+        <:- } else { -:>&nbsp;&nbsp;<: } :>|
+        <:- if $bug.is_merged > 0 { -:>
+        <abbr title="merged">=</abbr>
+        <:- } -:>
+        <:- if $bug.has_fixed { -:>
+        <abbr title="fixed versions">☺</abbr>
+        <:- } -:>
+        <:- if $bug.is_blocked { -:>
+        <abbr title="blocked by">♙</abbr>
+        <:- } -:>
+        <:- if $bug.is_blocking { -:>
+        <abbr title="blocks">♔</abbr>
+        <:- } -:>
+        <:- if $bug.is_forwarded { -:>
+        <abbr title="forwarded">↝</abbr>
+        <:- } -:>
+        <:- if $bug.archived { -:>
+        <abbr title="archived">♲</abbr>
+        <:- } -:>
+        <:- if $bug.is_affecting { -:>
+        <abbr title="affects">☣</abbr>
+        <:- } -:></span></font>]
+        [<: for $bug.packages.members_ref -> $package { -:>
+        <a href="<: $package.url :>"><: $package.name :></a>
+        <:- if ! $~package.is_last { -:>, <: } else if ! $~package.is_first { -:>.<:- } -:>
+        <:- } :>]
+        <a href="<: $bug.url :>"><: $bug.subject :></a>
+        <div id="extra_status_<: $bug.bug :>" class="shortbugstatusextra">
+            <table class="extra_status">
+                <tr><td>Reported by</td><td><a href="<: $bug.submitter_url :>"><: $bug.submitter.name :></a></td></tr>
+                <tr><td>Date</td><td><: $bug.created.iso8601 :></td></tr>
+      <:- if $bug.is_owned { -:>
+                <tr><td>Owned by</td><td><a href="<: $bug.owner_url :>"><: $bug.owner.name :></a></td></tr>
       <:- } :>
-      <span>Severity:
-          <:- if (isstrongseverity($status.severity)) { -:>
-          <em class="severity">)<: $status.severity :></em>
+                <tr><td>Severity</td><td>
+          <: if $bug.strong_severity { -:>
+          <em class="severity"><: $bug.severity :></em>
           <:- } else { -:>
-          <: $status.severity :>
-          <:- } -:></span>
-      <span>
-          <:- if $status.tags_array.size > 0 { -:>
-          Tags: <:  $status.tags_array.join(', ') :>;
+          <: $bug.severity :>
+          <:- } -:></td></tr>
+      <:- if $bug.tags.has_any_tags { -:>
+          <tr><td>Tags</td><td>
+          : if $bug.tags.has_tags {
+          <span class="tags"><:  $bug.tags.join_tags(', ') :></span>
+          : }
+          <: if $bug.tags.has_usertags  {
+              if $bug.tags.has_tags { :>, <: } -:>
+          <span class="usertags"><: $bug.tags.join_usertags(', ') :></span>
+          <:- } -:>
+          </td></tr>
+          <: } :>
+          <:- if $bug.is_merged > 0 { -:>
+          <tr><td>Merged with </td><td>
+          <: for $bug.mergedwith.members_ref -> $bug { -:>
+          <:- bug_url_subject($bug) -:>
+          <:- if ! $~bug.is_last {-:>, <: } else { -:>.<:- } -:>
           <:- } -:>
-      </span>
-      : if $status.mergedwith_array.size > 0 {
-      <span>Merged with <: bug_links(bug=>$status.mergedwith_array).join(",\n") :>;</span>
-      : }
-      : if $status.found_versions.size > 0 or $status.fixed_versions.size > 0 {
-      <a href="<:
-                  version_url(package => $status.package,
-                          found   => $status.found_versions,
-                          fixed   => $status.fixed_versions,
-                      ):>">
+          </td></tr>
           <:- } -:>
-          <:- if $status.found_versions.size > 0 { -:>
-          <span>Found in version<: if $status.found_versions.size > 1 { :>s<: } :>
-              <:- $status.found_versions.join(', ') -:>;
-          </span>
+          <:- if $bug.has_found { -:>
+          <tr><td>Found in version<: if $bug.status.found_count > 1 { :>s<: } :></td>
+              <td><a href="<: $bug.version_url :>"><:- $bug.status.found_join(', ') -:></a></td></tr>
           <:- } -:>
-          <:- if $status.fixed_versions.size > 0 { :>
-          <span>Fixed in version<: if $status.fixed_versions.size > 1 { :>s<: } :>
-              <:- $status.fixed_versions.join(', ') -:>;
-          </span>
+              <:- if $bug.has_fixed { :>
+              <tr><td>Fixed in version<: if $bug.status.fixed_count > 1 { :>s<: } :></td><td>
+                  <a href="<: $bug.version_url :>"><:- $bug.status.fixed_join(', ') -:></a></td></tr>
           <:- } -:>
-          <:- if $status.found_versions.size > 0 or $status.fixed_versions.size > 0 { -:>
+          <:- if $bug.has_found or $bug.has_fixed { -:>
       </a>
       <:- } -:>
-      <:-  if (length($status.forwarded)) { :>
-      <span><strong>Forwarded</strong> to 
-          <: $status.forwarded.split('\,\s+').map(maybelink).join(', ') :>
-      </span>
+      <:-  if $bug.is_forwarded { :>
+      <tr><td><strong>Forwarded</strong> to</td>
+          <td><: $bug.forwarded.split('\,\s+').map(maybelink).join(', ') :></td>
+      </tr>
       <:- } -:>
-      <:- if (length($status.done)) { -:>
-      <span><strong>Done:</strong>
-          <: $status.done :>
-      </span>
+      <:- if $bug.is_done { -:>
+      <tr><td><strong>Done:</strong></td><td>
+          <: $bug.done.name :></td>
+      </tr>
       <:- } -:>
-      <:- if  $status.archive_days >= 0  and 
-      defined($status.location) && $status.location != "archive" { -:>
-      <span><strong>Can be archived
-          <: if $status.archive_days == 0 { :>
+      <:- if  not $bug.archived and $bug.when_archiveable >= 0 { -:>
+      <tr><td><strong>Can be archived</strong></td><td><strong>
+          <: if $bug.when_archiveable == 0 { :>
           today
-          <: } else if $status.archive_days == 1 { :>
+          <: } else if $bug.when_archiveable == 1 { :>
           in 1 day
           <: } else { :>
-          in <: $status.archive_days :> days
-          <:- } :>;</strong></span>
-      <:- } else if defined($status.location) && $status.location == "archived" { -:>
-      <span><strong>Archived</strong></span>
+          in <: $bug.when_archiveable :> days
+          <:- } :></strong></td></tr>
+      <:- } else if $bug.archived { -:>
+      <tr><td><strong>Archived</strong></td></tr>
       <:- } -:>
-      <:- if $status.blockedby_array.count > 0 { :>
-      <span>Fix blocked by 
-          <: for $status.blockedby_array -> $bug { :>
-          <: bug_links("bug",bug.bug_num) :>: 
-          <: $bug.subject -:>
+      <:- if $bug.is_blocked { :>
+      <tr><td>Fix blocked by</td><td>
+          <: for $bug.blocked_by -> $bug { :>
+          <: bug_url_subject($bug) :>
           <:- if ! $~bug.is_last { -:>, <: } else { -:>.<:- } -:>
-          <:- } -:>
-      </span>
+          <:- } -:></td>
+      </tr>
       <:- } -:>
-      <:- if $status.blocks_array.count > 0 { :>
-      <span>Blocking fix for
-          <: for $status.blocks_array -> $bug { :>
-          <: bug_links("bug",bug.bug_num) :>: 
-          <: $bug.subject -:>
+      <:- if $bug.blocks.count > 0 { :>
+      <tr><td>Blocking fix for</td><td>
+          <: for $bug.blocks -> $bug { :>
+          <: bug_url_subject($bug) :>
           <:- if ! $~bug.is_last {-:>, <: } else { -:>.<:- } -:>
           <:- } -:>
-      </span>
+      </td></tr>
       <:- } -:>
       <:- macro days_ago->($what,$ago) {-:>
-      <span>
+      <tr>
           <:- if ($time - $ago) / 86400 > 60 { -:>
-          <strong><: $what :> <:  secs_to_english($time-$ago) :> ago.</strong>
+          <td class="ancient"><: $what :></td><td><:  secs_to_english($time-$ago) :> ago.</td>
           <:- } else if ($time - $ago) / 86400 > 30 { :>
-          <strong><: $what :> <:  secs_to_english($time-$ago) :> ago.</strong>
-          <:- } -:>;
-      </span>
+          <td class="old"><: $what :></td><td><:  secs_to_english($time-$ago) :> ago.</td>
+          <:- } -:>
+      </tr>
       <:- } -:>
-      <: days_ago("Filed",$status.date) :>
-      <: days_ago("Modified",$status.log_modified) :>
-      <:- if defined $status.archived and $status.archived {:>
+      <: days_ago("Filed",$bug.created.epoch) :>
+      <: days_ago("Modified",$bug.modified.epoch) :>
+            </table>
+      <:- if $bug.archived {:>
       <span>Bug is archived. No further changes may be made.</span>
       <:- } -:>
   </div>