From: Don Armstrong Date: Sat, 6 Jul 2019 04:01:33 +0000 (-0700) Subject: Merge branch 'mouseify' X-Git-Url: https://git.donarmstrong.com/?p=debbugs.git;a=commitdiff_plain;h=b1252b6797aa6a79d00a32165fb2fa8fb1bd9318;hp=06424150844462de782ae112aa26c80dfa8d9401 Merge branch 'mouseify' --- diff --git a/Debbugs/Bug.pm b/Debbugs/Bug.pm new file mode 100644 index 0000000..21a26e3 --- /dev/null +++ b/Debbugs/Bug.pm @@ -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 . + +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{(.+)/(.+)}) { # 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 index 0000000..9209485 --- /dev/null +++ b/Debbugs/Bug/Status.pm @@ -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 . + +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 index 0000000..06dfb3f --- /dev/null +++ b/Debbugs/Bug/Tag.pm @@ -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 . + +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: diff --git a/Debbugs/Bugs.pm b/Debbugs/Bugs.pm index 5879f72..127e472 100644 --- a/Debbugs/Bugs.pm +++ b/Debbugs/Bugs.pm @@ -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' => diff --git a/Debbugs/CGI.pm b/Debbugs/CGI.pm index 7cc7f41..dffa8ec 100644 --- a/Debbugs/CGI.pm +++ b/Debbugs/CGI.pm @@ -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; diff --git a/Debbugs/CGI/Pkgreport.pm b/Debbugs/CGI/Pkgreport.pm index 331073e..e3dcc12 100644 --- a/Debbugs/CGI/Pkgreport.pm +++ b/Debbugs/CGI/Pkgreport.pm @@ -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 = "

Summary

\n"; - if (@bugs == 0) { + if ($bugs->count == 0) { return "

No reports found!

\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 = "
  • "; ##%d: %s\n
    ", - $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 .= "
      \n" . join("", map( { $_->[ 2 ] } @status ) ) . "
    \n"; + $result .= "
      \n" . join("", map( { $_->[ 1 ] } @status ) ) . "
    \n"; } else { $header .= "
    \n
      \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 + (?package|tag|pending|severity) # field + = # equals + (?[^=|\&,\+]+(?:,[^=|\&,\+])*) #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 index 0000000..6e3d49d --- /dev/null +++ b/Debbugs/Collection.pm @@ -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 . + +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 index 0000000..3f40b0c --- /dev/null +++ b/Debbugs/Collection/Bug.pm @@ -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 . + +package Debbugs::Collection::Bug; + +=head1 NAME + +Debbugs::Collection::Bug -- Bug generation factory + +=head1 SYNOPSIS + +This collection extends L and contains members of +L. 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 + +=over + +=item package_collection + +Optional L which is used to look up packages + + +=item correspondent_collection + +Optional L which is used to look up correspondents + + +=item users + +Optional arrayref of L 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 index 0000000..43ac8c0 --- /dev/null +++ b/Debbugs/Collection/Correspondent.pm @@ -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 . + +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 index 0000000..055cbae --- /dev/null +++ b/Debbugs/Collection/Package.pm @@ -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 . + +package Debbugs::Collection::Package; + +=head1 NAME + +Debbugs::Collection::Package -- Package generation factory + +=head1 SYNOPSIS + +This collection extends L and contains members of +L. 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 + +=over + +=item correspondent_collection + +Optional L which is used to look up correspondents + + +=item versiontree + +Optional L which contains known package source versions + +=back + +=head1 Methods + +=head2 correspondent_collection + + $packages->correspondent_collection + +Returns the L for this package collection + +=head2 versiontree + +Returns the L 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 of all of the versions in this package +collection which are known to match. + +Effectively, this calls L 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 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. +For unqualified versions, calls L; 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{(?.+?)/(?.+)$}) { + 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 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 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 index 0000000..f461afe --- /dev/null +++ b/Debbugs/Collection/Version.pm @@ -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 . + +package Debbugs::Collection::Version; + +=head1 NAME + +Debbugs::Collection::Version -- Version generation factory + +=head1 SYNOPSIS + +This collection extends L and contains members of +L. 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 + +=over + +=item package_collection + +Optional L which is used to look up packages + +=item versions + +Optional arrayref of C 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: diff --git a/Debbugs/Config.pm b/Debbugs/Config.pm index d85261a..0d0abae 100644 --- a/Debbugs/Config.pm +++ b/Debbugs/Config.pm @@ -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 index 0000000..0044347 --- /dev/null +++ b/Debbugs/Correspondent.pm @@ -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 . + +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: diff --git a/Debbugs/DB/Result/BinPkg.pm b/Debbugs/DB/Result/BinPkg.pm index eb4002f..0e0c554 100644 --- a/Debbugs/DB/Result/BinPkg.pm +++ b/Debbugs/DB/Result/BinPkg.pm @@ -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 index 0000000..4836b05 --- /dev/null +++ b/Debbugs/DB/Result/BinPkgSrcPkg.pm @@ -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 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp"); + +=head1 TABLE: C + +=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 + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("bin_pkg_src_pkg_bin_pkg_src_pkg", ["bin_pkg", "src_pkg"]); + +=head2 C + +=over 4 + +=item * L + +=item * L + +=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 + +=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 + +=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; diff --git a/Debbugs/DB/Result/BugBinpackage.pm b/Debbugs/DB/Result/BugBinpackage.pm index f4a757c..2f2a29d 100644 --- a/Debbugs/DB/Result/BugBinpackage.pm +++ b/Debbugs/DB/Result/BugBinpackage.pm @@ -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 { diff --git a/Debbugs/DB/Result/BugStatus.pm b/Debbugs/DB/Result/BugStatus.pm index 0f33b4e..ee3efc8 100644 --- a/Debbugs/DB/Result/BugStatus.pm +++ b/Debbugs/DB/Result/BugStatus.pm @@ -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 diff --git a/Debbugs/DB/Result/SrcPkg.pm b/Debbugs/DB/Result/SrcPkg.pm index 76b710d..26e56a4 100644 --- a/Debbugs/DB/Result/SrcPkg.pm +++ b/Debbugs/DB/Result/SrcPkg.pm @@ -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 { diff --git a/Debbugs/Log.pm b/Debbugs/Log.pm index d824d9a..710a844 100644 --- a/Debbugs/Log.pm +++ b/Debbugs/Log.pm @@ -11,9 +11,10 @@ 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 index 0000000..6600e02 --- /dev/null +++ b/Debbugs/OOBase.pm @@ -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 . + +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 index 0000000..37473d0 --- /dev/null +++ b/Debbugs/OOTypes.pm @@ -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 . + +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 index 0000000..70f0e35 --- /dev/null +++ b/Debbugs/Package.pm @@ -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 . + +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 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 or C + +=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 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 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 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: diff --git a/Debbugs/User.pm b/Debbugs/User.pm index 2457e54..50a0965 100644 --- a/Debbugs/User.pm +++ b/Debbugs/User.pm @@ -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 index 0000000..71dc008 --- /dev/null +++ b/Debbugs/Version.pm @@ -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 . + +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 Cnew(%params|$param)> for a binary version + +=over + +=item schema + +L schema which can be used to look up versions + +=item package + +String representation of the package + +=item pkg + +L which refers to the package given. + +Only one of C or C should be given + +=item package_collection + +L which is used to generate a L +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 object corresponding to C. + +=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 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 index 0000000..25d7020 --- /dev/null +++ b/Debbugs/Version/Binary.pm @@ -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 . + +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 index 0000000..a23959c --- /dev/null +++ b/Debbugs/Version/Source.pm @@ -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 . + +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 index 0000000..1231bd8 --- /dev/null +++ b/Debbugs/VersionTree.pm @@ -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 . + +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: diff --git a/cgi/bugreport.cgi b/cgi/bugreport.cgi index 41884cf..088f43b 100755 --- a/cgi/bugreport.cgi +++ b/cgi/bugreport.cgi @@ -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(version graph}; -} - - - -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, diff --git a/cgi/pkgreport.cgi b/cgi/pkgreport.cgi index 8733d9a..3855928 100755 --- a/cgi/pkgreport.cgi +++ b/cgi/pkgreport.cgi @@ -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, diff --git a/debian/control b/debian/control index a85af5b..74b0749 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/sql/debbugs_schema.sql b/sql/debbugs_schema.sql index 3eaeee6..3a75bac 100644 --- a/sql/debbugs_schema.sql +++ b/sql/debbugs_schema.sql @@ -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 index fc1a67e..0000000 --- a/t/16_usertags.t +++ /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 index 0000000..f8262c3 --- /dev/null +++ b/t/22_oo_interface.t @@ -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); + diff --git a/t/fake_ftpdist b/t/fake_ftpdist index a388aad..792041c 100755 --- a/t/fake_ftpdist +++ b/t/fake_ftpdist @@ -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; diff --git a/t/lib/DebbugsTest.pm b/t/lib/DebbugsTest.pm index c18e86e..152bd58 100644 --- a/t/lib/DebbugsTest.pm +++ b/t/lib/DebbugsTest.pm @@ -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; diff --git a/templates/en_US/cgi/bugreport.tx b/templates/en_US/cgi/bugreport.tx index ce72b32..6e7fef4 100644 --- a/templates/en_US/cgi/bugreport.tx +++ b/templates/en_US/cgi/bugreport.tx @@ -1,5 +1,5 @@ -<: include "html/pre_title.tx" :>#<: $bug_num :> - <: $status.subject :> - <: $config.project :> <: $config.bug :> report logs<: include "html/post_title.tx" :> - +<: include "html/pre_title.tx" :>#<: $bug.id :> - <: $bug.subject :> - <: $config.project :> <: $config.bug :> report logs<: include "html/post_title.tx" :> +