#
# [Other people have contributed to this file; their copyrights should
# go here too.]
-# Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
+# Copyright 2007,2008,2009 by Don Armstrong <don@donarmstrong.com>.
package Debbugs::Control;
$DEBUG = 0 unless defined $DEBUG;
@EXPORT = ();
- %EXPORT_TAGS = (affects => [qw(affects)],
+ %EXPORT_TAGS = (reopen => [qw(reopen)],
+ submitter => [qw(set_submitter)],
+ severity => [qw(set_severity)],
+ affects => [qw(affects)],
summary => [qw(summary)],
owner => [qw(owner)],
title => [qw(set_title)],
found => [qw(set_found set_fixed)],
fixed => [qw(set_found set_fixed)],
package => [qw(set_package)],
+ block => [qw(set_blocks)],
+ tag => [qw(set_tag)],
archive => [qw(bug_archive bug_unarchive),
],
log => [qw(append_action_to_log),
use Debbugs::Config qw(:config);
use Debbugs::Common qw(:lock buglog :misc get_hashname);
-use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages);
+use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages split_status_fields);
use Debbugs::CGI qw(html_escape);
use Debbugs::Log qw(:misc);
use Debbugs::Recipients qw(:add);
use Debbugs::Text qw(:templates);
-use Debbugs::Mail qw(rfc822_date);
+use Debbugs::Mail qw(rfc822_date send_mail_message default_headers);
+use Debbugs::MIME qw(create_mime_message);
use Mail::RFC822::Address qw();
request_nn => {type => SCALAR,
optional => 1,
},
+ request_replyto => {type => SCALAR,
+ optional => 1,
+ },
);
# writebug($data->{bug_num},$data);
# print {$transcript} "$action\n";
# }
-# __end_control(\%info);
+# __end_control(%info);
# }
+
+=head2 set_blocks
+
+ eval {
+ set_block(bug => $ref,
+ transcript => $transcript,
+ ($dl > 0 ? (debug => $transcript):()),
+ requester => $header{from},
+ request_addr => $controlrequestaddr,
+ message => \@log,
+ affected_packages => \%affected_packages,
+ recipients => \%recipients,
+ block => [],
+ );
+ };
+ if ($@) {
+ $errors++;
+ print {$transcript} "Failed to set blockers of $ref: $@";
+ }
+
+Alters the set of bugs that block this bug from being fixed
+
+This requires altering both this bug (and those it's merged with) as
+well as the bugs that block this bug from being fixed (and those that
+it's merged with)
+
+=over
+
+=item block -- scalar or arrayref of blocking bugs to set, add or remove
+
+=item add -- if true, add blocking bugs
+
+=item remove -- if true, remove blocking bugs
+
+=back
+
+=cut
+
+sub set_blocks {
+ my %param = validate_with(params => \@_,
+ spec => {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ # specific options here
+ block => {type => SCALAR|ARRAYREF,
+ default => [],
+ },
+ add => {type => BOOLEAN,
+ default => 0,
+ },
+ remove => {type => BOOLEAN,
+ default => 0,
+ },
+ %common_options,
+ %append_action_options,
+ },
+ );
+ if ($param{add} and $param{remove}) {
+ croak "It's nonsensical to add and remove the same blocking bugs";
+ }
+ if (grep {$_ !~ /^\d+$/} make_list($param{block})) {
+ croak "Invalid blocking bug(s):".
+ join(', ',grep {$_ !~ /^\d+$/} make_list($param{block}));
+ }
+ my $mode = 'set';
+ if (exists $param{add}) {
+ $mode = 'add';
+ }
+ elsif (exists $param{remove}) {
+ $mode = 'remove';
+ }
+
+ my %info =
+ __begin_control(%param,
+ command => 'blocks'
+ );
+ my ($debug,$transcript) =
+ @info{qw(debug transcript)};
+ my @data = @{$info{data}};
+ my @bugs = @{$info{bugs}};
+
+
+ # The first bit of this code is ugly, and should be cleaned up.
+ # Its purpose is to populate %removed_blockers and %add_blockers
+ # with all of the bugs that should be added or removed as blockers
+ # of all of the bugs which are merged with $param{bug}
+ my %ok_blockers;
+ my %bad_blockers;
+ for my $blocker (make_list($param{block})) {
+ next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
+ my $data = read_bug(bug=>$blocker,
+ );
+ if (defined $data and not $data->{archive}) {
+ $data = split_status_fields($data);
+ $ok_blockers{$blocker} = 1;
+ my @merged_bugs;
+ push @merged_bugs, make_list($data->{mergedwith});
+ $ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
+ }
+ else {
+ $bad_blockers{$blocker} = 1;
+ }
+ }
+
+ # throw an error if we are setting the blockers and there is a bad
+ # blocker
+ if (keys %bad_blockers and $mode eq 'set') {
+ croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
+ keys %ok_blockers?'':" and no known blocking bug(s)";
+ }
+ # if there are no ok blockers and we are not setting the blockers,
+ # there's an error.
+ if (not keys %ok_blockers and $mode ne 'set') {
+ print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
+ if (keys %bad_blockers) {
+ croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
+ }
+ __end_control(%info);
+ return;
+ }
+
+ my @change_blockers = keys %ok_blockers;
+
+ my %removed_blockers;
+ my %added_blockers;
+ my $action = '';
+ my @blockers = map {split ' ', $_->{blockedby}} @data;
+ my %blockers;
+ @blockers{@blockers} = (1) x @blockers;
+
+ # it is nonsensical for a bug to block itself (or a merged
+ # partner); We currently don't allow removal because we'd possibly
+ # deadlock
+
+ my %bugs;
+ @bugs{@bugs} = (1) x @bugs;
+ for my $blocker (@change_blockers) {
+ if ($bugs{$blocker}) {
+ croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
+ }
+ }
+ @blockers = keys %blockers;
+ if ($param{add}) {
+ %removed_blockers = ();
+ for my $blocker (@change_blockers) {
+ next if exists $blockers{$blocker};
+ $blockers{$blocker} = 1;
+ $added_blockers{$blocker} = 1;
+ }
+ }
+ elsif ($param{remove}) {
+ %added_blockers = ();
+ for my $blocker (@change_blockers) {
+ next if exists $removed_blockers{$blocker};
+ delete $blockers{$blocker};
+ $removed_blockers{$blocker} = 1;
+ }
+ }
+ else {
+ @removed_blockers{@blockers} = (1) x @blockers;
+ %blockers = ();
+ for my $blocker (@change_blockers) {
+ next if exists $blockers{$blocker};
+ $blockers{$blocker} = 1;
+ if (exists $removed_blockers{$blocker}) {
+ delete $removed_blockers{$blocker};
+ }
+ else {
+ $added_blockers{$blocker} = 1;
+ }
+ }
+ }
+ my @new_blockers = keys %blockers;
+ for my $data (@data) {
+ my $old_data = dclone($data);
+ # remove blockers and/or add new ones as appropriate
+ if ($data->{blockedby} eq '') {
+ print {$transcript} "Was not blocked by any bugs.\n";
+ } else {
+ print {$transcript} "Was blocked by: $data->{blockedby}\n";
+ }
+ my @changed;
+ push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
+ push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
+ $action = ucfirst(join ('; ',@changed)) if @changed;
+ if (not @changed) {
+ print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n"
+ unless __internal_request();
+ next;
+ }
+ $data->{blockedby} = join(' ',keys %blockers);
+ append_action_to_log(bug => $data->{bug_num},
+ command => 'block',
+ old_data => $old_data,
+ new_data => $data,
+ get_lock => 0,
+ __return_append_to_log_options(
+ %param,
+ action => $action,
+ ),
+ )
+ if not exists $param{append_log} or $param{append_log};
+ writebug($data->{bug_num},$data);
+ print {$transcript} "$action\n";
+ }
+ # we do this bit below to avoid code duplication
+ my %mungable_blocks;
+ $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
+ $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
+ for my $add_remove (keys %mungable_blocks) {
+ my @munge_blockers;
+ my %munge_blockers;
+ my $block_locks = 0;
+ for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
+ next if $munge_blockers{$blocker};
+ my ($new_locks, @blocking_data) =
+ lock_read_all_merged_bugs($blocker,
+ ($param{archived}?'archive':()));
+ if (not @blocking_data) {
+ unfilelock() for $new_locks;
+ die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
+ }
+ for (map {$_->{bug_num}} @blocking_data) {
+ $munge_blockers{$_} = 1;
+ }
+ for my $data (@blocking_data) {
+ my $old_data = dclone($data);
+ my %blocks;
+ my @blocks = split ' ', $data->{blocks};
+ @blocks{@blocks} = (1) x @blocks;
+ @blocks = ();
+ for my $bug (@bugs) {
+ if ($add_remove eq 'remove') {
+ next unless exists $blocks{$bug};
+ delete $blocks{$bug};
+ }
+ else {
+ next if exists $blocks{$bug};
+ $blocks{$bug} = 1;
+ }
+ push @blocks, $bug;
+ }
+ $data->{blocks} = join(' ',sort keys %blocks);
+ my $action = ($add_remove eq 'add'?'Added':'Removed').
+ " indication that bug $data->{bug_num} blocks".
+ join(',',@blocks);
+ append_action_to_log(bug => $data->{bug_num},
+ command => 'block',
+ old_data => $old_data,
+ new_data => $data,
+ get_lock => 0,
+ __return_append_to_log_options(%param,
+ action => $action
+ )
+ );
+ }
+ __handle_affected_packages(%param,data=>\@blocking_data);
+ add_recipients(recipients => $param{recipients},
+ actions_taken => {blocks => 1},
+ data => \@blocking_data,
+ debug => $debug,
+ transcript => $transcript,
+ );
+
+ unfilelock() for $new_locks;
+ }
+ }
+ __end_control(%info);
+}
+
+
+
=head2 set_tag
eval {
# first things first, make the versions fully qualified source
# versions
for my $data (@data) {
- # The 'done' field gets a bit weird with version tracking,
- # because a bug may be closed by multiple people in different
- # branches. Until we have something more flexible, we set it
- # every time a bug is fixed, and clear it when a bug is found
- # in a version greater than any version in which the bug is
- # fixed or when a bug is found and there is no fixed version
my $action = 'Did not alter tags';
my %tag_added = ();
my %tag_removed = ();
my %fixed_removed = ();
- my @old_tags = split /\,\s*/, $data->{tags};
+ my @old_tags = split /\,\s*/, $data->{keywords};
my %tags;
@tags{@old_tags} = (1) x @old_tags;
my $reopened = 0;
print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
}
- $data->{tags} = join(', ',keys %tags); # double check this
+ $data->{keywords} = join(', ',keys %tags); # double check this
my @changed;
push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
writebug($data->{bug_num},$data);
print {$transcript} "$action\n";
}
- __end_control(\%info);
+ __end_control(%info);
}
push @change_submitter,$data->{bug_num};
}
}
- __end_control(\%info);
+ __end_control(%info);
my @params_for_subcalls =
map {exists $param{$_}?($_,$param{$_}):()}
(keys %common_options,
my $old_data = dclone($data);
print {$debug} "Going to change bug submitter\n";
if (((not defined $param{submitter} or not length $param{submitter}) and
- (not defined $data->{submitter} or not length $data->{submitter})) or
- $param{submitter} eq $data->{submitter}) {
+ (not defined $data->{originator} or not length $data->{originator})) or
+ (defined $param{submitter} and defined $data->{originator} and
+ $param{submitter} eq $data->{originator})) {
print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n"
unless __internal_request();
next;
}
else {
- if (defined $data->{submitter} and length($data->{submitter})) {
- $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{submitter}'";
+ if (defined $data->{originator} and length($data->{originator})) {
+ $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'";
$notify_old_submitter = 1;
}
else {
$action= "Set $config{bug} submitter to '$param{submitter}'.";
}
- $data->{submitter} = $param{submitter};
+ $data->{originator} = $param{submitter};
}
append_action_to_log(bug => $data->{bug_num},
command => 'submitter',
# notify old submitter
if ($notify_old_submitter and $param{notify_submitter}) {
send_mail_message(message =>
- create_mime_message(["X-Loop" => $config{maintainer_email},
- From => "$config{maintainer_email} ($config{project} $config{ubug} Tracking System)",
- To => $old_data->{submitter},
- Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
- "Message-ID" => "<$data->{bug_num}.$param{request_nn}.ackfwdd\@$config{email_domain}>",
- "In-Reply-To" => $param{request_msgid},
- References => join(' ',grep {defined $_} $param{request_msgid},$data->{msgid}),
- Precedence => 'bulk',
- "X-$gProject-PR-Message" => "submitter-changed $data->{bug_num}",
- "X-$gProject-PR-Package" => $data->{package},
- "X-$gProject-PR-Keywords" => $data->{keywords},
- # Only have a X-$gProject-PR-Source when we know the source package
- (defined($source_package) and length($source_package))?("X-$gProject-PR-Source" => $source_package):(),
+ create_mime_message([default_headers(queue_file => $param{request_nn},
+ data => $data,
+ msgid => $param{request_msgid},
+ msgtype => 'ack',
+ pr_msg => 'submitter-changed',
+ headers =>
+ [To => $old_data->{submitter},
+ Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
+ ],
+ )
],
- message_body_template('mail/submitter_changed',
- {old_data => $old_data,
- data => $data,
- replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
- config => \%config,
- })
+ __message_body_template('mail/submitter_changed',
+ {old_data => $old_data,
+ data => $data,
+ replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
+ config => \%config,
+ })
),
recipients => $old_data->{submitter},
);
}
if (keys %added_packages) {
$action .= "Added indication that $data->{bug_num} affects " .
- english_join([%added_packages]);
+ english_join([keys %added_packages]);
}
}
if (not length $action) {
sub __internal_request{
my ($l) = @_;
$l = 0 if not defined $l;
- if (defined +(caller(2+$l))[0] and +(caller(2+$l))[0] eq __PACKAGE__) {
+ if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
return 1;
}
return 0;
if (not @data) {
die "Unable to read any bugs successfully.";
}
- ###
- # XXX check the limit at this point, and die if it is exceeded.
- # This is currently not done
- ###
+ if (not $param{archived}) {
+ for my $data (@data) {
+ if ($data->{archived}) {
+ die "Not altering archived bugs; see unarchive.";
+ }
+ }
+ }
+ if (not __check_limit(data => \@data,
+ exists $param{limit}?(limit => $param{limit}):(),
+ )) {
+ die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
+ }
+
__handle_affected_packages(%param,data => \@data);
print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
print {$debug} "$param{bug} read $locks locks\n";
recipients => $param{recipients},
(exists $param{command}?(actions_taken => {$param{command} => 1}):()),
debug => $debug,
- transcript => $transcript,
+ (__internal_request()?(transcript => $transcript):()),
);
print {$debug} "$param{bug} read done\n";
@{$info{param}{bugs_affected}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
}
add_recipients(recipients => $info{param}{recipients},
- (exists $info{param}{command}?(actions_taken => {$info{param}{command} => 1}):()),
+ (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
data => $info{data},
debug => $info{debug},
transcript => $info{transcript},
}
+=head2 __check_limit
+
+ __check_limit(data => \@data, limit => $param{limit});
+
+
+Checks to make sure that bugs match any limits; each entry of @data
+much satisfy the limit.
+
+Returns true if there are no entries in data, or there are no keys in
+limit; returns false (0) if there are any entries which do not match.
+
+The limit hashref elements can contain an arrayref of scalars to
+match; regexes are also acccepted. At least one of the entries in each
+element needs to match the corresponding field in all data for the
+limit to succeed.
+
+=cut
+
+
+sub __check_limit{
+ my %param = validate_with(params => \@_,
+ spec => {data => {type => ARRAYREF|SCALAR,
+ },
+ limit => {type => HASHREF|UNDEF,
+ },
+ },
+ );
+ my @data = make_list($param{data});
+ if (not @data or
+ not defined $param{limit} or
+ not keys %{$param{limit}}) {
+ return 1;
+ }
+ for my $data (@data) {
+ for my $field (keys %{$param{limit}}) {
+ next unless exists $param{limit}{$field};
+ my $match = 0;
+ for my $limit (make_list($param{limit}{$field})) {
+ if (not ref $limit) {
+ if ($data->{$field} eq $limit) {
+ $match = 1;
+ last;
+ }
+ }
+ elsif (ref($limit) eq 'Regexp') {
+ if ($data->{$field} =~ $limit) {
+ $match = 1;
+ last;
+ }
+ }
+ else {
+ warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
+ }
+ }
+ if (not $match) {
+ return 0;
+ }
+ }
+ }
+ return 1;
+}
+
+
=head2 die
sig_die "foo"
}
+# =head2 __message_body_template
+#
+# message_body_template('mail/ack',{ref=>'foo'});
+#
+# Creates a message body using a template
+#
+# =cut
+
+sub __message_body_template{
+ my ($template,$extra_var) = @_;
+ $extra_var ||={};
+ my $hole_var = {'&bugurl' =>
+ sub{"$_[0]: ".
+ 'http://'.$config{cgi_domain}.'/'.
+ Debbugs::CGI::bug_url($_[0]);
+ }
+ };
+
+ my $body = fill_in_template(template => $template,
+ variables => {config => \%config,
+ %{$extra_var},
+ },
+ hole_var => $hole_var,
+ );
+ return fill_in_template(template => 'mail/message_body',
+ variables => {config => \%config,
+ %{$extra_var},
+ body => $body,
+ },
+ hole_var => $hole_var,
+ );
+}
+
+
1;
__END__