From 57207dfb5b9192725a127821ab3a3ff5b4ac13c7 Mon Sep 17 00:00:00 2001 From: Don Armstrong Date: Tue, 10 Jul 2012 17:13:18 -0700 Subject: [PATCH] add Service abstraction --- Debbugs/Control/Service.pm | 703 +++++++++++++++++++++++++++++++++++++ 1 file changed, 703 insertions(+) create mode 100644 Debbugs/Control/Service.pm diff --git a/Debbugs/Control/Service.pm b/Debbugs/Control/Service.pm new file mode 100644 index 0000000..5e7cb00 --- /dev/null +++ b/Debbugs/Control/Service.pm @@ -0,0 +1,703 @@ +# 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. +# +# [Other people have contributed to this file; their copyrights should +# go here too.] +# Copyright 2007,2008,2009 by Don Armstrong . + +package Debbugs::Control::Service; + +=head1 NAME + +Debbugs::Control::Service -- Handles the modification parts of scripts/service by calling Debbugs::Control + +=head1 SYNOPSIS + +use Debbugs::Control::Service; + + +=head1 DESCRIPTION + +This module contains the code to implement the grammar of control@. It +is abstracted here so that it can be called from process at submit +time. + +All of the public functions take the following options: + +=over + +=item debug -- scalar reference to which debbuging information is +appended + +=item transcript -- scalar reference to which transcript information +is appended + +=item affected_bugs -- hashref which is updated with bugs affected by +this function + + +=back + +Functions which should (probably) append to the .log file take the +following options: + +=over + +=item requester -- Email address of the individual who requested the change + +=item request_addr -- Address to which the request was sent + +=item request_nn -- Name of queue file which caused this request + +=item request_msgid -- Message id of message which caused this request + +=item location -- Optional location; currently ignored but may be +supported in the future for updating archived bugs upon archival + +=item message -- The original message which caused the action to be taken + +=item append_log -- Whether or not to append information to the log. + +=back + +B (for most functions) is a special option. When set to +false, no appending to the log is done at all. When it is not present, +the above information is faked, and appended to the log file. When it +is true, the above options must be present, and their values are used. + + +=head1 GENERAL FUNCTIONS + +=cut + +use warnings; +use strict; +use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT); +use base qw(Exporter); + +BEGIN{ + $VERSION = 1.00; + $DEBUG = 0 unless defined $DEBUG; + + @EXPORT = (); + %EXPORT_TAGS = (control => [qw(control_line)], + ); + @EXPORT_OK = (); + Exporter::export_ok_tags(keys %EXPORT_TAGS); + $EXPORT_TAGS{all} = [@EXPORT_OK]; +} + +use Debbugs::Config qw(:config); +use Debbugs::Control qw(:all); + +my %control_grammar = + (close => qr/(?i)^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/, + reassign => qr/(?i)^reassign\s+\#?(-?\d+)\s+ # bug and command + (?:(?:((?:src:|source:)?$config{package_name_re}) # new package + (?:\s+((?:$config{package_name_re}\/)? + $config{package_version_re}))?)| # optional version + ((?:src:|source:)?$config{package_name_re} # multiple package form + (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+)) + \s*$/x, + reopen => qr/(?i)^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/, + found => qr{^(?:(?i)found)\s+\#?(-?\d+) + (?:\s+((?:$config{package_name_re}\/)? + $config{package_version_re} + # allow for multiple packages + (?:\s*,\s*(?:$config{package_name_re}\/)? + $config{package_version_re})*) + )?$}x, + notfound => qr{^(?:(?i)notfound)\s+\#?(-?\d+) + \s+((?:$config{package_name_re}\/)? + $config{package_version_re} + # allow for multiple packages + (?:\s*,\s*(?:$config{package_name_re}\/)? + $config{package_version_re})* + )$}x, + fixed => qr{^(?:(?i)fixed)\s+\#?(-?\d+) + \s+((?:$config{package_name_re}\/)? + $config{package_version_re} + # allow for multiple packages + (?:\s*,\s*(?:$config{package_name_re}\/)? + $config{package_version_re})*) + \s*$}x, + notfixed => qr{^(?:(?i)notfixed)\s+\#?(-?\d+) + \s+((?:$config{package_name_re}\/)? + $config{package_version_re} + # allow for multiple packages + (?:\s*,\s*(?:$config{package_name_re}\/)? + $config{package_version_re})*) + \s*$}x, + submitter => qr/(?i)^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/. + forwarded => qr/(?i)^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/, + notforwarded => qr/(?i)^notforwarded\s+\#?(-?\d+)$/, + severity => qr/(?i)^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/, + tag => qr/(?i)^tags?\s+\#?(-?\d+)\s+(\S.*)$/, + block => qr/(?i)^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/, + retitle => qr/(?i)^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/, + unmerge => qr/(?i)^unmerge\s+\#?(-?\d+)$/, + merge => qr/(?i)^merge\s+#?(-?\d+(\s+#?-?\d+)+)\s*$/, + forcemerge => qr/(?i)^forcemerge\s+\#?(-?\d+(?:\s+\#?-?\d+)+)\s*$/, + clone => qr/(?i)^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/, + package => qr/(?i)^package\:?\s+(\S.*\S)?\s*$/, + limit => qr/(?i)^limit\:?\s+(\S.*\S)\s*$/, + affects => qr/(?i)^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/, + summary => qr/(?i)^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/, + owner => qr/(?i)^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/, + noowner => qr/(?i)^noowner\s+\#?(-?\d+)\s*$/, + unarchive => qr/(?i)^unarchive\s+#?(\d+)$/, + archive => qr/(?i)^archive\s+#?(\d+)$/, + ); + +sub valid_control { + my ($line,$matches) = @_; + my @matches; + for my $ctl (keys %control_grammar) { + if (@matches = $line =~ $control_grammar{$ctl}) { + @{$matches} = @matches if defined $matches and ref($matches) eq 'ARRAY'; + return $ctl; + } + } + @{$matches} = () if defined $matches and ref($matches) eq 'ARRAY'; + return undef; +} + +sub control_line { + my %param = + validate_with(params => \@_, + spec => {line => {type => SCALAR, + }, + clonebugs => {type => HASHREF, + }, + common_control_options => {type => ARRAYREF, + }, + errors => {type => SCALARREF, + }, + transcript => {type => FILEHANDLE, + }, + ok => {type => SCALARREF, + }, + }, + ); + my $line = $param{line}; + my @matches; + my $ctl = valid_control($line,\@matches); + my $transcript = $param{transcript}; + if (not defined $ctl) { + ${$param{errors}}++; + print {$param{transcript}} "Unknown command or invalid options to control\n"; + return; + } + my $ref = $matches[1]; + $ref = $param{clonebugs}{$ref} if exists $param{clonebugs}{$ref}; + ${$param{ok}}++; + my $errors = 0; + my $terminate_control = 0; + + if ($ctl eq 'close') { + if (defined $matches[2]) { + eval { + set_fixed(@{$param{common_control_options}}, + bug => $ref, + fixed => $matches[2], + add => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to add fixed version '$matches[2]' to $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + eval { + set_done(@{$param{common_control_options}}, + done => 1, + bug => $ref, + reopen => 0, + notify_submitter => 1, + clear_fixed => 0, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to mark $ref as done: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'reassign') { + my @new_packages; + if (not defined $matches[2]) { + push @new_packages, split /\s*\,\s*/,$matches[4]; + } + else { + push @new_packages, $matches[2]; + } + @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages; + my $version= $matches[3]; + eval { + set_package(@{$param{common_control_options}}, + bug => $ref, + package => \@new_packages, + ); + # if there is a version passed, we make an internal call + # to set_found + if (defined($version) && length $version) { + set_found(@{$param{common_control_options}}, + bug => $ref, + found => $version, + ); + } + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'reopen') { + my $new_submitter = $matches[2]; + if (defined $new_submitter) { + if ($new_submitter eq '=') { + undef $new_submitter; + } + elsif ($new_submitter eq '!') { + $new_submitter = $replyto; + } + } + eval { + set_done(@{$param{common_control_options}}, + bug => $ref, + reopen => 1, + defined $new_submitter? (submitter => $new_submitter):(), + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'found') { + my @versions; + if (defined $matches[2]) { + @versions = split /\s*,\s*/,$matches[2]; + eval { + set_found(@{$param{common_control_options}}, + bug => $ref, + found => \@versions, + add => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + else { + eval { + set_fixed(@{$param{common_control_options}}, + bug => $ref, + fixed => [], + reopen => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + } + elsif ($ctl eq 'notfound') { + my @versions; + @versions = split /\s*,\s*/,$matches[2]; + eval { + set_found(@{$param{common_control_options}}, + bug => $ref, + found => \@versions, + remove => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + elsif ($ctl eq 'fixed') { + my @versions; + @versions = split /\s*,\s*/,$matches[2]; + eval { + set_fixed(@{$param{common_control_options}}, + bug => $ref, + fixed => \@versions, + add => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + elsif ($ctl eq 'notfixed') { + my @versions; + @versions = split /\s*,\s*/,$matches[2]; + eval { + set_fixed(@{$param{common_control_options}}, + bug => $ref, + fixed => \@versions, + remove => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + elsif ($ctl eq 'submitter') { + my $newsubmitter = $matches[2] eq '!' ? $replyto : $matches[2]; + if (not Mail::RFC822::Address::valid($newsubmitter)) { + print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n"; + $errors++; + } + else { + eval { + set_submitter(@{$param{common_control_options}}, + bug => $ref, + submitter => $newsubmitter, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + } elsif ($ctl eq 'forwarded') { + my $forward_to= $matches[2]; + eval { + set_forwarded(@{$param{common_control_options}}, + bug => $ref, + forwarded => $forward_to, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'notforwarded') { + eval { + set_forwarded(@{$param{common_control_options}}, + bug => $ref, + forwarded => undef, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'severity') { + my $newseverity= $matches[2]; + if (exists $gObsoleteSeverities{$newseverity}) { + print {$transcript} "Severity level \`$newseverity' is obsolete. " . + "Use $gObsoleteSeverities{$newseverity} instead.\n\n"; + $errors++; + } elsif (not defined first {$_ eq $newseverity} + (@gSeverityList, "$gDefaultSeverity")) { + print {$transcript} "Severity level \`$newseverity' is not known.\n". + "Recognized are: $gShowSeverities.\n\n"; + $errors++; + } else { + eval { + set_severity(@{$param{common_control_options}}, + bug => $ref, + severity => $newseverity, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n"; + } + } + } elsif ($ctl eq 'tag') { + my $tags = $matches[2]; + my @tags = map {m/^([+=-])(.+)/ ? ($1,$2):($_)} split /[\s,]+/, $tags; + # this is an array of hashrefs which contain two elements, the + # first of which is the array of tags, the second is the + # option to pass to set_tag (we use a hashref here to make it + # more obvious what is happening) + my @tag_operations; + my @badtags; + for my $tag (@tags) { + if ($tag =~ /^[=+-]$/) { + if ($tag eq '=') { + @tag_operations = {tags => [], + option => [], + }; + } + elsif ($tag eq '-') { + push @tag_operations, + {tags => [], + option => [remove => 1], + }; + } + elsif ($tag eq '+') { + push @tag_operations, + {tags => [], + option => [add => 1], + }; + } + next; + } + if (not defined first {$_ eq $tag} @{$config{tags}}) { + push @badtags, $tag; + next; + } + if (not @tag_operations) { + @tag_operations = {tags => [], + option => [add => 1], + }; + } + push @{$tag_operations[-1]{tags}},$tag; + } + if (@badtags) { + print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n". + "Recognized are: ".join(' ', @gTags).".\n\n"; + $errors++; + } + eval { + for my $operation (@tag_operations) { + set_tag(@{$param{common_control_options}}, + bug => $ref, + tag => [@{$operation->{tags}}], + warn_on_bad_tags => 0, # don't warn on bad tags, + # 'cause we do that above + @{$operation->{option}}, + ); + } + }; + if ($@) { + # we intentionally have two errors here if there is a bad + # tag and the above fails for some reason + $errors++; + print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/i) { + my $add_remove = defined $matches[1] && $matches[1] eq 'un'; + $ref = $matches[2]; + $ref = exists $param{clonebugs}{$ref} ? $param{clonebugs}{$ref}; + my @blockers = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_} split /[\s,]+/, $matches[3]; + eval { + set_blocks(@{$param{common_control_options}}, + bug => $ref, + block => \@blockers, + $add_remove ? (remove => 1):(add => 1), + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'retitle') { + my $newtitle= $matches[2]; + eval { + set_title(@{$param{common_control_options}}, + bug => $ref, + title => $newtitle, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'unmerge') { + eval { + set_merged(@{$param{common_control_options}}, + bug => $ref, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to unmerge $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'merge') { + my @tomerge; + ($ref,@tomerge) = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_} + split(/\s+#?/,$matches[1]); + eval { + set_merged(@{$param{common_control_options}}, + bug => $ref, + merge_with => \@tomerge, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to merge $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'forcemerge') { + my @tomerge; + ($ref,@tomerge) = map {exists $param{clonebugs}{$_}?$param{clonebugs}{$_}:$_} + split(/\s+#?/,$matches[1]); + eval { + set_merged(@{$param{common_control_options}}, + bug => $ref, + merge_with => \@tomerge, + force => 1, + masterbug => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'clone') { + my $origref = $matches[1]; + my @newclonedids = split /\s+/, $matches[2]; + my $newbugsneeded = scalar(@newclonedids); + + $bug_affected{$ref} = 1; + eval { + my %new_clones; + clone_bug(@{$param{common_control_options}}, + bug => $ref, + new_bugs => \@newclonedids, + new_clones => \%new_clones, + ); + %{$param{clonebugs}} = (%{$param{clonebugs}}, + %new_clones); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clone $ref: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'package') { + my @pkgs = split /\s+/, $matches[1]; + if (scalar(@pkgs) > 0) { + %limit_pkgs = map { ($_, 1) } @pkgs; + $limit{package} = [@pkgs]; + print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n"; + print {$transcript} "Limit currently set to"; + for my $limit_field (keys %limit) { + print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n"; + } + print {$transcript} "\n"; + } else { + %limit_pkgs = (); + $limit{package} = []; + print {$transcript} "Limit cleared.\n\n"; + } + } elsif ($ctl eq 'limit') { + my ($field,@options) = split /\s+/, $matches[1]; + $field = lc($field); + if ($field =~ /^(?:clear|unset|blank)$/) { + %limit = (); + print {$transcript} "Limit cleared.\n\n"; + } + elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') { + # %limit can actually contain regexes, but because they're + # not evaluated in Safe, DO NOT allow them through without + # fixing this. + $limit{$field} = [@options]; + print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n"; + print {$transcript} "Limit currently set to"; + for my $limit_field (keys %limit) { + print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n"; + } + print {$transcript} "\n"; + } + else { + print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n"; + $errors++; + # this needs to be fixed + syntax error for fixing it + last; + } + } elsif ($ctl eq 'affects') { + my $add_remove = $matches[2]; + my $packages = $matches[3]; + # if there isn't a package given, assume that we should unset + # affects; otherwise default to adding + if (not defined $packages or + not length $packages) { + $packages = ''; + $add_remove ||= '='; + } + elsif (not defined $add_remove or + not length $add_remove) { + $add_remove = '+'; + } + eval { + affects(@{$param{common_control_options}}, + bug => $ref, + package => [splitpackages($matches[3])], + ($add_remove eq '+'?(add => 1):()), + ($add_remove eq '-'?(remove => 1):()), + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n"; + } + + } elsif ($ctl eq 'summary') { + my $summary_msg = length($matches[2])?$matches[2]:undef; + eval { + summary(@{$param{common_control_options}}, + bug => $ref, + summary => $summary_msg, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n"; + } + + } elsif ($ctl eq 'owner') { + my $newowner = $matches[2]; + if ($newowner eq '!') { + $newowner = $replyto; + } + eval { + owner(@{$param{common_control_options}}, + bug => $ref, + owner => $newowner, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'noowner') { + eval { + owner(@{$param{common_control_options}}, + bug => $ref, + owner => undef, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n"; + } + } elsif ($ctl eq 'unarchive') { + eval { + bug_unarchive(@{$param{common_control_options}}, + bug => $ref, + recipients => \%recipients, + ); + }; + if ($@) { + $errors++; + } + } elsif ($ctl eq 'archive') { + eval { + bug_archive(@{$param{common_control_options}}, + bug => $ref, + ignore_time => 1, + archive_unarchived => 0, + ); + }; + if ($@) { + $errors++; + } + } + if ($errors) { + ${$param{errors}}+=$errors; + } + return($errors,$terminate_control); +} + +1; + +__END__ -- 2.39.2