]> git.donarmstrong.com Git - debbugs.git/blobdiff - Debbugs/Control.pm
move Debbugs to lib
[debbugs.git] / Debbugs / Control.pm
diff --git a/Debbugs/Control.pm b/Debbugs/Control.pm
deleted file mode 100644 (file)
index 1f8b3aa..0000000
+++ /dev/null
@@ -1,3919 +0,0 @@
-# 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 <don@donarmstrong.com>.
-
-package Debbugs::Control;
-
-=head1 NAME
-
-Debbugs::Control -- Routines for modifying the state of bugs
-
-=head1 SYNOPSIS
-
-use Debbugs::Control;
-
-
-=head1 DESCRIPTION
-
-This module is an abstraction of a lot of functions which originally
-were only present in service.in, but as time has gone on needed to be
-called from elsewhere.
-
-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<append_log> (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 Exporter qw(import);
-
-BEGIN{
-     $VERSION = 1.00;
-     $DEBUG = 0 unless defined $DEBUG;
-
-     @EXPORT = ();
-     %EXPORT_TAGS = (done    => [qw(set_done)],
-                    submitter => [qw(set_submitter)],
-                    severity => [qw(set_severity)],
-                    affects => [qw(affects)],
-                    summary => [qw(summary)],
-                    outlook => [qw(outlook)],
-                    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)],
-                    block   => [qw(set_blocks)],
-                    merge   => [qw(set_merged)],
-                    tag     => [qw(set_tag)],
-                    clone   => [qw(clone_bug)],
-                    archive => [qw(bug_archive bug_unarchive),
-                               ],
-                    limit   => [qw(check_limit)],
-                    log     => [qw(append_action_to_log),
-                               ],
-                   );
-     @EXPORT_OK = ();
-     Exporter::export_ok_tags(keys %EXPORT_TAGS);
-     $EXPORT_TAGS{all} = [@EXPORT_OK];
-}
-
-use Debbugs::Config qw(:config);
-use Debbugs::Common qw(:lock buglog :misc get_hashname sort_versions);
-use Debbugs::UTF8;
-use Debbugs::Status qw(bug_archiveable :read :hook writebug new_bug splitpackages split_status_fields get_bug_status);
-use Debbugs::CGI qw(html_escape);
-use Debbugs::Log qw(:misc :write);
-use Debbugs::Recipients qw(:add);
-use Debbugs::Packages qw(:versions :mapping);
-
-use Data::Dumper qw();
-use Params::Validate qw(validate_with :types);
-use File::Path qw(mkpath);
-use File::Copy qw(copy);
-use IO::File;
-
-use Debbugs::Text qw(:templates);
-
-use Debbugs::Mail qw(rfc822_date send_mail_message default_headers encode_headers);
-use Debbugs::MIME qw(create_mime_message);
-
-use Mail::RFC822::Address qw();
-
-use POSIX qw(strftime);
-
-use Storable qw(dclone nfreeze);
-use List::AllUtils qw(first max);
-use Encode qw(encode_utf8);
-
-use Carp;
-
-# These are a set of options which are common to all of these functions
-
-my %common_options = (debug       => {type => SCALARREF|HANDLE,
-                                     optional => 1,
-                                    },
-                     transcript  => {type => SCALARREF|HANDLE,
-                                     optional => 1,
-                                    },
-                     affected_bugs => {type => HASHREF,
-                                       optional => 1,
-                                      },
-                     affected_packages => {type => HASHREF,
-                                           optional => 1,
-                                          },
-                     recipients    => {type => HASHREF,
-                                       default => {},
-                                      },
-                     limit         => {type => HASHREF,
-                                       default => {},
-                                      },
-                     show_bug_info => {type => BOOLEAN,
-                                       default => 1,
-                                      },
-                     request_subject => {type => SCALAR,
-                                         default => 'Unknown Subject',
-                                        },
-                     request_msgid    => {type => SCALAR,
-                                          default => '',
-                                         },
-                     request_nn       => {type => SCALAR,
-                                          optional => 1,
-                                         },
-                     request_replyto   => {type => SCALAR,
-                                           optional => 1,
-                                          },
-                     locks             => {type => HASHREF,
-                                           optional => 1,
-                                          },
-                    );
-
-
-my %append_action_options =
-     (action => {type => SCALAR,
-                optional => 1,
-               },
-      requester => {type => SCALAR,
-                   optional => 1,
-                  },
-      request_addr => {type => SCALAR,
-                      optional => 1,
-                     },
-      location => {type => SCALAR,
-                  optional => 1,
-                 },
-      message  => {type => SCALAR|ARRAYREF,
-                  optional => 1,
-                 },
-      append_log => {type => BOOLEAN,
-                    optional => 1,
-                    depends => [qw(requester request_addr),
-                                qw(message),
-                               ],
-                   },
-      # locks is both an append_action option, and a common option;
-      # it's ok for it to be in both places.
-      locks     => {type => HASHREF,
-                   optional => 1,
-                  },
-     );
-
-our $locks = 0;
-
-
-# this is just a generic stub for Debbugs::Control functions.
-#
-# =head2 set_foo
-#
-#      eval {
-#          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 set foo $ref bar: $@";
-#      }
-#
-# Foo frobinates
-#
-# =cut
-#
-# 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 ($debug,$transcript) =
-#      @info{qw(debug transcript)};
-#     my @data = @{$info{data}};
-#     my @bugs = @{$info{bugs}};
-#
-#     my $action = '';
-#     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_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 ($param{add}) {
-       $mode = 'add';
-    }
-    elsif ($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->{archived}) {
-           $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') {
-       __end_control(%info);
-       croak "Unknown/archived blocking bug(s):".join(', ',keys %bad_blockers).
-           keys %ok_blockers?'':" and no good 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) {
-           __end_control(%info);
-           croak "Unknown/archived 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}) {
-           __end_control(%info);
-           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;
-           }
-       }
-    }
-    for my $data (@data) {
-       my $old_data = dclone($data);
-       # remove blockers and/or add new ones as appropriate
-       if ($data->{blockedby} eq '') {
-           print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
-       } else {
-           print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
-       }
-       if ($data->{blocks} eq '') {
-           print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
-       } else {
-           print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\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 blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
-           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;
-    my $new_locks = 0;
-    for my $add_remove (keys %mungable_blocks) {
-       my %munge_blockers;
-       for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
-           next if $munge_blockers{$blocker};
-           my ($temp_locks, @blocking_data) =
-               lock_read_all_merged_bugs(bug => $blocker,
-                                         ($param{archived}?(location => 'archive'):()),
-                                         exists $param{locks}?(locks => $param{locks}):(),
-                                        );
-           $locks+= $temp_locks;
-           $new_locks+=$temp_locks;
-           if (not @blocking_data) {
-               for (1..$new_locks) {
-                   unfilelock(exists $param{locks}?$param{locks}:());
-                   $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
-                                                                  )
-                                   );
-               writebug($data->{bug_num},$data);
-           }
-           __handle_affected_packages(%param,data=>\@blocking_data);
-           add_recipients(recipients => $param{recipients},
-                          actions_taken => {blocks => 1},
-                          data       => \@blocking_data,
-                          debug      => $debug,
-                          transcript => $transcript,
-                         );
-
-           for (1..$new_locks) {
-               unfilelock(exists $param{locks}?$param{locks}:());
-               $locks--;
-           }
-       }
-    }
-    __end_control(%info);
-}
-
-
-
-=head2 set_tag
-
-     eval {
-           set_tag(bug          => $ref,
-                   transcript   => $transcript,
-                   ($dl > 0 ? (debug => $transcript):()),
-                   requester    => $header{from},
-                   request_addr => $controlrequestaddr,
-                   message      => \@log,
-                    affected_packages => \%affected_packages,
-                   recipients   => \%recipients,
-                   tag          => [],
-                    add          => 1,
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set tag on $ref: $@";
-       }
-
-
-Sets, adds, or removes the specified tags on a bug
-
-=over
-
-=item tag -- scalar or arrayref of tags to set, add or remove
-
-=item add -- if true, add tags
-
-=item remove -- if true, remove tags
-
-=item warn_on_bad_tags -- if true (the default) warn if bad tags are
-passed.
-
-=back
-
-=cut
-
-sub set_tag {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        # specific options here
-                                        tag    => {type => SCALAR|ARRAYREF,
-                                                   default => [],
-                                                  },
-                                        add      => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        remove   => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        warn_on_bad_tags => {type => BOOLEAN,
-                                                             default => 1,
-                                                            },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    if ($param{add} and $param{remove}) {
-       croak "It's nonsensical to add and remove the same tags";
-    }
-
-    my %info =
-       __begin_control(%param,
-                       command  => 'tag'
-                      );
-    my $transcript = $info{transcript};
-    my @data = @{$info{data}};
-    my @tags = make_list($param{tag});
-    if (not @tags and ($param{remove} or $param{add})) {
-       if ($param{remove}) {
-           print {$transcript} "Requested to remove no tags; doing nothing.\n";
-       }
-       else {
-           print {$transcript} "Requested to add no tags; doing nothing.\n";
-       }
-       __end_control(%info);
-       return;
-    }
-    # first things first, make the versions fully qualified source
-    # versions
-    for my $data (@data) {
-       my $action = 'Did not alter tags';
-       my %tag_added = ();
-       my %tag_removed = ();
-       my @old_tags = split /\,?\s+/, $data->{keywords};
-       my %tags;
-       @tags{@old_tags} = (1) x @old_tags;
-       my $old_data = dclone($data);
-       if (not $param{add} and not $param{remove}) {
-           $tag_removed{$_} = 1 for @old_tags;
-           %tags = ();
-       }
-       my @bad_tags = ();
-       for my $tag (@tags) {
-           if (not $param{remove} and
-               not defined first {$_ eq $tag} @{$config{tags}}) {
-               push @bad_tags, $tag;
-               next;
-           }
-           if ($param{add}) {
-               if (not exists $tags{$tag}) {
-                   $tags{$tag} = 1;
-                   $tag_added{$tag} = 1;
-               }
-           }
-           elsif ($param{remove}) {
-               if (exists $tags{$tag}) {
-                   delete $tags{$tag};
-                   $tag_removed{$tag} = 1;
-               }
-           }
-           else {
-               if (exists $tag_removed{$tag}) {
-                   delete $tag_removed{$tag};
-               }
-               else {
-                   $tag_added{$tag} = 1;
-               }
-               $tags{$tag} = 1;
-           }
-       }
-       if (@bad_tags and $param{warn_on_bad_tags}) {
-           print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
-           print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
-       }
-       $data->{keywords} = join(' ',keys %tags);
-
-       my @changed;
-       push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
-       push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
-       $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";
-           next;
-       }
-       $action .= '.';
-       append_action_to_log(bug => $data->{bug_num},
-                            get_lock => 0,
-                            command  => 'tag',
-                            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_severity
-
-     eval {
-           set_severity(bug          => $ref,
-                        transcript   => $transcript,
-                        ($dl > 0 ? (debug => $transcript):()),
-                        requester    => $header{from},
-                        request_addr => $controlrequestaddr,
-                        message      => \@log,
-                         affected_packages => \%affected_packages,
-                        recipients   => \%recipients,
-                        severity     => 'normal',
-                        );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set the severity of bug $ref: $@";
-       }
-
-Sets the severity of a bug. If severity is not passed, is undefined,
-or has zero length, sets the severity to the default severity.
-
-=cut
-
-sub set_severity {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        # specific options here
-                                        severity => {type => SCALAR|UNDEF,
-                                                     default => $config{default_severity},
-                                                    },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    if (not defined $param{severity} or
-       not length $param{severity}
-       ) {
-       $param{severity} = $config{default_severity};
-    }
-
-    # check validity of new severity
-    if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
-       die "Severity '$param{severity}' is not a valid severity level";
-    }
-    my %info =
-       __begin_control(%param,
-                       command  => 'severity'
-                      );
-    my $transcript = $info{transcript};
-    my @data = @{$info{data}};
-
-    my $action = '';
-    for my $data (@data) {
-       if (not defined $data->{severity}) {
-           $data->{severity} = $param{severity};
-           $action = "Severity set to '$param{severity}'";
-       }
-       else {
-           if ($data->{severity} eq '') {
-               $data->{severity} = $config{default_severity};
-           }
-           if ($data->{severity} eq $param{severity}) {
-               print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
-               next;
-           }
-           $action = "Severity set to '$param{severity}' from '$data->{severity}'";
-           $data->{severity} = $param{severity};
-       }
-       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_done
-
-     eval {
-           set_done(bug          => $ref,
-                    transcript   => $transcript,
-                    ($dl > 0 ? (debug => $transcript):()),
-                    requester    => $header{from},
-                    request_addr => $controlrequestaddr,
-                    message      => \@log,
-                     affected_packages => \%affected_packages,
-                    recipients   => \%recipients,
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set foo $ref bar: $@";
-       }
-
-Foo frobinates
-
-=cut
-
-sub set_done {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        reopen    => {type => BOOLEAN,
-                                                      default => 0,
-                                                     },
-                                        submitter => {type => SCALAR,
-                                                      optional => 1,
-                                                     },
-                                        clear_fixed => {type => BOOLEAN,
-                                                        default => 1,
-                                                       },
-                                        notify_submitter => {type => BOOLEAN,
-                                                             default => 1,
-                                                            },
-                                        original_report => {type => SCALARREF,
-                                                            optional => 1,
-                                                           },
-                                        done => {type => SCALAR|UNDEF,
-                                                 optional => 1,
-                                                },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-
-    if (exists $param{submitter} and
-       not Mail::RFC822::Address::valid($param{submitter})) {
-       die "New submitter address '$param{submitter}' is not a valid e-mail address";
-    }
-    if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
-       $param{done} = $param{requester};
-    }
-    if (exists $param{done} and
-       (not defined $param{done} or
-        not length $param{done})) {
-       delete $param{done};
-       $param{reopen} = 1;
-    }
-
-    my %info =
-       __begin_control(%param,
-                       command  => $param{reopen}?'reopen':'done',
-                      );
-    my $transcript = $info{transcript};
-    my @data = @{$info{data}};
-    my $action ='';
-
-    if ($param{reopen}) {
-       # avoid warning multiple times if there are fixed versions
-       my $warn_fixed = 1;
-       for my $data (@data) {
-           if (not exists $data->{done} or
-               not defined $data->{done} or
-               not length $data->{done}) {
-               print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
-               __end_control(%info);
-               return;
-           }
-           if (@{$data->{fixed_versions}} and $warn_fixed) {
-               print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
-               print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
-               $warn_fixed = 0;
-           }
-       }
-       $action = "Bug reopened";
-       for my $data (@data) {
-           my $old_data = dclone($data);
-           $data->{done} = '';
-           append_action_to_log(bug => $data->{bug_num},
-                                command => 'done',
-                                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);
-       if (exists $param{submitter}) {
-           set_submitter(bug => $param{bug},
-                         submitter => $param{submitter},
-                         hash_slice(%param,
-                                    keys %common_options,
-                                    keys %append_action_options)
-                        );
-       }
-       # clear the fixed revisions
-       if ($param{clear_fixed}) {
-           set_fixed(fixed => [],
-                     bug => $param{bug},
-                     reopen => 0,
-                     hash_slice(%param,
-                                keys %common_options,
-                                keys %append_action_options),
-                    );
-       }
-    }
-    else {
-       my %submitter_notified;
-       my $orig_report_set = 0;
-       for my $data (@data) {
-           if (exists $data->{done} and
-               defined $data->{done} and
-               length $data->{done}) {
-               print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
-               __end_control(%info);
-               return;
-           }
-       }
-       for my $data (@data) {
-           my $old_data = dclone($data);
-           my $hash = get_hashname($data->{bug_num});
-           my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
-               die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
-           my $orig_report;
-           {
-               local $/;
-               $orig_report= <$report_fh>;
-           }
-           close $report_fh;
-           if (not $orig_report_set and defined $orig_report and
-               length $orig_report and
-               exists $param{original_report}){
-               ${$param{original_report}} = $orig_report;
-               $orig_report_set = 1;
-           }
-
-           $action = "Marked $config{bug} as done";
-
-           # set done to the requester
-           $data->{done} = exists $param{done}?$param{done}:$param{requester};
-           append_action_to_log(bug => $data->{bug_num},
-                                command => 'done',
-                                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";
-           # get the original report
-           if ($param{notify_submitter}) {
-               my $submitter_message;
-               if(not exists $submitter_notified{$data->{originator}}) {
-                   $submitter_message =
-                       create_mime_message([default_headers(queue_file => $param{request_nn},
-                                                            data => $data,
-                                                            msgid => $param{request_msgid},
-                                                            msgtype => 'notifdone',
-                                                            pr_msg  => 'they-closed',
-                                                            headers =>
-                                                            [To => $data->{submitter},
-                                                             Subject => "$config{ubug}#$data->{bug_num} ".
-                                                             "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
-                                                            ],
-                                                           )
-                                           ],
-                                           __message_body_template('mail/process_your_bug_done',
-                                                                   {data     => $data,
-                                                                    replyto  => (exists $param{request_replyto} ?
-                                                                                 $param{request_replyto} :
-                                                                                 $param{requester} || 'Unknown'),
-                                                                    markedby => $param{requester},
-                                                                    subject => $param{request_subject},
-                                                                    messageid => $param{request_msgid},
-                                                                    config   => \%config,
-                                                                   }),
-                                           [join('',make_list($param{message})),$orig_report]
-                                          );
-                   send_mail_message(message => $submitter_message,
-                                     recipients => $old_data->{submitter},
-                                    );
-                   $submitter_notified{$data->{originator}} = $submitter_message;
-               }
-               else {
-                   $submitter_message = $submitter_notified{$data->{originator}};
-               }
-               append_action_to_log(bug => $data->{bug_num},
-                                    action => "Notification sent",
-                                    requester => '',
-                                    request_addr => $data->{originator},
-                                    desc => "$config{bug} acknowledged by developer.",
-                                    recips => [$data->{originator}],
-                                    message => $submitter_message,
-                                    get_lock => 0,
-                                   );
-           }
-       }
-       __end_control(%info);
-       if (exists $param{fixed}) {
-           set_fixed(fixed => $param{fixed},
-                     bug => $param{bug},
-                     reopen => 0,
-                     hash_slice(%param,
-                                keys %common_options,
-                                keys %append_action_options
-                               ),
-                    );
-       }
-    }
-}
-
-
-=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 ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    my $action = '';
-    # here we only concern ourselves with the first of the merged bugs
-    for my $data ($data[0]) {
-       my $notify_old_submitter = 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->{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";
-           next;
-       }
-       else {
-           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->{originator} = $param{submitter};
-       }
-        append_action_to_log(bug => $data->{bug_num},
-                            command => '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";
-       # notify old submitter
-       if ($notify_old_submitter and $param{notify_submitter}) {
-           send_mail_message(message =>
-                             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,
-                                                                         })
-                                                ),
-                             recipients => $old_data->{submitter},
-                            );
-       }
-    }
-    __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";
-    }
-    $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
-    my %info =
-       __begin_control(%param,
-                       command  => 'forwarded'
-                      );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    my $action = '';
-    for my $data (@data) {
-       my $old_data = dclone($data);
-       print {$debug} "Going to change bug forwarded\n";
-       if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
-           (not defined $param{forwarded} and
-            defined $data->{forwarded} and not length $data->{forwarded})) {
-           print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
-           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 => '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 $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";
-           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 => '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 ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    # 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";
-           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 => '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 ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    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;
-               }
-               elsif (not grep {$version eq $_} @svers) {
-                    # The $version was not equal to one of the source
-                    # versions, so it's probably unqualified (or just
-                    # wrong). Delete it, and use the source versions
-                    # instead.
-                   if (exists $found_versions{$version}) {
-                       delete $found_versions{$version};
-                       $found_removed{$version} = 1;
-                   }
-               }
-               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_versions(map {m{([^/]+)$}; $1;}
-                                                   @svers);
-                   # determine if we need to reopen
-                   my @fixed_order = sort_versions(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 = 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";
-           next;
-       }
-       $action .= '.';
-       append_action_to_log(bug => $data->{bug_num},
-                            get_lock => 0,
-                            command  => '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 fixed versions of a package
-
-If the fixed versions are empty (or end up being empty after this
-call) or the greatest fixed version is less than the greatest found
-version and the reopen option is true, the bug is reopened.
-
-This function is also called by the reopen function, which causes all
-of the fixed versions to be 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 ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    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;
-               }
-               else {
-                   if (exists $fixed_versions{$version}) {
-                       $fixed_removed{$version} = 1;
-                       delete $fixed_versions{$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 = 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";
-           next;
-       }
-       $action .= '.';
-       append_action_to_log(bug => $data->{bug_num},
-                            command  => '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 set_merged
-
-     eval {
-           set_merged(bug          => $ref,
-                      transcript   => $transcript,
-                      ($dl > 0 ? (debug => $transcript):()),
-                      requester    => $header{from},
-                      request_addr => $controlrequestaddr,
-                      message      => \@log,
-                       affected_packages => \%affected_packages,
-                      recipients   => \%recipients,
-                      merge_with   => 12345,
-                       add          => 1,
-                       force        => 1,
-                       allow_reassign => 1,
-                       reassign_same_source_only => 1,
-                      );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to set merged on $ref: $@";
-       }
-
-
-Sets, adds, or removes the specified merged bugs of a bug
-
-By default, requires
-
-=cut
-
-sub set_merged {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        # specific options here
-                                        merge_with => {type => ARRAYREF|SCALAR,
-                                                       optional => 1,
-                                                      },
-                                        remove   => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        force    => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        masterbug => {type => BOOLEAN,
-                                                      default => 0,
-                                                     },
-                                        allow_reassign => {type => BOOLEAN,
-                                                           default => 0,
-                                                          },
-                                        reassign_different_sources => {type => BOOLEAN,
-                                                                       default => 1,
-                                                                      },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
-    my %merging;
-    @merging{@merging} = (1) x @merging;
-    if (grep {$_ !~ /^\d+$/} @merging) {
-       croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
-    }
-    $param{locks} = {} if not exists $param{locks};
-    my %info =
-       __begin_control(%param,
-                       command  => 'merge'
-                      );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    if (not @merging and exists $param{merge_with}) {
-       print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
-       __end_control(%info);
-       return;
-    }
-    my @data = @{$info{data}};
-    my %data;
-    my %merged_bugs;
-    for my $data (@data) {
-       $data{$data->{bug_num}} = $data;
-       my @merged_bugs = split / /, $data->{mergedwith};
-       @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
-    }
-    # handle unmerging
-    my $new_locks = 0;
-    if (not exists $param{merge_with}) {
-       delete $merged_bugs{$param{bug}};
-       if (not keys %merged_bugs) {
-           print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
-           __end_control(%info);
-           return;
-       }
-       my $action = "Disconnected #$param{bug} from all other report(s).";
-       for my $data (@data) {
-           my $old_data = dclone($data);
-           if ($data->{bug_num} == $param{bug}) {
-               $data->{mergedwith} = '';
-           }
-           else {
-               $data->{mergedwith} =
-                   join(' ',
-                        sort {$a <=> $b}
-                        grep {$_ != $data->{bug_num}}
-                        keys %merged_bugs);
-           }
-           append_action_to_log(bug => $data->{bug_num},
-                                command  => 'merge',
-                                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);
-       return;
-    }
-    # lock and load all of the bugs we need
-    my ($data,$n_locks) =
-       __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
-                                   data => \@data,
-                                   locks => $param{locks},
-                                   debug => $debug,
-                                  );
-    $new_locks += $n_locks;
-    %data = %{$data};
-    @data = values %data;
-    if (not check_limit(data => [@data],
-                         exists $param{limit}?(limit => $param{limit}):(),
-                         transcript => $transcript,
-                        )) {
-       die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
-    }
-    for my $data (@data) {
-       $data{$data->{bug_num}} = $data;
-       $merged_bugs{$data->{bug_num}} = 1;
-       my @merged_bugs = split / /, $data->{mergedwith};
-       @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
-       if (exists $param{affected_bugs}) {
-           $param{affected_bugs}{$data->{bug_num}} = 1;
-       }
-    }
-    __handle_affected_packages(%param,data => [@data]);
-    my %bug_info_shown; # which bugs have had information shown
-    $bug_info_shown{$param{bug}} = 1;
-    add_recipients(data => [@data],
-                  recipients => $param{recipients},
-                  (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
-                  debug      => $debug,
-                  (__internal_request()?(transcript => $transcript):()),
-                 );
-
-    # Figure out what the ideal state is for the bug, 
-    my ($merge_status,$bugs_to_merge) =
-       __calculate_merge_status(\@data,\%data,$param{bug});
-    # find out if we actually have any bugs to merge
-    if (not $bugs_to_merge) {
-       print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
-       for (1..$new_locks) {
-           unfilelock($param{locks});
-           $locks--;
-       }
-       __end_control(%info);
-       return;
-    }
-    # see what changes need to be made to merge the bugs
-    # check to make sure that the set of changes we need to make is allowed
-    my ($disallowed_changes,$changes) = 
-       __calculate_merge_changes(\@data,$merge_status,\%param);
-    # at this point, stop if there are disallowed changes, otherwise
-    # make the allowed changes, and then reread the bugs in question
-    # to get the new data, then recaculate the merges; repeat
-    # reloading and recalculating until we try too many times or there
-    # are no changes to make.
-
-    my $attempts = 0;
-    # we will allow at most 4 times through this; more than 1
-    # shouldn't really happen.
-    my %bug_changed;
-    while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
-       if ($attempts > 1) {
-           print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
-       }
-       if (@{$disallowed_changes}) {
-           # figure out the problems
-           print {$transcript} "Unable to merge bugs because:\n";
-           for my $change (@{$disallowed_changes}) {
-               print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
-           }
-           if ($attempts > 0) {
-               __end_control(%info);
-               croak "Some bugs were altered while attempting to merge";
-           }
-           else {
-               __end_control(%info);
-               croak "Did not alter merged bugs";
-           }
-       }
-       my @bugs_to_change = keys %{$changes};
-       for my $change_bug (@bugs_to_change) {
-           next unless exists $changes->{$change_bug};
-           $bug_changed{$change_bug}++;
-           print {$transcript} __bug_info($data{$change_bug}) if
-               $param{show_bug_info} and not __internal_request(1);
-           $bug_info_shown{$change_bug} = 1;
-           __allow_relocking($param{locks},[keys %data]);
-           eval {
-           for my $change (@{$changes->{$change_bug}}) {
-               if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
-                   my %target_blockedby;
-                   @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
-                   my %unhandled_targets = %target_blockedby;
-                   for my $key (split / /,$change->{orig_value}) {
-                       delete $unhandled_targets{$key};
-                       next if exists $target_blockedby{$key};
-                       set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
-                                  block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
-                                  remove => 1,
-                                  hash_slice(%param,
-                                             keys %common_options,
-                                             keys %append_action_options),
-                                 );
-                   }
-                   for my $key (keys %unhandled_targets) {
-                       set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
-                                  block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
-                                  add   => 1,
-                                  hash_slice(%param,
-                                             keys %common_options,
-                                             keys %append_action_options),
-                                 );
-                   }
-               }
-               else {
-                   $change->{function}->(bug => $change->{bug},
-                                         $change->{key}, $change->{func_value},
-                                         exists $change->{options}?@{$change->{options}}:(),
-                                         hash_slice(%param,
-                                                    keys %common_options,
-                                                    keys %append_action_options),
-                                        );
-               }
-           }
-       };
-           if ($@) {
-               __disallow_relocking($param{locks});
-               __end_control(%info);
-               croak "Failure while trying to adjust bugs, please report this as a bug: $@";
-           }
-           __disallow_relocking($param{locks});
-           my ($data,$n_locks) =
-               __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
-                                           data => \@data,
-                                           locks => $param{locks},
-                                           debug => $debug,
-                                           reload_all => 1,
-                                          );
-           $new_locks += $n_locks;
-           $locks += $n_locks;
-           %data = %{$data};
-           @data = values %data;
-           ($merge_status,$bugs_to_merge) =
-               __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
-           ($disallowed_changes,$changes) = 
-               __calculate_merge_changes(\@data,$merge_status,\%param);
-           $attempts = max(values %bug_changed);
-       }
-    }
-    if ($param{show_bug_info} and not __internal_request(1)) {
-       for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
-           next if $bug_info_shown{$data->{bug_num}};
-           print {$transcript} __bug_info($data);
-       }
-    }
-    if (keys %{$changes} or @{$disallowed_changes}) {
-       print {$transcript} "After four attempts, the following changes were unable to be made:\n";
-       for (1..$new_locks) {
-           unfilelock($param{locks});
-           $locks--;
-       }
-       __end_control(%info);
-       for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
-           print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
-       }
-       die "Unable to modify bugs so they could be merged";
-       return;
-    }
-
-    # finally, we can merge the bugs
-    my $action = "Merged ".join(' ',sort { $a <=> $b } keys %merged_bugs);
-    for my $data (@data) {
-       my $old_data = dclone($data);
-       $data->{mergedwith} =
-           join(' ',
-                sort { $a <=> $b }
-                grep {$_ != $data->{bug_num}}
-                keys %merged_bugs);
-       append_action_to_log(bug => $data->{bug_num},
-                            command  => 'merge',
-                            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";
-    # unlock the extra locks that we got earlier
-    for (1..$new_locks) {
-       unfilelock($param{locks});
-       $locks--;
-    }
-    __end_control(%info);
-}
-
-sub __allow_relocking{
-    my ($locks,$bugs) = @_;
-
-    my @locks = (@{$bugs},'merge');
-    for my $lock (@locks) {
-       my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
-       next unless @lockfiles;
-       $locks->{relockable}{$lockfiles[0]} = 0;
-    }
-}
-
-sub __disallow_relocking{
-    my ($locks) = @_;
-    delete $locks->{relockable};
-}
-
-sub __lock_and_load_merged_bugs{
-    my %param =
-       validate_with(params => \@_,
-                     spec =>
-                     {bugs_to_load => {type => ARRAYREF,
-                                       default => sub {[]},
-                                      },
-                      data         => {type => HASHREF|ARRAYREF,
-                                      },
-                      locks        => {type => HASHREF,
-                                       default => sub {{};},
-                                      },
-                      reload_all => {type => BOOLEAN,
-                                     default => 0,
-                                    },
-                      debug           => {type => HANDLE,
-                                         },
-                     },
-                    );
-    my %data;
-    my $new_locks = 0;
-    if (ref($param{data}) eq 'ARRAY') {
-       for my $data (@{$param{data}}) {
-           $data{$data->{bug_num}} = dclone($data);
-       }
-    }
-    else {
-       %data = %{dclone($param{data})};
-    }
-    my @bugs_to_load = @{$param{bugs_to_load}};
-    if ($param{reload_all}) {
-       push @bugs_to_load, keys %data;
-    }
-    my %temp;
-    @temp{@bugs_to_load} = (1) x @bugs_to_load;
-    @bugs_to_load = keys %temp;
-    my %loaded_this_time;
-    my $bug_to_load;
-    while ($bug_to_load = shift @bugs_to_load) {
-       if (not $param{reload_all}) {
-           next if exists $data{$bug_to_load};
-       }
-       else {
-           next if $loaded_this_time{$bug_to_load};
-       }
-       my $lock_bug = 1;
-       if ($param{reload_all}) {
-           if (exists $data{$bug_to_load}) {
-               $lock_bug = 0;
-           }
-       }
-       my $data =
-           read_bug(bug => $bug_to_load,
-                    lock => $lock_bug,
-                    locks => $param{locks},
-                   ) or
-                       die "Unable to load bug $bug_to_load";
-       print {$param{debug}} "read bug $bug_to_load\n";
-       $data{$data->{bug_num}} = $data;
-       $new_locks += $lock_bug;
-       $loaded_this_time{$data->{bug_num}} = 1;
-       push @bugs_to_load,
-           grep {not exists $data{$_}}
-               split / /,$data->{mergedwith};
-    }
-    return (\%data,$new_locks);
-}
-
-
-sub __calculate_merge_status{
-    my ($data_a,$data_h,$master_bug,$merge_status) = @_;
-    my %merge_status = %{$merge_status // {}};
-    my %merged_bugs;
-    my $bugs_to_merge = 0;
-    for my $data (@{$data_a}) {
-       # check to see if this bug is unmerged in the set
-       if (not length $data->{mergedwith} or
-           grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
-           $merged_bugs{$data->{bug_num}} = 1;
-           $bugs_to_merge = 1;
-       }
-    }
-    for my $data (@{$data_a}) {
-       # the master_bug is the bug that every other bug is made to
-       # look like. However, if merge is set, tags, fixed and found
-       # are merged.
-       if ($data->{bug_num} == $master_bug) {
-           for (qw(package forwarded severity done owner summary outlook affects)) {
-               $merge_status{$_} = $data->{$_}
-           }
-           # bugs which are in the newly merged set and are also
-           # blocks/blockedby must be removed before merging
-           for (qw(blocks blockedby)) {
-               $merge_status{$_} =
-                   join(' ',grep {not exists $merged_bugs{$_}}
-                        split / /,$data->{$_});
-           }
-       }
-       if (defined $merge_status) {
-           next unless $data->{bug_num} == $master_bug;
-       }
-       $merge_status{tag} = {} if not exists $merge_status{tag};
-       for my $tag (split /\s+/, $data->{keywords}) {
-           $merge_status{tag}{$tag} = 1;
-       }
-       $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
-       for (qw(fixed found)) {
-           @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
-       }
-    }
-    # if there is a non-source qualified version with a corresponding
-    # source qualified version, we only want to merge the source
-    # qualified version(s)
-    for (qw(fixed found)) {
-       my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
-       for my $unqualified_version (@unqualified_versions) {
-           if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
-               delete $merge_status{"${_}_versions"}{$unqualified_version};
-           }
-       }
-    }
-    return (\%merge_status,$bugs_to_merge);
-}
-
-
-
-sub __calculate_merge_changes{
-    my ($datas,$merge_status,$param) = @_;
-    my %changes;
-    my @disallowed_changes;
-    for my $data (@{$datas}) {
-       # things that can be forced
-       #
-       # * func is the function to set the new value
-       #
-       # * key is the key of the function to set the value,
-
-       # * modify_value is a function which is called to modify the new
-       # value so that the function will accept it
-
-        # * options is an ARRAYREF of options to pass to the function
-
-       # * allowed is a BOOLEAN which controls whether this setting
-       # is allowed to be different by default.
-       my %force_functions =
-           (forwarded => {func => \&set_forwarded,
-                          key  => 'forwarded',
-                          options => [],
-                         },
-            severity  => {func => \&set_severity,
-                          key  => 'severity',
-                          options => [],
-                         },
-            blocks    => {func => \&set_blocks,
-                          modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
-                          key  => 'block',
-                          options => [],
-                         },
-            blockedby => {func => \&set_blocks,
-                          modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
-                          key  => 'block',
-                          options => [],
-                         },
-            done      => {func => \&set_done,
-                          key  => 'done',
-                          options => [],
-                         },
-            owner     => {func => \&owner,
-                          key  => 'owner',
-                          options => [],
-                         },
-            summary   => {func => \&summary,
-                          key  => 'summary',
-                          options => [],
-                         },
-            outlook   => {func => \&outlook,
-                          key  => 'outlook',
-                          options => [],
-                         },
-            affects   => {func => \&affects,
-                          key  => 'package',
-                          options => [],
-                         },
-            package   => {func => \&set_package,
-                          key  => 'package',
-                          options => [],
-                         },
-            keywords   => {func => \&set_tag,
-                           key  => 'tag',
-                           modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
-                           allowed => 1,
-                          },
-            fixed_versions => {func => \&set_fixed,
-                               key => 'fixed',
-                               modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
-                               allowed => 1,
-                              },
-            found_versions => {func => \&set_found,
-                               key   => 'found',
-                               modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
-                               allowed => 1,
-                              },
-           );
-       for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
-           # if the ideal bug already has the field set properly, we
-           # continue on.
-           if ($field eq 'keywords'){
-               next if join(' ',sort split /\s+/,$data->{keywords}) eq
-                   join(' ',sort keys %{$merge_status->{tag}});
-           }
-           elsif ($field =~ /^(?:fixed|found)_versions$/) {
-               next if join(' ', sort @{$data->{$field}}) eq
-                   join(' ',sort keys %{$merge_status->{$field}});
-           }
-           elsif ($field eq 'done') {
-               # for done, we only care if the bug is done or not
-               # done, not the value it's set to.
-               if (defined $merge_status->{$field} and length $merge_status->{$field} and
-                   defined $data->{$field}         and length $data->{$field}) {
-                   next;
-               }
-               elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
-                      (not defined $data->{$field}         or not length $data->{$field})
-                     ) {
-                   next;
-               }
-           }
-           elsif ($merge_status->{$field} eq $data->{$field}) {
-               next;
-           }
-           my $change =
-               {field => $field,
-                bug => $data->{bug_num},
-                orig_value => $data->{$field},
-                func_value   =>
-                (exists $force_functions{$field}{modify_value} ?
-                 $force_functions{$field}{modify_value}->($merge_status->{$field}):
-                 $merge_status->{$field}),
-                value    => $merge_status->{$field},
-                function => $force_functions{$field}{func},
-                key      => $force_functions{$field}{key},
-                options  => $force_functions{$field}{options},
-                allowed  => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
-               };
-           $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
-           $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
-           if ($param->{force} or $change->{allowed}) {
-               if ($field ne 'package' or $change->{allowed}) {
-                   push @{$changes{$data->{bug_num}}},$change;
-                   next;
-               }
-               if ($param->{allow_reassign}) {
-                   if ($param->{reassign_different_sources}) {
-                       push @{$changes{$data->{bug_num}}},$change;
-                       next;
-                   }
-                   # allow reassigning if binary_to_source returns at
-                   # least one of the same source packages
-                   my @merge_status_source =
-                       binary_to_source(package => $merge_status->{package},
-                                        source_only => 1,
-                                       );
-                   my @other_bug_source =
-                       binary_to_source(package => $data->{package},
-                                        source_only => 1,
-                                       );
-                   my %merge_status_sources;
-                   @merge_status_sources{@merge_status_source} =
-                       (1) x @merge_status_source;
-                   if (grep {$merge_status_sources{$_}} @other_bug_source) {
-                       push @{$changes{$data->{bug_num}}},$change;
-                       next;
-                   }
-               }
-           }
-           push @disallowed_changes,$change;
-       }
-       # blocks and blocked by are weird; we have to go through and
-       # set blocks to the other half of the merged bugs
-    }
-    return (\@disallowed_changes,\%changes);
-}
-
-=head2 affects
-
-     eval {
-           affects(bug          => $ref,
-                   transcript   => $transcript,
-                   ($dl > 0 ? (debug => $transcript):()),
-                   requester    => $header{from},
-                   request_addr => $controlrequestaddr,
-                   message      => \@log,
-                    affected_packages => \%affected_packages,
-                   recipients   => \%recipients,
-                   packages     => undef,
-                    add          => 1,
-                    remove       => 0,
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as affecting $packages: $@";
-       }
-
-This marks a bug as affecting packages which the bug is not actually
-in. This should only be used in cases where fixing the bug instantly
-resolves the problem in the other packages.
-
-By default, the packages are set to the list of packages passed.
-However, if you pass add => 1 or remove => 1, the list of packages
-passed are added or removed from the affects list, respectively.
-
-=cut
-
-sub affects {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        # specific options here
-                                        package => {type => SCALAR|ARRAYREF|UNDEF,
-                                                    default => [],
-                                                   },
-                                        add      => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        remove   => {type => BOOLEAN,
-                                                     default => 0,
-                                                    },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    if ($param{add} and $param{remove}) {
-        croak "Asking to both add and remove affects is nonsensical";
-    }
-    if (not defined $param{package}) {
-       $param{package} = [];
-    }
-    my %info =
-       __begin_control(%param,
-                       command  => 'affects'
-                      );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    my $action = '';
-    for my $data (@data) {
-       $action = '';
-        print {$debug} "Going to change affects\n";
-        my @packages = splitpackages($data->{affects});
-        my %packages;
-        @packages{@packages} = (1) x @packages;
-        if ($param{add}) {
-             my @added = ();
-             for my $package (make_list($param{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(\@added);
-             }
-        }
-        elsif ($param{remove}) {
-             my @removed = ();
-             for my $package (make_list($param{package})) {
-                  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(\@removed);
-        }
-        else {
-             my %added_packages = ();
-             my %removed_packages = %packages;
-             %packages = ();
-             for my $package (make_list($param{package})) {
-                  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([keys %added_packages]);
-             }
-        }
-       if (not length $action) {
-           print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
-           next;
-       }
-        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,
-                                                           ),
-                            )
-              if not exists $param{append_log} or $param{append_log};
-         writebug($data->{bug_num},$data);
-         print {$transcript} "$action\n";
-     }
-    __end_control(%info);
-}
-
-
-=head1 SUMMARY FUNCTIONS
-
-=head2 summary
-
-     eval {
-           summary(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 mark $ref with summary foo: $@";
-       }
-
-Handles all setting of summary fields
-
-If summary is undef, unsets the summary
-
-If summary is 0 or -1, sets the summary to the first paragraph contained in
-the message passed.
-
-If summary is a positive integer, sets the summary to the message specified.
-
-Otherwise, sets summary to the value passed.
-
-=cut
-
-
-sub summary {
-    # outlook and summary are exactly the same, basically
-    return _summary('summary',@_);
-}
-
-=head1 OUTLOOK FUNCTIONS
-
-=head2 outlook
-
-     eval {
-           outlook(bug          => $ref,
-                   transcript   => $transcript,
-                   ($dl > 0 ? (debug => $transcript):()),
-                   requester    => $header{from},
-                   request_addr => $controlrequestaddr,
-                   message      => \@log,
-                    affected_packages => \%affected_packages,
-                   recipients   => \%recipients,
-                   outlook      => undef,
-                   );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref with outlook foo: $@";
-       }
-
-Handles all setting of outlook fields
-
-If outlook is undef, unsets the outlook
-
-If outlook is 0, sets the outlook to the first paragraph contained in
-the message passed.
-
-If outlook is a positive integer, sets the outlook to the message specified.
-
-Otherwise, sets outlook to the value passed.
-
-=cut
-
-
-sub outlook {
-    return _summary('outlook',@_);
-}
-
-sub _summary {
-    my ($cmd,@params) = @_;
-    my %param = validate_with(params => \@params,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        # specific options here
-                                        $cmd , {type => SCALAR|UNDEF,
-                                                default => 0,
-                                               },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    my %info =
-       __begin_control(%param,
-                       command  => $cmd,
-                      );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
-    my @data = @{$info{data}};
-    # figure out the log that we're going to use
-    my $summary = '';
-    my $summary_msg = '';
-    my $action = '';
-    if (not defined $param{$cmd}) {
-        # do nothing
-        print {$debug} "Removing $cmd fields\n";
-        $action = "Removed $cmd";
-    }
-    elsif ($param{$cmd} =~ /^-?\d+$/) {
-        my $log = [];
-        my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
-        if ($param{$cmd} == 0 or $param{$cmd} == -1) {
-             $log = $param{message};
-             $summary_msg = @records + 1;
-        }
-        else {
-             if (($param{$cmd} - 1 ) > $#records) {
-                  die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
-             }
-             my $record = $records[($param{$cmd} - 1 )];
-             if ($record->{type} !~ /incoming-recv|recips/) {
-                  die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
-             }
-             $summary_msg = $param{$cmd};
-             $log = [$record->{text}];
-        }
-        my $p_o = Debbugs::MIME::parse(join('',@{$log}));
-        my $body = $p_o->{body};
-        my $in_pseudoheaders = 0;
-        my $paragraph = '';
-        # walk through body until we get non-blank lines
-        for my $line (@{$body}) {
-             if ($line =~ /^\s*$/) {
-                  if (length $paragraph) {
-                       if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
-                            $paragraph = '';
-                            next;
-                       }
-                       last;
-                  }
-                  $in_pseudoheaders = 0;
-                  next;
-             }
-             # skip a paragraph if it looks like it's control or
-             # pseudo-headers
-             if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity|Control)\:\s+\S}xi or #pseudo headers
-                 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
-                                \#|reopen|close|(?:not|)(?:fixed|found)|clone|
-                                debug|(?:not|)forwarded|priority|
-                                (?:un|)block|limit|(?:un|)archive|
-                                reassign|retitle|affects|package|
-                                outlook|
-                                (?:un|force|)merge|user(?:category|tags?|)
-                            )\s+\S}xis) {
-                  if (not length $paragraph) {
-                       print {$debug} "Found control/pseudo-headers and skiping them\n";
-                       $in_pseudoheaders = 1;
-                       next;
-                  }
-             }
-             next if $in_pseudoheaders;
-             $paragraph .= $line ." \n";
-        }
-        print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
-        $summary = $paragraph;
-        $summary =~ s/[\n\r]/ /g;
-        if (not length $summary) {
-             die "Unable to find $cmd message to use";
-        }
-        # trim off a trailing spaces
-        $summary =~ s/\ *$//;
-    }
-    else {
-       $summary = $param{$cmd};
-    }
-    for my $data (@data) {
-        print {$debug} "Going to change $cmd\n";
-        if (((not defined $summary or not length $summary) and
-             (not defined $data->{$cmd} or not length $data->{$cmd})) or
-            $summary eq $data->{$cmd}) {
-            print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
-            next;
-        }
-        if (length $summary) {
-             if (length $data->{$cmd}) {
-                  $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
-             }
-             else {
-                  $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
-             }
-        }
-        my $old_data = dclone($data);
-        $data->{$cmd} = $summary;
-        append_action_to_log(bug => $data->{bug_num},
-                             command => $cmd,
-                             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";
-     }
-    __end_control(%info);
-}
-
-
-
-=head2 clone_bug
-
-     eval {
-           clone_bug(bug          => $ref,
-                     transcript   => $transcript,
-                     ($dl > 0 ? (debug => $transcript):()),
-                     requester    => $header{from},
-                     request_addr => $controlrequestaddr,
-                     message      => \@log,
-                      affected_packages => \%affected_packages,
-                     recipients   => \%recipients,
-                    );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to clone bug $ref bar: $@";
-       }
-
-Clones the given bug.
-
-We currently don't support cloning merged bugs, but this could be
-handled by internally unmerging, cloning, then remerging the bugs.
-
-=cut
-
-sub clone_bug {
-    my %param = validate_with(params => \@_,
-                             spec   => {bug => {type   => SCALAR,
-                                                regex  => qr/^\d+$/,
-                                               },
-                                        new_bugs => {type => ARRAYREF,
-                                                    },
-                                        new_clones => {type => HASHREF,
-                                                       default => {},
-                                                      },
-                                        %common_options,
-                                        %append_action_options,
-                                       },
-                            );
-    my %info =
-       __begin_control(%param,
-                       command  => 'clone'
-                      );
-    my $transcript = $info{transcript};
-    my @data = @{$info{data}};
-
-    my $action = '';
-    for my $data (@data) {
-       if (length($data->{mergedwith})) {
-           die "Bug is marked as being merged with others. Use an existing clone.\n";
-       }
-    }
-    if (@data != 1) {
-       die "Not exactly one bug‽ This shouldn't happen.";
-    }
-    my $data = $data[0];
-    my %clones;
-    for my $newclone_id (@{$param{new_bugs}}) {
-       my $new_bug_num = new_bug(copy => $data->{bug_num});
-       $param{new_clones}{$newclone_id} = $new_bug_num;
-       $clones{$newclone_id} = $new_bug_num;
-    }
-    my @new_bugs = sort values %clones;
-    my @collapsed_ids;
-    for my $new_bug (@new_bugs) {
-       # no collapsed ids or the higher collapsed id is not one less
-       # than the next highest new bug
-       if (not @collapsed_ids or 
-           $collapsed_ids[-1][1]+1 != $new_bug) {
-           push @collapsed_ids,[$new_bug,$new_bug];
-       }
-       else {
-           $collapsed_ids[-1][1] = $new_bug;
-       }
-    }
-    my @collapsed;
-    for my $ci (@collapsed_ids) {
-       if ($ci->[0] == $ci->[1]) {
-           push @collapsed,$ci->[0];
-       }
-       else {
-           push @collapsed,$ci->[0].'-'.$ci->[1]
-       }
-    }
-    my $collapsed_str = english_join(\@collapsed);
-    $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
-    for my $new_bug (@new_bugs) {
-       append_action_to_log(bug => $new_bug,
-                            get_lock => 1,
-                            __return_append_to_log_options(
-                                                           %param,
-                                                           action => $action,
-                                                          ),
-                           )
-           if not exists $param{append_log} or $param{append_log};
-    }
-    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);
-    # bugs that this bug is blocking are also blocked by the new clone(s)
-    for my $bug (split ' ', $data->{blocks}) {
-       for my $new_bug (@new_bugs) {
-           set_blocks(bug => $bug,
-                      block => $new_bug,
-                      add => 1,
-                      hash_slice(%param,
-                                 keys %common_options,
-                                 keys %append_action_options),
-                     );
-       }
-    }
-    # bugs that are blocking this bug are also blocking the new clone(s)
-    for my $bug (split ' ', $data->{blockedby}) {
-       for my $new_bug (@new_bugs) {
-           set_blocks(bug => $new_bug,
-                      block => $bug,
-                      add => 1,
-                      hash_slice(%param,
-                                 keys %common_options,
-                                 keys %append_action_options),
-                     );
-       }
-    }
-}
-
-
-
-=head1 OWNER FUNCTIONS
-
-=head2 owner
-
-     eval {
-           owner(bug          => $ref,
-                 transcript   => $transcript,
-                 ($dl > 0 ? (debug => $transcript):()),
-                 requester    => $header{from},
-                 request_addr => $controlrequestaddr,
-                 message      => \@log,
-                 recipients   => \%recipients,
-                 owner        => undef,
-                );
-       };
-       if ($@) {
-           $errors++;
-           print {$transcript} "Failed to mark $ref as having an owner: $@";
-       }
-
-Handles all setting of the owner field; given an owner of undef or of
-no length, indicates that a bug is not owned by anyone.
-
-=cut
-
-sub owner {
-     my %param = validate_with(params => \@_,
-                              spec   => {bug => {type   => SCALAR,
-                                                 regex  => qr/^\d+$/,
-                                                },
-                                         owner => {type => SCALAR|UNDEF,
-                                                  },
-                                         %common_options,
-                                         %append_action_options,
-                                        },
-                             );
-     my %info =
-        __begin_control(%param,
-                        command  => 'owner',
-                       );
-     my ($debug,$transcript) =
-       @info{qw(debug transcript)};
-     my @data = @{$info{data}};
-     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}) {
-             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";
-                 next;
-             }
-             $param{owner} = '';
-             $action = "Removed annotation that $config{bug} was owned by " .
-                 "$data->{owner}.";
-         }
-         else {
-             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,
-                                             action => $action,
-                                            ),
-                             )
-              if not exists $param{append_log} or $param{append_log};
-         writebug($data->{bug_num},$data);
-         print {$transcript} "$action\n";
-     }
-     __end_control(%info);
-}
-
-
-=head1 ARCHIVE FUNCTIONS
-
-
-=head2 bug_archive
-
-     my $error = '';
-     eval {
-        bug_archive(bug => $bug_num,
-                    debug => \$debug,
-                    transcript => \$transcript,
-                   );
-     };
-     if ($@) {
-        $errors++;
-        transcript("Unable to archive $bug_num\n");
-        warn $@;
-     }
-     transcript($transcript);
-
-
-This routine archives a bug
-
-=over
-
-=item bug -- bug number
-
-=item check_archiveable -- check wether a bug is archiveable before
-archiving; defaults to 1
-
-=item archive_unarchived -- whether to archive bugs which have not
-previously been archived; defaults to 1. [Set to 0 when used from
-control@]
-
-=item ignore_time -- whether to ignore time constraints when archiving
-a bug; defaults to 0.
-
-=back
-
-=cut
-
-sub bug_archive {
-     my %param = validate_with(params => \@_,
-                              spec   => {bug => {type   => SCALAR,
-                                                 regex  => qr/^\d+$/,
-                                                },
-                                         check_archiveable => {type => BOOLEAN,
-                                                               default => 1,
-                                                              },
-                                         archive_unarchived => {type => BOOLEAN,
-                                                                default => 1,
-                                                               },
-                                         ignore_time => {type => BOOLEAN,
-                                                         default => 0,
-                                                        },
-                                         %common_options,
-                                         %append_action_options,
-                                        },
-                             );
-     my %info = __begin_control(%param,
-                               command => 'archive',
-                               );
-     my ($debug,$transcript) = @info{qw(debug transcript)};
-     my @data = @{$info{data}};
-     my @bugs = @{$info{bugs}};
-     my $action = "$config{bug} archived.";
-     if ($param{check_archiveable} and
-        not bug_archiveable(bug=>$param{bug},
-                            ignore_time => $param{ignore_time},
-                           )) {
-         print {$transcript} "Bug $param{bug} cannot be archived\n";
-         die "Bug $param{bug} cannot be archived";
-     }
-     if (not $param{archive_unarchived} and
-        not exists $data[0]{unarchived}
-       ) {
-         print {$transcript} "$param{bug} has not been archived previously\n";
-         die "$param{bug} has not been archived previously";
-     }
-     add_recipients(recipients => $param{recipients},
-                   data => \@data,
-                   debug      => $debug,
-                   transcript => $transcript,
-                  );
-     print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
-     for my $bug (@bugs) {
-        if ($param{check_archiveable}) {
-            die "Bug $bug cannot be archived (but $param{bug} can?)"
-                unless bug_archiveable(bug=>$bug,
-                                       ignore_time => $param{ignore_time},
-                                      );
-        }
-     }
-     # If we get here, we can archive/remove this bug
-     print {$debug} "$param{bug} removing\n";
-     for my $bug (@bugs) {
-         #print "$param{bug} removing $bug\n" if $debug;
-         my $dir = get_hashname($bug);
-         # 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,
-                               )
-                             )
-              if not exists $param{append_log} or $param{append_log};
-         my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
-         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") 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";
-         }
-         unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
-         print {$debug} "deleted $bug (from $param{bug})\n";
-     }
-     bughook_archive(@bugs);
-     __end_control(%info);
-}
-
-=head2 bug_unarchive
-
-     my $error = '';
-     eval {
-        bug_unarchive(bug => $bug_num,
-                      debug => \$debug,
-                      transcript => \$transcript,
-                     );
-     };
-     if ($@) {
-        $errors++;
-        transcript("Unable to archive bug: $bug_num");
-     }
-     transcript($transcript);
-
-This routine unarchives a bug
-
-=cut
-
-sub bug_unarchive {
-     my %param = validate_with(params => \@_,
-                              spec   => {bug => {type   => SCALAR,
-                                                 regex  => qr/^\d+/,
-                                                },
-                                         %common_options,
-                                         %append_action_options,
-                                        },
-                             );
-
-     my %info = __begin_control(%param,
-                               archived=>1,
-                               command=>'unarchive');
-     my ($debug,$transcript) =
-        @info{qw(debug transcript)};
-     my @bugs = @{$info{bugs}};
-     my $action = "$config{bug} unarchived.";
-     my @files_to_remove;
-     ## error out if we're unarchiving unarchived bugs
-     for my $data (@{$info{data}}) {
-        if (not defined $data->{archived} or
-            not $data->{archived}
-           ) {
-            __end_control(%info);
-            croak("Bug $data->{bug_num} was not archived; not unarchiving it.");
-        }
-     }
-     for my $bug (@bugs) {
-         print {$debug} "$param{bug} removing $bug\n";
-         my $dir = get_hashname($bug);
-         my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
-         mkpath("archive/$dir");
-         foreach my $file (@files_to_copy) {
-              # die'ing here sucks
-              link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
-                   copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
-                        die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
-         }
-         push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
-         print {$transcript} "Unarchived $config{bug} $bug\n";
-     }
-     unlink(@files_to_remove) or die "Unable to unlink bugs";
-     # 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!";
-         }
-         $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,
-                               )
-                             )
-              if not exists $param{append_log} or $param{append_log};
-         writebug($bug,$newdata);
-     }
-     __end_control(%info);
-}
-
-=head2 append_action_to_log
-
-     append_action_to_log
-
-This should probably be moved to Debbugs::Log; have to think that out
-some more.
-
-=cut
-
-sub append_action_to_log{
-     my %param = validate_with(params => \@_,
-                              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,
-                                                       default => '',
-                                                      },
-                                         request_addr => {type => SCALAR,
-                                                          default => '',
-                                                         },
-                                         location => {type => SCALAR,
-                                                      optional => 1,
-                                                     },
-                                         message  => {type => SCALAR|ARRAYREF,
-                                                      default => '',
-                                                     },
-                                         recips   => {type => SCALAR|ARRAYREF,
-                                                      optional => 1
-                                                     },
-                                         desc       => {type => SCALAR,
-                                                        default => '',
-                                                       },
-                                         get_lock   => {type => BOOLEAN,
-                                                        default => 1,
-                                                       },
-                                         locks      => {type => HASHREF,
-                                                        optional => 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});
-     die "Unable to find .log for $param{bug}"
-         if not defined $log_location;
-     if ($param{get_lock}) {
-         filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
-         $locks++;
-     }
-     my @records;
-     my $logfh = IO::File->new(">>$log_location") or
-        die "Unable to open $log_location for appending: $!";
-     # 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;
-               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 .= "<!-- new_data:\n";
-        my %nd;
-        for my $key (keys %{$new_data}) {
-            if (not exists $Debbugs::Status::fields{$key}) {
-                warn "No such field $key";
-                next;
-            }
-            $nd{$key} = $new_data->{$key};
-            # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
-        }
-        $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
-        $data_diff .= "-->\n";
-        $data_diff .= "<!-- old_data:\n";
-        my %od;
-        for my $key (keys %{$old_data}) {
-            if (not exists $Debbugs::Status::fields{$key}) {
-                warn "No such field $key";
-                next;
-            }
-            $od{$key} = $old_data->{$key};
-            # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
-        }
-        $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
-        $data_diff .= "-->\n";
-     }
-     my $msg = join('',
-                   (exists $param{command} ?
-                    "<!-- command:".html_escape(encode_utf8_safely($param{command}))." -->\n":""
-                   ),
-                   (length $param{requester} ?
-                    "<!-- requester: ".html_escape(encode_utf8_safely($param{requester}))." -->\n":""
-                   ),
-                   (length $param{request_addr} ?
-                    "<!-- request_addr: ".html_escape(encode_utf8_safely($param{request_addr}))." -->\n":""
-                   ),
-                   "<!-- time:".time()." -->\n",
-                   $data_diff,
-                   "<strong>".html_escape(encode_utf8_safely($param{action}))."</strong>\n");
-     if (length $param{requester}) {
-          $msg .= "Request was from <code>".html_escape(encode_utf8_safely($param{requester}))."</code>\n";
-     }
-     if (length $param{request_addr}) {
-          $msg .= "to <code>".html_escape(encode_utf8_safely($param{request_addr}))."</code>";
-     }
-     if (length $param{desc}) {
-         $msg .= ":<br>\n".encode_utf8_safely($param{desc})."\n";
-     }
-     else {
-         $msg .= ".\n";
-     }
-     push @records, {type => 'html',
-                    text => $msg,
-                   };
-     $msg = '';
-     if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
-        push @records, {type => exists $param{recips}?'recips':'incoming-recv',
-                        exists $param{recips}?(recips => [map {encode_utf8_safely($_)} make_list($param{recips})]):(),
-                        text => join('',make_list($param{message})),
-                       };
-     }
-     write_log_records(logfh=>$logfh,
-                      records => \@records,
-                     );
-     close $logfh or die "Unable to close $log_location: $!";
-     if ($param{get_lock}) {
-         unfilelock(exists $param{locks}?$param{locks}:());
-         $locks--;
-     }
-
-
-}
-
-
-=head1 PRIVATE FUNCTIONS
-
-=head2 __handle_affected_packages
-
-     __handle_affected_packages(affected_packages => {},
-                                data => [@data],
-                               )
-
-
-
-=cut
-
-sub __handle_affected_packages{
-     my %param = validate_with(params => \@_,
-                              spec   => {%common_options,
-                                         data => {type => ARRAYREF|HASHREF
-                                                 },
-                                        },
-                              allow_extra => 1,
-                             );
-     for my $data (make_list($param{data})) {
-         next unless exists $data->{package} and defined $data->{package};
-         my @packages = split /\s*,\s*/,$data->{package};
-         @{$param{affected_packages}}{@packages} = (1) x @packages;
-      }
-}
-
-=head2 __handle_debug_transcript
-
-     my ($debug,$transcript) = __handle_debug_transcript(%param);
-
-Returns a debug and transcript filehandle
-
-
-=cut
-
-sub __handle_debug_transcript{
-     my %param = validate_with(params => \@_,
-                              spec   => {%common_options},
-                              allow_extra => 1,
-                             );
-     my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
-     my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
-     return ($debug,$transcript);
-}
-
-=head2 __bug_info
-
-     __bug_info($data)
-
-Produces a small bit of bug information to kick out to the transcript
-
-=cut
-
-sub __bug_info{
-     my $return = '';
-     for my $data (@_) {
-        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(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
-       return 1;
-    }
-    return 0;
-}
-
-sub __return_append_to_log_options{
-     my %param = @_;
-     my $action = $param{action} if exists $param{action};
-     if (not exists $param{requester}) {
-         $param{requester} = $config{control_internal_requester};
-     }
-     if (not exists $param{request_addr}) {
-         $param{request_addr} = $config{control_internal_request_addr};
-     }
-     if (not exists $param{message}) {
-         my $date = rfc822_date();
-         $param{message} =
-              encode_headers(fill_in_template(template  => 'mail/fake_control_message',
-                                              variables => {request_addr => $param{request_addr},
-                                                            requester    => $param{requester},
-                                                            date         => $date,
-                                                            action       => $action
-                                                           },
-                                             ));
-     }
-     if (not defined $action) {
-         carp "Undefined action!";
-         $action = "unknown action";
-     }
-     return (action => $action,
-            hash_slice(%param,keys %append_action_options),
-           );
-}
-
-=head2 __begin_control
-
-     my %info = __begin_control(%param,
-                               archived=>1,
-                               command=>'unarchive');
-     my ($debug,$transcript) = @info{qw(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 $lockhash;
-
-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} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
-#    print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
-    $lockhash = $param{locks} if exists $param{locks};
-    my @data = ();
-    my $old_die = $SIG{__DIE__};
-    $SIG{__DIE__} = *sig_die{CODE};
-
-    ($new_locks, @data) =
-       lock_read_all_merged_bugs(bug => $param{bug},
-                                 $param{archived}?(location => 'archive'):(),
-                                 exists $param{locks} ? (locks => $param{locks}):(),
-                                );
-    $locks += $new_locks;
-    if (not @data) {
-       die "Unable to read any bugs successfully.";
-    }
-    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}):(),
-                         transcript => $transcript,
-                        )) {
-       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";
-    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,
-                  (__internal_request()?(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,
-           exists $param{locks}?(locks => $param{locks}):(),
-          );
-}
-
-=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(exists $info{locks}?$info{locks}:());
-           $locks--;
-       }
-    }
-    $SIG{__DIE__} = $info{old_die};
-    if (exists $info{param}{affected_bugs}) {
-       @{$info{param}{affected_bugs}}{@{$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 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|HASHREF,
-                                                 },
-                                        limit => {type => HASHREF|UNDEF,
-                                                 },
-                                        transcript  => {type => SCALARREF|HANDLE,
-                                                        optional => 1,
-                                                       },
-                                       },
-                            );
-    my @data = make_list($param{data});
-    if (not @data or
-       not defined $param{limit} or
-       not keys %{$param{limit}}) {
-       return 1;
-    }
-    my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
-    my $going_to_fail = 0;
-    for my $data (@data) {
-       $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
-                                                  status => dclone($data),
-                                                 ));
-       for my $field (keys %{$param{limit}}) {
-           next unless exists $param{limit}{$field};
-           my $match = 0;
-           my @data_fields = make_list($data->{$field});
-LIMIT:     for my $limit (make_list($param{limit}{$field})) {
-               if (not ref $limit) {
-                   for my $data_field (@data_fields) {
-                       if ($data_field eq $limit) {
-                           $match = 1;
-                           last LIMIT;
-                       }
-                   }
-               }
-               elsif (ref($limit) eq 'Regexp') {
-                   for my $data_field (@data_fields) {
-                       if ($data_field =~ $limit) {
-                           $match = 1;
-                           last LIMIT;
-                       }
-                   }
-               }
-               else {
-                   warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
-               }
-           }
-           if (not $match) {
-               $going_to_fail = 1;
-               print {$transcript} qq($field: ').join(', ',map{qq("$_")} make_list($data->{$field})).
-                   "' does not match at least one of ".
-                   join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
-           }
-       }
-    }
-    return $going_to_fail?0:1;
-}
-
-
-=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(defined $lockhash?$lockhash:()); }
-           $locks = 0;
-       }
-    }
-}
-
-
-# =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]: ".
-                            $config{cgi_domain}.'/'.
-                                Debbugs::CGI::bug_links(bug => $_[0],
-                                                        links_only => 1,
-                                                       );
-                    }
-                   };
-
-     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,
-                           );
-}
-
-sub __all_undef_or_equal {
-    my @values = @_;
-    return 1 if @values == 1 or @values == 0;
-    my $not_def = grep {not defined $_} @values;
-    if ($not_def == @values) {
-       return 1;
-    }
-    if ($not_def > 0 and $not_def != @values) {
-       return 0;
-    }
-    my $first_val = shift @values;
-    for my $val (@values) {
-       if ($first_val ne $val) {
-           return 0;
-       }
-    }
-    return 1;
-}
-
-
-1;
-
-__END__