From: Don Armstrong Date: Fri, 6 Mar 2009 04:21:31 +0000 (-0800) Subject: * Abstract out start and end control bits in Debbugs::Control X-Git-Tag: release/2.6.0~461^2~44 X-Git-Url: https://git.donarmstrong.com/?a=commitdiff_plain;h=3018a661ac3ebf562d955eb14d2246b0baa19e57;p=debbugs.git * Abstract out start and end control bits in Debbugs::Control * Move package, found, fixed, title, submitter changing commands to Debbugs::Control from service --- diff --git a/Debbugs/Control.pm b/Debbugs/Control.pm index bedc7d8..4b400d1 100644 --- a/Debbugs/Control.pm +++ b/Debbugs/Control.pm @@ -81,6 +81,11 @@ BEGIN{ %EXPORT_TAGS = (affects => [qw(affects)], summary => [qw(summary)], owner => [qw(owner)], + title => [qw(set_title)], + forward => [qw(set_forwarded)], + found => [qw(set_found set_fixed)], + fixed => [qw(set_found set_fixed)], + package => [qw(set_package)], archive => [qw(bug_archive bug_unarchive), ], log => [qw(append_action_to_log), @@ -97,6 +102,7 @@ use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages); use Debbugs::CGI qw(html_escape); use Debbugs::Log qw(:misc); use Debbugs::Recipients qw(:add); +use Debbugs::Packages qw(:versions :mapping); use Params::Validate qw(validate_with :types); use File::Path qw(mkpath); @@ -106,8 +112,12 @@ use Debbugs::Text qw(:templates); use Debbugs::Mail qw(rfc822_date); +use Mail::RFC822::Address qw(); + use POSIX qw(strftime); +use Storable qw(dclone nfreeze); + use Carp; # These are a set of options which are common to all of these functions @@ -130,6 +140,9 @@ my %common_options = (debug => {type => SCALARREF|HANDLE, limit => {type => HASHREF, default => {}, }, + show_bug_info => {type => BOOLEAN, + default => 1, + }, ); @@ -160,79 +173,868 @@ my %append_action_options = # this is just a generic stub for Debbugs::Control functions. # -# =head2 foo +# =head2 set_foo # # eval { -# foo(bug => $ref, -# transcript => $transcript, -# ($dl > 0 ? (debug => $transcript):()), -# requester => $header{from}, -# request_addr => $controlrequestaddr, -# message => \@log, -# affected_packages => \%affected_packages, -# recipients => \%recipients, -# summary => undef, -# ); +# set_foo(bug => $ref, +# transcript => $transcript, +# ($dl > 0 ? (debug => $transcript):()), +# requester => $header{from}, +# request_addr => $controlrequestaddr, +# message => \@log, +# affected_packages => \%affected_packages, +# recipients => \%recipients, +# summary => undef, +# ); # }; # if ($@) { # $errors++; -# print {$transcript} "Failed to foo $ref bar: $@"; +# print {$transcript} "Failed to set foo $ref bar: $@"; # } # # Foo frobinates # # =cut # -# sub foo { -# my %param = validate_with(params => \@_, -# spec => {bug => {type => SCALAR, -# regex => qr/^\d+$/, -# }, -# # specific options here -# %common_options, -# %append_action_options, -# }, -# ); -# our $locks = 0; -# $locks = 0; -# local $SIG{__DIE__} = sub { -# if ($locks) { -# for (1..$locks) { unfilelock(); } -# $locks = 0; -# } -# }; -# my ($debug,$transcript) = __handle_debug_transcript(%param); -# my (@data); -# ($locks, @data) = lock_read_all_merged_bugs($param{bug}); -# __handle_affected_packages(data => \@data,%param); -# print {$transcript} __bug_info(@data); -# add_recipients(data => \@data, -# recipients => $param{recipients} -# debug => $debug, -# transcript => $transcript, -# ); -# for my $data (@data) { -# append_action_to_log(bug => $data->{bug_num}, -# 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"; -# add_recipients(data => $data, -# recipients => $param{recipients}, -# debug => $debug, -# transcript => $transcript, -# ); -# } -# if ($locks) { -# for (1..$locks) { unfilelock(); } -# } -# -# } +## sub set_foo { +## my %param = validate_with(params => \@_, +## spec => {bug => {type => SCALAR, +## regex => qr/^\d+$/, +## }, +## # specific options here +## %common_options, +## %append_action_options, +## }, +## ); +## my %info = +## __begin_control(%param, +## command => 'foo' +## ); +## my ($new_locks,$debug,$transcript) = +## @info{qw(new_locks debug transcript)}; +## my @data = @{$info{data}}; +## my @bugs = @{$info{bugs}}; +## +## for my $data (@data) { +## append_action_to_log(bug => $data->{bug_num}, +## 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"; +## } +## __end_control(\%info); +## } + +=head2 set_submitter + + eval { + set_submitter(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + submitter => $new_submitter, + notify_submitter => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the forwarded-to-address of $ref: $@"; + } + +Sets the submitter of a bug. If notify_submitter is true (the +default), notifies the old submitter of a bug on changes + +=cut + +sub set_submitter { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + submitter => {type => SCALAR, + }, + notify_submitter => {type => BOOLEAN, + default => 1, + }, + %common_options, + %append_action_options, + }, + ); + if (not Mail::RFC822::Address::valid($param{submitter})) { + die "New submitter address $param{submitter} is not a valid e-mail address"; + } + my %info = + __begin_control(%param, + command => 'submitter' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my $action = ''; + # here we only concern ourselves with the first of the merged bugs + for my $data ($data[0]) { + 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}) { + 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}'"; + } + else { + $action= "Set $config{bug} submitter to '$param{submitter}'."; + } + $data->{submitter} = $param{submitter}; + } + append_action_to_log(bug => $data->{bug_num}, + command => 'set_submitter', + new_data => $data, + old_data => $old_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"; + } + __end_control(%info); +} + + + +=head2 set_forwarded + + eval { + set_forwarded(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + forwarded => $forward_to, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the forwarded-to-address of $ref: $@"; + } + +Sets the location to which a bug is forwarded. Given an undef +forwarded, unsets forwarded. + + +=cut + +sub set_forwarded { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + forwarded => {type => SCALAR|UNDEF, + }, + %common_options, + %append_action_options, + }, + ); + if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) { + die "Non-printable characters are not allowed in the forwarded field"; + } + my %info = + __begin_control(%param, + command => 'forwarded' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my $action = ''; + for my $data (@data) { + my $old_data = dclone($data); + print {$debug} "Going to change bug forwarded\n"; + if (((not defined $param{forwarded} or not length $param{forwarded}) and + (not defined $data->{forwarded} or not length $data->{forwarded})) or + $param{forwarded} eq $data->{forwarded}) { + print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n" + unless __internal_request(); + next; + } + else { + if (not defined $param{forwarded}) { + $action= "Unset $config{bug} forwarded-to-address"; + } + elsif (defined $data->{forwarded} and length($data->{forwarded})) { + $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'"; + } + else { + $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'."; + } + $data->{forwarded} = $param{forwarded}; + } + append_action_to_log(bug => $data->{bug_num}, + command => 'set_forwarded', + new_data => $data, + old_data => $old_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"; + } + __end_control(%info); +} + + + + +=head2 set_title + + eval { + set_title(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + title => $new_title, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the title of $ref: $@"; + } + +Sets the title of a specific bug + + +=cut + +sub set_title { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + title => {type => SCALAR, + }, + %common_options, + %append_action_options, + }, + ); + if ($param{title} =~ /[^[:print:]]/) { + die "Non-printable characters are not allowed in bug titles"; + } + + my %info = __begin_control(%param, + command => 'title', + ); + my ($debug,$transcript) = + @info{qw(debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my $action = ''; + for my $data (@data) { + my $old_data = dclone($data); + print {$debug} "Going to change bug title\n"; + if (defined $data->{subject} and length($data->{subject}) and + $data->{subject} eq $param{title}) { + print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n" + unless __internal_request(); + next; + } + else { + if (defined $data->{subject} and length($data->{subject})) { + $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'"; + } else { + $action= "Set $config{bug} title to '$param{title}'."; + } + $data->{subject} = $param{title}; + } + append_action_to_log(bug => $data->{bug_num}, + command => 'set_title', + new_data => $data, + old_data => $old_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"; + } + __end_control(%info); +} + + +=head2 set_package + + eval { + set_package(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + package => $new_package, + is_source => 0, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to assign or reassign $ref to a package: $@"; + } + +Indicates that a bug is in a particular package. If is_source is true, +indicates that the package is a source package. [Internally, this +causes src: to be prepended to the package name.] + +The default for is_source is 0. As a special case, if the package +starts with 'src:', it is assumed to be a source package and is_source +is overridden. + +The package option must match the package_name_re regex. + +=cut + +sub set_package { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + package => {type => SCALAR|ARRAYREF, + }, + is_source => {type => BOOLEAN, + default => 0, + }, + %common_options, + %append_action_options, + }, + ); + my @new_packages = map {splitpackages($_)} make_list($param{package}); + if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) { + croak "Invalid package name '". + join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages). + "'"; + } + my %info = __begin_control(%param, + command => 'package', + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + # clean up the new package + my $new_package = + join(',', + map {my $temp = $_; + ($temp =~ s/^src:// or + $param{is_source}) ? 'src:'.$temp:$temp; + } @new_packages); + + my $action = ''; + my $package_reassigned = 0; + for my $data (@data) { + my $old_data = dclone($data); + print {$debug} "Going to change assigned package\n"; + if (defined $data->{package} and length($data->{package}) and + $data->{package} eq $new_package) { + print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n" + unless __internal_request(); + next; + } + else { + if (defined $data->{package} and length($data->{package})) { + $package_reassigned = 1; + $action= "$config{bug} reassigned from package '$data->{package}'". + " to '$new_package'."; + } else { + $action= "$config{bug} assigned to package '$new_package'."; + } + $data->{package} = $new_package; + } + append_action_to_log(bug => $data->{bug_num}, + command => 'set_package', + new_data => $data, + old_data => $old_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"; + } + __end_control(%info); + # Only clear the fixed/found versions if the package has been + # reassigned + if ($package_reassigned) { + my @params_for_found_fixed = + map {exists $param{$_}?($_,$param{$_}):()} + ('bug', + keys %common_options, + keys %append_action_options, + ); + set_found(found => [], + @params_for_found_fixed, + ); + set_fixed(fixed => [], + @params_for_found_fixed, + ); + } +} + +=head2 set_found + + eval { + set_found(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + found => [], + add => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set found on $ref: $@"; + } + + +Sets, adds, or removes the specified found versions of a package + +If the version list is empty, and the bug is currently not "done", +causes the done field to be cleared. + +If any of the versions added to found are greater than any version in +which the bug is fixed (or when the bug is found and there are no +fixed versions) the done field is cleared. + +=cut + +sub set_found { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + found => {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 versions"; + } + + my %info = + __begin_control(%param, + command => 'found' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my %versions; + for my $version (make_list($param{found})) { + next unless defined $version; + $versions{$version} = + [make_source_versions(package => [splitpackages($data[0]{package})], + warnings => $transcript, + debug => $debug, + guess_source => 0, + versions => $version, + ) + ]; + # This is really ugly, but it's what we have to do + if (not @{$versions{$version}}) { + print {$transcript} "Unable to make a source version for version '$version'\n"; + } + } + if (not keys %versions and ($param{remove} or $param{add})) { + if ($param{remove}) { + print {$transcript} "Requested to remove no versions; doing nothing.\n"; + } + else { + print {$transcript} "Requested to add no versions; doing nothing.\n"; + } + __end_control(%info); + return; + } + # 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 found versions'; + my %found_added = (); + my %found_removed = (); + my %fixed_removed = (); + my $reopened = 0; + my $old_data = dclone($data); + if (not $param{add} and not $param{remove}) { + $found_removed{$_} = 1 for @{$data->{found_versions}}; + $data->{found_versions} = []; + } + my %found_versions; + @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}}; + my %fixed_versions; + @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}}; + for my $version (keys %versions) { + if ($param{add}) { + my @svers = @{$versions{$version}}; + if (not @svers) { + @svers = $version; + } + for my $sver (@svers) { + if (not exists $found_versions{$sver}) { + $found_versions{$sver} = 1; + $found_added{$sver} = 1; + } + # if the found we are adding matches any fixed + # versions, remove them + my @temp = grep m{(^|/)\Q$sver\E}, keys %fixed_versions; + delete $fixed_versions{$_} for @temp; + $fixed_removed{$_} = 1 for @temp; + } + + # We only care about reopening the bug if the bug is + # not done + if (defined $data->{done} and length $data->{done}) { + my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);} + map {m{([^/]+)$}; $1;} @svers; + # determine if we need to reopen + my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);} + map {m{([^/]+)$}; $1;} keys %fixed_versions; + if (not @fixed_order or + (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) { + $reopened = 1; + $data->{done} = ''; + } + } + } + elsif ($param{remove}) { + # in the case of removal, we only concern ourself with + # the version passed, not the source version it maps + # to + my @temp = grep m{(^|/)\Q$version\E}, keys %found_versions; + delete $found_versions{$_} for @temp; + $found_removed{$_} = 1 for @temp; + } + else { + # set the keys to exactly these values + my @svers = @{$versions{$version}}; + if (not @svers) { + @svers = $version; + } + for my $sver (@svers) { + if (not exists $found_versions{$sver}) { + $found_versions{$sver} = 1; + if (exists $found_removed{$sver}) { + delete $found_removed{$sver}; + } + else { + $found_added{$sver} = 1; + } + } + } + } + } + + $data->{found_versions} = [keys %found_versions]; + $data->{fixed_versions} = [keys %fixed_versions]; + + my @changed; + push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added; + push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed; +# push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added; + push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed; + $action = "$config{bug} ".ucfirst(join ('; ',@changed)) if @changed; + if ($reopened) { + $action .= " and reopened" + } + if (not $reopened and not @changed) { + print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n" + unless __internal_request(); + next; + } + $action .= '.'; + append_action_to_log(bug => $data->{bug_num}, + get_lock => 0, + command => 'set_found', + old_data => $old_data, + new_data => $data, + __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"; + } + __end_control(%info); +} + +=head2 set_fixed + + eval { + set_fixed(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + fixed => [], + add => 1, + reopen => 0, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set fixed on $ref: $@"; + } + + +Sets, adds, or removes the specified found versions of a package + +If the version list is empty, and the bug is currently not "done", +causes the done field to be cleared. + +If any of the versions added to found are greater than any version in +which the bug is fixed (or when the bug is found and there are no +fixed versions) the done field is cleared. + +=cut + +sub set_fixed { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+$/, + }, + # specific options here + fixed => {type => SCALAR|ARRAYREF, + default => [], + }, + add => {type => BOOLEAN, + default => 0, + }, + remove => {type => BOOLEAN, + default => 0, + }, + reopen => {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 versions"; + } + my %info = + __begin_control(%param, + command => 'fixed' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my %versions; + for my $version (make_list($param{fixed})) { + next unless defined $version; + $versions{$version} = + [make_source_versions(package => [splitpackages($data[0]{package})], + warnings => $transcript, + debug => $debug, + guess_source => 0, + versions => $version, + ) + ]; + # This is really ugly, but it's what we have to do + if (not @{$versions{$version}}) { + print {$transcript} "Unable to make a source version for version '$version'\n"; + } + } + if (not keys %versions and ($param{remove} or $param{add})) { + if ($param{remove}) { + print {$transcript} "Requested to remove no versions; doing nothing.\n"; + } + else { + print {$transcript} "Requested to add no versions; doing nothing.\n"; + } + __end_control(%info); + return; + } + # first things first, make the versions fully qualified source + # versions + for my $data (@data) { + my $old_data = dclone($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 fixed versions'; + my %found_added = (); + my %found_removed = (); + my %fixed_added = (); + my %fixed_removed = (); + my $reopened = 0; + if (not $param{add} and not $param{remove}) { + $fixed_removed{$_} = 1 for @{$data->{fixed_versions}}; + $data->{fixed_versions} = []; + } + my %found_versions; + @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]}; + my %fixed_versions; + @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]}; + for my $version (keys %versions) { + if ($param{add}) { + my @svers = @{$versions{$version}}; + if (not @svers) { + @svers = $version; + } + for my $sver (@svers) { + if (not exists $fixed_versions{$sver}) { + $fixed_versions{$sver} = 1; + $fixed_added{$sver} = 1; + } + } + } + elsif ($param{remove}) { + # in the case of removal, we only concern ourself with + # the version passed, not the source version it maps + # to + my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions; + delete $fixed_versions{$_} for @temp; + $fixed_removed{$_} = 1 for @temp; + } + else { + # set the keys to exactly these values + my @svers = @{$versions{$version}}; + if (not @svers) { + @svers = $version; + } + for my $sver (@svers) { + if (not exists $fixed_versions{$sver}) { + $fixed_versions{$sver} = 1; + if (exists $fixed_removed{$sver}) { + delete $fixed_removed{$sver}; + } + else { + $fixed_added{$sver} = 1; + } + } + } + } + } + + $data->{found_versions} = [keys %found_versions]; + $data->{fixed_versions} = [keys %fixed_versions]; + + # If we're supposed to consider reopening, reopen if the + # fixed versions are empty or the greatest found version + # is greater than the greatest fixed version + if ($param{reopen} and defined $data->{done} + and length $data->{done}) { + my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);} + map {m{([^/]+)$}; $1;} @{$data->{found_versions}}; + # determine if we need to reopen + my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);} + map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}}; + if (not @fixed_order or + (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) { + $reopened = 1; + $data->{done} = ''; + } + } + + my @changed; + push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added; + push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed; + push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added; + push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed; + $action = "$config{bug} ".ucfirst(join ('; ',@changed)) if @changed; + if ($reopened) { + $action .= " and reopened" + } + if (not $reopened and not @changed) { + print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n" + unless __internal_request(); + next; + } + $action .= '.'; + append_action_to_log(bug => $data->{bug_num}, + command => 'set_fixed', + new_data => $data, + old_data => $old_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"; + } + __end_control(%info); +} + + =head2 affects @@ -287,26 +1089,17 @@ sub affects { if ($param{add} and $param{remove}) { croak "Asking to both add and remove affects is nonsensical"; } - our $locks = 0; - $locks = 0; - local $SIG{__DIE__} = sub { - if ($locks) { - for (1..$locks) { unfilelock(); } - $locks = 0; - } - }; - my ($debug,$transcript) = __handle_debug_transcript(%param); - my (@data); - ($locks, @data) = lock_read_all_merged_bugs($param{bug}); - __handle_affected_packages(data => \@data,%param); - print {$transcript} __bug_info(@data); - add_recipients(data => \@data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); - my $action = 'Did not alter affected packages'; + my %info = + __begin_control(%param, + command => 'affects' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + my $action = ''; for my $data (@data) { + $action = ''; print {$debug} "Going to change affects\n"; my @packages = splitpackages($data->{affects}); my %packages; @@ -314,38 +1107,60 @@ sub affects { if ($param{add}) { my @added = (); for my $package (make_list($param{packages})) { - if (not $packages{$package}) { - $packages{$package} = 1; - push @added,$package; - } + next unless defined $package and length $package; + if (not $packages{$package}) { + $packages{$package} = 1; + push @added,$package; + } } if (@added) { $action = "Added indication that $data->{bug_num} affects ". - english_join(', ',' and ',@added); + english_join(\@added); } } elsif ($param{remove}) { my @removed = (); for my $package (make_list($param{packages})) { if ($packages{$package}) { + next unless defined $package and length $package; delete $packages{$package}; push @removed,$package; } } $action = "Removed indication that $data->{bug_num} affects " . - english_join(', ',' and ',@removed); + english_join(\@removed); } else { + my %added_packages = (); + my %removed_packages = %packages; %packages = (); for my $package (make_list($param{packages})) { + next unless defined $package and length $package; $packages{$package} = 1; + delete $removed_packages{$package}; + $added_packages{$package} = 1; + } + if (keys %removed_packages) { + $action = "Removed indication that $data->{bug_num} affects ". + english_join([keys %removed_packages]); + $action .= "\n" if keys %added_packages; + } + if (keys %added_packages) { + $action .= "Added indication that $data->{bug_num} affects " . + english_join([%added_packages]); } - $action = "Noted that $data->{bug_num} affects ". - english_join(', ',' and ', keys %packages); } + if (not length $action) { + print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n" + unless __internal_request(); + } + my $old_data = dclone($data); $data->{affects} = join(',',keys %packages); append_action_to_log(bug => $data->{bug_num}, get_lock => 0, + command => 'affects', + new_data => $data, + old_data => $old_data, __return_append_to_log_options( %param, action => $action, @@ -354,16 +1169,8 @@ sub affects { if not exists $param{append_log} or $param{append_log}; writebug($data->{bug_num},$data); print {$transcript} "$action\n"; - add_recipients(data => $data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); - } - if ($locks) { - for (1..$locks) { unfilelock(); } } - + __end_control(%info); } @@ -415,32 +1222,22 @@ sub summary { }, ); croak "summary must be numeric or undef" if - defined $param{summary} and not $param{summary} =~ /^\d+$/; - our $locks = 0; - $locks = 0; - local $SIG{__DIE__} = sub { - if ($locks) { - for (1..$locks) { unfilelock(); } - $locks = 0; - } - }; - my ($debug,$transcript) = __handle_debug_transcript(%param); - my (@data); - ($locks, @data) = lock_read_all_merged_bugs($param{bug}); - __handle_affected_packages(data => \@data,%param); - print {$transcript} __bug_info(@data); - add_recipients(data => \@data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); + defined $param{summary} and not $param{summary} =~ /^\d+$/; + my %info = + __begin_control(%param, + command => 'summary' + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; # figure out the log that we're going to use my $summary = ''; my $summary_msg = ''; my $action = ''; if (not defined $param{summary}) { # do nothing - print {$debug} "Removing summary fields"; + print {$debug} "Removing summary fields\n"; $action = 'Removed summary'; } else { @@ -497,15 +1294,22 @@ sub summary { } print {$debug} "Summary is going to be '$paragraph'\n"; $summary = $paragraph; - $summary =~ s/[\n\r]//g; + $summary =~ s/[\n\r]/ /g; if (not length $summary) { die "Unable to find summary message to use"; } - # trim off a trailing space - $summary =~ s/\ $//; + # trim off a trailing spaces + $summary =~ s/\ *$//; } for my $data (@data) { - print {$debug} "Going to change summary"; + print {$debug} "Going to change summary\n"; + if (((not defined $summary or not length $summary) and + (not defined $data->{summary} or not length $data->{summary})) or + $summary eq $data->{summary}) { + print {$transcript} "Ignoring request to change the summary of bug $param{bug} to the same value\n" + unless __internal_request(); + next; + } if (length $summary) { if (length $data->{summary}) { $action = "Summary replaced with message bug $param{bug} message $summary_msg"; @@ -514,8 +1318,12 @@ sub summary { $action = "Summary recorded from message bug $param{bug} message $summary_msg"; } } + my $old_data = dclone($data); $data->{summary} = $summary; append_action_to_log(bug => $data->{bug_num}, + command => 'summary', + old_data => $old_data, + new_data => $data, get_lock => 0, __return_append_to_log_options( %param, @@ -525,16 +1333,8 @@ sub summary { if not exists $param{append_log} or $param{append_log}; writebug($data->{bug_num},$data); print {$transcript} "$action\n"; - add_recipients(data => $data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); } - if ($locks) { - for (1..$locks) { unfilelock(); } - } - + __end_control(%info); } @@ -576,44 +1376,46 @@ sub owner { %append_action_options, }, ); - our $locks = 0; - $locks = 0; - local $SIG{__DIE__} = sub { - if ($locks) { - for (1..$locks) { unfilelock(); } - $locks = 0; - } - }; - my ($debug,$transcript) = __handle_debug_transcript(%param); - my (@data); - ($locks, @data) = lock_read_all_merged_bugs($param{bug}); - __handle_affected_packages(data => \@data,%param); - print {$transcript} __bug_info(@data); - @data and defined $data[0] or die "No bug found for $param{bug}"; - add_recipients(data => \@data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); + my %info = + __begin_control(%param, + command => 'owner', + ); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; my $action = ''; for my $data (@data) { print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n"; print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n"; if (not defined $param{owner} or not length $param{owner}) { - $param{owner} = ''; - $action = "Removed annotation that $config{bug} was owned by " . - "$data->{owner}."; + if (not defined $data->{owner} or not length $data->{owner}) { + print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n" + unless __internal_request(); + next; + } + $param{owner} = ''; + $action = "Removed annotation that $config{bug} was owned by " . + "$data->{owner}."; } else { - if (length $data->{owner}) { - $action = "Owner changed from $data->{owner} to $param{owner}."; - } - else { - $action = "Owner recorded as $param{owner}." - } + if ($data->{owner} eq $param{owner}) { + print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n"; + next; + } + if (length $data->{owner}) { + $action = "Owner changed from $data->{owner} to $param{owner}."; + } + else { + $action = "Owner recorded as $param{owner}." + } } + my $old_data = dclone($data); $data->{owner} = $param{owner}; append_action_to_log(bug => $data->{bug_num}, + command => 'owner', + new_data => $data, + old_data => $old_data, get_lock => 0, __return_append_to_log_options( %param, @@ -623,15 +1425,8 @@ sub owner { if not exists $param{append_log} or $param{append_log}; writebug($data->{bug_num},$data); print {$transcript} "$action\n"; - add_recipients(data => $data, - recipients => $param{recipients}, - debug => $debug, - transcript => $transcript, - ); - } - if ($locks) { - for (1..$locks) { unfilelock(); } } + __end_control(%info); } @@ -693,16 +1488,13 @@ sub bug_archive { %append_action_options, }, ); - our $locks = 0; - $locks = 0; - local $SIG{__DIE__} = sub { - if ($locks) { - for (1..$locks) { unfilelock(); } - $locks = 0; - } - }; + my %info = __begin_control(%param, + command => 'archive', + ); + my ($new_locks,$debug,$transcript) = @info{qw(data debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; my $action = "$config{bug} archived."; - my ($debug,$transcript) = __handle_debug_transcript(%param); if ($param{check_archiveable} and not bug_archiveable(bug=>$param{bug}, ignore_time => $param{ignore_time}, @@ -711,14 +1503,6 @@ sub bug_archive { die "Bug $param{bug} cannot be archived"; } print {$debug} "$param{bug} considering\n"; - my (@data); - ($locks, @data) = lock_read_all_merged_bugs($param{bug}); - __handle_affected_packages(data => \@data,%param); - print {$transcript} __bug_info(@data); - print {$debug} "$param{bug} read $locks\n"; - @data and defined $data[0] or die "No bug found for $param{bug}"; - print {$debug} "$param{bug} read done\n"; - if (not $param{archive_unarchived} and not exists $data[0]{unarchived} ) { @@ -730,7 +1514,6 @@ sub bug_archive { debug => $debug, transcript => $transcript, ); - my @bugs = map {$_->{bug_num}} @data; print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n"; for my $bug (@bugs) { if ($param{check_archiveable}) { @@ -748,6 +1531,12 @@ sub bug_archive { # First indicate that this bug is being archived append_action_to_log(bug => $bug, get_lock => 0, + command => 'archive', + # we didn't actually change the data + # when we archived, so we don't pass + # a real new_data or old_data + new_data => {}, + old_data => {}, __return_append_to_log_options( %param, action => $action, @@ -758,8 +1547,12 @@ sub bug_archive { if ($config{save_old_bugs}) { mkpath("$config{spool_dir}/archive/$dir"); foreach my $file (@files_to_remove) { - link( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" ) or - copy( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" ); + link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or + copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or + # we need to bail out here if things have + # gone horribly wrong to avoid removing a + # bug altogether + die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!"; } print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n"; @@ -768,14 +1561,7 @@ sub bug_archive { print {$transcript} "deleted $bug (from $param{bug})\n"; } bughook_archive(@bugs); - if (exists $param{bugs_affected}) { - @{$param{bugs_affected}}{@bugs} = (1) x @bugs; - } - print {$debug} "$param{bug} unlocking $locks\n"; - if ($locks) { - for (1..$locks) { unfilelock(); } - } - print {$debug} "$param{bug} unlocking done\n"; + __end_control(%info); } =head2 bug_unarchive @@ -806,29 +1592,15 @@ sub bug_unarchive { %append_action_options, }, ); - our $locks = 0; - local $SIG{__DIE__} = sub { - if ($locks) { - for (1..$locks) { unfilelock(); } - $locks = 0; - } - }; + + my %info = __begin_control(%param, + archived=>1, + command=>'unarchive'); + my ($new_locks,$debug,$transcript) = + @info{qw(new_locks debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; my $action = "$config{bug} unarchived."; - my ($debug,$transcript) = __handle_debug_transcript(%param); - print {$debug} "$param{bug} considering\n"; - my @data = (); - ($locks, @data) = lock_read_all_merged_bugs($param{bug},'archive'); - __handle_affected_packages(data => \@data,%param); - print {$transcript} __bug_info(@data); - print {$debug} "$param{bug} read $locks\n"; - if (not @data or not defined $data[0]) { - print {$transcript} "No bug found for $param{bug}\n"; - die "No bug found for $param{bug}"; - } - print {$debug} "$param{bug} read done\n"; - my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data; - print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n"; - print {$debug} "$param{bug} unarchiving\n"; my @files_to_remove; for my $bug (@bugs) { print {$debug} "$param{bug} removing $bug\n"; @@ -848,6 +1620,7 @@ sub bug_unarchive { # Indicate that this bug has been archived previously for my $bug (@bugs) { my $newdata = readbug($bug); + my $old_data = dclone($newdata); if (not defined $newdata) { print {$transcript} "$config{bug} $bug disappeared!\n"; die "Bug $bug disappeared!"; @@ -855,6 +1628,9 @@ sub bug_unarchive { $newdata->{unarchived} = time; append_action_to_log(bug => $bug, get_lock => 0, + command => 'unarchive', + new_data => $newdata, + old_data => $old_data, __return_append_to_log_options( %param, action => $action, @@ -862,20 +1638,8 @@ sub bug_unarchive { ) if not exists $param{append_log} or $param{append_log}; writebug($bug,$newdata); - add_recipients(recipients => $param{recipients}, - data => $newdata, - debug => $debug, - transcript => $transcript, - ); - } - print {$debug} "$param{bug} unlocking $locks\n"; - if ($locks) { - for (1..$locks) { unfilelock(); }; } - if (exists $param{bugs_affected}) { - @{$param{bugs_affected}}{@bugs} = (1) x @bugs; - } - print {$debug} "$param{bug} unlocking done\n"; + __end_control(%info); } =head2 append_action_to_log @@ -892,6 +1656,15 @@ sub append_action_to_log{ spec => {bug => {type => SCALAR, regex => qr/^\d+/, }, + new_data => {type => HASHREF, + optional => 1, + }, + old_data => {type => HASHREF, + optional => 1, + }, + command => {type => SCALAR, + optional => 1, + }, action => {type => SCALAR, }, requester => {type => SCALAR, @@ -912,7 +1685,14 @@ sub append_action_to_log{ get_lock => {type => BOOLEAN, default => 1, }, - } + # we don't use + # append_action_options here + # because some of these + # options aren't actually + # optional, even though the + # original function doesn't + # require them + }, ); # Fix this to use $param{location} my $log_location = buglog($param{bug}); @@ -923,9 +1703,101 @@ sub append_action_to_log{ } my $log = IO::File->new(">>$log_location") or die "Unable to open $log_location for appending: $!"; - my $msg = "\6\n". - "\n". - "".html_escape($param{action})."\n"; + # determine difference between old and new + my $data_diff = ''; + if (exists $param{old_data} and exists $param{new_data}) { + my $old_data = dclone($param{old_data}); + my $new_data = dclone($param{new_data}); + for my $key (keys %{$old_data}) { + if (not exists $Debbugs::Status::fields{$key}) { + delete $old_data->{$key}; + next; + } + next unless exists $new_data->{$key}; + next unless defined $new_data->{$key}; + if (not defined $old_data->{$key}) { + delete $old_data->{$key}; + next; + } + if (ref($new_data->{$key}) and + ref($old_data->{$key}) and + ref($new_data->{$key}) eq ref($old_data->{$key})) { + local $Storable::canonical = 1; + # print STDERR Dumper($new_data,$old_data,$key); + if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) { + delete $new_data->{$key}; + delete $old_data->{$key}; + } + } + elsif ($new_data->{$key} eq $old_data->{$key}) { + delete $new_data->{$key}; + delete $old_data->{$key}; + } + } + for my $key (keys %{$new_data}) { + if (not exists $Debbugs::Status::fields{$key}) { + delete $new_data->{$key}; + next; + } + next unless exists $old_data->{$key}; + next unless defined $old_data->{$key}; + if (not defined $new_data->{$key} or + not exists $Debbugs::Status::fields{$key}) { + delete $new_data->{$key}; + next; + } + if (ref($new_data->{$key}) and + ref($old_data->{$key}) and + ref($new_data->{$key}) eq ref($old_data->{$key})) { + local $Storable::canonical = 1; + if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) { + delete $new_data->{$key}; + delete $old_data->{$key}; + } + } + elsif ($new_data->{$key} eq $old_data->{$key}) { + delete $new_data->{$key}; + delete $old_data->{$key}; + } + } + $data_diff .= "\n"; + $data_diff .= "\n"; + } + my $msg = join('',"\6\n", + (exists $param{command} ? + "\n":"" + ), + (length $param{requester} ? + "\n":"" + ), + (length $param{request_addr} ? + "\n":"" + ), + "\n", + $data_diff, + "".html_escape($param{action})."\n"); if (length $param{requester}) { $msg .= "Request was from ".html_escape($param{requester})."\n"; } @@ -975,8 +1847,9 @@ sub __handle_affected_packages{ ); for my $data (make_list($param{data})) { next unless exists $data->{package} and defined $data->{package}; - $param{affected_packages}{$data->{package}} = 1; - } + my @packages = split /\s*,\s*/,$data->{package}; + @{$param{affected_packages}}{@packages} = (1) x @packages; + } } =head2 __handle_debug_transcript @@ -1009,14 +1882,45 @@ Produces a small bit of bug information to kick out to the transcript sub __bug_info{ my $return = ''; for my $data (@_) { - $return .= "Bug ".($data->{bug_num}||''). - " [".($data->{package}||''). "] ". - ($data->{subject}||'')."\n"; + next unless defined $data and exists $data->{bug_num}; + $return .= "Bug #".($data->{bug_num}||''). + ((defined $data->{done} and length $data->{done})? + " {Done: $data->{done}}":'' + ). + " [".($data->{package}||'(no package)'). "] ". + ($data->{subject}||'(no subject)')."\n"; } return $return; } +=head2 __internal_request + + __internal_request() + __internal_request($level) + +Returns true if the caller of the function calling __internal_request +belongs to __PACKAGE__ + +This allows us to be magical, and don't bother to print bug info if +the second caller is from this package, amongst other things. + +An optional level is allowed, which increments the number of levels to +check by the given value. [This is basically for use by internal +functions like __begin_control which are always called by +C<__PACKAGE__>. + +=cut + +sub __internal_request{ + my ($l) = @_; + $l = 0 if not defined $l; + if (defined +(caller(2+$l))[0] and +(caller(2+$l))[0] eq __PACKAGE__) { + return 1; + } + return 0; +} + sub __return_append_to_log_options{ my %param = @_; my $action = $param{action} if exists $param{action}; @@ -1046,6 +1950,151 @@ sub __return_append_to_log_options{ ); } +=head2 __begin_control + + my %info = __begin_control(%param, + archived=>1, + command=>'unarchive'); + my ($new_locks,$debug,$transcript) = @info{qw(new_locksa debug transcript)}; + my @data = @{$info{data}}; + my @bugs = @{$info{bugs}}; + + +Starts the process of modifying a bug; handles all of the generic +things that almost every control request needs + +Returns a hash containing + +=over + +=item new_locks -- number of new locks taken out by this call + +=item debug -- the debug file handle + +=item transcript -- the transcript file handle + +=item data -- an arrayref containing the data of the bugs +corresponding to this request + +=item bugs -- an arrayref containing the bug numbers of the bugs +corresponding to this request + +=back + +=cut + +our $locks = 0; + +sub __begin_control { + my %param = validate_with(params => \@_, + spec => {bug => {type => SCALAR, + regex => qr/^\d+/, + }, + archived => {type => BOOLEAN, + default => 0, + }, + command => {type => SCALAR, + optional => 1, + }, + %common_options, + }, + allow_extra => 1, + ); + my $new_locks; + my ($debug,$transcript) = __handle_debug_transcript(@_); + print {$debug} "$param{bug} considering\n"; + my @data = (); + my $old_die = $SIG{__DIE__}; + $SIG{__DIE__} = *sig_die{CODE}; + + ($new_locks, @data) = + lock_read_all_merged_bugs($param{bug}, + ($param{archived}?'archive':())); + $locks += $new_locks; + 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 + ### + __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"; + if (not @data or not defined $data[0]) { + print {$transcript} "No bug found for $param{bug}\n"; + die "No bug found for $param{bug}"; + } + + add_recipients(data => \@data, + recipients => $param{recipients}, + (exists $param{command}?(actions_taken => {$param{command} => 1}):()), + debug => $debug, + transcript => $transcript, + ); + + print {$debug} "$param{bug} read done\n"; + my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data; + print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n"; + return (data => \@data, + bugs => \@bugs, + old_die => $old_die, + new_locks => $new_locks, + debug => $debug, + transcript => $transcript, + param => \%param, + ); +} + +=head2 __end_control + + __end_control(%info); + +Handles tearing down from a control request + +=cut + +sub __end_control { + my %info = @_; + if (exists $info{new_locks} and $info{new_locks} > 0) { + print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n"; + for (1..$info{new_locks}) { + unfilelock(); + } + } + $SIG{__DIE__} = $info{old_die}; + if (exists $info{param}{bugs_affected}) { + @{$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}):()), + data => $info{data}, + debug => $info{debug}, + transcript => $info{transcript}, + ); + __handle_affected_packages(%{$info{param}},data=>$info{data}); +} + + +=head2 die + + sig_die "foo" + +We override die to specially handle unlocking files in the cases where +we are called via eval. [If we're not called via eval, it doesn't +matter.] + +=cut + +sub sig_die{ + #if ($^S) { # in eval + if ($locks) { + for (1..$locks) { unfilelock(); } + $locks = 0; + } + #} +} + 1; diff --git a/scripts/service b/scripts/service index 94b2571..d0ebe53 100755 --- a/scripts/service +++ b/scripts/service @@ -33,6 +33,8 @@ use Debbugs::Control qw(:all); use Debbugs::Log qw(:misc); use Debbugs::Text qw(:templates); +use Scalar::Util qw(looks_like_number); + use Mail::RFC822::Address; chdir($config{spool_dir}) or @@ -281,7 +283,7 @@ END } elsif (m/^usercategory\s+(\S+)(\s+\[hidden\])?\s*$/i) { $ok++; my $catname = $1; - my $hidden = ($2 ne ""); + my $hidden = (defined $2 and $2 ne ""); my $prefix = ""; my @cats; @@ -341,8 +343,11 @@ END push @ords, "$ord DEF"; $catsec--; } - @ords = sort { my ($a1, $a2, $b1, $b2) = split / /, "$a $b"; - $a1 <=> $b1 || $a2 <=> $b2; } @ords; + @ords = sort { + my ($a1, $a2, $b1, $b2) = split / /, "$a $b"; + ((looks_like_number($a1) and looks_like_number($a2))?$a1 <=> $b1:$a1 cmp $b1) || + ((looks_like_number($a2) and looks_like_number($b2))?$a2 <=> $b2:$a2 cmp $b2); + } @ords; $cats[-1]->{"ord"} = [map { m/^.* (\S+)/; $1 eq "DEF" ? $catsec + 1 : $1 } @ords]; } elsif ($o eq "*") { $catsec = 0; @@ -441,6 +446,7 @@ END } elsif (m/^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/i) { $ok++; $ref= $1; + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; $bug_affected{$ref}=1; my $version= $2; if (&setbug) { @@ -501,39 +507,54 @@ END } while (&getnextbug); } } - } elsif (m/^reassign\s+\#?(-?\d+)\s+(\S+)(?:\s+(\d.*))?$/i) { + } elsif (m/^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*$/xi) { $ok++; $ref= $1; - my $newpackage= $2; + my @new_packages; + if (not defined $2) { + push @new_packages, split /\s*\,\s*/,$4; + } + else { + push @new_packages, $2; + } + @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages; + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; $bug_affected{$ref}=1; my $version= $3; - $newpackage =~ y/A-Z/a-z/; - if (&setbug) { - if (length($data->{package})) { - $action= "$gBug reassigned from package \`$data->{package}'". - " to \`$newpackage'."; - } else { - $action= "$gBug assigned to package \`$newpackage'."; - } - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - $data->{package}= $newpackage; - $data->{found_versions}= []; - $data->{fixed_versions}= []; - # TODO: what if $newpackage is a source package? - addfoundversions($data, $data->{package}, $version, 'binary'); - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - } while (&getnextbug); - } + eval { + set_package(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + recipients => \%recipients, + package => \@new_packages, + ); + # if there is a version passed, we make an internal call + # to set_found + if (defined($version) && length $version) { + set_found(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + recipients => \%recipients, + version => $version, + ); + } + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear fixed versions and reopen on $ref: $@"; + } } elsif (m/^reopen\s+\#?(-?\d+)$/i ? ($noriginator='', 1) : m/^reopen\s+\#?(-?\d+)\s+\=$/i ? ($noriginator='', 1) : m/^reopen\s+\#?(-?\d+)\s+\!$/i ? ($noriginator=$replyto, 1) : @@ -565,131 +586,155 @@ END } while (&getnextbug); } } - } elsif (m{^found\s+\#?(-?\d+) + } elsif (m{^(?:(?i)found)\s+\#?(-?\d+) (?:\s+((?:$config{package_name_re}\/)? - $config{package_version_re}))?$}ix) { + $config{package_version_re} + # allow for multiple packages + (?:\s*,\s*(?:$config{package_name_re}\/)? + $config{package_version_re})*) + )?$}x) { $ok++; $ref= $1; - my $version= $2; - if (&setbug) { - if (!length($data->{done}) and not defined($version)) { - print {$transcript} "$gBug is already open, cannot reopen.\n\n"; + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + my @versions; + if (defined $2) { + @versions = split /\s*,\s*/,$2; + eval { + set_found(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + found => \@versions, + add => 1, + ); + }; + if ($@) { $errors++; - &nochangebug; - } else { - $action= - defined($version) ? - "$gBug marked as found in version $version." : - "$gBug reopened."; - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - # 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 - if (defined $version) { - my ($version_only) = $version =~ m{([^/]+)$}; - addfoundversions($data, $data->{package}, $version, 'binary'); - my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);} - map {s{.+/}{}; $_;} @{$data->{fixed_versions}}; - if (not @fixed_order or (Debbugs::Versions::Dpkg::vercmp($version_only,$fixed_order[-1]) >= 0)) { - $action = "$gBug marked as found in version $version and reopened." - if length $data->{done}; - $data->{done} = ''; - } - } else { - # Versionless found; assume old-style "not fixed at - # all". - $data->{fixed_versions} = []; - $data->{done} = ''; - } - } while (&getnextbug); - } - } - } elsif (m[^notfound\s+\#?(-?\d+)\s+ - ((?:$config{package_name_re}\/)? - \S+)\s*$]ix) { + print {$transcript} "Failed to add found on $ref: $@"; + } + } + else { + eval { + set_fixed(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + fixed => [], + reopen => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear fixed versions and reopen on $ref: $@"; + } + } + } + elsif (m{^(?:(?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) { $ok++; $ref= $1; - my $version= $2; - if (&setbug) { - $action= "$gBug no longer marked as found in version $version."; - if (length($data->{done})) { - $extramessage= "(By the way, this $gBug is currently marked as done.)\n"; - } - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - removefoundversions($data, $data->{package}, $version, 'binary'); - } while (&getnextbug); - } - } - elsif (m[^fixed\s+\#?(-?\d+)\s+ - ((?:$config{package_name_re}\/)? - $config{package_version_re})\s*$]ix) { + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + my @versions; + @versions = split /\s*,\s*/,$2; + eval { + set_found(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + found => \@versions, + remove => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to remove found on $ref: $@"; + } + } + elsif (m{^(?:(?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) { $ok++; $ref= $1; - my $version= $2; - if (&setbug) { - $action= - defined($version) ? - "$gBug marked as fixed in version $version." : - "$gBug reopened."; - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - addfixedversions($data, $data->{package}, $version, 'binary'); - } while (&getnextbug); + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + my @versions; + @versions = split /\s*,\s*/,$2; + eval { + set_fixed(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + fixed => \@versions, + add => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to add fixed on $ref: $@"; } - } - elsif (m[^notfixed\s+\#?(-?\d+)\s+ - ((?:$config{package_name_re}\/)? - \S+)\s*$]ix) { + } + elsif (m{^(?:(?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) { $ok++; $ref= $1; - my $version= $2; - if (&setbug) { - $action= - defined($version) ? - "$gBug no longer marked as fixed in version $version." : - "$gBug reopened."; - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - removefixedversions($data, $data->{package}, $version, 'binary'); - } while (&getnextbug); + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + my @versions; + @versions = split /\s*,\s*/,$2; + eval { + set_fixed(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + fixed => \@versions, + remove => 1, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to remove fixed on $ref: $@"; } - } - elsif (m/^submitter\s+\#?(-?\d+)\s+\!$/i ? ($newsubmitter=$replyto, 1) : - m/^submitter\s+\#?(-?\d+)\s+(\S.*\S)$/i ? ($newsubmitter=$2, 1) : 0) { + } + elsif (m/^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/i) { $ok++; $ref= $1; $bug_affected{$ref}=1; - if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) { - $ref = $clonebugs{$ref}; - } - if (not Mail::RFC822::Address::valid($newsubmitter)) { - transcript("$newsubmitter is not a valid e-mail address; not changing submitter\n"); + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + my $newsubmitter = $2 eq '!' ? $replyto : $2; + if (not Mail::RFC822::Address::valid($newsubmitter)) { + print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n"; $errors++; } elsif (&getbug) { @@ -748,51 +793,47 @@ END } elsif (m/^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/i) { $ok++; $ref= $1; - my $whereto= $2; - $bug_affected{$ref}=1; - if (&setbug) { - if (length($data->{forwarded})) { - $action= "Forwarded-to-address changed from $data->{forwarded} to $whereto."; - } else { - $action= "Noted your statement that $gBug has been forwarded to $whereto."; - } - if (length($data->{done})) { - $extramessage= "(By the way, this $gBug is currently marked as done.)\n"; - } - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - actions_taken => {forwarded => 1}, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - $data->{forwarded}= $whereto; - } while (&getnextbug); - } + my $forward_to= $2; + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + $bug_affected{$ref} = 1; + eval { + set_forwarded(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + forwarded => $forward_to, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the forwarded-to-address of $ref: $@"; + } } elsif (m/^notforwarded\s+\#?(-?\d+)$/i) { $ok++; $ref= $1; - $bug_affected{$ref}=1; - if (&setbug) { - if (!length($data->{forwarded})) { - print {$transcript} "$gBug is not marked as having been forwarded.\n\n"; - &nochangebug; - } else { - $action= "Removed annotation that $gBug had been forwarded to $data->{forwarded}."; - do { - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - $data->{forwarded}= ''; - } while (&getnextbug); - } - } - } elsif (m/^severity\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i || - m/^priority\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) { + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + $bug_affected{$ref} = 1; + eval { + set_forwarded(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + affected_packages => \%affected_packages, + recipients => \%recipients, + forwarded => undef, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to clear the forwarded-to-address of $ref: $@"; + } + } elsif (m/^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) { $ok++; $ref= $1; $bug_affected{$ref}=1; @@ -997,34 +1038,23 @@ END } elsif (m/^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/i) { $ok++; $ref= $1; my $newtitle= $2; - $bug_affected{$ref}=1; - if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) { - $ref = $clonebugs{$ref}; + $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; + $bug_affected{$ref} = 1; + eval { + set_title(bug => $ref, + transcript => $transcript, + ($dl > 0 ? (debug => $transcript):()), + requester => $header{from}, + request_addr => $controlrequestaddr, + message => \@log, + recipients => \%recipients, + title => $newtitle, + ); + }; + if ($@) { + $errors++; + print {$transcript} "Failed to set the title of $ref: $@"; } - if (&getbug) { - if (&checkpkglimit) { - &foundbug; - $affected_packages{$data->{package}} = 1; - add_recipients(data => $data, - recipients => \%recipients, - transcript => $transcript, - ($dl > 0 ? (debug => $transcript):()), - ); - my $oldtitle = $data->{subject}; - $data->{subject}= $newtitle; - $action= "Changed $gBug title to `$newtitle' from `$oldtitle'."; - &savebug; - print {$transcript} "$action\n"; - if (length($data->{done})) { - print {$transcript} "(By the way, that $gBug is currently marked as done.)\n"; - } - print {$transcript} "\n"; - } else { - &cancelbug; - } - } else { - ¬foundbug; - } } elsif (m/^unmerge\s+\#?(-?\d+)$/i) { $ok++; $ref= $1; @@ -1282,7 +1312,7 @@ END }; if ($@) { $errors++; - print {$transcript} "Failed to give $ref a summary: $@"; + print {$transcript} "Failed to mark $ref as affecting package(s): $@"; } } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) { @@ -1796,6 +1826,11 @@ our $doc; sub sendtxthelpraw { my ($relpath,$description) = @_; $doc=''; + if (not -e "$gDocDir/$relpath") { + print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n"; + warn "Help text $gDocDir/$relpath not found"; + return; + } open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!"; while() { $doc.=$_; } close(D);