]> git.donarmstrong.com Git - debbugs.git/commitdiff
merge changes from don source
authorDebian BTS <debbugs@rietz>
Thu, 23 Jul 2009 13:42:26 +0000 (13:42 +0000)
committerDebian BTS <debbugs@rietz>
Thu, 23 Jul 2009 13:42:26 +0000 (13:42 +0000)
47 files changed:
Debbugs/Bugs.pm
Debbugs/CGI.pm
Debbugs/CGI/Bugreport.pm
Debbugs/CGI/Pkgreport.pm
Debbugs/Common.pm
Debbugs/Config.pm
Debbugs/Control.pm
Debbugs/Log.pm
Debbugs/MIME.pm
Debbugs/Mail.pm
Debbugs/Packages.pm
Debbugs/Status.pm
MANIFEST.SKIP [new file with mode: 0644]
Mail/CrossAssassin.pm [new file with mode: 0644]
Makefile
Makefile.PL
bin/add_bug_to_estraier
cgi/bugreport.cgi
cgi/pkgreport.cgi
debian/changelog
debian/conffiles [deleted file]
debian/control
debian/copyright
debian/debbugs-web.conffiles [deleted file]
debian/debbugs.install
debian/dirs [deleted file]
debian/libdebbugs-perl.install
debian/postinst [deleted file]
debian/postrm [deleted file]
debian/rules
examples/debian/versions/update-mldbm
scripts/Mail/CrossAssassin.pm [deleted file]
scripts/gen-indices
scripts/process
scripts/service
t/01_pod.t [new file with mode: 0644]
t/03_status.t [new file with mode: 0644]
t/06_mail_handling.t
t/07_control_limit.t [new file with mode: 0644]
t/lib/DebbugsTest.pm
templates/en_US/cgi/bugreport.tmpl
templates/en_US/cgi/bugreport_pkginfo.tmpl
templates/en_US/cgi/short_bug_status.tmpl
templates/en_US/mail/fake_control_message.tmpl
templates/en_US/mail/process_ack.tmpl
templates/en_US/mail/process_no_package.tmpl
templates/en_US/mail/submitter_changed.tmpl [new file with mode: 0644]

index e16ef83a0e9a7a66b829cb9f7021d848e41ccbde..eff1593640e87c2c6f41ae5d9eb70b1d55bed7e1 100644 (file)
@@ -572,20 +572,27 @@ sub get_bugs_flatfile{
      }
      my $unmaintained_packages = 0;
      # unmaintained packages is a special case
-     for my $maint (make_list(exists $param{maint}?$param{maint}:[])) {
+     my @maints = make_list(exists $param{maint}?$param{maint}:[]);
+     $param{maint} = [];
+     for my $maint (@maints) {
          if (defined $maint and $maint eq '' and not $unmaintained_packages) {
               $unmaintained_packages = 1;
               our %maintainers = %{getmaintainers()};
-              $param{function} = [exists $param{function}?
-                                  (ref $param{function}?@{$param{function}}:$param{function}):(),
+              $param{function} = [(exists $param{function}?
+                                   (ref $param{function}?@{$param{function}}:$param{function}):()),
                                   sub {my %d=@_;
-                                       foreach my $try (splitpackages($d{"pkg"})) {
+                                       foreach my $try (make_list($d{"pkg"})) {
+                                            next unless length $try;
+                                            ($try) = $try =~ m/^(?:src:)?(.+)/;
                                             return 1 if not exists $maintainers{$try};
                                        }
                                        return 0;
                                   }
                                  ];
          }
+         elsif (defined $maint and $maint ne '') {
+              push @{$param{maint}},$maint;
+         }
      }
      # We handle src packages, maint and maintenc by mapping to the
      # appropriate binary packages, then removing all packages which
@@ -770,7 +777,11 @@ sub __bug_matches {
     my ($hash, $status) = @_;
     foreach my $key( keys( %$hash ) ) {
         my $value = $hash->{$key};
+       next unless exists $field_match{$key};
        my $sub = $field_match{$key};
+       if (not defined $sub) {
+           die "No defined subroutine for key: $key";
+       }
        return 1 if ($sub->($key, $value, $status));
     }
     return 0;
index 980a71d0b963e23d81a71a750c2592a8998ca954..9d96ed9e287f2608ea638d760ea4b54473b61430 100644 (file)
@@ -297,7 +297,7 @@ sub quitcgi {
 }
 
 
-=head HTML
+=head1 HTML
 
 =head2 htmlize_packagelinks
 
@@ -404,7 +404,7 @@ sub package_links {
                                       %options,
                                       $type => $_,
                                      ),
-                            $_);
+                            ($type eq 'src'?'src:':'').$_);
                       } make_list($param{$type}) if exists $param{$type};
      }
      for my $type (qw(maint owner submitter correspondent)) {
@@ -536,8 +536,8 @@ the split links with commas and spaces.
 sub maybelink {
     my ($links,$regex,$join) = @_;
     if (not defined $regex and not defined $join) {
-        $links =~ s{((?:ftp|http|https)://[\S~-]+?/?)([\)\'\:\.\,]?(?:\s|\.<|$))}
-                   {q(<a href=").html_escape($1).q(">).html_escape($1).q(</a>).$2}geimo;
+        $links =~ s{(.*?)((?:(?:ftp|http|https)://[\S~-]+?/?)?)([\)\'\:\.\,]?(?:\s|\.<|$))}
+                   {html_escape($1).(length $2?q(<a href=").html_escape($2).q(">).html_escape($2).q(</a>):'').html_escape($3)}geimo;
         return $links;
     }
     $join = ' ' if not defined $join;
index 90cf5272e1d7f5d73f20a005ac57d46759b21eb7..57f8d8fe647838babaaf7859a284e64466b663a3 100644 (file)
@@ -292,6 +292,7 @@ sub handle_email_message{
                    terse       => $param{terse},
                    exists $param{msg}?(msg=>$param{msg}):(),
                    exists $param{att}?(att=>$param{att}):(),
+                   exists $param{trim_headers}?(trim_headers=>$param{trim_headers}):(),
                   );
      return $output;
 
index feb85a91280da6bc05d61cc4b82dea91ae179d0e..84663ed8fcc27b17aa0562282c5d734b69eef730 100644 (file)
@@ -308,8 +308,6 @@ sub pkg_htmlizebugs {
      my $header = '';
      my $footer = "<h2 class=\"outstanding\">Summary</h2>\n";
 
-     my @dummy = ($gRemoveAge); #, @gSeverityList, @gSeverityDisplay);  #, $gHTMLExpireNote);
-
      if (@bugs == 0) {
          return "<HR><H2>No reports found!</H2></HR>\n";
      }
index 87b355924458444536aff9a27c2a1d3a52ee593e..d8eaf40b2b336c808ebb12327e8aa25e4df34c3d 100644 (file)
@@ -40,10 +40,13 @@ BEGIN{
      @EXPORT = ();
      %EXPORT_TAGS = (util   => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
                                qw(appendfile buglog getparsedaddrs getmaintainers),
+                               qw(bug_status),
                                qw(getmaintainers_reverse),
                                qw(getpseudodesc),
                               ],
-                    misc   => [qw(make_list globify_scalar english_join checkpid)],
+                    misc   => [qw(make_list globify_scalar english_join checkpid),
+                               qw(cleanup_eval_fail),
+                              ],
                     date   => [qw(secs_to_english)],
                     quit   => [qw(quit)],
                     lock   => [qw(filelock unfilelock lockpid)],
@@ -64,6 +67,8 @@ use Debbugs::MIME qw(decode_rfc1522);
 use Mail::Address;
 use Cwd qw(cwd);
 
+use Params::Validate qw(validate_with :types);
+
 use Fcntl qw(:flock);
 
 our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
@@ -175,6 +180,23 @@ sub buglog {
     return undef;
 }
 
+=head2 bug_status
+
+     bug_status($bugnum)
+
+
+Returns the path to the summary file corresponding to the bug.
+
+Returns undef if the bug does not exist.
+
+=cut
+
+sub bug_status{
+    my ($bugnum) = @_;
+    my $location = getbuglocation($bugnum, 'summary');
+    return getbugcomponent($bugnum, 'summary', $location) if ($location);
+    return undef;
+}
 
 =head2 appendfile
 
@@ -506,22 +528,56 @@ sub make_list {
 
 =head2 english_join
 
-     print english_join(', ',' and ',@list);
+     print english_join(list => \@list);
+     print english_join(\@list);
 
 Joins list properly to make an english phrase.
 
+=over
+
+=item normal -- how to separate most values; defaults to ', '
+
+=item last -- how to separate the last two values; defaults to ', and '
+
+=item only_two -- how to separate only two values; defaults to ' and '
+
+=item list -- ARRAYREF values to join; if the first argument is an
+ARRAYREF, it's assumed to be the list of values to join
+
+=back
 
+In cases where C<list> is empty, returns ''; when there is only one
+element, returns that element.
 
 =cut
 
 sub english_join {
-     my ($normal,$last,@list) = @_;
-     if (@list <= 1) {
-         return @list?$list[0]:'';
-     }
-     my $ret = $last . pop(@list);
-     $ret = join($normal,@list) . $ret;
-     return $ret;
+    if (ref $_[0] eq 'ARRAY') {
+       return english_join(list=>$_[0]);
+    }
+    my %param = validate_with(params => \@_,
+                             spec  => {normal => {type => SCALAR,
+                                                  default => ', ',
+                                                 },
+                                       last   => {type => SCALAR,
+                                                  default => ', and ',
+                                                 },
+                                       only_two => {type => SCALAR,
+                                                    default => ' and ',
+                                                   },
+                                       list     => {type => ARRAYREF,
+                                                   },
+                                      },
+                            );
+    my @list = @{$param{list}};
+    if (@list <= 1) {
+       return @list?$list[0]:'';
+    }
+    elsif (@list == 2) {
+       return join($param{only_two},@list);
+    }
+    my $ret = $param{last} . pop(@list);
+    return join($param{normal},@list) . $ret;
 }
 
 
@@ -561,6 +617,42 @@ sub globify_scalar {
      return IO::File->new('/dev/null','w');
 }
 
+=head2 cleanup_eval_fail()
+
+     print "Something failed with: ".cleanup_eval_fail($@);
+
+Does various bits of cleanup on the failure message from an eval (or
+any other die message)
+
+Takes at most two options; the first is the actual failure message
+(usually $@ and defaults to $@), the second is the debug level
+(defaults to $DEBUG).
+
+If debug is non-zero, the code at which the failure occured is output.
+
+=cut
+
+sub cleanup_eval_fail {
+    my ($error,$debug) = @_;
+    if (not defined $error or not @_) {
+       $error = $@ || 'unknown reason';
+    }
+    if (@_ <= 1) {
+       $debug = $DEBUG || 0;
+    }
+    $debug = 0 if not defined $debug;
+
+    if ($debug > 0) {
+       return $error;
+    }
+    # ditch the "at foo/bar/baz.pm line 5"
+    $error =~ s/\sat\s\S+\sline\s\d+//;
+    # ditch trailing multiple periods in case there was a cascade of
+    # die messages.
+    $error =~ s/\.+$/\./;
+    return $error;
+}
+
 
 1;
 
index c936a4dd505a9d99a64483a2317dd09ce19e42a2..d70dc290e9bca1375c7ab0aa21742967609c7e2d 100644 (file)
@@ -272,8 +272,6 @@ Email address where packages with an unknown maintainer will be sent
 
 Default: $config{maintainer_email}
 
-=back
-
 =cut
 
 set_default(\%config,'unknown_maintainer_email',$config{maintainer_email});
@@ -354,11 +352,39 @@ set_default(\%config,'bug_subscription_domain',$config{list_domain});
 
 =over
 
+=item mailer
+
+Name of the mailer to use
+
+Default: exim
+
 =cut
 
 set_default(\%config,'mailer','exim');
+
+
+=item bug
+
+Default: bug
+
+=item ubug
+
+Default: ucfirst($config{bug});
+
+=item bugs
+
+Default: bugs
+
+=item ubugs
+
+Default: ucfirst($config{ubugs});
+
+=cut
+
 set_default(\%config,'bug','bug');
+set_default(\%config,'ubug',ucfirst($config{bug}));
 set_default(\%config,'bugs','bugs');
+set_default(\%config,'ubugs',ucfirst($config{bugs}));
 
 =item remove_age
 
index bedc7d88a6f3445c67ee253ecd9e1a91dcea05b4..77785ceaeced06d9abf0855b1f5b967a6c0af8da 100644 (file)
@@ -5,7 +5,7 @@
 #
 # [Other people have contributed to this file; their copyrights should
 # go here too.]
-# Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
+# Copyright 2007,2008,2009 by Don Armstrong <don@donarmstrong.com>.
 
 package Debbugs::Control;
 
@@ -49,6 +49,10 @@ following options:
 
 =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
 
@@ -78,9 +82,18 @@ BEGIN{
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
-     %EXPORT_TAGS = (affects => [qw(affects)],
+     %EXPORT_TAGS = (reopen    => [qw(reopen)],
+                    submitter => [qw(set_submitter)],
+                    severity => [qw(set_severity)],
+                    affects => [qw(affects)],
                     summary => [qw(summary)],
                     owner   => [qw(owner)],
+                    title   => [qw(set_title)],
+                    forward => [qw(set_forwarded)],
+                    found   => [qw(set_found set_fixed)],
+                    fixed   => [qw(set_found set_fixed)],
+                    package => [qw(set_package)],
+                    block   => [qw(set_blocks)],
                     archive => [qw(bug_archive bug_unarchive),
                                ],
                     log     => [qw(append_action_to_log),
@@ -93,10 +106,11 @@ BEGIN{
 
 use Debbugs::Config qw(:config);
 use Debbugs::Common qw(:lock buglog :misc get_hashname);
-use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages);
+use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages split_status_fields);
 use Debbugs::CGI qw(html_escape);
 use Debbugs::Log qw(:misc);
 use Debbugs::Recipients qw(:add);
+use Debbugs::Packages qw(:versions :mapping);
 
 use Params::Validate qw(validate_with :types);
 use File::Path qw(mkpath);
@@ -104,10 +118,16 @@ use IO::File;
 
 use Debbugs::Text qw(:templates);
 
-use Debbugs::Mail qw(rfc822_date);
+use Debbugs::Mail qw(rfc822_date send_mail_message default_headers);
+use Debbugs::MIME qw(create_mime_message);
+
+use Mail::RFC822::Address qw();
 
 use POSIX qw(strftime);
 
+use Storable qw(dclone nfreeze);
+use List::Util qw(first);
+
 use Carp;
 
 # These are a set of options which are common to all of these functions
@@ -130,109 +150,1560 @@ my %common_options = (debug       => {type => SCALARREF|HANDLE,
                      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,
+                                          },
                     );
 
 
-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),
-                               ],
-                   },
-     );
+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),
+                               ],
+                   },
+     );
+
+
+# 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 (exists $param{add}) {
+       $mode = 'add';
+    }
+    elsif (exists $param{remove}) {
+       $mode = 'remove';
+    }
+
+    my %info =
+       __begin_control(%param,
+                       command  => 'blocks'
+                      );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+
+
+    # The first bit of this code is ugly, and should be cleaned up.
+    # Its purpose is to populate %removed_blockers and %add_blockers
+    # with all of the bugs that should be added or removed as blockers
+    # of all of the bugs which are merged with $param{bug}
+    my %ok_blockers;
+    my %bad_blockers;
+    for my $blocker (make_list($param{block})) {
+       next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
+       my $data = read_bug(bug=>$blocker,
+                          );
+       if (defined $data and not $data->{archive}) {
+           $data = split_status_fields($data);
+           $ok_blockers{$blocker} = 1;
+           my @merged_bugs;
+           push @merged_bugs, make_list($data->{mergedwith});
+           $ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
+       }
+       else {
+           $bad_blockers{$blocker} = 1;
+       }
+    }
+
+    # throw an error if we are setting the blockers and there is a bad
+    # blocker
+    if (keys %bad_blockers and $mode eq 'set') {
+       croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
+           keys %ok_blockers?'':" and no known blocking bug(s)";
+    }
+    # if there are no ok blockers and we are not setting the blockers,
+    # there's an error.
+    if (not keys %ok_blockers and $mode ne 'set') {
+       print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
+       if (keys %bad_blockers) {
+           croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
+       }
+       __end_control(%info);
+       return;
+    }
+
+    my @change_blockers = keys %ok_blockers;
+
+    my %removed_blockers;
+    my %added_blockers;
+    my $action = '';
+    my @blockers = map {split ' ', $_->{blockedby}} @data;
+    my %blockers;
+    @blockers{@blockers} = (1) x @blockers;
+
+    # it is nonsensical for a bug to block itself (or a merged
+    # partner); We currently don't allow removal because we'd possibly
+    # deadlock
+
+    my %bugs;
+    @bugs{@bugs} = (1) x @bugs;
+    for my $blocker (@change_blockers) {
+       if ($bugs{$blocker}) {
+           croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
+       }
+    }
+    @blockers = keys %blockers;
+    if ($param{add}) {
+       %removed_blockers = ();
+       for my $blocker (@change_blockers) {
+           next if exists $blockers{$blocker};
+           $blockers{$blocker} = 1;
+           $added_blockers{$blocker} = 1;
+       }
+    }
+    elsif ($param{remove}) {
+       %added_blockers = ();
+       for my $blocker (@change_blockers) {
+           next if exists $removed_blockers{$blocker};
+           delete $blockers{$blocker};
+           $removed_blockers{$blocker} = 1;
+       }
+    }
+    else {
+       @removed_blockers{@blockers} = (1) x @blockers;
+       %blockers = ();
+       for my $blocker (@change_blockers) {
+           next if exists $blockers{$blocker};
+           $blockers{$blocker} = 1;
+           if (exists $removed_blockers{$blocker}) {
+               delete $removed_blockers{$blocker};
+           }
+           else {
+               $added_blockers{$blocker} = 1;
+           }
+       }
+    }
+    my @new_blockers = keys %blockers;
+    for my $data (@data) {
+       my $old_data = dclone($data);
+       # remove blockers and/or add new ones as appropriate
+       if ($data->{blockedby} eq '') {
+           print {$transcript} "Was not blocked by any bugs.\n";
+       } else {
+           print {$transcript} "Was blocked by: $data->{blockedby}\n";
+       }
+       my @changed;
+       push @changed, 'added blocking bug(s) '.english_join([keys %added_blockers]) if keys %added_blockers;
+       push @changed, 'removed blocking bug(s) '.english_join([keys %removed_blockers]) if keys %removed_blockers;
+       $action = ucfirst(join ('; ',@changed)) if @changed;
+       if (not @changed) {
+           print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n"
+               unless __internal_request();
+           next;
+       }
+       $data->{blockedby} = join(' ',keys %blockers);
+       append_action_to_log(bug => $data->{bug_num},
+                            command  => 'block',
+                            old_data => $old_data,
+                            new_data => $data,
+                            get_lock => 0,
+                            __return_append_to_log_options(
+                                                           %param,
+                                                           action => $action,
+                                                          ),
+                           )
+           if not exists $param{append_log} or $param{append_log};
+       writebug($data->{bug_num},$data);
+       print {$transcript} "$action\n";
+    }
+    # we do this bit below to avoid code duplication
+    my %mungable_blocks;
+    $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
+    $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
+    for my $add_remove (keys %mungable_blocks) {
+       my @munge_blockers;
+       my %munge_blockers;
+       my $block_locks = 0;
+       for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
+           next if $munge_blockers{$blocker};
+           my ($new_locks, @blocking_data) =
+               lock_read_all_merged_bugs($blocker,
+                                         ($param{archived}?'archive':()));
+           if (not @blocking_data) {
+               unfilelock() for $new_locks;
+               die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
+           }
+           for (map {$_->{bug_num}} @blocking_data) {
+               $munge_blockers{$_} = 1;
+           }
+           for my $data (@blocking_data) {
+               my $old_data = dclone($data);
+               my %blocks;
+               %blocks = split ' ', $data->{blocks};
+               my @blocks;
+               for my $bug (@bugs) {
+                   if ($add_remove eq 'remove') {
+                       next unless exists $blocks{$bug};
+                       delete $blocks{$bug};
+                   }
+                   else {
+                       next if exists $blocks{$bug};
+                       $blocks{$bug} = 1;
+                   }
+                   push @blocks, $bug;
+               }
+               $data->{blocks} = join(' ',sort keys %blocks);
+               my $action = ($add_remove eq 'add'?'Added':'Removed').
+                   " indication that bug $data->{bug_num} blocks".
+                   join(',',@blocks);
+               append_action_to_log(bug => $data->{bug_num},
+                                    command => 'block',
+                                    old_data => $old_data,
+                                    new_data => $data,
+                                    get_lock => 0,
+                                    __return_append_to_log_options(%param,
+                                                                  action => $action
+                                                                  )
+                                   );
+           }
+           __handle_affected_packages(%param,data=>\@blocking_data);
+           add_recipients(recipients => $param{recipients},
+                          actions_taken => {blocks => 1},
+                          data       => \@blocking_data,
+                          debug      => $debug,
+                          transcript => $transcript,
+                         );
+
+           unfilelock() for $new_locks;
+       }
+    }
+    __end_control(%info);
+}
+
+
+
+=head2 set_tag
+
+     eval {
+           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 ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+    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 %fixed_removed = ();
+       my @old_tags = split /\,\s*/, $data->{tags};
+       my %tags;
+       @tags{@old_tags} = (1) x @old_tags;
+       my $reopened = 0;
+       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->{tags} = join(', ',keys %tags); # double check this
+
+       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"
+               unless __internal_request();
+           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 defafult 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 ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+
+    my $action = '';
+    for my $data (@data) {
+       if (not defined $data->{severity}) {
+           $data->{severity} = $param{severity};
+           $action = "Severity set to '$param{severity}'\n";
+       }
+       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}'\n";
+           $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 reopen
+
+     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 reopen {
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type   => SCALAR,
+                                                regex  => qr/^\d+$/,
+                                               },
+                                        # specific options here
+                                        submitter => {type => SCALAR|UNDEF,
+                                                      default => undef,
+                                                     },
+                                        %common_options,
+                                        %append_action_options,
+                                       },
+                            );
+
+    $param{submitter} = undef if defined $param{submitter} and
+       not length $param{submitter};
+
+    if (defined $param{submitter} and
+       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  => 'reopen'
+                      );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+    my $action ='';
+
+    my $warn_fixed = 1; # avoid warning multiple times if there are
+                        # fixed versions
+    my @change_submitter = ();
+    my @bugs_to_reopen = ();
+    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} "you may need to use 'found' to remove fixed versions.\n";
+           $warn_fixed = 0;
+       }
+       if (defined $param{submitter} and length $param{submitter}
+           and $data->{originator} ne $param{submitter}) {
+           push @change_submitter,$data->{bug_num};
+       }
+    }
+    __end_control(%info);
+    my @params_for_subcalls = 
+       map {exists $param{$_}?($_,$param{$_}):()}
+           (keys %common_options,
+            keys %append_action_options,
+           );
+
+    for my $bug (@change_submitter) {
+       set_submitter(bug=>$bug,
+                     submitter => $param{submitter},
+                     @params_for_subcalls,
+                    );
+    }
+    set_fixed(fixed => [],
+             bug => $param{bug},
+             reopen => 1,
+            );
+}
+
+
+=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 @bugs = @{$info{bugs}};
+    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"
+               unless __internal_request();
+           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";
+    }
+    my %info =
+       __begin_control(%param,
+                       command  => 'forwarded'
+                      );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+    my $action = '';
+    for my $data (@data) {
+       my $old_data = dclone($data);
+       print {$debug} "Going to change bug forwarded\n";
+       if (((not defined $param{forwarded} or not length $param{forwarded}) and
+             (not defined $data->{forwarded} or not length $data->{forwarded})) or
+            $param{forwarded} eq $data->{forwarded}) {
+           print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n"
+               unless __internal_request();
+           next;
+       }
+       else {
+           if (not defined $param{forwarded}) {
+               $action= "Unset $config{bug} forwarded-to-address";
+           }
+           elsif (defined $data->{forwarded} and length($data->{forwarded})) {
+               $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'";
+           }
+           else {
+               $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
+           }
+           $data->{forwarded} = $param{forwarded};
+       }
+        append_action_to_log(bug => $data->{bug_num},
+                            command => 'forwarded',
+                            new_data => $data,
+                            old_data => $old_data,
+                            get_lock => 0,
+                            __return_append_to_log_options(
+                                                           %param,
+                                                           action => $action,
+                                                          ),
+                           )
+           if not exists $param{append_log} or $param{append_log};
+       writebug($data->{bug_num},$data);
+       print {$transcript} "$action\n";
+    }
+    __end_control(%info);
+}
+
+
+
+
+=head2 set_title
+
+     eval {
+           set_title(bug          => $ref,
+                     transcript   => $transcript,
+                     ($dl > 0 ? (debug => $transcript):()),
+                     requester    => $header{from},
+                     request_addr => $controlrequestaddr,
+                     message      => \@log,
+                      affected_packages => \%affected_packages,
+                     recipients   => \%recipients,
+                     title        => $new_title,
+                      );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set the title of $ref: $@";
+       }
+
+Sets the title of a specific bug
+
+
+=cut
+
+sub set_title {
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type   => SCALAR,
+                                                regex  => qr/^\d+$/,
+                                               },
+                                        # specific options here
+                                        title => {type => SCALAR,
+                                                 },
+                                        %common_options,
+                                        %append_action_options,
+                                       },
+                            );
+    if ($param{title} =~ /[^[:print:]]/) {
+       die "Non-printable characters are not allowed in bug titles";
+    }
+
+    my %info = __begin_control(%param,
+                              command  => 'title',
+                             );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+    my $action = '';
+    for my $data (@data) {
+       my $old_data = dclone($data);
+       print {$debug} "Going to change bug title\n";
+       if (defined $data->{subject} and length($data->{subject}) and
+           $data->{subject} eq $param{title}) {
+           print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n"
+               unless __internal_request();
+           next;
+       }
+       else {
+           if (defined $data->{subject} and length($data->{subject})) {
+               $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'";
+           } else {
+               $action= "Set $config{bug} title to '$param{title}'.";
+           }
+           $data->{subject} = $param{title};
+       }
+        append_action_to_log(bug => $data->{bug_num},
+                            command => '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}};
+    my @bugs = @{$info{bugs}};
+    # clean up the new package
+    my $new_package =
+       join(',',
+            map {my $temp = $_;
+                 ($temp =~ s/^src:// or
+                  $param{is_source}) ? 'src:'.$temp:$temp;
+             } @new_packages);
+
+    my $action = '';
+    my $package_reassigned = 0;
+    for my $data (@data) {
+       my $old_data = dclone($data);
+       print {$debug} "Going to change assigned package\n";
+       if (defined $data->{package} and length($data->{package}) and
+           $data->{package} eq $new_package) {
+           print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n"
+               unless __internal_request();
+           next;
+       }
+       else {
+           if (defined $data->{package} and length($data->{package})) {
+               $package_reassigned = 1;
+               $action= "$config{bug} reassigned from package '$data->{package}'".
+                   " to '$new_package'.";
+           } else {
+               $action= "$config{bug} assigned to package '$new_package'.";
+           }
+           $data->{package} = $new_package;
+       }
+        append_action_to_log(bug => $data->{bug_num},
+                            command => '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 @bugs = @{$info{bugs}};
+    my %versions;
+    for my $version (make_list($param{found})) {
+       next unless defined $version;
+       $versions{$version} =
+           [make_source_versions(package => [splitpackages($data[0]{package})],
+                                 warnings => $transcript,
+                                 debug    => $debug,
+                                 guess_source => 0,
+                                 versions     => $version,
+                                )
+           ];
+       # This is really ugly, but it's what we have to do
+       if (not @{$versions{$version}}) {
+           print {$transcript} "Unable to make a source version for version '$version'\n";
+       }
+    }
+    if (not keys %versions and ($param{remove} or $param{add})) {
+       if ($param{remove}) {
+           print {$transcript} "Requested to remove no versions; doing nothing.\n";
+       }
+       else {
+           print {$transcript} "Requested to add no versions; doing nothing.\n";
+       }
+       __end_control(%info);
+       return;
+    }
+    # first things first, make the versions fully qualified source
+    # versions
+    for my $data (@data) {
+       # The 'done' field gets a bit weird with version tracking,
+       # because a bug may be closed by multiple people in different
+       # branches. Until we have something more flexible, we set it
+       # every time a bug is fixed, and clear it when a bug is found
+       # in a version greater than any version in which the bug is
+       # fixed or when a bug is found and there is no fixed version
+       my $action = 'Did not alter found versions';
+       my %found_added = ();
+       my %found_removed = ();
+       my %fixed_removed = ();
+       my $reopened = 0;
+       my $old_data = dclone($data);
+       if (not $param{add} and not $param{remove}) {
+           $found_removed{$_} = 1 for @{$data->{found_versions}};
+           $data->{found_versions} = [];
+       }
+       my %found_versions;
+       @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
+       my %fixed_versions;
+       @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
+       for my $version (keys %versions) {
+           if ($param{add}) {
+               my @svers = @{$versions{$version}};
+               if (not @svers) {
+                   @svers = $version;
+               }
+               for my $sver (@svers) {
+                   if (not exists $found_versions{$sver}) {
+                       $found_versions{$sver} = 1;
+                       $found_added{$sver} = 1;
+                   }
+                   # if the found we are adding matches any fixed
+                   # versions, remove them
+                   my @temp = grep m{(^|/)\Q$sver\E}, keys %fixed_versions;
+                   delete $fixed_versions{$_} for @temp;
+                   $fixed_removed{$_} = 1 for @temp;
+               }
+
+               # We only care about reopening the bug if the bug is
+               # not done
+               if (defined $data->{done} and length $data->{done}) {
+                   my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
+                       map {m{([^/]+)$}; $1;} @svers;
+                   # determine if we need to reopen
+                   my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
+                       map {m{([^/]+)$}; $1;} keys %fixed_versions;
+                   if (not @fixed_order or
+                       (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
+                       $reopened = 1;
+                       $data->{done} = '';
+                   }
+               }
+           }
+           elsif ($param{remove}) {
+               # in the case of removal, we only concern ourself with
+               # the version passed, not the source version it maps
+               # to
+               my @temp = grep m{(^|/)\Q$version\E}, keys %found_versions;
+               delete $found_versions{$_} for @temp;
+               $found_removed{$_} = 1 for @temp;
+           }
+           else {
+               # set the keys to exactly these values
+               my @svers = @{$versions{$version}};
+               if (not @svers) {
+                   @svers = $version;
+               }
+               for my $sver (@svers) {
+                   if (not exists $found_versions{$sver}) {
+                       $found_versions{$sver} = 1;
+                       if (exists $found_removed{$sver}) {
+                           delete $found_removed{$sver};
+                       }
+                       else {
+                           $found_added{$sver} = 1;
+                       }
+                   }
+               }
+           }
+       }
+
+       $data->{found_versions} = [keys %found_versions];
+       $data->{fixed_versions} = [keys %fixed_versions];
+
+       my @changed;
+       push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
+       push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
+#      push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
+       push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
+       $action = "$config{bug} ".ucfirst(join ('; ',@changed)) if @changed;
+       if ($reopened) {
+           $action .= " and reopened"
+       }
+       if (not $reopened and not @changed) {
+           print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n"
+               unless __internal_request();
+           next;
+       }
+       $action .= '.';
+       append_action_to_log(bug => $data->{bug_num},
+                            get_lock => 0,
+                            command  => '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 @bugs = @{$info{bugs}};
+    my %versions;
+    for my $version (make_list($param{fixed})) {
+       next unless defined $version;
+       $versions{$version} =
+           [make_source_versions(package => [splitpackages($data[0]{package})],
+                                 warnings => $transcript,
+                                 debug    => $debug,
+                                 guess_source => 0,
+                                 versions     => $version,
+                                )
+           ];
+       # This is really ugly, but it's what we have to do
+       if (not @{$versions{$version}}) {
+           print {$transcript} "Unable to make a source version for version '$version'\n";
+       }
+    }
+    if (not keys %versions and ($param{remove} or $param{add})) {
+       if ($param{remove}) {
+           print {$transcript} "Requested to remove no versions; doing nothing.\n";
+       }
+       else {
+           print {$transcript} "Requested to add no versions; doing nothing.\n";
+       }
+       __end_control(%info);
+       return;
+    }
+    # first things first, make the versions fully qualified source
+    # versions
+    for my $data (@data) {
+       my $old_data = dclone($data);
+       # The 'done' field gets a bit weird with version tracking,
+       # because a bug may be closed by multiple people in different
+       # branches. Until we have something more flexible, we set it
+       # every time a bug is fixed, and clear it when a bug is found
+       # in a version greater than any version in which the bug is
+       # fixed or when a bug is found and there is no fixed version
+       my $action = 'Did not alter fixed versions';
+       my %found_added = ();
+       my %found_removed = ();
+       my %fixed_added = ();
+       my %fixed_removed = ();
+       my $reopened = 0;
+       if (not $param{add} and not $param{remove}) {
+           $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
+           $data->{fixed_versions} = [];
+       }
+       my %found_versions;
+       @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
+       my %fixed_versions;
+       @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
+       for my $version (keys %versions) {
+           if ($param{add}) {
+               my @svers = @{$versions{$version}};
+               if (not @svers) {
+                   @svers = $version;
+               }
+               for my $sver (@svers) {
+                   if (not exists $fixed_versions{$sver}) {
+                       $fixed_versions{$sver} = 1;
+                       $fixed_added{$sver} = 1;
+                   }
+               }
+           }
+           elsif ($param{remove}) {
+               # in the case of removal, we only concern ourself with
+               # the version passed, not the source version it maps
+               # to
+               my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
+               delete $fixed_versions{$_} for @temp;
+               $fixed_removed{$_} = 1 for @temp;
+           }
+           else {
+               # set the keys to exactly these values
+               my @svers = @{$versions{$version}};
+               if (not @svers) {
+                   @svers = $version;
+               }
+               for my $sver (@svers) {
+                   if (not exists $fixed_versions{$sver}) {
+                       $fixed_versions{$sver} = 1;
+                       if (exists $fixed_removed{$sver}) {
+                           delete $fixed_removed{$sver};
+                       }
+                       else {
+                           $fixed_added{$sver} = 1;
+                       }
+                   }
+               }
+           }
+       }
+
+       $data->{found_versions} = [keys %found_versions];
+       $data->{fixed_versions} = [keys %fixed_versions];
+
+       # If we're supposed to consider reopening, reopen if the
+       # fixed versions are empty or the greatest found version
+       # is greater than the greatest fixed version
+       if ($param{reopen} and defined $data->{done}
+           and length $data->{done}) {
+           my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
+               map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
+           # determine if we need to reopen
+           my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
+                   map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
+           if (not @fixed_order or
+               (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
+               $reopened = 1;
+               $data->{done} = '';
+           }
+       }
+
+       my @changed;
+       push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
+       push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
+       push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
+       push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
+       $action = "$config{bug} ".ucfirst(join ('; ',@changed)) if @changed;
+       if ($reopened) {
+           $action .= " and reopened"
+       }
+       if (not $reopened and not @changed) {
+           print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n"
+               unless __internal_request();
+           next;
+       }
+       $action .= '.';
+       append_action_to_log(bug => $data->{bug_num},
+                            command  => '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);
+}
 
 
-# this is just a generic stub for Debbugs::Control functions.
-#
-# =head2 foo
-#
-#      eval {
-#          foo(bug          => $ref,
-#              transcript   => $transcript,
-#              ($dl > 0 ? (debug => $transcript):()),
-#              requester    => $header{from},
-#              request_addr => $controlrequestaddr,
-#              message      => \@log,
-#               affected_packages => \%affected_packages,
-#              recipients   => \%recipients,
-#              summary      => undef,
-#              );
-#      };
-#      if ($@) {
-#          $errors++;
-#          print {$transcript} "Failed to foo $ref bar: $@";
-#      }
-#
-# Foo frobinates
-#
-# =cut
-#
-# sub foo {
-#     my %param = validate_with(params => \@_,
-#                            spec   => {bug => {type   => SCALAR,
-#                                               regex  => qr/^\d+$/,
-#                                              },
-#                                       # specific options here
-#                                       %common_options,
-#                                       %append_action_options,
-#                                      },
-#                           );
-#     our $locks = 0;
-#     $locks = 0;
-#     local $SIG{__DIE__} = sub {
-#      if ($locks) {
-#          for (1..$locks) { unfilelock(); }
-#          $locks = 0;
-#      }
-#     };
-#     my ($debug,$transcript) = __handle_debug_transcript(%param);
-#     my (@data);
-#     ($locks, @data) = lock_read_all_merged_bugs($param{bug});
-#     __handle_affected_packages(data => \@data,%param);
-#     print {$transcript} __bug_info(@data);
-#     add_recipients(data => \@data,
-#                   recipients => $param{recipients}
-#                   debug      => $debug,
-#                   transcript => $transcript,
-#                  );
-#     for my $data (@data) {
-#       append_action_to_log(bug => $data->{bug_num},
-#                            get_lock => 0,
-#                            __return_append_to_log_options(
-#                                                           %param,
-#                                                           action => $action,
-#                                                          ),
-#                           )
-#             if not exists $param{append_log} or $param{append_log};
-#        writebug($data->{bug_num},$data);
-#        print {$transcript} "$action\n";
-#        add_recipients(data => $data,
-#                       recipients => $param{recipients},
-#                       debug      => $debug,
-#                       transcript => $transcript,
-#                      );
-#      }
-#      if ($locks) {
-#        for (1..$locks) { unfilelock(); }
-#      }
-#
-# }
 
 =head2 affects
 
@@ -287,26 +1758,17 @@ sub affects {
     if ($param{add} and $param{remove}) {
         croak "Asking to both add and remove affects is nonsensical";
     }
-    our $locks = 0;
-    $locks = 0;
-    local $SIG{__DIE__} = sub {
-       if ($locks) {
-           for (1..$locks) { unfilelock(); }
-           $locks = 0;
-       }
-    };
-    my ($debug,$transcript) = __handle_debug_transcript(%param);
-    my (@data);
-    ($locks, @data) = lock_read_all_merged_bugs($param{bug});
-    __handle_affected_packages(data => \@data,%param);
-    print {$transcript} __bug_info(@data);
-    add_recipients(data => \@data,
-                  recipients => $param{recipients},
-                  debug      => $debug,
-                  transcript => $transcript,
-                 );
-    my $action = 'Did not alter affected packages';
+    my %info =
+       __begin_control(%param,
+                       command  => 'affects'
+                      );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
+    my $action = '';
     for my $data (@data) {
+       $action = '';
         print {$debug} "Going to change affects\n";
         my @packages = splitpackages($data->{affects});
         my %packages;
@@ -314,38 +1776,60 @@ sub affects {
         if ($param{add}) {
              my @added = ();
              for my $package (make_list($param{packages})) {
-                  if (not $packages{$package}) {
-                       $packages{$package} = 1;
-                       push @added,$package;
-                  }
+                 next unless defined $package and length $package;
+                 if (not $packages{$package}) {
+                     $packages{$package} = 1;
+                     push @added,$package;
+                 }
              }
              if (@added) {
                   $action = "Added indication that $data->{bug_num} affects ".
-                       english_join(', ',' and ',@added);
+                       english_join(\@added);
              }
         }
         elsif ($param{remove}) {
              my @removed = ();
              for my $package (make_list($param{packages})) {
                   if ($packages{$package}) {
+                      next unless defined $package and length $package;
                        delete $packages{$package};
                        push @removed,$package;
                   }
              }
              $action = "Removed indication that $data->{bug_num} affects " .
-                  english_join(', ',' and ',@removed);
+                  english_join(\@removed);
         }
         else {
+             my %added_packages = ();
+             my %removed_packages = %packages;
              %packages = ();
              for my $package (make_list($param{packages})) {
+                  next unless defined $package and length $package;
                   $packages{$package} = 1;
+                  delete $removed_packages{$package};
+                  $added_packages{$package} = 1;
+             }
+             if (keys %removed_packages) {
+                 $action = "Removed indication that $data->{bug_num} affects ".
+                     english_join([keys %removed_packages]);
+                 $action .= "\n" if keys %added_packages;
+             }
+             if (keys %added_packages) {
+                 $action .= "Added indication that $data->{bug_num} affects " .
+                  english_join([%added_packages]);
              }
-             $action = "Noted that $data->{bug_num} affects ".
-                  english_join(', ',' and ', keys %packages);
         }
+       if (not length $action) {
+           print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n"
+               unless __internal_request();
+       }
+        my $old_data = dclone($data);
         $data->{affects} = join(',',keys %packages);
         append_action_to_log(bug => $data->{bug_num},
                              get_lock => 0,
+                             command => 'affects',
+                             new_data => $data,
+                             old_data => $old_data,
                              __return_append_to_log_options(
                                                             %param,
                                                             action => $action,
@@ -354,16 +1838,8 @@ sub affects {
               if not exists $param{append_log} or $param{append_log};
          writebug($data->{bug_num},$data);
          print {$transcript} "$action\n";
-         add_recipients(data => $data,
-                        recipients => $param{recipients},
-                        debug      => $debug,
-                        transcript => $transcript,
-                       );
-     }
-     if ($locks) {
-         for (1..$locks) { unfilelock(); }
      }
-
+    __end_control(%info);
 }
 
 
@@ -415,32 +1891,22 @@ sub summary {
                                        },
                             );
     croak "summary must be numeric or undef" if
-        defined $param{summary} and not $param{summary} =~ /^\d+$/;
-    our $locks = 0;
-    $locks = 0;
-    local $SIG{__DIE__} = sub {
-       if ($locks) {
-           for (1..$locks) { unfilelock(); }
-           $locks = 0;
-       }
-    };
-    my ($debug,$transcript) = __handle_debug_transcript(%param);
-    my (@data);
-    ($locks, @data) = lock_read_all_merged_bugs($param{bug});
-    __handle_affected_packages(data => \@data,%param);
-    print {$transcript} __bug_info(@data);
-    add_recipients(data => \@data,
-                  recipients => $param{recipients},
-                  debug      => $debug,
-                  transcript => $transcript,
-                 );
+       defined $param{summary} and not $param{summary} =~ /^\d+$/;
+    my %info =
+       __begin_control(%param,
+                       command  => 'summary'
+                      );
+    my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+    my @data = @{$info{data}};
+    my @bugs = @{$info{bugs}};
     # figure out the log that we're going to use
     my $summary = '';
     my $summary_msg = '';
     my $action = '';
     if (not defined $param{summary}) {
         # do nothing
-        print {$debug} "Removing summary fields";
+        print {$debug} "Removing summary fields\n";
         $action = 'Removed summary';
     }
     else {
@@ -497,15 +1963,22 @@ sub summary {
         }
         print {$debug} "Summary is going to be '$paragraph'\n";
         $summary = $paragraph;
-        $summary =~ s/[\n\r]//g;
+        $summary =~ s/[\n\r]/ /g;
         if (not length $summary) {
              die "Unable to find summary message to use";
         }
-        # trim off a trailing space
-        $summary =~ s/\ $//;
+        # trim off a trailing spaces
+        $summary =~ s/\ *$//;
     }
     for my $data (@data) {
-        print {$debug} "Going to change summary";
+        print {$debug} "Going to change summary\n";
+        if (((not defined $summary or not length $summary) and
+             (not defined $data->{summary} or not length $data->{summary})) or
+            $summary eq $data->{summary}) {
+            print {$transcript} "Ignoring request to change the summary of bug $param{bug} to the same value\n"
+                unless __internal_request();
+            next;
+        }
         if (length $summary) {
              if (length $data->{summary}) {
                   $action = "Summary replaced with message bug $param{bug} message $summary_msg";
@@ -514,8 +1987,12 @@ sub summary {
                   $action = "Summary recorded from message bug $param{bug} message $summary_msg";
              }
         }
+        my $old_data = dclone($data);
         $data->{summary} = $summary;
         append_action_to_log(bug => $data->{bug_num},
+                             command => 'summary',
+                             old_data => $old_data,
+                             new_data => $data,
                              get_lock => 0,
                              __return_append_to_log_options(
                                                             %param,
@@ -525,16 +2002,8 @@ sub summary {
               if not exists $param{append_log} or $param{append_log};
          writebug($data->{bug_num},$data);
          print {$transcript} "$action\n";
-         add_recipients(data => $data,
-                        recipients => $param{recipients},
-                        debug      => $debug,
-                        transcript => $transcript,
-                       );
      }
-     if ($locks) {
-         for (1..$locks) { unfilelock(); }
-     }
-
+    __end_control(%info);
 }
 
 
@@ -576,44 +2045,46 @@ sub owner {
                                          %append_action_options,
                                         },
                              );
-     our $locks = 0;
-     $locks = 0;
-     local $SIG{__DIE__} = sub {
-         if ($locks) {
-              for (1..$locks) { unfilelock(); }
-              $locks = 0;
-         }
-     };
-     my ($debug,$transcript) = __handle_debug_transcript(%param);
-     my (@data);
-     ($locks, @data) = lock_read_all_merged_bugs($param{bug});
-     __handle_affected_packages(data => \@data,%param);
-     print {$transcript} __bug_info(@data);
-     @data and defined $data[0] or die "No bug found for $param{bug}";
-     add_recipients(data => \@data,
-                   recipients => $param{recipients},
-                   debug      => $debug,
-                   transcript => $transcript,
-                  );
+     my %info =
+        __begin_control(%param,
+                        command  => 'owner',
+                       );
+     my ($debug,$transcript) =
+       @info{qw(debug transcript)};
+     my @data = @{$info{data}};
+     my @bugs = @{$info{bugs}};
      my $action = '';
      for my $data (@data) {
          print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
          print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
          if (not defined $param{owner} or not length $param{owner}) {
-              $param{owner} = '';
-              $action = "Removed annotation that $config{bug} was owned by " .
-                   "$data->{owner}.";
+             if (not defined $data->{owner} or not length $data->{owner}) {
+                 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n"
+                     unless __internal_request();
+                 next;
+             }
+             $param{owner} = '';
+             $action = "Removed annotation that $config{bug} was owned by " .
+                 "$data->{owner}.";
          }
          else {
-              if (length $data->{owner}) {
-                   $action = "Owner changed from $data->{owner} to $param{owner}.";
-              }
-              else {
-                   $action = "Owner recorded as $param{owner}."
-              }
+             if ($data->{owner} eq $param{owner}) {
+                 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
+                 next;
+             }
+             if (length $data->{owner}) {
+                 $action = "Owner changed from $data->{owner} to $param{owner}.";
+             }
+             else {
+                 $action = "Owner recorded as $param{owner}."
+             }
          }
+         my $old_data = dclone($data);
          $data->{owner} = $param{owner};
          append_action_to_log(bug => $data->{bug_num},
+                              command => 'owner',
+                              new_data => $data,
+                              old_data => $old_data,
                               get_lock => 0,
               __return_append_to_log_options(
                                              %param,
@@ -623,15 +2094,8 @@ sub owner {
               if not exists $param{append_log} or $param{append_log};
          writebug($data->{bug_num},$data);
          print {$transcript} "$action\n";
-         add_recipients(data => $data,
-                        recipients => $param{recipients},
-                        debug      => $debug,
-                        transcript => $transcript,
-                       );
-     }
-     if ($locks) {
-         for (1..$locks) { unfilelock(); }
      }
+     __end_control(%info);
 }
 
 
@@ -693,16 +2157,13 @@ sub bug_archive {
                                          %append_action_options,
                                         },
                              );
-     our $locks = 0;
-     $locks = 0;
-     local $SIG{__DIE__} = sub {
-         if ($locks) {
-              for (1..$locks) { unfilelock(); }
-              $locks = 0;
-         }
-     };
+     my %info = __begin_control(%param,
+                               command => 'archive',
+                               );
+     my ($debug,$transcript) = @info{qw(debug transcript)};
+     my @data = @{$info{data}};
+     my @bugs = @{$info{bugs}};
      my $action = "$config{bug} archived.";
-     my ($debug,$transcript) = __handle_debug_transcript(%param);
      if ($param{check_archiveable} and
         not bug_archiveable(bug=>$param{bug},
                             ignore_time => $param{ignore_time},
@@ -711,14 +2172,6 @@ sub bug_archive {
          die "Bug $param{bug} cannot be archived";
      }
      print {$debug} "$param{bug} considering\n";
-     my (@data);
-     ($locks, @data) = lock_read_all_merged_bugs($param{bug});
-     __handle_affected_packages(data => \@data,%param);
-     print {$transcript} __bug_info(@data);
-     print {$debug} "$param{bug} read $locks\n";
-     @data and defined $data[0] or die "No bug found for $param{bug}";
-     print {$debug} "$param{bug} read done\n";
-
      if (not $param{archive_unarchived} and
         not exists $data[0]{unarchived}
        ) {
@@ -730,7 +2183,6 @@ sub bug_archive {
                    debug      => $debug,
                    transcript => $transcript,
                   );
-     my @bugs = map {$_->{bug_num}} @data;
      print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
      for my $bug (@bugs) {
         if ($param{check_archiveable}) {
@@ -748,6 +2200,12 @@ sub bug_archive {
          # First indicate that this bug is being archived
          append_action_to_log(bug => $bug,
                               get_lock => 0,
+                              command => 'archive',
+                              # we didn't actually change the data
+                              # when we archived, so we don't pass
+                              # a real new_data or old_data
+                              new_data => {},
+                              old_data => {},
                               __return_append_to_log_options(
                                 %param,
                                 action => $action,
@@ -758,8 +2216,12 @@ sub bug_archive {
          if ($config{save_old_bugs}) {
               mkpath("$config{spool_dir}/archive/$dir");
               foreach my $file (@files_to_remove) {
-                   link( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" ) or
-                        copy( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" );
+                  link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
+                      copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
+                          # we need to bail out here if things have
+                          # gone horribly wrong to avoid removing a
+                          # bug altogether
+                          die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
               }
 
               print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
@@ -768,14 +2230,7 @@ sub bug_archive {
          print {$transcript} "deleted $bug (from $param{bug})\n";
      }
      bughook_archive(@bugs);
-     if (exists $param{bugs_affected}) {
-         @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
-     }
-     print {$debug} "$param{bug} unlocking $locks\n";
-     if ($locks) {
-         for (1..$locks) { unfilelock(); }
-     }
-     print {$debug} "$param{bug} unlocking done\n";
+     __end_control(%info);
 }
 
 =head2 bug_unarchive
@@ -806,29 +2261,15 @@ sub bug_unarchive {
                                          %append_action_options,
                                         },
                              );
-     our $locks = 0;
-     local $SIG{__DIE__} = sub {
-         if ($locks) {
-              for (1..$locks) { unfilelock(); }
-              $locks = 0;
-         }
-     };
+
+     my %info = __begin_control(%param,
+                               archived=>1,
+                               command=>'unarchive');
+     my ($debug,$transcript) =
+        @info{qw(debug transcript)};
+     my @data = @{$info{data}};
+     my @bugs = @{$info{bugs}};
      my $action = "$config{bug} unarchived.";
-     my ($debug,$transcript) = __handle_debug_transcript(%param);
-     print {$debug} "$param{bug} considering\n";
-     my @data = ();
-     ($locks, @data) = lock_read_all_merged_bugs($param{bug},'archive');
-     __handle_affected_packages(data => \@data,%param);
-     print {$transcript} __bug_info(@data);
-     print {$debug} "$param{bug} read $locks\n";
-     if (not @data or not defined $data[0]) {
-        print {$transcript} "No bug found for $param{bug}\n";
-        die "No bug found for $param{bug}";
-     }
-     print {$debug} "$param{bug} read done\n";
-     my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
-     print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
-     print {$debug} "$param{bug} unarchiving\n";
      my @files_to_remove;
      for my $bug (@bugs) {
          print {$debug} "$param{bug} removing $bug\n";
@@ -848,6 +2289,7 @@ sub bug_unarchive {
      # Indicate that this bug has been archived previously
      for my $bug (@bugs) {
          my $newdata = readbug($bug);
+         my $old_data = dclone($newdata);
          if (not defined $newdata) {
               print {$transcript} "$config{bug} $bug disappeared!\n";
               die "Bug $bug disappeared!";
@@ -855,6 +2297,9 @@ sub bug_unarchive {
          $newdata->{unarchived} = time;
          append_action_to_log(bug => $bug,
                               get_lock => 0,
+                              command => 'unarchive',
+                              new_data => $newdata,
+                              old_data => $old_data,
                               __return_append_to_log_options(
                                 %param,
                                 action => $action,
@@ -862,20 +2307,8 @@ sub bug_unarchive {
                              )
               if not exists $param{append_log} or $param{append_log};
          writebug($bug,$newdata);
-         add_recipients(recipients => $param{recipients},
-                        data       => $newdata,
-                        debug      => $debug,
-                        transcript => $transcript,
-                       );
-     }
-     print {$debug} "$param{bug} unlocking $locks\n";
-     if ($locks) {
-         for (1..$locks) { unfilelock(); };
-     }
-     if (exists $param{bugs_affected}) {
-         @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
      }
-     print {$debug} "$param{bug} unlocking done\n";
+     __end_control(%info);
 }
 
 =head2 append_action_to_log
@@ -892,6 +2325,15 @@ sub append_action_to_log{
                               spec   => {bug => {type   => SCALAR,
                                                  regex  => qr/^\d+/,
                                                 },
+                                         new_data => {type => HASHREF,
+                                                      optional => 1,
+                                                     },
+                                         old_data => {type => HASHREF,
+                                                      optional => 1,
+                                                     },
+                                         command  => {type => SCALAR,
+                                                      optional => 1,
+                                                     },
                                          action => {type => SCALAR,
                                                    },
                                          requester => {type => SCALAR,
@@ -912,7 +2354,14 @@ sub append_action_to_log{
                                          get_lock   => {type => BOOLEAN,
                                                         default => 1,
                                                        },
-                                        }
+                                         # we don't use
+                                         # append_action_options here
+                                         # because some of these
+                                         # options aren't actually
+                                         # optional, even though the
+                                         # original function doesn't
+                                         # require them
+                                        },
                              );
      # Fix this to use $param{location}
      my $log_location = buglog($param{bug});
@@ -923,9 +2372,101 @@ sub append_action_to_log{
      }
      my $log = IO::File->new(">>$log_location") or
          die "Unable to open $log_location for appending: $!";
-     my $msg = "\6\n".
-         "<!-- time:".time." -->\n".
-          "<strong>".html_escape($param{action})."</strong>\n";
+     # determine difference between old and new
+     my $data_diff = '';
+     if (exists $param{old_data} and exists $param{new_data}) {
+        my $old_data = dclone($param{old_data});
+        my $new_data = dclone($param{new_data});
+        for my $key (keys %{$old_data}) {
+            if (not exists $Debbugs::Status::fields{$key}) {
+                delete $old_data->{$key};
+                next;
+            }
+            next unless exists $new_data->{$key};
+            next unless defined $new_data->{$key};
+            if (not defined $old_data->{$key}) {
+                delete $old_data->{$key};
+                next;
+            }
+            if (ref($new_data->{$key}) and
+                ref($old_data->{$key}) and
+                ref($new_data->{$key}) eq ref($old_data->{$key})) {
+               local $Storable::canonical = 1;
+               # print STDERR Dumper($new_data,$old_data,$key);
+               if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
+                   delete $new_data->{$key};
+                   delete $old_data->{$key};
+               }
+            }
+            elsif ($new_data->{$key} eq $old_data->{$key}) {
+                delete $new_data->{$key};
+                delete $old_data->{$key};
+            }
+        }
+        for my $key (keys %{$new_data}) {
+            if (not exists $Debbugs::Status::fields{$key}) {
+                delete $new_data->{$key};
+                next;
+            }
+            next unless exists $old_data->{$key};
+            next unless defined $old_data->{$key};
+            if (not defined $new_data->{$key} or
+                not exists $Debbugs::Status::fields{$key}) {
+                delete $new_data->{$key};
+                next;
+            }
+            if (ref($new_data->{$key}) and
+                ref($old_data->{$key}) and
+                ref($new_data->{$key}) eq ref($old_data->{$key})) {
+               local $Storable::canonical = 1;
+               if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
+                   delete $new_data->{$key};
+                   delete $old_data->{$key};
+               }
+            }
+            elsif ($new_data->{$key} eq $old_data->{$key}) {
+                delete $new_data->{$key};
+                delete $old_data->{$key};
+            }
+        }
+        $data_diff .= "<!-- 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([\%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([\%od],[qw(old_data)]));
+        $data_diff .= "-->\n";
+     }
+     my $msg = join('',"\6\n",
+                   (exists $param{command} ?
+                    "<!-- command:".html_escape($param{command})." -->\n":""
+                   ),
+                   (length $param{requester} ?
+                    "<!-- requester: ".html_escape($param{requester})." -->\n":""
+                   ),
+                   (length $param{request_addr} ?
+                    "<!-- request_addr: ".html_escape($param{request_addr})." -->\n":""
+                   ),
+                   "<!-- time:".time()." -->\n",
+                   $data_diff,
+                   "<strong>".html_escape($param{action})."</strong>\n");
      if (length $param{requester}) {
           $msg .= "Request was from <code>".html_escape($param{requester})."</code>\n";
      }
@@ -975,8 +2516,9 @@ sub __handle_affected_packages{
                              );
      for my $data (make_list($param{data})) {
          next unless exists $data->{package} and defined $data->{package};
-         $param{affected_packages}{$data->{package}} = 1;
-     }
+         my @packages = split /\s*,\s*/,$data->{package};
+         @{$param{affected_packages}}{@packages} = (1) x @packages;
+      }
 }
 
 =head2 __handle_debug_transcript
@@ -1009,14 +2551,45 @@ Produces a small bit of bug information to kick out to the transcript
 sub __bug_info{
      my $return = '';
      for my $data (@_) {
-         $return .= "Bug ".($data->{bug_num}||'').
-              " [".($data->{package}||''). "] ".
-                   ($data->{subject}||'')."\n";
+        next unless defined $data and exists $data->{bug_num};
+         $return .= "Bug #".($data->{bug_num}||'').
+             ((defined $data->{done} and length $data->{done})?
+               " {Done: $data->{done}}":''
+              ).
+              " [".($data->{package}||'(no package)'). "] ".
+                   ($data->{subject}||'(no subject)')."\n";
      }
      return $return;
 }
 
 
+=head2 __internal_request
+
+     __internal_request()
+     __internal_request($level)
+
+Returns true if the caller of the function calling __internal_request
+belongs to __PACKAGE__
+
+This allows us to be magical, and don't bother to print bug info if
+the second caller is from this package, amongst other things.
+
+An optional level is allowed, which increments the number of levels to
+check by the given value. [This is basically for use by internal
+functions like __begin_control which are always called by
+C<__PACKAGE__>.
+
+=cut
+
+sub __internal_request{
+    my ($l) = @_;
+    $l = 0 if not defined $l;
+    if (defined +(caller(2+$l))[0] and +(caller(2+$l))[0] eq __PACKAGE__) {
+       return 1;
+    }
+    return 0;
+}
+
 sub __return_append_to_log_options{
      my %param = @_;
      my $action = $param{action} if exists $param{action};
@@ -1046,6 +2619,250 @@ sub __return_append_to_log_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 $locks = 0;
+
+sub __begin_control {
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type   => SCALAR,
+                                                regex  => qr/^\d+/,
+                                               },
+                                        archived => {type => BOOLEAN,
+                                                     default => 0,
+                                                    },
+                                        command  => {type => SCALAR,
+                                                     optional => 1,
+                                                    },
+                                        %common_options,
+                                       },
+                             allow_extra => 1,
+                            );
+    my $new_locks;
+    my ($debug,$transcript) = __handle_debug_transcript(@_);
+    print {$debug} "$param{bug} considering\n";
+    my @data = ();
+    my $old_die = $SIG{__DIE__};
+    $SIG{__DIE__} = *sig_die{CODE};
+
+    ($new_locks, @data) =
+       lock_read_all_merged_bugs($param{bug},
+                                 ($param{archived}?'archive':()));
+    $locks += $new_locks;
+    if (not @data) {
+       die "Unable to read any bugs successfully.";
+    }
+    if (not __check_limit(data => \@data,
+                         exists $param{limit}?(limit => $param{limit}):(),
+                        )) {
+       die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
+    }
+
+    __handle_affected_packages(%param,data => \@data);
+    print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
+    print {$debug} "$param{bug} read $locks locks\n";
+    if (not @data or not defined $data[0]) {
+       print {$transcript} "No bug found for $param{bug}\n";
+       die "No bug found for $param{bug}";
+    }
+
+    add_recipients(data => \@data,
+                  recipients => $param{recipients},
+                  (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
+                  debug      => $debug,
+                  transcript => $transcript,
+                 );
+
+    print {$debug} "$param{bug} read done\n";
+    my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
+    print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
+    return (data       => \@data,
+           bugs       => \@bugs,
+           old_die    => $old_die,
+           new_locks  => $new_locks,
+           debug      => $debug,
+           transcript => $transcript,
+           param      => \%param,
+          );
+}
+
+=head2 __end_control
+
+     __end_control(%info);
+
+Handles tearing down from a control request
+
+=cut
+
+sub __end_control {
+    my %info = @_;
+    if (exists $info{new_locks} and $info{new_locks} > 0) {
+       print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
+       for (1..$info{new_locks}) {
+           unfilelock();
+       }
+    }
+    $SIG{__DIE__} = $info{old_die};
+    if (exists $info{param}{bugs_affected}) {
+       @{$info{param}{bugs_affected}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
+    }
+    add_recipients(recipients => $info{param}{recipients},
+                  (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
+                  data       => $info{data},
+                  debug      => $info{debug},
+                  transcript => $info{transcript},
+                 );
+    __handle_affected_packages(%{$info{param}},data=>$info{data});
+}
+
+
+=head2 __check_limit
+
+     __check_limit(data => \@data, limit => $param{limit});
+
+
+Checks to make sure that bugs match any limits; each entry of @data
+much satisfy the limit.
+
+Returns true if there are no entries in data, or there are no keys in
+limit; returns false (0) if there are any entries which do not match.
+
+The limit hashref elements can contain an arrayref of scalars to
+match; regexes are also acccepted. At least one of the entries in each
+element needs to match the corresponding field in all data for the
+limit to succeed.
+
+=cut
+
+
+sub __check_limit{
+    my %param = validate_with(params => \@_,
+                             spec   => {data  => {type => ARRAYREF|SCALAR,
+                                                 },
+                                        limit => {type => HASHREF|UNDEF,
+                                                 },
+                                       },
+                            );
+    my @data = make_list($param{data});
+    if (not @data or
+       not defined $param{limit} or
+       not keys %{$param{limit}}) {
+       return 1;
+    }
+    for my $data (@data) {
+       for my $field (keys %{$param{limit}}) {
+           next unless exists $param{limit}{$field};
+           my $match = 0;
+           for my $limit (make_list($param{limit}{$field})) {
+               if (not ref $limit) {
+                   if ($data->{$field} eq $limit) {
+                       $match = 1;
+                       last;
+                   }
+               }
+               elsif (ref($limit) eq 'Regexp') {
+                   if ($data->{$field} =~ $limit) {
+                       $match = 1;
+                       last;
+                   }
+               }
+               else {
+                   warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
+               }
+           }
+           if (not $match) {
+               return 0;
+           }
+       }
+    }
+    return 1;
+}
+
+
+=head2 die
+
+     sig_die "foo"
+
+We override die to specially handle unlocking files in the cases where
+we are called via eval. [If we're not called via eval, it doesn't
+matter.]
+
+=cut
+
+sub sig_die{
+    #if ($^S) { # in eval
+       if ($locks) {
+           for (1..$locks) { unfilelock(); }
+           $locks = 0;
+       }
+    #}
+}
+
+
+# =head2 __message_body_template
+#
+#      message_body_template('mail/ack',{ref=>'foo'});
+#
+# Creates a message body using a template
+#
+# =cut
+
+sub __message_body_template{
+     my ($template,$extra_var) = @_;
+     $extra_var ||={};
+     my $hole_var = {'&bugurl' =>
+                    sub{"$_[0]: ".
+                            'http://'.$config{cgi_domain}.'/'.
+                                Debbugs::CGI::bug_url($_[0]);
+                    }
+                   };
+
+     my $body = fill_in_template(template => $template,
+                                variables => {config => \%config,
+                                              %{$extra_var},
+                                             },
+                                hole_var => $hole_var,
+                               );
+     return fill_in_template(template => 'mail/message_body',
+                            variables => {config => \%config,
+                                          %{$extra_var},
+                                          body => $body,
+                                         },
+                            hole_var => $hole_var,
+                           );
+}
+
 
 1;
 
index 5e7635147470ffc7137454fa4906d8ddc981618c..268958e61a8e95ca3c8e0ca641275ae20e771069 100644 (file)
@@ -340,6 +340,8 @@ sub read_log_records
 Takes a filehandle and a list of records as input, and prints the .log
 format representation of those records to that filehandle.
 
+=back
+
 =cut
 
 sub write_log_records (*@)
@@ -392,8 +394,6 @@ sub escape_log {
 }
 
 
-=back
-
 =head1 CAVEATS
 
 This module does none of the formatting that bugreport.cgi et al do. It's
index 0d35b0fb85e8b259fdf37d3faad373dc5f51087f..2e1d611c94a8085524633d3aaa37d78cf194246d 100644 (file)
 
 package Debbugs::MIME;
 
+=head1 NAME
+
+Debbugs::MIME -- Mime handling routines for debbugs
+
+=head1 SYNOPSIS
+
+ use Debbugs::MIME qw(parse decode_rfc1522);
+
+=head1 DESCRIPTION
+
+
+=head1 BUGS
+
+None known.
+
+=cut
+
+use warnings;
 use strict;
 
 use base qw(Exporter);
@@ -22,6 +40,7 @@ BEGIN {
 }
 
 use File::Path;
+use File::Temp qw();
 use MIME::Parser;
 
 use POSIX qw(strftime);
@@ -65,8 +84,8 @@ sub parse
     my (@headerlines, @bodylines);
 
     my $parser = MIME::Parser->new();
-    mkdir "mime.tmp.$$", 0777;
-    $parser->output_under("mime.tmp.$$");
+    my $tempdir = File::Temp::tempdir();
+    $parser->output_under($tempdir);
     my $entity = eval { $parser->parse_data($_[0]) };
 
     if ($entity and $entity->head->tags) {
@@ -94,7 +113,7 @@ sub parse
        @bodylines = @msg[$i .. $#msg];
     }
 
-    rmtree "mime.tmp.$$", 0, 1;
+    rmtree $tempdir, 0, 1;
 
     # Remove blank lines.
     shift @bodylines while @bodylines and $bodylines[0] !~ /\S/;
index a19399dff5434ac735643df06fd1b1d32e345e49..054b04c2bbb22ab3c92ebef1e8a3d88303e0c7f4 100644 (file)
@@ -49,14 +49,20 @@ use Debbugs::MIME qw(encode_rfc1522);
 use Debbugs::Config qw(:config);
 use Params::Validate qw(:types validate_with);
 
+use Debbugs::Packages;
+
 BEGIN{
      ($VERSION) = q$Revision: 1.1 $ =~ /^Revision:\s+([^\s+])/;
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
-     @EXPORT_OK = qw(send_mail_message get_addresses encode_headers rfc822_date);
+     %EXPORT_TAGS = (addresses => [qw(get_addresses)],
+                    misc      => [qw(rfc822_date)],
+                    mail      => [qw(send_mail_message encode_headers default_headers)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(keys %EXPORT_TAGS);
      $EXPORT_TAGS{all} = [@EXPORT_OK];
-
 }
 
 # We set this here so it can be overridden for testing purposes
@@ -77,6 +83,199 @@ sub get_addresses {
 }
 
 
+=head2 default_headers
+
+      my @head = default_headers(queue_file => 'foo',
+                                 data       => $data,
+                                 msgid      => $header{'message-id'},
+                                 msgtype    => 'error',
+                                 headers    => [...],
+                                );
+      create_mime_message(\@headers,
+                         ...
+                         );
+
+This function is generally called to generate the headers for
+create_mime_message (and anything else that needs a set of default
+headers.)
+
+In list context, returns an array of headers. In scalar context,
+returns headers for shoving in a mail message after encoding using
+encode_headers.
+
+=head3 options
+
+=over
+
+=item queue_file -- the queue file which will generate this set of
+headers (refered to as $nn in lots of the code)
+
+=item data -- the data of the bug which this message involves; can be
+undefined if there is no bug involved.
+
+=item msgid -- the Message-ID: of the message which will generate this
+set of headers
+
+=item msgtype -- the type of message that this is.
+
+=item pr_msg -- the pr message field
+
+=item headers -- a set of headers which will override the default
+headers; these headers will be passed through (and may be reordered.)
+If a particular header is undef, it overrides the default, but isn't
+passed through.
+
+=back
+
+=head3 default headers
+
+=over
+
+=item X-Loop -- set to the maintainer e-mail
+
+=item From -- set to the maintainer e-mail
+
+=item To -- set to Unknown recipients
+
+=item Subject -- set to Unknown subject
+
+=item Message-ID -- set appropriately (see code)
+
+=item Precedence -- set to bulk
+
+=item References -- set to the full set of message ids that are known
+(from data and the msgid option)
+
+=item In-Reply-To -- set to msg id or the msgid from data
+
+=item X-Project-PR-Message -- set to pr_msg with the bug number appended
+
+=item X-Project-PR-Package -- set to the package of the bug
+
+=item X-Project-PR-Keywords -- set to the keywords of the bug
+
+=item X-Project-PR-Source -- set to the source of the bug
+
+=back
+
+=cut
+
+sub default_headers {
+    my %param = validate_with(params => \@_,
+                             spec   => {queue_file => {type => SCALAR,
+                                                       optional => 1,
+                                                      },
+                                        data       => {type => HASHREF,
+                                                       optional => 1,
+                                                      },
+                                        msgid      => {type => SCALAR,
+                                                       optional => 1,
+                                                      },
+                                        msgtype    => {type => SCALAR,
+                                                       default => 'misc',
+                                                       optional => 1,
+                                                      },
+                                        pr_msg     => {type => SCALAR,
+                                                       default => 'misc',
+                                                      },
+                                        headers    => {type => ARRAYREF,
+                                                       default => [],
+                                                      },
+                                       },
+                            );
+    my @header_order = (qw(X-Loop From To subject),
+                       qw(Message-ID In-Reply-To References));
+    my %header_order;
+    @header_order{map {lc $_} @header_order} = 0..$#header_order;
+    my %set_headers;
+    my @ordered_headers;
+    my @temp = @{$param{headers}};
+    my @other_headers;
+    while (my ($header,$value) = splice @temp,0,2) {
+       if (exists $header_order{lc($header)}) {
+           push @{$ordered_headers[$header_order{lc($header)}]},
+               ($header,$value);
+       }
+       else {
+           push @other_headers,($header,$value);
+       }
+       $set_headers{lc($header)} = 1;
+    }
+
+    # calculate our headers
+    my $bug_num = exists $param{data} ? $param{data}{bug_num} : 'x';
+    my $nn = $param{queue_file};
+    # handle the user giving the actual queue filename instead of nn
+    $nn =~ s/^[a-zA-Z]([a-zA-Z])/$1/;
+    $nn = lc($nn);
+    my @msgids;
+    if (exists $param{msgid} and defined $param{msgid}) {
+       push @msgids, $param{msgid}
+    }
+    elsif (exists $param{data} and defined $param{data}{msgid}) {
+       push @msgids, $param{data}{msgid}
+    }
+    my %default_header;
+    $default_header{'X-Loop'} = $config{maintainer_email};
+    $default_header{From}     = "$config{maintainer_email} ($config{project} $config{ubug} Tracking System)";
+    $default_header{To}       = "Unknown recipients";
+    $default_header{Subject}  = "Unknown subject";
+    $default_header{'Message-ID'} = "<handler.${bug_num}.${nn}.$param{msgtype}\@$config{email_domain}>";
+    if (@msgids) {
+       $default_header{'In-Reply-To'} = $msgids[0];
+       $default_header{'References'} = join(' ',@msgids);
+    }
+    $default_header{Precedence} = 'bulk';
+    $default_header{"X-$config{project}-PR-Message"} = $param{pr_msg} . (exists $param{data} ? ' '.$param{data}{bug_num}:'');
+    $default_header{Date} = rfc822_date();
+    if (exists $param{data}) {
+       if (defined $param{data}{keywords}) {
+           $default_header{"X-$config{project}-PR-Keywords"} = $param{data}{keywords};
+       }
+       if (defined $param{data}{package}) {
+           $default_header{"X-$config{project}-PR-Package"} = $param{data}{package};
+           if ($param{data}{package} =~ /^src:(.+)$/) {
+               $default_header{"X-$config{project}-PR-Source"} = $1;
+           }
+           else {
+               my $pkg_src = Debbugs::Packages::getpkgsrc();
+               $default_header{"X-$config{project}-PR-Source"} = $pkg_src->{$param{data}{package}};
+           }
+       }
+    }
+    for my $header (sort keys %default_header) {
+       next if $set_headers{lc($header)};
+       if (exists $header_order{lc($header)}) {
+           push @{$ordered_headers[$header_order{lc($header)}]},
+               ($header,$default_header{$header});
+       }
+       else {
+           push @other_headers,($header,$header_order{lc($header)});
+       }
+    }
+    my @headers;
+    for my $hdr1 (@ordered_headers) {
+       next if not defined $hdr1;
+       my @temp = @{$hdr1};
+       while (my ($header,$value) = splice @temp,0,2) {
+           next if not defined $value;
+           push @headers,($header,$value);
+       }
+    }
+    push @headers,@other_headers;
+    if (wantarray) {
+       return @headers;
+    }
+    else {
+       my $headers = '';
+       while (my ($header,$value) = splice @headers,0,2) {
+           $headers .= "${header}: $value\n";
+       }
+       return $headers;
+    }
+}
+
+
 
 =head2 send_mail_message
 
index 191b4531f955a02c95964cedb195adad09c794bc..f9020bae8095aab35aa7db4557ceb31b50be9ac4 100644 (file)
@@ -15,13 +15,15 @@ use strict;
 use base qw(Exporter);
 use vars qw($VERSION @EXPORT_OK %EXPORT_TAGS @EXPORT);
 
+use Carp;
+
 use Debbugs::Config qw(:config :globals);
 
 BEGIN {
     $VERSION = 1.00;
 
      @EXPORT = ();
-     %EXPORT_TAGS = (versions => [qw(getversions get_versions)],
+     %EXPORT_TAGS = (versions => [qw(getversions get_versions make_source_versions)],
                     mapping  => [qw(getpkgsrc getpkgcomponent getsrcpkgs),
                                  qw(binarytosource sourcetobinary makesourceversions)
                                 ],
@@ -35,7 +37,7 @@ use Fcntl qw(O_RDONLY);
 use MLDBM qw(DB_File Storable);
 use Storable qw(dclone);
 use Params::Validate qw(validate_with :types);
-use Debbugs::Common qw(make_list);
+use Debbugs::Common qw(make_list globify_scalar);
 
 use List::Util qw(min max);
 
@@ -58,9 +60,7 @@ may not make sense in other contexts.)
 
 =head1 METHODS
 
-=over 8
-
-=item getpkgsrc
+=head2 getpkgsrc
 
 Returns a reference to a hash of binary package names to their corresponding
 source package names.
@@ -94,7 +94,7 @@ sub getpkgsrc {
     return $_pkgsrc;
 }
 
-=item getpkgcomponent
+=head2 getpkgcomponent
 
 Returns a reference to a hash of binary package names to the component of
 the archive containing those binary packages (e.g. "main", "contrib",
@@ -108,7 +108,7 @@ sub getpkgcomponent {
     return $_pkgcomponent;
 }
 
-=item getsrcpkgs
+=head2 getsrcpkgs
 
 Returns a list of the binary packages produced by a given source package.
 
@@ -121,7 +121,7 @@ sub getsrcpkgs {
     return @{$_srcpkg->{$src}};
 }
 
-=item binarytosource
+=head2 binarytosource
 
 Returns a reference to the source package name and version pair
 corresponding to a given binary package name, version, and architecture.
@@ -167,6 +167,9 @@ sub binarytosource {
     elsif (exists $binary{$binver}) {
         if (defined $binarch) {
              my $src = $binary{$binver}{$binarch};
+             if (not defined $src and exists $binary{$binver}{all}) {
+                 $src = $binary{$binver}{all};
+             }
              return () unless defined $src; # not on this arch
              # Copy the data to avoid tiedness problems.
              return dclone($src);
@@ -194,7 +197,7 @@ sub binarytosource {
     return ();
 }
 
-=item sourcetobinary
+=head2 sourcetobinary
 
 Returns a list of references to triplets of binary package names, versions,
 and architectures corresponding to a given source package name and version.
@@ -219,9 +222,8 @@ sub sourcetobinary {
     # avoid autovivification
     my $source = $_sourcetobinary{$srcname};
     return () unless defined $source;
-    my %source = %{$source};
-    if (exists $source{$srcver}) {
-        my $bin = $source{$srcver};
+    if (exists $source->{$srcver}) {
+        my $bin = $source->{$srcver};
         return () unless defined $bin;
         return @$bin;
     }
@@ -231,7 +233,7 @@ sub sourcetobinary {
     return map [$_, $srcver], @srcpkgs;
 }
 
-=item getversions
+=head2 getversions
 
 Returns versions of the package in a distribution at a specific
 architecture
@@ -340,8 +342,8 @@ sub get_versions{
               for my $arch (exists $param{arch}?
                             make_list($param{arch}):
                             (grep {not $param{no_source_arch} or
-                                        $_ ne 'source'
-                              } keys %{$version->{$dist}})) {
+                                       $_ ne 'source'
+                                   } keys %{$version->{$dist}})) {
                    next unless defined $version->{$dist}{$arch};
                    for my $ver (ref $version->{$dist}{$arch} ?
                                 keys %{$version->{$dist}{$arch}} :
@@ -349,7 +351,9 @@ sub get_versions{
                                ) {
                         my $f_ver = $ver;
                         if ($param{source}) {
-                             ($f_ver) = makesourceversions($package,$arch,$ver);
+                             ($f_ver) = make_source_versions(package => $package,
+                                                             arch => $arch,
+                                                             versions => $ver);
                              next unless defined $f_ver;
                         }
                         if ($param{time}) {
@@ -369,7 +373,7 @@ sub get_versions{
 }
 
 
-=item makesourceversions
+=head2 makesourceversions
 
      @{$cgi_var{found}} = makesourceversions($cgi_var{package},undef,@{$cgi_var{found}});
 
@@ -382,57 +386,145 @@ version numbers differ from binary version numbers.
 
 our %_sourceversioncache = ();
 sub makesourceversions {
-    my $pkg = shift;
-    my $arch = shift;
-    my %sourceversions;
-    die "Package $pkg is multiple packages; split on , and call makesourceversions multiple times"
-        if $pkg =~ /,/;
+    my ($package,$arch,@versions) = @_;
+    die "Package $package is multiple packages; split on , and call makesourceversions multiple times"
+        if $package =~ /,/;
+    return make_source_versions(package => $package,
+                               (defined $arch)?(arch => $arch):(),
+                               versions => \@versions
+                              );
+}
+
+=head2 make_source_versions
+
+     make_source_versions(package => 'foo',
+                          arch    => 'source',
+                          versions => '0.1.1',
+                          guess_source => 1,
+                          debug    => \$debug,
+                          warnings => \$warnings,
+                         );
+
+An extended version of makesourceversions (which calls this function
+internally) that allows for multiple packages, architectures, and
+outputs warnings and debugging information to provided SCALARREFs or
+HANDLEs.
+
+The guess_source option determines whether the source package is
+guessed at if there is no obviously correct package. Things that use
+this function for non-transient output should set this to false,
+things that use it for transient output can set this to true.
+Currently it defaults to true, but that is not a sane option.
+
+
+=cut
 
-    for my $version (@_) {
-        if ($version =~ m[/]) {
+sub make_source_versions {
+    my %param = validate_with(params => \@_,
+                             spec   => {package => {type => SCALAR|ARRAYREF,
+                                                   },
+                                        arch    => {type => SCALAR|ARRAYREF|UNDEF,
+                                                    default => ''
+                                                   },
+                                        versions => {type => SCALAR|ARRAYREF,
+                                                     default => [],
+                                                    },
+                                        guess_source => {type => BOOLEAN,
+                                                         default => 1,
+                                                        },
+                                        source_version_cache => {type => HASHREF,
+                                                                 optional => 1,
+                                                                },
+                                        debug    => {type => SCALARREF|HANDLE,
+                                                     optional => 1,
+                                                    },
+                                        warnings => {type => SCALARREF|HANDLE,
+                                                     optional => 1,
+                                                    },
+                                       },
+                            );
+    my ($warnings) = globify_scalar(exists $param{warnings}?$param{warnings}:undef);
+    my ($debug)    = globify_scalar(exists $param{debug}   ?$param{debug}   :undef);
+
+    my @packages = grep {defined $_ and length $_ } make_list($param{package});
+    my @archs    = grep {defined $_ } make_list ($param{arch});
+    if (not @archs) {
+       push @archs, '';
+    }
+    if (not exists $param{source_version_cache}) {
+       $param{source_version_cache} = \%_sourceversioncache;
+    }
+    if (grep {/,/} make_list($param{package})) {
+       croak "Package names contain ,; split on /,/ and call make_source_versions with an arrayref of packages"
+    }
+    my %sourceversions;
+    for my $version (make_list($param{versions})) {
+        if ($version =~ m{(.+)/([^/]+)$}) {
+           # check to see if this source version is even possible
+           my @bin_versions = sourcetobinary($1,$2);
+           if (not @bin_versions or
+               @{$bin_versions[0]} != 3) {
+               print {$warnings} "The source $1 and version $2 do not appear to match any binary packages\n";
+           }
             # Already a source version.
             $sourceversions{$version} = 1;
         } else {
-            my $cachearch = (defined $arch) ? $arch : '';
-            my $cachekey = "$pkg/$cachearch/$version";
-            if (exists($_sourceversioncache{$cachekey})) {
-                for my $v (@{$_sourceversioncache{$cachekey}}) {
-                   $sourceversions{$v} = 1;
+           if (not @packages) {
+               croak "You must provide at least one package if the versions are not fully qualified";
+           }
+           for my $pkg (@packages) {
+               for my $arch (@archs) {
+                   my $cachearch = (defined $arch) ? $arch : '';
+                   my $cachekey = "$pkg/$cachearch/$version";
+                   if (exists($param{source_version_cache}{$cachekey})) {
+                       for my $v (@{$param{source_version_cache}{$cachekey}}) {
+                           $sourceversions{$v} = 1;
+                       }
+                       next;
+                   }
+                   elsif ($param{guess_source} and
+                          exists$param{source_version_cache}{$cachekey.'/guess'}) {
+                       for my $v (@{$param{source_version_cache}{$cachekey.'/guess'}}) {
+                           $sourceversions{$v} = 1;
+                       }
+                       next;
+                   }
+                   my @srcinfo = binarytosource($pkg, $version, $arch);
+                   if (not @srcinfo) {
+                       # We don't have explicit information about the
+                       # binary-to-source mapping for this version
+                       # (yet).
+                       print {$warnings} "There is no source info for the package '$pkg' at version '$version' with architecture '$arch'\n";
+                       if ($param{guess_source}) {
+                           # Lets guess it
+                           my $pkgsrc = getpkgsrc();
+                           if (exists $pkgsrc->{$pkg}) {
+                               @srcinfo = ([$pkgsrc->{$pkg}, $version]);
+                           } elsif (getsrcpkgs($pkg)) {
+                               # If we're looking at a source package
+                               # that doesn't have a binary of the
+                               # same name, just try the same
+                               # version.
+                               @srcinfo = ([$pkg, $version]);
+                           } else {
+                               next;
+                           }
+                           # store guesses in a slightly different location
+                           $param{source_version_cache}{$cachekey.'/guess'} = [ map { "$_->[0]/$_->[1]" } @srcinfo ];
+                       }
+                   }
+                   else {
+                       # only store this if we didn't have to guess it
+                       $param{source_version_cache}{$cachekey} = [ map { "$_->[0]/$_->[1]" } @srcinfo ];
+                   }
+                   $sourceversions{"$_->[0]/$_->[1]"} = 1 foreach @srcinfo;
                }
-                next;
-            }
-
-            my @srcinfo = binarytosource($pkg, $version, $arch);
-            unless (@srcinfo) {
-                # We don't have explicit information about the
-                # binary-to-source mapping for this version (yet). Since
-                # this is a CGI script and our output is transient, we can
-                # get away with just looking in the unversioned map; if it's
-                # wrong (as it will be when binary and source package
-                # versions differ), too bad.
-                my $pkgsrc = getpkgsrc();
-                if (exists $pkgsrc->{$pkg}) {
-                    @srcinfo = ([$pkgsrc->{$pkg}, $version]);
-                } elsif (getsrcpkgs($pkg)) {
-                    # If we're looking at a source package that doesn't have
-                    # a binary of the same name, just try the same version.
-                    @srcinfo = ([$pkg, $version]);
-                } else {
-                    next;
-                }
-            }
-            $sourceversions{"$_->[0]/$_->[1]"} = 1 foreach @srcinfo;
-            $_sourceversioncache{$cachekey} = [ map { "$_->[0]/$_->[1]" } @srcinfo ];
+           }
         }
     }
-
     return sort keys %sourceversions;
 }
 
 
 
-=back
-
-=cut
-
 1;
index fe902649fc5376c2e5e6046a4626f7ab6e27abae..93b13ce51b0d793a35eba4ca81919736a2091268 100644 (file)
@@ -5,7 +5,7 @@
 #
 # [Other people have contributed to this file; their copyrights should
 # go here too.]
-# Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
+# Copyright 2007-9 by Don Armstrong <don@donarmstrong.com>.
 
 package Debbugs::Status;
 
@@ -40,13 +40,15 @@ use Params::Validate qw(validate_with :types);
 use Debbugs::Common qw(:util :lock :quit :misc);
 use Debbugs::Config qw(:config);
 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
-use Debbugs::Packages qw(makesourceversions getversions get_versions binarytosource);
+use Debbugs::Packages qw(makesourceversions make_source_versions getversions get_versions binarytosource);
 use Debbugs::Versions;
 use Debbugs::Versions::Dpkg;
 use POSIX qw(ceil);
 
+use Storable qw(dclone);
 use List::Util qw(min max);
 
+use Carp qw(croak);
 
 BEGIN{
      $VERSION = 1.00;
@@ -54,7 +56,7 @@ BEGIN{
 
      @EXPORT = ();
      %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
-                               qw(isstrongseverity bug_presence),
+                               qw(isstrongseverity bug_presence split_status_fields),
                               ],
                     read   => [qw(readbug read_bug lockreadbug lockreadbugmerge),
                                qw(lock_read_all_merged_bugs),
@@ -64,9 +66,10 @@ BEGIN{
                                  qw(removefoundversions removefixedversions)
                                 ],
                     hook     => [qw(bughook bughook_archive)],
+                    fields   => [qw(%fields)],
                    );
      @EXPORT_OK = ();
-     Exporter::export_ok_tags(qw(status read write versions hook));
+     Exporter::export_ok_tags(qw(status read write versions hook fields));
      $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
@@ -81,8 +84,9 @@ location. Valid locations are those understood by L</getbugcomponent>
 
 =cut
 
-
-my %fields = (originator     => 'submitter',
+# these probably shouldn't be imported by most people, but
+# Debbugs::Control needs them, so they're now exportable
+our %fields = (originator     => 'submitter',
               date           => 'date',
               subject        => 'subject',
               msgid          => 'message-id',
@@ -104,6 +108,7 @@ my %fields = (originator     => 'submitter',
              affects        => 'affects',
              );
 
+
 # Fields which need to be RFC1522-decoded in format versions earlier than 3.
 my @rfc1522_fields = qw(originator subject done forwarded owner);
 
@@ -251,8 +256,10 @@ sub read_bug{
            $data{$field} = decode_rfc1522($data{$field});
        }
     }
+    my $status_modified = (stat($status))[9];
     # Add log last modified time
     $data{log_modified} = (stat($log))[9];
+    $data{last_modified} = max($status_modified,$data{log_modified});
     $data{location} = $location;
     $data{archived} = (defined($location) and ($location eq 'archive'))?1:0;
     $data{bug_num} = $param{bug};
@@ -260,6 +267,110 @@ sub read_bug{
     return \%data;
 }
 
+=head2 split_status_fields
+
+     my @data = split_status_fields(@data);
+
+Splits splittable status fields (like package, tags, blocks,
+blockedby, etc.) into arrayrefs (use make_list on these). Keeps the
+passed @data intact using dclone.
+
+In scalar context, returns only the first element of @data.
+
+=cut
+
+our $ditch_empty = sub{
+    my @t = @_;
+    my $splitter = shift @t;
+    return grep {length $_} map {split $splitter} @t;
+};
+
+my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)};
+my %split_fields =
+    (package        => \&splitpackages,
+     affects        => \&splitpackages,
+     blocks         => $ditch_empty_space,
+     blockedby      => $ditch_empty_space,
+     tags           => sub {return &{$ditch_empty}(qr/\s*\,\s*/,@_)},
+     found_versions => $ditch_empty_space,
+     fixed_versions => $ditch_empty_space,
+     mergedwith     => $ditch_empty_space,
+    );
+
+sub split_status_fields {
+    my @data = @{dclone(\@_)};
+    for my $data (@data) {
+       next if not defined $data;
+       croak "Passed an element which is not a hashref to split_status_field".ref($data) if
+           not (ref($data) and ref($data) eq 'HASH');
+       for my $field (keys %{$data}) {
+           next unless defined $data->{$field};
+           if (exists $split_fields{$field}) {
+               next if ref($data->{$field});
+               my @elements;
+               if (ref($split_fields{$field}) eq 'CODE') {
+                   @elements = &{$split_fields{$field}}($data->{$field});
+               }
+               elsif (not ref($split_fields{$field}) or
+                      UNIVERSAL::isa($split_fields{$field},'Regex')
+                     ) {
+                   @elements = split $split_fields{$field}, $data->{$field};
+               }
+               if (@elements != 1) {
+                   $data->{$field} = \@elements;
+               }
+               else {
+                   $data->{$field} = $elements[0];
+               }
+           }
+       }
+    }
+    return wantarray?@data:$data[0];
+}
+
+=head2 join_status_fields
+
+     my @data = join_status_fields(@data);
+
+Handles joining the splitable status fields. (Basically, the inverse
+of split_status_fields.
+
+Primarily called from makestatus, but may be useful for other
+functions after calling split_status_fields (or for legacy functions
+if we transition to split fields by default).
+
+=cut
+
+sub join_status_fields {
+    my %join_fields =
+       (package        => ', ',
+        affects        => ', ',
+        blocks         => ' ',
+        blockedby      => ' ',
+        tags           => ', ',
+        found_versions => ' ',
+        fixed_versions => ' ',
+        found_date     => ' ',
+        fixed_date     => ' ',
+        mergedwith     => ' ',
+       );
+    my @data = @{dclone(\@_)};
+    for my $data (@data) {
+       next if not defined $data;
+       croak "Passed an element which is not a hashref to split_status_field: ".
+           ref($data)
+               if ref($data) ne 'HASH';
+       for my $field (keys %{$data}) {
+           next unless defined $data->{$field};
+           next unless ref($data->{$field}) eq 'ARRAY';
+           next unless exists $join_fields{$field};
+           $data->{$field} = join($join_fields{$field},@{$data->{$field}});
+       }
+    }
+    return wantarray?@data:$data[0];
+}
+
+
 =head2 lockreadbug
 
      lockreadbug($bug_num,$location)
@@ -325,7 +436,7 @@ sub lock_read_all_merged_bugs {
     my $locks = 0;
     my @data = (lockreadbug(@_));
     if (not @data or not defined $data[0]) {
-       return ($locks,undef);
+       return ($locks,());
     }
     $locks++;
     if (not length $data[0]->{mergedwith}) {
@@ -339,7 +450,7 @@ sub lock_read_all_merged_bugs {
     if (not @data or not defined $data[0]) {
        unfilelock(); #for merge lock above
        $locks--;
-       return ($locks,undef);
+       return ($locks,());
     }
     $locks++;
     my @bugs = split / /, $data[0]->{mergedwith};
@@ -353,7 +464,7 @@ sub lock_read_all_merged_bugs {
                }
                $locks = 0;
                warn "Unable to read bug: $bug while handling merged bug: $bug_num";
-               return ($locks,undef);
+               return ($locks,());
            }
            $locks++;
            push @data,$newdata;
@@ -404,10 +515,7 @@ sub makestatus {
                   [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
         }
     }
-
-    for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
-        $newdata{$field} = join ' ', @{$newdata{$field}||[]};
-    }
+    %newdata = %{join_status_fields(\%newdata)};
 
     if ($version < 3) {
         for my $field (@rfc1522_fields) {
@@ -651,7 +759,7 @@ Split a package string from the status file into a list of package names.
 sub splitpackages {
     my $pkgs = shift;
     return unless defined $pkgs;
-    return map lc, split /[ \t?,()]+/, $pkgs;
+    return grep {length $_} map lc, split /[\s,()?]+/, $pkgs;
 }
 
 
@@ -879,9 +987,8 @@ dist, arch, and version. [The entries in this array must be in the
 "source/version" format.] Eventually this can be used to for caching.
 
 =item indicatesource -- if true, indicate which source packages this
-bug could belong to. Defaults to false. [Note that eventually we will
-properly allow bugs that only affect a source package, and this will
-become always on.]
+bug could belong to (or does belong to in the case of bugs assigned to
+a source package). Defaults to true.
 
 =back
 
@@ -920,7 +1027,7 @@ sub get_bug_status {
                                                             optional => 1,
                                                            },
                                          indicatesource => {type => BOOLEAN,
-                                                            default => 0,
+                                                            default => 1,
                                                            },
                                         },
                              );
@@ -951,15 +1058,30 @@ sub get_bug_status {
      $status{tags} = $status{keywords};
      my %tags = map { $_ => 1 } split ' ', $status{tags};
 
+     $status{package} = '' if not defined $status{package};
      $status{"package"} =~ s/\s*$//;
-     if ($param{indicatesource} and $status{package} ne '') {
-         $status{source} = join(', ',binarytosource($status{package}));
-     }
-     else {
-         $status{source} = 'unknown';
+     # if we aren't supposed to indicate the source, we'll return
+     # unknown here.
+     $status{source} = 'unknown';
+     if ($param{indicatesource}) {
+        my @packages = split /\s*,\s*/, $status{package};
+        my @source;
+        for my $package (@packages) {
+            next if $package eq '';
+            if ($package =~ /^src\:$/) {
+                push @source,$1;
+            }
+            else {
+                push @source, binarytosource($package);
+            }
+        }
+        if (@source) {
+            $status{source} = join(', ',@source);
+        }
      }
+
      $status{"package"} = 'unknown' if ($status{"package"} eq '');
-     $status{"severity"} = 'normal' if ($status{"severity"} eq '');
+     $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq '');
 
      $status{"pending"} = 'pending';
      $status{"pending"} = 'forwarded'      if (length($status{"forwarded"}));
@@ -1086,10 +1208,15 @@ sub bug_presence {
                       $allowed_distributions{$tag} = 1;
                   }
               }
-              foreach my $arch (make_list($param{arch})) {
+              foreach my $arch (make_list(exists $param{arch}?$param{arch}:undef)) {
                    for my $package (split /\s*,\s*/, $status{package}) {
-                        my @versions;
-                        foreach my $dist (make_list($param{dist})) {
+                        my @versions = ();
+                        my $source = 0;
+                        if ($package =~ /^src:(.+)$/) {
+                            $source = 1;
+                            $package = $1;
+                        }
+                        foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) {
                              # if some distributions are disallowed,
                              # and this isn't an allowed
                              # distribution, then we ignore this
@@ -1099,13 +1226,17 @@ sub bug_presence {
                                  not exists $allowed_distributions{$dist}) {
                                   next;
                              }
-                             push @versions, getversions($package, $dist, $arch);
+                             push @versions, get_versions(package => $package,
+                                                          dist    => $dist,
+                                                          ($source?(arch => 'source'):
+                                                           (defined $arch?(arch => $arch):())),
+                                                         );
                         }
                         next unless @versions;
-                        my @temp = makesourceversions($package,
-                                                      $arch,
-                                                      @versions
-                                                     );
+                        my @temp = make_source_versions(package => $package,
+                                                        arch => $arch,
+                                                        versions => \@versions,
+                                                       );
                         @sourceversions{@temp} = (1) x @temp;
                    }
               }
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..97af9a2
--- /dev/null
@@ -0,0 +1,7 @@
+^debian(?:\/|.*)$
+^\.bzr.*$
+^\.shelf.*$
+(?:^|\/)\.exists
+(?:^|\/)\.\#
+^blib(?:\/|.*)$
+~$
\ No newline at end of file
diff --git a/Mail/CrossAssassin.pm b/Mail/CrossAssassin.pm
new file mode 100644 (file)
index 0000000..b8c676f
--- /dev/null
@@ -0,0 +1,98 @@
+# CrossAssassin.pm 2004/04/12 blarson 
+
+package Mail::CrossAssassin;
+
+use strict;
+require Exporter;
+our @ISA = qw(Exporter);
+our @EXPORT = qw(ca_init ca_keys ca_set ca_score ca_expire);
+our $VERSION = 0.1;
+
+use Digest::MD5 qw(md5_base64);
+use DB_File;
+
+our %database;
+our $init;
+our $addrpat = '\b\d{3,8}(?:-(?:close|done|forwarded|maintonly|submitter|quiet))?\@bugs\.debian\.org';
+
+sub ca_init(;$$) {
+    my $ap = shift;
+    $addrpat = $ap if(defined $ap);
+    my $dir = shift;
+    return if ($init && ! defined($dir));
+    $dir = "$ENV{'HOME'}/.crosssassassin" unless (defined($dir));
+    (mkdir $dir or die "Could not create \"$dir\"") unless (-d $dir);
+    untie %database;
+    tie %database, 'DB_File', "$dir/Crossdb"
+       or die "Could not initialize crosassasin database \"$dir/Crossdb\": $!";
+    $init = 1;
+}
+
+sub ca_keys($) {
+    my $body = shift;
+    my @keys;
+    my $m = join('',@$body);
+    $m =~ s/\n(?:\s*\n)+/\n/gm;
+    if (length($m) > 4000) {
+       my $m2 = $m;
+       $m2 =~ s/\S\S+/\*/gs;
+       push @keys, '0'.md5_base64($m2);
+    }
+#    $m =~ s/^--.*$/--/m;
+    $m =~ s/$addrpat/LOCAL\@ADDRESS/iogm;
+    push @keys, '1'.md5_base64($m);
+    return join(' ',@keys);
+}
+
+sub ca_set($) {
+    my @keys = split(' ', $_[0]);
+    my $now = time;
+    my $score = 0;
+    my @scores;
+    foreach my $k (@keys) {
+       my ($count,$date) = split(' ',$database{$k});
+        $count++;
+        $score = $count if ($count > $score);
+        $database{$k} = "$count $now";
+       push @scores, $count;
+    }
+    return (wantarray ? @scores : $score);
+}
+
+sub ca_score($) {
+    my @keys = split(' ', $_[0]);
+    my $score = 0;
+    my @scores;
+    my $i = 0;
+    foreach my $k (@keys) {
+       my ($count,$date) = split(' ',$database{$k});
+       $score = $count if ($count > $score);
+       $i++;
+       push @scores, $count;
+    }
+    return (wantarray ? @scores : $score);
+}
+
+sub ca_expire($) {
+    my $when = shift;
+    my @ret;
+    my $num = 0;
+    my $exp = 0;
+    while (my ($k, $v) = each %database) {
+       $num++;
+       my ($count, $date) = split(' ', $v);
+       if ($date <= $when) {
+           delete $database{$k};
+           $exp++;
+       }
+    }
+    return ($num, $exp);
+}
+
+END {
+    return unless($init);
+    untie %database;
+    undef($init);
+}
+
+1;
index 152e75099ce994e117c0b2e9ac5e9e8f44a2d1de..df174b82fef57881c5de12f59308b095ec0e1303 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,7 @@ man_dir               := $(DESTDIR)/usr/share/man
 man8_dir       := $(man_dir)/man8
 examples_dir   := $(doc_dir)/examples
 
-scripts_in     := $(foreach script, $(filter-out scripts/config scripts/errorlib scripts/text, $(wildcard scripts/*)),$(patsubst scripts/%,%,$(script)))
+scripts_in     = $(foreach script, $(filter-out scripts/config% scripts/errorlib scripts/text, $(wildcard scripts/*)),$(patsubst scripts/%,%,$(script)))
 htmls_in       := $(wildcard html/*.html.in)
 cgis           := $(wildcard cgi/*.cgi cgi/*.pl)
 
@@ -23,8 +23,21 @@ cgis         := $(wildcard cgi/*.cgi cgi/*.pl)
 install_exec   := install -m755 -p
 install_data   := install -m644 -p
 
-test:
-       perl -MTest::Harness -I. -e 'runtests(glob(q(t/*.t)))'
+PERL ?= /usr/bin/perl
+
+all: build test
+
+build:
+       $(PERL) Makefile.PL
+       $(MAKE) -f Makefile.perl
+
+test: build
+       $(PERL) -MTest::Harness -I. -e 'runtests(glob(q(t/*.t)))'
+
+clean:
+       if [ -e Makefile.perl ]; then \
+               $(MAKE) -f Makefile.perl clean; \
+       fi;
 
 install: install_mostfiles
        # install basic debbugs documentation
@@ -48,7 +61,7 @@ $(var_dir)/spool/db-h $(scripts_dir) $(examples_dir) $(man8_dir); \
 
 
        # install the scripts
-       $(foreach script,$(scripts_in), $(exec $(install_exec) $(script) $(scripts_dir)/$(script)))
+       $(foreach script,$(scripts_in), $(install_exec) scripts/$(script) $(scripts_dir);)
        $(install_data) scripts/errorlib $(scripts_dir)/errorlib
 
        # install examples
@@ -90,4 +103,4 @@ $(var_dir)/spool/db-h $(scripts_dir) $(examples_dir) $(man8_dir); \
        $(foreach tmpl, $(wildcard templates/*/*/*.tmpl), $(exec $(install_data) $(tmpl) $(template_dir)/$(patsubst templates/%,%,$(tmpl))))
 
 
-.PHONY: test
\ No newline at end of file
+.PHONY: test build
\ No newline at end of file
index 06b43cca3cfd3a4a80fb39abbc73592a2ffbe8be..0063970eb560560f070067615513697fbc4d4d83 100644 (file)
@@ -3,7 +3,7 @@
 use ExtUtils::MakeMaker;
 
 WriteMakefile(FIRST_MAKEFILE => 'Makefile.perl',
-             PMLIBDIRS => ['Debbugs'],
+             PMLIBDIRS => ['Debbugs','Mail'],
              EXE_FILES => ['bin/local-debbugs',
                            'bin/add_bug_to_estraier',
                           ],
index 0acfbab705018b1f29587f9159790e4b2926ff8e..381b7d7b4612efc574670eb2ea57ccb74802fd35 100755 (executable)
@@ -16,7 +16,7 @@ use Pod::Usage;
 
 =head1 NAME
 
-add_bug_to_estraier
+add_bug_to_estraier -- add a bug log to an estraier database
 
 =head1 SYNOPSIS
 
index c50d58604722280f86924e3333ae30b8ed4193bf..790e1a2ce5dc1a23f9eb385f45ba6f3792e189b7 100755 (executable)
@@ -15,7 +15,7 @@ use Debbugs::Config qw(:globals :text);
 use Debbugs::Log qw(read_log_records);
 use Debbugs::CGI qw(:url :html :util);
 use Debbugs::CGI::Bugreport qw(:all);
-use Debbugs::Common qw(buglog getmaintainers make_list);
+use Debbugs::Common qw(buglog getmaintainers make_list bug_status);
 use Debbugs::Packages qw(getpkgsrc);
 use Debbugs::Status qw(splitpackages get_bug_status isstrongseverity);
 
@@ -23,6 +23,8 @@ use Scalar::Util qw(looks_like_number);
 
 use Debbugs::Text qw(:templates);
 
+use List::Util qw(max);
+
 
 use CGI::Simple;
 my $q = new CGI::Simple;
@@ -63,6 +65,34 @@ my %bugusertags;
 my %ut;
 my %seen_users;
 
+my $buglog = buglog($ref);
+my $bug_status = bug_status($ref);
+if (not defined $buglog or not defined $bug_status) {
+     print $q->header(-status => "404 No such bug",
+                     -type => "text/html",
+                     -charset => 'utf-8',
+                    );
+     print fill_in_template(template=>'cgi/no_such_bug',
+                           variables => {modify_time => strftime('%a, %e %b %Y %T UTC', gmtime),
+                                         bug_num     => $ref,
+                                        },
+                          );
+     exit 0;
+}
+
+# the log should almost always be newer, but just in case
+my $log_mtime = +(stat $buglog)[9] || time;
+my $status_mtime = +(stat $bug_status)[9] || time;
+my $mtime = strftime '%a, %d %b %Y %T GMT', gmtime(max($status_mtime,$log_mtime));
+
+if ($q->request_method() eq 'HEAD' and not defined($att) and not $mbox) {
+     print $q->header(-type => "text/html",
+                     -charset => 'utf-8',
+                     (length $mtime)?(-last_modified => $mtime):(),
+                    );
+     exit 0;
+}
+
 for my $user (map {split /[\s*,\s*]+/} make_list($param{users}||[])) {
     next unless length($user);
     add_user($user,\%ut,\%bugusertags,\%seen_users);
@@ -94,33 +124,6 @@ $mbox = 1 if $mbox_status_message or $mbox_maint;
 my $archive = $param{'archive'} eq 'yes';
 my $repeatmerged = $param{'repeatmerged'} eq 'yes';
 
-my $buglog = buglog($ref);
-if (not defined $buglog) {
-     print $q->header(-status => "404 No such bug",
-                     -type => "text/html",
-                     -charset => 'utf-8',
-                    );
-     print fill_in_template(template=>'cgi/no_such_bug',
-                           variables => {modify_time => strftime('%a, %e %b %Y %T UTC', gmtime),
-                                         bug_num     => $ref,
-                                        },
-                          );
-     exit 0;
-}
-
-my @stat = stat $buglog;
-my $mtime = '';
-if (@stat) {
-     $mtime = strftime '%a, %d %b %Y %T GMT', gmtime($stat[9]);
-}
-
-if ($q->request_method() eq 'HEAD' and not defined($att) and not $mbox) {
-     print $q->header(-type => "text/html",
-                     -charset => 'utf-8',
-                     (length $mtime)?(-last_modified => $mtime):(),
-                    );
-     exit 0;
-}
 
 
 my $buglogfh;
@@ -237,6 +240,7 @@ else {
                                     msg_num => $msg_num,
                                     att => $att,
                                     msg => $msg,
+                                    trim_headers => $trim_headers,
                                    );
          exit 0;
      }
@@ -288,10 +292,20 @@ my %package;
 my @packages = splitpackages($status{package});
 
 foreach my $pkg (@packages) {
-     $package{$pkg} = {maintainer => exists($maintainer{$pkg}) ? $maintainer{$pkg} : '(unknown)',
-                      exists($pkgsrc{$pkg}) ? (source => $pkgsrc{$pkg}) : (),
-                      package    => $pkg,
-                     };
+     if ($pkg =~ /^src\:/) {
+         my ($srcpkg) = $pkg =~ /^src:(.*)/;
+         $package{$pkg} = {maintainer => exists($maintainer{$srcpkg}) ? $maintainer{$srcpkg} : '(unknown)',
+                           source     => $srcpkg,
+                           package    => $pkg,
+                           is_source  => 1,
+                          };
+     }
+     else {
+         $package{$pkg} = {maintainer => exists($maintainer{$pkg}) ? $maintainer{$pkg} : '(unknown)',
+                           exists($pkgsrc{$pkg}) ? (source => $pkgsrc{$pkg}) : (),
+                           package    => $pkg,
+                          };
+     }
 }
 
 # fixup various bits of the status
index f8eb6a44aecb0091f54120f037f850826c8ded95..580dbf29989569bb36dc3d5c439a727d8e05f096 100755 (executable)
@@ -226,16 +226,15 @@ if (defined $param{maintenc}) {
      delete $param{maintenc}
 }
 
-
-if (not grep {exists $param{$_}} keys %package_search_keys and exists $param{users}) {
-     $param{usertag} = [make_list($param{users})];
-}
-
 if (exists $param{pkg}) {
      $param{package} = $param{pkg};
      delete $param{pkg};
 }
 
+if (not grep {exists $param{$_}} keys %package_search_keys and exists $param{users}) {
+     $param{usertag} = [make_list($param{users})];
+}
+
 my %bugusertags;
 my %ut;
 my %seen_users;
@@ -425,7 +424,7 @@ if (defined $pseudodesc and defined $pkg and exists $pseudodesc->{$pkg}) {
      delete $param{dist};
 }
 
-# output infomration about the packages
+# output information about the packages
 
 for my $package (make_list($param{package}||[])) {
      print generate_package_info(binary => 1,
index 09f23e173e9804662073f1a4ad41eebf9a7ee0a4..832a5dbf79b0184741d37b0ae74b43e7807f8612 100644 (file)
@@ -1,4 +1,4 @@
-debbugs (2.4.2) UNRELEASED; urgency=low
+debbugs (2.4.2~exp0) UNRELEASED; urgency=low
 
   [ Anthony Towns ]
   * Add "package" command to service (control@) to limit the bugs that
@@ -213,13 +213,15 @@ debbugs (2.4.2) UNRELEASED; urgency=low
     (closes: #465332,#458822)
   * Deal properly with \r line endings (closes: #467190)
   * Distinguish between reports and followups (closes: #459866)
-  * Allow for the archiving of bugs in removed packages (closes: #475622, #470146)
+  * Allow for the archiving of bugs in removed packages
+    (closes: #475622, #470146)
   * Add Text::Template based templating system (closes: #36814)
   * Add new uservalue feature to Debbugs::User
   * Don't serialize things as date/time in soap (closes: #484789)
   * Link to packages in bugreport page (closes: #229067)
   * Totally revamp the pkgreport templates (closes: #434504)
   * Add correspondent option to track bug correpondents (closes: #485804)
+  * Fix addition of correspondents in gen-indices (closes: #511850)
   * Allow clicking anywhere outside the extra status box to close the
     extra status box (closes: #499990) Thanks to James Vega for the patch.
   * Return 404 when a bug number that does not exist is used
@@ -237,7 +239,20 @@ debbugs (2.4.2) UNRELEASED; urgency=low
   * Display link to full log again (closes: #507506)
   * Add Last-Modified: header support to mbox download (closes: #456786)
   * Add Date headers if missing (closes: #458757)
-  * Indiciate what machine has built webpages (closes: #507022)
+  * Indicate what machine has built webpages (closes: #507022)
+  * Indicate the update location of source (closes: #512306)
+  * Use get_addresses to parse X-Debbugs-Cc: to allow multiple Cc:'s
+    (closes: #514183)
+  * Calculate last modified using summary as well as log (closes: #515063)
+  * Ditch 'as before' (closes: #514677)
+  * Don't have reply/subscribe links for archived bugs (closes: #511864)
+  * Fix issue with no-maintainer bugs assigned to multiple packages
+    (closes: #528249)
+  * Properly html_escape un-processed parts in maybelink (closes: #530506)
+  * Add urls to the bottom of all messages we send out (closes: #9596)
+  * Allow for tag nnn = baz + foo - bar in service (closes: #505189)
+  * Allow trailinng periods after the control stop commands (closes:
+    #517834)
 
   
  -- Colin Watson <cjwatson@debian.org>  Fri, 20 Jun 2003 18:57:25 +0100
diff --git a/debian/conffiles b/debian/conffiles
deleted file mode 100644 (file)
index e69de29..0000000
index 075eee73f9792d231914d46f05f240d9ecd25068..786a0ef22285be91601da104b023f50c7130de17 100644 (file)
@@ -3,13 +3,13 @@ Section: misc
 Priority: extra
 Maintainer: Debbugs developers <debian-debbugs@lists.debian.org>
 Uploaders: Josip Rodin <joy-packages@debian.org>, Colin Watson <cjwatson@debian.org>, Don Armstrong <don@debian.org>
-Standards-Version: 3.2.1
+Standards-Version: 3.8.1
 Build-Depends-Indep: debhelper, libparams-validate-perl,
- libmailtools-perl, libmime-perl, libio-stringy-perl, libmldbm-perl,
+ libmailtools-perl, libmime-tools-perl, libio-stringy-perl, libmldbm-perl,
  liburi-perl, libsoap-lite-perl, libcgi-simple-perl,
  libhttp-server-simple-perl, libtest-www-mechanize-perl,
  libmail-rfc822-address-perl, libsafe-hole-perl, libuser-perl,
- libconfig-simple-perl
+ libconfig-simple-perl, libtest-pod-perl
 
 Package: debbugs
 Architecture: all
@@ -30,10 +30,11 @@ Description: The bug tracking system based on the active Debian BTS
 
 Package: libdebbugs-perl
 Architecture: all
-Depends: ${perl:Depends}, libmailtools-perl, ed, libmime-perl,
+Depends: ${perl:Depends}, libmailtools-perl, ed, libmime-tools-perl,
  libio-stringy-perl, libmldbm-perl, liburi-perl, libsoap-lite-perl,
  libcgi-simple-perl, libparams-validate-perl, libtext-template-perl,
  libsafe-hole-perl, libmail-rfc822-address-perl
+Section: perl
 Description: modules used by the active Debian BTS
  Debian has a bug tracking system which files details of bugs reported by
  users and developers. Each bug is given a number, and is kept on file until
index 63a77c9207bd16f64b600ffb8281544a290d5e02..7ba75071af081e76353ab644102d1e09d5b0cf89 100644 (file)
@@ -29,10 +29,12 @@ useful, but without any warranty; without even the implied warranty of
 merchantability or fitness for a particular purpose. See the GNU General
 Public License for more details.
 
-You should have received a copy of the GNU General Public License along
-with this program, or one should be available above; if not, write to the
-Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
-02111-1307, USA.
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 On Debian systems, the full text of the GPL can be found in
-/usr/share/common-licenses/GPL.
+/usr/share/common-licenses/GPL-2.
+
+
+Portions of the bug system copyrighted by Don Armstrong are available
+under the terms of the GPL version 2 or later, at your option.
\ No newline at end of file
diff --git a/debian/debbugs-web.conffiles b/debian/debbugs-web.conffiles
deleted file mode 100644 (file)
index e3ce09b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/etc/debbugs/html/Access.html.in
-/etc/debbugs/html/Developer.html.in
-/etc/debbugs/html/Reporting.html.in
-/etc/debbugs/html/index.html.in
-/etc/debbugs/html/server-control.html.in
-/etc/debbugs/html/server-refcard.html.in
-/etc/debbugs/html/server-request.html.in
index 3e0bcd87479df35a46a313f469b1b4be4e818dc6..38ec1f1f24e0cf434667d4a34757c48c2b9b3783 100644 (file)
@@ -2,6 +2,10 @@ usr/lib/debbugs
 usr/share/man/man8
 usr/sbin
 usr/share/doc/debbugs/examples
-var/lib/debbugs
+var/lib/debbugs/spool
+var/lib/debbugs/indices
 usr/bin/add_bug_to_estraier
 usr/share/man/man1/add_bug_*
+# there currently isn't a Mail::Crossassassin manpage
+#usr/share/man/man3/Mail*
+usr/share/perl5/Mail*
\ No newline at end of file
diff --git a/debian/dirs b/debian/dirs
deleted file mode 100644 (file)
index bfac206..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-etc/debbugs/html
-etc/debbugs/indices
-usr/lib/debbugs
-usr/sbin
-usr/share/doc/debbugs/examples
-var/lib/debbugs/indices
-var/lib/debbugs/www/cgi
-var/lib/debbugs/www/db
-var/lib/debbugs/www/txt
-var/lib/debbugs/spool/lock
-var/lib/debbugs/spool/archive
-var/lib/debbugs/spool/incoming
-var/lib/debbugs/spool/db-h
index c4acdfa739b642c40c49fb84756ae06799e0a4c9..bc1da7eaf90cccce28eb371f0415961ed53fe6da 100644 (file)
@@ -1,3 +1,3 @@
-usr/share/man/man3
-usr/share/perl5
+usr/share/man/man3/Debbugs*
+usr/share/perl5/Debbugs*
 #etc/debbugs/config
diff --git a/debian/postinst b/debian/postinst
deleted file mode 100755 (executable)
index 4b76f38..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/sh -e
-
-if [ "$1" = "configure" ]; then
-  /usr/sbin/debbugsconfig
-  if dpkg --compare-versions "$2" lt 2.4; then
-    spool=`perl -e 'require "/etc/debbugs/config"; print $gSpoolDir;'`
-    if [ -d "$spool/db" ]; then
-      if [ -d "$spool/db-h" ]; then
-        echo "Cannot migrate bug database to hashed format, because" >&2
-        echo "$spool/db-h already exists." >&2
-        echo "Rectify the situation and run the following command by hand:" >&2
-        echo "  /usr/sbin/debbugs-dbhash \"$spool/db\" \"$spool/db-h\"" >&2
-      else
-        echo "Migrating bug database to hashed format." >&2
-        /usr/sbin/debbugs-dbhash "$spool/db" "$spool/db-h"
-        echo "You can remove bug logs from $spool/db" >&2
-        echo "after ensuring that the new database works." >&2
-      fi
-    fi
-  fi
-fi
-
-if [ -f /etc/debbugs/nextnumber ]; then
-  rm -f /etc/debbugs/nextnumber
-fi
-
-#DEBHELPER#
diff --git a/debian/postrm b/debian/postrm
deleted file mode 100644 (file)
index 40d32b9..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-
-if [ "$1" = "purge" ]; then
-  rm -rf /etc/debbugs /var/lib/debbugs
-fi
-
-#DEBHELPER#
index 4c751ff88cefe09c876f76273605e0f1478c0c27..7d6f7d68acae293781990f5d826aa0912f6ea3a2 100755 (executable)
@@ -45,7 +45,7 @@ binary-indep: build install
        dh_installdirs
        $(MAKE) install_mostfiles DESTDIR=$(DEST_DIR)
        $(MAKE) -f Makefile.perl install PREFIX=$(DEST_DIR)/usr
-       dh_install --sourcedir=debian/tmp --list-missing
+       dh_install --sourcedir=debian/tmp --fail-missing
        dh_installdocs
        dh_installchangelogs
        dh_strip
index f45fa6d910583dbc6514b1b1c941e53a590510ba..3bb03bc765302addad8ba1d7f4ac9dee27f53929 100755 (executable)
@@ -33,7 +33,7 @@ for archive in $ARCHIVES; do
         if [ "$suite" != "oldstable" ] || [ -d /org/bugs.debian.org/etc/indices/$archive/$suite ]; then
        case $suite in
            oldstable|stable|proposed-updates)
-               ARCHES='alpha arm hppa i386 ia64 m68k mips mipsel powerpc s390 sparc'
+               ARCHES='alpha amd64 arm hppa i386 ia64 m68k mips mipsel powerpc s390 sparc'
                ;;
            testing|testing-proposed-updates)
                ARCHES='alpha amd64 arm hppa i386 ia64 mips mipsel powerpc s390 sparc'
diff --git a/scripts/Mail/CrossAssassin.pm b/scripts/Mail/CrossAssassin.pm
deleted file mode 100644 (file)
index b8c676f..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-# CrossAssassin.pm 2004/04/12 blarson 
-
-package Mail::CrossAssassin;
-
-use strict;
-require Exporter;
-our @ISA = qw(Exporter);
-our @EXPORT = qw(ca_init ca_keys ca_set ca_score ca_expire);
-our $VERSION = 0.1;
-
-use Digest::MD5 qw(md5_base64);
-use DB_File;
-
-our %database;
-our $init;
-our $addrpat = '\b\d{3,8}(?:-(?:close|done|forwarded|maintonly|submitter|quiet))?\@bugs\.debian\.org';
-
-sub ca_init(;$$) {
-    my $ap = shift;
-    $addrpat = $ap if(defined $ap);
-    my $dir = shift;
-    return if ($init && ! defined($dir));
-    $dir = "$ENV{'HOME'}/.crosssassassin" unless (defined($dir));
-    (mkdir $dir or die "Could not create \"$dir\"") unless (-d $dir);
-    untie %database;
-    tie %database, 'DB_File', "$dir/Crossdb"
-       or die "Could not initialize crosassasin database \"$dir/Crossdb\": $!";
-    $init = 1;
-}
-
-sub ca_keys($) {
-    my $body = shift;
-    my @keys;
-    my $m = join('',@$body);
-    $m =~ s/\n(?:\s*\n)+/\n/gm;
-    if (length($m) > 4000) {
-       my $m2 = $m;
-       $m2 =~ s/\S\S+/\*/gs;
-       push @keys, '0'.md5_base64($m2);
-    }
-#    $m =~ s/^--.*$/--/m;
-    $m =~ s/$addrpat/LOCAL\@ADDRESS/iogm;
-    push @keys, '1'.md5_base64($m);
-    return join(' ',@keys);
-}
-
-sub ca_set($) {
-    my @keys = split(' ', $_[0]);
-    my $now = time;
-    my $score = 0;
-    my @scores;
-    foreach my $k (@keys) {
-       my ($count,$date) = split(' ',$database{$k});
-        $count++;
-        $score = $count if ($count > $score);
-        $database{$k} = "$count $now";
-       push @scores, $count;
-    }
-    return (wantarray ? @scores : $score);
-}
-
-sub ca_score($) {
-    my @keys = split(' ', $_[0]);
-    my $score = 0;
-    my @scores;
-    my $i = 0;
-    foreach my $k (@keys) {
-       my ($count,$date) = split(' ',$database{$k});
-       $score = $count if ($count > $score);
-       $i++;
-       push @scores, $count;
-    }
-    return (wantarray ? @scores : $score);
-}
-
-sub ca_expire($) {
-    my $when = shift;
-    my @ret;
-    my $num = 0;
-    my $exp = 0;
-    while (my ($k, $v) = each %database) {
-       $num++;
-       my ($count, $date) = split(' ', $v);
-       if ($date <= $when) {
-           delete $database{$k};
-           $exp++;
-       }
-    }
-    return ($num, $exp);
-}
-
-END {
-    return unless($init);
-    untie %database;
-    undef($init);
-}
-
-1;
index 0e34ce80aa81c4b9ca4a0b824cd3bf91f4219c1f..d2cd9044b68af1bb8999f7f48128e16d3f9eb8c6 100755 (executable)
@@ -20,6 +20,8 @@ use Pod::Usage;
 use File::stat;
 use List::Util qw(min);
 
+use Debbugs::Common qw(make_list);
+
 =head1 NAME
 
 gen-indices - Generates index files for the cgi scripts
@@ -79,7 +81,7 @@ pod2usage(-verbose=>2) if $options{man};
 
 use Debbugs::Config qw(:config);
 use Debbugs::Common qw(getparsedaddrs getbugcomponent lockpid);
-use Debbugs::Status qw(readbug);
+use Debbugs::Status qw(readbug split_status_fields);
 use Debbugs::Log;
 
 chdir($config{spool_dir}) or die "chdir $config{spool_dir} failed: $!";
@@ -106,7 +108,7 @@ if (not lockpid($config{spool_dir}.'/lock/gen-indices')) {
 }
 
 # NB: The reverse index is special; it's used to clean up during updates to bugs
-my @indexes = ('package', 'tag', 'severity','owner','submitter-email','status','correspondent','reverse');
+my @indexes = ('package', 'tag', 'severity','owner','submitter-email','status','correspondent','affects','reverse');
 my $indexes;
 my %slow_index = ();
 my %fast_index = ();
@@ -196,10 +198,11 @@ while (my $dir = shift @dirs) {
                     next;
                }
                next if $stat->mtime < $time;
-               my $fdata = readbug($bug, $initialdir);
+               my $fdata = split_status_fields(readbug($bug, $initialdir));
                $modification_made = 1;
-               addbugtoindex("package", $bug, split /[\s,]+/, $fdata->{"package"});
-               addbugtoindex("tag", $bug, split /[\s,]+/, $fdata->{"keywords"});
+               addbugtoindex("package", $bug, make_list($fdata->{package}));
+               addbugtoindex("tag", $bug, make_list($fdata->{keywords}));
+               addbugtoindex("affects", $bug, make_list($fdata->{"affects"}));
                addbugtoindex('submitter-email', $bug,
                              map {lc($_->address)} getparsedaddrs($fdata->{originator}));
                addbugtoindex("severity", $bug, $fdata->{"severity"});
@@ -208,15 +211,15 @@ while (my $dir = shift @dirs) {
                # handle log entries
                # do this in eval to avoid exploding on jacked logs
                eval {
-                    my $log = Debbugs::Log->new(bug_num => $bug);
-                    while (my $record = $log->read_record()) {
-                         next unless $record->{type} eq 'incoming-recv';
-                         # we use a regex here, because a full mime parse will be slow.
-                         my ($from) = $record->{text} =~ /^From:\s+(.+?)^\S/ism;
-                         addbugtoindex('correspondent',$bug,
-                                       map {lc($_->address)} getparsedaddrs($from)
-                                      );
-                    }
+                   my $log = Debbugs::Log->new(bug_num => $bug);
+                   my @correspondents;
+                   while (my $record = $log->read_record()) {
+                       next unless $record->{type} eq 'incoming-recv';
+                       # we use a regex here, because a full mime parse will be slow.
+                       my ($from) = $record->{text} =~ /^From:\s+(.+?)^\S/ism;
+                       push @correspondents, map {lc($_->address)} getparsedaddrs($from);
+                   }
+                   addbugtoindex('correspondent',$bug,@correspondents) if @correspondents;
                };
                if ($@) {
                     print STDERR "Problem dealing with log of $bug: $@";
index cbaaa500aae2c3c91fc33e316d95a13402041131..e3243caa3c60093babf8ecd4729e983d5e7ffe12 100755 (executable)
@@ -13,7 +13,7 @@ use IO::File;
 
 use MIME::Parser;
 use Debbugs::MIME qw(decode_rfc1522 create_mime_message getmailbody);
-use Debbugs::Mail qw(send_mail_message encode_headers);
+use Debbugs::Mail qw(send_mail_message encode_headers get_addresses);
 use Debbugs::Packages qw(getpkgsrc);
 use Debbugs::User qw(read_usertags write_usertags);
 use Debbugs::Common qw(:lock get_hashname);
@@ -224,7 +224,7 @@ if ($tryref >= 0)
 {
      my $bfound;
     ($bfound, $data)= &lockreadbugmerge($tryref);
-    if ($bfound) { 
+    if ($bfound and not $data->{archived}) {
         $ref= $tryref; 
     } else {
         &sendmessage(create_mime_message(
@@ -236,7 +236,7 @@ if ($tryref >= 0)
           References    => join(' ',grep {defined $_} $header{'message-id'},$data->{msgid}),
           Precedence    => 'bulk',
           "X-$gProject-PR-Message" => 'error',
-         ],message_body_template('process_unknown_bug_number',
+         ],message_body_template('mail/process_unknown_bug_number',
                                  {subject => $subject,
                                   date    => $header{date},
                                   baddress => $baddress,
@@ -449,8 +449,8 @@ if ($codeletter eq 'D' || $codeletter eq 'F')
               To            => "$data->{originator}",
               Subject       => "$gBug#$ref closed by $markedby ($header{'subject'})",
               "Message-ID"  => "<handler.$ref.$nn.notifdone\@$gEmailDomain>",
-              "In-Reply-To" => "$data->{msgid}",
-              References    => join(' ',grep {defined $_} $header{'message-id'},$data->{msgid}),
+              (defined $data->{msgid})?("In-Reply-To" => $data->{msgid}):(),
+              References    => join(' ',grep {defined $_} ($header{'message-id'},$data->{msgid})),
               "X-$gProject-PR-Message"  => "they-closed $ref",
               "X-$gProject-PR-Package"  => "$data->{package}",
               "X-$gProject-PR-Keywords" => "$data->{keywords}",
@@ -499,7 +499,8 @@ if ($ref<0) { # new bug report
     $data->{fixed_versions} = [];
 
     if (defined $pheader{source}) {
-        $data->{package} = $pheader{source};
+       # source packages are identified by the src: prefix
+        $data->{package} = 'src:'.$pheader{source};
     } elsif (defined $pheader{package}) {
         $data->{package} = $pheader{package};
     } elsif (defined $config{default_package}) {
@@ -633,7 +634,7 @@ my $newsubject= $subject;  $newsubject =~ s/^$gBug#$ref:*\s*//;
 
 my $xcchdr= $header{ 'x-debbugs-cc' } || '';
 if ($xcchdr =~ m/\S/) {
-    push(@resentccs,$xcchdr);
+    push(@resentccs,get_addresses($xcchdr));
     $resentccexplain.= fill_template('mail/xdebbugscc',
                                     {xcchdr => $xcchdr},
                                    );
index 94b2571b5a08dca6fdc121b47f8bba4c68fb81e2..3f05dc082e3fe3b82acb28de4fb8e9b2246ccaf7 100755 (executable)
@@ -33,6 +33,10 @@ use Debbugs::Control qw(:all);
 use Debbugs::Log qw(:misc);
 use Debbugs::Text qw(:templates);
 
+use Scalar::Util qw(looks_like_number);
+
+use List::Util qw(first);
+
 use Mail::RFC822::Address;
 
 chdir($config{spool_dir}) or
@@ -114,8 +118,29 @@ my $transcript = IO::Scalar->new(\$transcript_scalar) or
      die "Unable to create new IO::Scalar";
 print {$transcript} "Processing commands for $controlrequestaddr:\n\n";
 
-# debug level
+
 my $dl = 0;
+my %affected_packages;
+my %recipients;
+# this is the hashref which is passed to all control calls
+my %limit = ();
+
+
+my @common_control_options =
+    (($dl > 0 ? (debug => $transcript):()),
+     transcript        => $transcript,
+     requester         => $header{from},
+     request_addr      => $controlrequestaddr,
+     request_msgid     => $header{'message-id'},
+     request_subject   => $header{subject},
+     request_nn        => $nn,
+     request_replyto   => $replyto,
+     message           => \@log,
+     affected_packages => \%affected_packages,
+     recipients        => \%recipients,
+     limit             => \%limit,
+    );
+
 my $state= 'idle';
 my $lowstate= 'idle';
 my $mergelowstate= 'idle';
@@ -156,10 +181,6 @@ our $mismatch;
 our $action;
 
 
-# recipients of mail
-my %recipients;
-# affected_packages
-my %affected_packages;
 my $ok = 0;
 my $unknowns = 0;
 my $procline=0;
@@ -183,7 +204,7 @@ for ($procline=0; $procline<=$#bodylines; $procline++) {
     print {$transcript} "> $_\n";
     next if m/^\s*\#/;
     $action= '';
-    if (m/^stop\s*$/i || m/^quit\s*$/i || m/^--\s*$/ || m/^thank(?:s|\s*you)?\s*$/i || m/^kthxbye\s*$/i) {
+    if (m/^(?:stop|quit|--|thank(?:s|\s*you)?|kthxbye)\.*\s*$/i) {
        print {$transcript} "Stopping processing here.\n\n";
         last;
     } elsif (m/^debug\s+(\d+)$/i && $1 >= 0 && $1 <= 1000) {
@@ -281,7 +302,7 @@ END
     } elsif (m/^usercategory\s+(\S+)(\s+\[hidden\])?\s*$/i) {
         $ok++;
        my $catname = $1;
-       my $hidden = ($2 ne "");
+       my $hidden = (defined $2 and $2 ne "");
 
         my $prefix = "";
         my @cats;
@@ -341,8 +362,11 @@ END
                    push @ords, "$ord DEF";
                    $catsec--;
                }
-               @ords = sort { my ($a1, $a2, $b1, $b2) = split / /, "$a $b";
-                              $a1 <=> $b1 || $a2 <=> $b2; } @ords;
+               @ords = sort {
+                   my ($a1, $a2, $b1, $b2) = split / /, "$a $b";
+                   ((looks_like_number($a1) and looks_like_number($a2))?$a1 <=> $b1:$a1 cmp $b1) ||
+                   ((looks_like_number($a2) and looks_like_number($b2))?$a2 <=> $b2:$a2 cmp $b2);
+               } @ords;
                $cats[-1]->{"ord"} = [map { m/^.* (\S+)/; $1 eq "DEF" ? $catsec + 1 : $1 } @ords];
             } elsif ($o eq "*") {
                $catsec = 0;
@@ -441,6 +465,7 @@ END
     } elsif (m/^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/i) {
        $ok++;
        $ref= $1;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref}=1;
        my $version= $2;
        if (&setbug) {
@@ -501,530 +526,356 @@ END
                 } while (&getnextbug);
             }
         }
-    } elsif (m/^reassign\s+\#?(-?\d+)\s+(\S+)(?:\s+(\d.*))?$/i) {
+    } elsif (m/^reassign\s+\#?(-?\d+)\s+ # bug and command
+              (?:(?:((?:src:|source:)?$config{package_name_re}) # new package
+              (?:\s+((?:$config{package_name_re}\/)?
+                      $config{package_version_re}))?)| # optional version
+              ((?:src:|source:)?$config{package_name_re} # multiple package form
+              (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+))
+              \s*$/xi) {
         $ok++;
         $ref= $1;
-       my $newpackage= $2;
+       my @new_packages;
+       if (not defined $2) {
+           push @new_packages, split /\s*\,\s*/,$4;
+       }
+       else {
+           push @new_packages, $2;
+       }
+       @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref}=1;
         my $version= $3;
-       $newpackage =~ y/A-Z/a-z/;
-        if (&setbug) {
-            if (length($data->{package})) {
-                $action= "$gBug reassigned from package \`$data->{package}'".
-                         " to \`$newpackage'.";
-            } else {
-                $action= "$gBug assigned to package \`$newpackage'.";
-            }
-            do {
-               $affected_packages{$data->{package}} = 1;
-                add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-                $data->{package}= $newpackage;
-                $data->{found_versions}= [];
-                $data->{fixed_versions}= [];
-                # TODO: what if $newpackage is a source package?
-                addfoundversions($data, $data->{package}, $version, 'binary');
-                add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-            } while (&getnextbug);
-        }
-    } elsif (m/^reopen\s+\#?(-?\d+)$/i ? ($noriginator='', 1) :
-             m/^reopen\s+\#?(-?\d+)\s+\=$/i ? ($noriginator='', 1) :
-             m/^reopen\s+\#?(-?\d+)\s+\!$/i ? ($noriginator=$replyto, 1) :
-             m/^reopen\s+\#?(-?\d+)\s+(\S.*\S)$/i ? ($noriginator=$2, 1) : 0) {
+       eval {
+           set_package(@common_control_options,
+                       bug          => $ref,
+                       package      => \@new_packages,
+                      );
+           # if there is a version passed, we make an internal call
+           # to set_found
+           if (defined($version) && length $version) {
+               set_found(@common_control_options,
+                         bug          => $ref,
+                         version      => $version,
+                        );
+           }
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif (m/^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/i) {
         $ok++;
         $ref= $1;
-       $bug_affected{$ref}=1;
-        if (&setbug) {
-            if (@{$data->{fixed_versions}}) {
-                print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\nyou may need to use 'found' to remove fixed versions.\n";
-            }
-            if (!length($data->{done})) {
-                print {$transcript} "$gBug is already open, cannot reopen.\n\n";
-                &nochangebug;
-            } else {
-                $action=
-                    $noriginator eq '' ? "$gBug reopened, originator not changed." :
-                        "$gBug reopened, originator set to $noriginator.";
-                do {
-                   $affected_packages{$data->{package}} = 1;
-                    add_recipients(data => $data,
-                                  recipients => \%recipients,
-                                  transcript   => $transcript,
-                                  ($dl > 0 ? (debug => $transcript):()),
-                                 );
-                    $data->{originator}= $noriginator eq '' ?  $data->{originator} : $noriginator;
-                    $data->{fixed_versions}= [];
-                    $data->{done}= '';
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m{^found\s+\#?(-?\d+)
+       $bug_affected{$ref}=1; 
+       my $new_submitter = $2;
+       if (defined $new_submitter) {
+           if ($new_submitter eq '=') {
+               undef $new_submitter;
+           }
+           elsif ($new_submitter eq '!') {
+               $new_submitter = $replyto;
+           }
+       }
+       eval {
+           reopen(@common_control_options,
+                  bug          => $ref,
+                  submitter    => $new_submitter,
+                 );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif (m{^(?:(?i)found)\s+\#?(-?\d+)
               (?:\s+((?:$config{package_name_re}\/)?
-                   $config{package_version_re}))?$}ix) {
+                   $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*)
+           )?$}x) {
         $ok++;
         $ref= $1;
-        my $version= $2;
-        if (&setbug) {
-            if (!length($data->{done}) and not defined($version)) {
-                print {$transcript} "$gBug is already open, cannot reopen.\n\n";
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       my @versions;
+        if (defined $2) {
+           @versions = split /\s*,\s*/,$2;
+           eval {
+               set_found(@common_control_options,
+                         bug          => $ref,
+                         found        => \@versions,
+                         add          => 1,
+                        );
+           };
+           if ($@) {
                $errors++;
-                &nochangebug;
-            } else {
-                $action=
-                    defined($version) ?
-                        "$gBug marked as found in version $version." :
-                        "$gBug reopened.";
-                do {
-                    $affected_packages{$data->{package}} = 1;
-                   add_recipients(data => $data,
-                                  recipients => \%recipients,
-                                  transcript   => $transcript,
-                                  ($dl > 0 ? (debug => $transcript):()),
-                                 );
-                    # The 'done' field gets a bit weird with version
-                    # tracking, because a bug may be closed by multiple
-                    # people in different branches. Until we have something
-                    # more flexible, we set it every time a bug is fixed,
-                    # and clear it when a bug is found in a version greater
-                   # than any version in which the bug is fixed or when
-                   # a bug is found and there is no fixed version
-                   if (defined $version) {
-                       my ($version_only) = $version =~ m{([^/]+)$};
-                        addfoundversions($data, $data->{package}, $version, 'binary');
-                       my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
-                            map {s{.+/}{}; $_;} @{$data->{fixed_versions}};
-                       if (not @fixed_order or (Debbugs::Versions::Dpkg::vercmp($version_only,$fixed_order[-1]) >= 0)) {
-                            $action = "$gBug marked as found in version $version and reopened."
-                                 if length $data->{done};
-                            $data->{done} = '';
-                       }
-                    } else {
-                        # Versionless found; assume old-style "not fixed at
-                        # all".
-                        $data->{fixed_versions} = [];
-                        $data->{done} = '';
-                    }
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m[^notfound\s+\#?(-?\d+)\s+
-              ((?:$config{package_name_re}\/)?
-                   \S+)\s*$]ix) {
+               print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+       else {
+           eval {
+               set_fixed(@common_control_options,
+                         bug          => $ref,
+                         fixed        => [],
+                         reopen       => 1,
+                        );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+    }
+    elsif (m{^(?:(?i)notfound)\s+\#?(-?\d+)
+            \s+((?:$config{package_name_re}\/)?
+                $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*
+           )$}x) {
         $ok++;
         $ref= $1;
-        my $version= $2;
-        if (&setbug) {
-            $action= "$gBug no longer marked as found in version $version.";
-            if (length($data->{done})) {
-                $extramessage= "(By the way, this $gBug is currently marked as done.)\n";
-            }
-            do {
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-                removefoundversions($data, $data->{package}, $version, 'binary');
-            } while (&getnextbug);
-       }
-   }
-    elsif (m[^fixed\s+\#?(-?\d+)\s+
-            ((?:$config{package_name_re}\/)?
-                 $config{package_version_re})\s*$]ix) {
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       my @versions;
+        @versions = split /\s*,\s*/,$2;
+       eval {
+           set_found(@common_control_options,
+                     bug          => $ref,
+                     found        => \@versions,
+                     remove       => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    }
+    elsif (m{^(?:(?i)fixed)\s+\#?(-?\d+)
+            \s+((?:$config{package_name_re}\/)?
+                   $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*)
+           \s*$}x) {
         $ok++;
         $ref= $1;
-        my $version= $2;
-        if (&setbug) {
-            $action=
-                 defined($version) ?
-                      "$gBug marked as fixed in version $version." :
-                           "$gBug reopened.";
-                do {
-                    $affected_packages{$data->{package}} = 1;
-                   add_recipients(data => $data,
-                                  recipients => \%recipients,
-                                  transcript   => $transcript,
-                                  ($dl > 0 ? (debug => $transcript):()),
-                                 );
-                    addfixedversions($data, $data->{package}, $version, 'binary');
-              } while (&getnextbug);
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       my @versions;
+        @versions = split /\s*,\s*/,$2;
+       eval {
+           set_fixed(@common_control_options,
+                     bug          => $ref,
+                     fixed        => \@versions,
+                     add          => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
        }
-   }
-    elsif (m[^notfixed\s+\#?(-?\d+)\s+
-            ((?:$config{package_name_re}\/)?
-                 \S+)\s*$]ix) {
+    }
+    elsif (m{^(?:(?i)notfixed)\s+\#?(-?\d+)
+            \s+((?:$config{package_name_re}\/)?
+                   $config{package_version_re}
+               # allow for multiple packages
+               (?:\s*,\s*(?:$config{package_name_re}\/)?
+                   $config{package_version_re})*)
+           \s*$}x) {
         $ok++;
         $ref= $1;
-        my $version= $2;
-        if (&setbug) {
-            $action=
-                 defined($version) ?
-                      "$gBug no longer marked as fixed in version $version." :
-                           "$gBug reopened.";
-                do {
-                    $affected_packages{$data->{package}} = 1;
-                   add_recipients(data => $data,
-                                  recipients => \%recipients,
-                                  transcript   => $transcript,
-                                  ($dl > 0 ? (debug => $transcript):()),
-                                 );
-                    removefixedversions($data, $data->{package}, $version, 'binary');
-              } while (&getnextbug);
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       my @versions;
+        @versions = split /\s*,\s*/,$2;
+       eval {
+           set_fixed(@common_control_options,
+                     bug          => $ref,
+                     fixed        => \@versions,
+                     remove       => 1,
+                    );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n";
        }
-   }
-    elsif (m/^submitter\s+\#?(-?\d+)\s+\!$/i ? ($newsubmitter=$replyto, 1) :
-             m/^submitter\s+\#?(-?\d+)\s+(\S.*\S)$/i ? ($newsubmitter=$2, 1) : 0) {
+    }
+    elsif (m/^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/i) {
         $ok++;
         $ref= $1;
        $bug_affected{$ref}=1;
-        if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) {
-            $ref = $clonebugs{$ref};
-        }
-       if (not Mail::RFC822::Address::valid($newsubmitter)) {
-            transcript("$newsubmitter is not a valid e-mail address; not changing submitter\n");
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       my $newsubmitter = $2 eq '!' ? $replyto : $2;
+        if (not Mail::RFC822::Address::valid($newsubmitter)) {
+            print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n";
             $errors++;
        }
-        elsif (&getbug) {
-            if (&checkpkglimit) {
-                &foundbug;
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-                $oldsubmitter= $data->{originator};
-                $data->{originator}= $newsubmitter;
-                $action= "Changed $gBug submitter from $oldsubmitter to $newsubmitter.";
-                &savebug;
-                print {$transcript} "$action\n";
-                if (length($data->{done})) {
-                    print {$transcript} "(By the way, that $gBug is currently marked as done.)\n";
-                }
-                print {$transcript} "\n";
-                $message= <<END;
-From: $gMaintainerEmail ($gProject $gBug Tracking System)
-To: $oldsubmitter
-Subject: $gBug#$ref submitter address changed
-         ($header{'subject'})
-References: $header{'message-id'} $data->{msgid}
-In-Reply-To: $data->{msgid}
-Message-ID: <handler.$ref.$nn.newsubmitter.$midix\@$gEmailDomain>
-Reply-To: $ref\@$gEmailDomain
-X-$gProject-PR-Message: submitter-changed $ref
-
-The submitter address recorded for your $gBug report
-#$ref: $data->{subject}
-has been changed.
-
-The old submitter address for this report was
-$oldsubmitter.
-The new submitter address is
-$newsubmitter.
-
-This change was made by
-$replyto.
-If it was incorrect, please contact them directly.
-
-$gMaintainer
-(administrator, $gProject $gBugs database)
-
-END
-                &sendmailmessage($message,$oldsubmitter);
-            } else {
-                &cancelbug;
-            }
-        } else {
-            &notfoundbug;
+       else {
+           eval {
+               set_submitter(@common_control_options,
+                             bug       => $ref,
+                             submitter => $newsubmitter,
+                            );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n";
+           }
         }
     } elsif (m/^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/i) {
         $ok++;
         $ref= $1;
-       my $whereto= $2;
-       $bug_affected{$ref}=1;
-        if (&setbug) {
-            if (length($data->{forwarded})) {
-    $action= "Forwarded-to-address changed from $data->{forwarded} to $whereto.";
-            } else {
-    $action= "Noted your statement that $gBug has been forwarded to $whereto.";
-            }
-            if (length($data->{done})) {
-                $extramessage= "(By the way, this $gBug is currently marked as done.)\n";
-            }
-            do {
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              actions_taken => {forwarded => 1},
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-               $data->{forwarded}= $whereto;
-            } while (&getnextbug);
-        }
+       my $forward_to= $2;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       $bug_affected{$ref} = 1;
+       eval {
+           set_forwarded(@common_control_options,
+                         bug          => $ref,
+                         forwarded    => $forward_to,
+                          );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
     } elsif (m/^notforwarded\s+\#?(-?\d+)$/i) {
         $ok++;
         $ref= $1;
-       $bug_affected{$ref}=1;
-        if (&setbug) {
-            if (!length($data->{forwarded})) {
-                print {$transcript} "$gBug is not marked as having been forwarded.\n\n";
-                &nochangebug;
-            } else {
-    $action= "Removed annotation that $gBug had been forwarded to $data->{forwarded}.";
-                do {
-                    $affected_packages{$data->{package}} = 1;
-                   add_recipients(data => $data,
-                                  recipients => \%recipients,
-                                  transcript   => $transcript,
-                                  ($dl > 0 ? (debug => $transcript):()),
-                                 );
-                    $data->{forwarded}= '';
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m/^severity\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i ||
-       m/^priority\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) {
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       $bug_affected{$ref} = 1;
+       eval {
+           set_forwarded(@common_control_options,
+                         bug          => $ref,
+                         forwarded    => undef,
+                          );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n";
+       }
+    } elsif (m/^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) {
         $ok++;
         $ref= $1;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref}=1;
         my $newseverity= $2;
-        if (!grep($_ eq $newseverity, @gSeverityList, "$gDefaultSeverity")) {
-            print {$transcript} "Severity level \`$newseverity' is not known.\n".
-                 "Recognized are: $gShowSeverities.\n\n";
-           $errors++;
-        } elsif (exists $gObsoleteSeverities{$newseverity}) {
+        if (exists $gObsoleteSeverities{$newseverity}) {
             print {$transcript} "Severity level \`$newseverity' is obsolete. " .
                 "Use $gObsoleteSeverities{$newseverity} instead.\n\n";
                $errors++;
-        } elsif (&setbug) {
-            my $printseverity= $data->{severity};
-            $printseverity= "$gDefaultSeverity" if $printseverity eq '';
-           $action= "Severity set to \`$newseverity' from \`$printseverity'";
-           do {
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-                if (defined $gStrongList and isstrongseverity($newseverity)) {
-                    addbcc("$gStrongList\@$gListDomain");
-                }
-                $data->{severity}= $newseverity;
-            } while (&getnextbug);
-        }
-    } elsif (m/^tags?\s+\#?(-?\d+)\s+(([=+-])\s*)?(\S.*)?$/i) {
+        } elsif (not defined first {$_ eq $newseverity}
+           (@gSeverityList, "$gDefaultSeverity")) {
+            print {$transcript} "Severity level \`$newseverity' is not known.\n".
+                 "Recognized are: $gShowSeverities.\n\n";
+           $errors++;
+        } else {
+           eval {
+               set_severity(@common_control_options,
+                            bug => $ref,
+                            severity => $newseverity,
+                           );
+           };
+           if ($@) {
+               $errors++;
+               print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n";
+           }
+       }
+    } elsif (m/^tags?\s+\#?(-?\d+)\s+(\S.*)$/i) {
        $ok++;
        $ref = $1;
-       my $addsubcode = $3;
-       my $tags = $4;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref}=1;
-       my $addsub = "add";
-       if (defined $addsubcode) {
-           $addsub = "sub" if ($addsubcode eq "-");
-           $addsub = "add" if ($addsubcode eq "+");
-           $addsub = "set" if ($addsubcode eq "=");
-       }
-       my @okaytags = ();
-       my @badtags = ();
-       foreach my $t (split /[\s,]+/, $tags) {
-           if (!grep($_ eq $t, @gTags)) {
-               push @badtags, $t;
-           } else {
-               push @okaytags, $t;
+       my $tags = $2;
+       my @tags = split /[\s,]+/, $tags;
+       # this is an array of hashrefs which contain two elements, the
+       # first of which is the array of tags, the second is the
+       # option to pass to set_tags (we use a hashref here to make it
+       # more obvious what is happening)
+       my @tag_operations = {tags => [],
+                             option => []
+                            };
+       my $alter_type = '=';
+       my @badtags;
+       for my $tag (@tags) {
+           if ($tag =~ /^[=+-]$/) {
+               if ($tag eq '=') {
+                   @tag_operations = {tags => [],
+                                      option => [],
+                                     };
+               }
+               elsif ($tag eq '-') {
+                   push @tag_operations,
+                       {tags => [],
+                        option => [remove => 1],
+                       };
+               }
+               elsif ($tag eq '+') {
+                   push @tag_operations,
+                       {tags => [],
+                        option => [add => 1]
+                       };
+               }
+               next;
+           }
+           if (not defined first {$_ eq $tag} @{$config{tags}}) {
+               push @badtags, $tag;
+               next;
            }
+           push @{$tag_operations[-1]{tags}},$tag;
        }
        if (@badtags) {
             print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n".
                 "Recognized are: ".join(' ', @gTags).".\n\n";
            $errors++;
        }
-       if (&setbug) {
-           if ($data->{keywords} eq '') {
-               print {$transcript} "There were no tags set.\n";
-           } else {
-               print {$transcript} "Tags were: $data->{keywords}\n";
-           }
-           if ($addsub eq "set") {
-               $action= "Tags set to: " . join(", ", @okaytags);
-           } elsif ($addsub eq "add") {
-               $action= "Tags added: " . join(", ", @okaytags);
-           } elsif ($addsub eq "sub") {
-               $action= "Tags removed: " . join(", ", @okaytags);
+       eval {
+           for my $operation (@tag_operations) {
+               set_tags(@common_control_options,
+                        bug => $ref,
+                        tags => [@{$operation->{$tags}}],
+                        warn_on_bad_tags => 0, # don't warn on bad tags,
+                        # 'cause we do that above
+                        @{$operation->{option}},
+                       );
            }
-           do {
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-               $data->{keywords} = '' if ($addsub eq "set");
-               # Allow removing obsolete tags.
-               if ($addsub eq "sub") {
-                   foreach my $t (@badtags) {
-                       $data->{keywords} = join ' ', grep $_ ne $t, 
-                           split ' ', $data->{keywords};
-                   }
-               }
-               # Now process all other additions and subtractions.
-               foreach my $t (@okaytags) {
-                   $data->{keywords} = join ' ', grep $_ ne $t, 
-                       split ' ', $data->{keywords};
-                   $data->{keywords} = "$t $data->{keywords}" unless($addsub eq "sub");
-               }
-               $data->{keywords} =~ s/\s*$//;
-            } while (&getnextbug);
+       };
+       if ($@) {
+           # we intentionally have two errors here if there is a bad
+           # tag and the above fails for some reason
+           $errors++;
+           print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n";
        }
-    } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(by|with)\s+(\S.*)?$/i) {
+    } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/i) {
        $ok++;
-       my $bugnum = $2; my $blockers = $4;
-       my $addsub = "add";
-       $addsub = "sub" if (defined $1 and $1 eq "un");
-       if ($bugnum =~ m/^-\d+$/ && defined $clonebugs{$bugnum}) {
-            $bugnum = $clonebugs{$bugnum};
-       }
-
-       my @okayblockers;
-       my @badblockers;
-       foreach my $b (split /[\s,]+/, $blockers) {
-           $b=~s/^\#//;
-           if ($b=~/[0-9]+/) {
-               $ref=$b;
-               if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) {
-                    $ref = $clonebugs{$ref};
-               }
-               if (&getbug) {
-                   &foundbug;
-                   push @okayblockers, $ref;
-
-                   # add to the list all bugs that are merged with $b,
-                   # because all of their data must be kept in sync
-                   my @thisbugmergelist= split(/ /,$data->{mergedwith});
-                   &cancelbug;
-
-                   foreach $ref (@thisbugmergelist) {
-                       if (&getbug) {
-                          push @okayblockers, $ref;
-                          &cancelbug;
-                       }
-                   }
-               }
-               else {
-                   &notfoundbug;
-                    push @badblockers, $ref;
-               }
-           }
-           else {
-                push @badblockers, $b;
-           }
-       }
-       if (@badblockers) {
-            print {$transcript} "Unknown blocking bug/s: ".join(', ', @badblockers).".\n";
+        $ref= $2;
+       my $add_remove = defined $1 && $1 eq 'un';
+       my @blockers = split /[\s,]+/, $3;
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       $bug_affected{$ref} = 1;
+       eval {
+            set_blocks(@common_control_options,
+                       bug          => $ref,
+                       block        => \@blockers,
+                       $add_remove ? (remove => 1):(add => 1),
+                      );
+       };
+       if ($@) {
            $errors++;
-       }
-       
-       $ref=$bugnum;
-       if (&setbug) {
-           if ($data->{blockedby} eq '') {
-               print {$transcript} "Was not blocked by any bugs.\n";
-           } else {
-               print {$transcript} "Was blocked by: $data->{blockedby}\n";
-           }
-           if ($addsub eq "set") {
-               $action= "Blocking bugs of $bugnum set to: " . join(", ", @okayblockers);
-           } elsif ($addsub eq "add") {
-               $action= "Blocking bugs of $bugnum added: " . join(", ", @okayblockers);
-           } elsif ($addsub eq "sub") {
-               $action= "Blocking bugs of $bugnum removed: " . join(", ", @okayblockers);
-           }
-           my %removedblocks;
-           my %addedblocks;
-           do {
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-               my @oldblockerlist = split ' ', $data->{blockedby};
-               $data->{blockedby} = '' if ($addsub eq "set");
-               foreach my $b (@okayblockers) {
-                       $data->{blockedby} = manipset($data->{blockedby}, $b,
-                               ($addsub ne "sub"));
-               }
-
-               foreach my $b (@oldblockerlist) {
-                       if (! grep { $_ eq $b } split ' ', $data->{blockedby}) {
-                               push @{$removedblocks{$b}}, $ref;
-                       }
-               }
-               foreach my $b (split ' ', $data->{blockedby}) {
-                       if (! grep { $_ eq $b } @oldblockerlist) {
-                               push @{$addedblocks{$b}}, $ref;
-                       }
-               }
-            } while (&getnextbug);
-
-           # Now that the blockedby data is updated, change blocks data
-           # to match the changes.
-           foreach $ref (keys %addedblocks) {
-               if (&getbug) {
-                   foreach my $b (@{$addedblocks{$ref}}) {
-                       $data->{blocks} = manipset($data->{blocks}, $b, 1);
-                   }
-                   &savebug;
-                }
-           }
-           foreach $ref (keys %removedblocks) {
-               if (&getbug) {
-                   foreach my $b (@{$removedblocks{$ref}}) {
-                       $data->{blocks} = manipset($data->{blocks}, $b, 0);
-                   }
-                   &savebug;
-                }
-           }
+           print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n";
        }
     } elsif (m/^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/i) {
         $ok++;
         $ref= $1; my $newtitle= $2;
-       $bug_affected{$ref}=1;
-       if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) {
-           $ref = $clonebugs{$ref};
+       $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
+       $bug_affected{$ref} = 1;
+       eval {
+            set_title(@common_control_options,
+                      bug          => $ref,
+                      title        => $newtitle,
+                     );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n";
        }
-        if (&getbug) {
-            if (&checkpkglimit) {
-                &foundbug;
-                $affected_packages{$data->{package}} = 1;
-               add_recipients(data => $data,
-                              recipients => \%recipients,
-                              transcript   => $transcript,
-                              ($dl > 0 ? (debug => $transcript):()),
-                             );
-               my $oldtitle = $data->{subject};
-                $data->{subject}= $newtitle;
-                $action= "Changed $gBug title to `$newtitle' from `$oldtitle'.";
-                &savebug;
-                print {$transcript} "$action\n";
-                if (length($data->{done})) {
-                    print {$transcript} "(By the way, that $gBug is currently marked as done.)\n";
-                }
-                print {$transcript} "\n";
-            } else {
-                &cancelbug;
-            }
-        } else {
-            &notfoundbug;
-        }
     } elsif (m/^unmerge\s+\#?(-?\d+)$/i) {
        $ok++;
        $ref= $1;
@@ -1254,12 +1105,38 @@ END
        my @pkgs = split /\s+/, $1;
        if (scalar(@pkgs) > 0) {
                %limit_pkgs = map { ($_, 1) } @pkgs;
+               $limit{package} = [@pkgs];
                print {$transcript} "Ignoring bugs not assigned to: " .
                        join(" ", keys(%limit_pkgs)) . "\n\n";
        } else {
                %limit_pkgs = ();
                print {$transcript} "Not ignoring any bugs.\n\n";
        }
+    } elsif (m/^limit\:?\s+(\S.*\S)\s*$/) {
+       $ok++;
+       my ($field,@options) = split /\s+/, $1;
+       $field = lc($field);
+       if ($field =~ /^(?:clear|unset|blank)$/) {
+           %limit = ();
+           print {$transcript} "Limit cleared.\n\n";
+       }
+       elsif (exists $Debbugs::Status::fields{$field} ) {
+           # %limit can actually contain regexes, but because they're
+           # not evaluated in Safe, DO NOT allow them through without
+           # fixing this.
+           $limit{$field} = [@options];
+           print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n";
+           print {$transcript} "Limit currently set to ";
+           for my $limit_field (keys %limit) {
+               print {$transcript} "  '$limit_field':".join(', ',map {qq('$_')} @options)."\n";
+           }
+           print {$transcript} "\n";
+       }
+       else {
+           print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n";
+           $errors++;
+           last;
+       }
     } elsif (m/^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/i) {
        $ok++;
         $ref = $1;
@@ -1268,13 +1145,8 @@ END
        $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref} = 1;
        eval {
-            affects(bug          => $ref,
-                    transcript   => $transcript,
-                    ($dl > 0 ? (debug => $transcript):()),
-                    requester    => $header{from},
-                    request_addr => $controlrequestaddr,
-                    message      => \@log,
-                    recipients   => \%recipients,
+            affects(@common_control_options,
+                    bug => $ref,
                     packages     => [splitpackages($3)],
                     ($add_remove eq '+'?(add => 1):()),
                     ($add_remove eq '-'?(remove => 1):()),
@@ -1282,7 +1154,7 @@ END
        };
        if ($@) {
            $errors++;
-           print {$transcript} "Failed to give $ref a summary: $@";
+           print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n";
        }
 
     } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) {
@@ -1292,19 +1164,14 @@ END
        $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref} = 1;
        eval {
-           summary(bug          => $ref,
-                   transcript   => $transcript,
-                   ($dl > 0 ? (debug => $transcript):()),
-                   requester    => $header{from},
-                   request_addr => $controlrequestaddr,
-                   message      => \@log,
-                   recipients   => \%recipients,
+           summary(@common_control_options,
+                   bug          => $ref,
                    summary      => $summary_msg,
                   );
        };
        if ($@) {
            $errors++;
-           print {$transcript} "Failed to give $ref a summary: $@";
+           print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n";
        }
 
     } elsif (m/^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/i) {
@@ -1317,19 +1184,14 @@ END
        }
        $bug_affected{$ref} = 1;
        eval {
-           owner(bug          => $ref,
-                 transcript   => $transcript,
-                 ($dl > 0 ? (debug => $transcript):()),
-                 requester    => $header{from},
-                 request_addr => $controlrequestaddr,
-                 message      => \@log,
-                 recipients   => \%recipients,
+           owner(@common_control_options,
+                 bug          => $ref,
                  owner        => $newowner,
                 );
        };
        if ($@) {
            $errors++;
-           print {$transcript} "Failed to mark $ref as having an owner: $@";
+           print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n";
        }
     } elsif (m/^noowner\s+\#?(-?\d+)\s*$/i) {
         $ok++;
@@ -1337,19 +1199,14 @@ END
        $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
        $bug_affected{$ref} = 1;
        eval {
-           owner(bug          => $ref,
-                 transcript   => $transcript,
-                 ($dl > 0 ? (debug => $transcript):()),
-                 requester    => $header{from},
-                 request_addr => $controlrequestaddr,
-                 message      => \@log,
-                 recipients   => \%recipients,
+           owner(@common_control_options,
+                 bug          => $ref,
                  owner        => undef,
                 );
        };
        if ($@) {
            $errors++;
-           print {$transcript} "Failed to mark $ref as not having an owner: $@";
+           print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n";
        }
     } elsif (m/^unarchive\s+#?(\d+)$/i) {
         $ok++;
@@ -1357,13 +1214,8 @@ END
         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
         $bug_affected{$ref} = 1;
         eval {
-             bug_unarchive(bug        => $ref,
-                           transcript => $transcript,
-                           ($dl > 0 ? (debug => $transcript):()),
-                           affected_bugs => \%bug_affected,
-                           requester => $header{from},
-                           request_addr => $controlrequestaddr,
-                           message => \@log,
+             bug_unarchive(@common_control_options,
+                           bug        => $ref,
                            recipients => \%recipients,
                           );
         };
@@ -1376,16 +1228,10 @@ END
         $ref = $clonebugs{$ref} if exists $clonebugs{$ref};
         $bug_affected{$ref} = 1;
         eval {
-             bug_archive(bug => $ref,
-                         transcript => $transcript,
-                         ($dl > 0 ? (debug => $transcript):()),
+             bug_archive(@common_control_options,
+                         bug => $ref,
                          ignore_time => 1,
                          archive_unarchived => 0,
-                         affected_bugs => \%bug_affected,
-                         requester => $header{from},
-                         request_addr => $controlrequestaddr,
-                         message => \@log,
-                         recipients => \%recipients,
                         );
         };
         if ($@) {
@@ -1796,6 +1642,11 @@ our $doc;
 sub sendtxthelpraw {
     my ($relpath,$description) = @_;
     $doc='';
+    if (not -e "$gDocDir/$relpath") {
+       print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n";
+       warn "Help text $gDocDir/$relpath not found";
+       return;
+    }
     open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!";
     while(<D>) { $doc.=$_; }
     close(D);
diff --git a/t/01_pod.t b/t/01_pod.t
new file mode 100644 (file)
index 0000000..1d7c422
--- /dev/null
@@ -0,0 +1,5 @@
+# -*- mode: cperl; -*-
+use Test::More;
+eval "use Test::Pod 1.00";
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+all_pod_files_ok();
diff --git a/t/03_status.t b/t/03_status.t
new file mode 100644 (file)
index 0000000..eec2d54
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- mode: cperl;-*-
+
+use Test::More tests => 6;
+
+use warnings;
+use strict;
+
+use utf8;
+use Encode;
+
+use_ok('Debbugs::Status');
+
+my $data = {package => 'foo, bar, baz',
+           blocks  => '1 2 3',
+           blockedby => '',
+           tags      => 'foo, bar  , baz',
+          };
+
+my @temp = Debbugs::Status::split_status_fields($data);
+is_deeply($temp[0]{package},[qw(foo bar baz)],
+         'split_status_fields splits packages properly',
+        );
+is_deeply($temp[0]{blocks},[qw(1 2 3)],
+         'split_status_fields splits blocks properly',
+        );
+is_deeply($temp[0]{blockedby},[],
+         'split_status_fields handles empty fields properly',
+        );
+is_deeply($temp[0]{tags},[qw(foo bar baz)],
+         'split_status_fields splits tags properly',
+        );
+my $temp = Debbugs::Status::split_status_fields($data);
+is_deeply(Debbugs::Status::split_status_fields($temp),$temp,
+         'recursively calling split_status_fields returns the same thing');
index eb4f65699a3b9fa68465e0c06bea80bd990fb245..1c6709420d8af4644d5c100777d5ddb2f643ee6f 100644 (file)
@@ -1,7 +1,7 @@
 # -*- mode: cperl;-*-
 # $Id: 05_mail.t,v 1.1 2005/08/17 21:46:17 don Exp $
 
-use Test::More tests => 96;
+use Test::More tests => 102;
 
 use warnings;
 use strict;
@@ -70,10 +70,13 @@ ok(-e "$spool_dir/db-h/01/1.report",'report file created');
 # sent out. 1) ack to submitter 2) mail to maintainer
 
 # This keeps track of the previous size of the sendmail directory
-my $SD_SIZE_PREV = 0;
-my $SD_SIZE_NOW = dirsize($sendmail_dir);
-ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 2,'submit messages appear to have been sent out properly');
-$SD_SIZE_PREV=$SD_SIZE_NOW;
+my $SD_SIZE = 0;
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,2,
+                     $sendmail_dir,
+                     'submit messages appear to have been sent out properly',
+                    );
+
 
 # now send a message to the bug
 
@@ -89,9 +92,10 @@ Severity: normal
 This is a silly bug
 EOF
 
-$SD_SIZE_NOW = dirsize($sendmail_dir);
-ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 2,'1@bugs.something messages appear to have been sent out properly');
-$SD_SIZE_PREV=$SD_SIZE_NOW;
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,2,
+                     $sendmail_dir,
+                     '1@bugs.something messages appear to have been sent out properly');
 
 # just check to see that control doesn't explode
 send_message(to => 'control@bugs.something',
@@ -105,9 +109,10 @@ retitle 1 new title
 thanks
 EOF
 
-$SD_SIZE_NOW = dirsize($sendmail_dir);
-ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 1,'control@bugs.something messages appear to have been sent out properly');
-$SD_SIZE_PREV=$SD_SIZE_NOW;
+$SD_SIZE =
+   num_messages_sent($SD_SIZE,1,
+                    $sendmail_dir,
+                    'control@bugs.something messages appear to have been sent out properly');
 # now we need to check to make sure the control message was processed without errors
 ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug")) == 0,
    'control@bugs.something message was parsed without errors');
@@ -120,16 +125,17 @@ ok($status->{severity} eq 'wishlist','bug 1 wishlisted');
 
 # now we're going to go through and methododically test all of the control commands.
 my @control_commands =
-     (severity_wishlist => {command => 'severity',
+     (
+      severity_wishlist => {command => 'severity',
                            value   => 'wishlist',
                            status_key => 'severity',
                            status_value => 'wishlist',
                           },
-      reassign_bar => {command => 'reassign',
-                      value   => 'bar',
-                      status_key => 'package',
-                      status_value => 'bar',
-                     },
+      reassign_bar_baz => {command => 'reassign',
+                          value   => 'bar,baz',
+                          status_key => 'package',
+                          status_value => 'bar,baz',
+                         },
       reassign_foo => {command => 'reassign',
                       value   => 'foo',
                       status_key => 'package',
@@ -227,6 +233,16 @@ my @control_commands =
                       status_key => 'mergedwith',
                       status_value => '2',
                      },
+      unmerge      => {command => 'unmerge',
+                      value   => '',
+                      status_key => 'mergedwith',
+                      status_value => '',
+                     },
+      block        => {command => 'block',
+                      value   => ' with 2',
+                      status_key => 'blockedby',
+                      status_value => '2',
+                     },
       summary      => {command => 'summary',
                       value   => '5',
                       status_key => 'summary',
@@ -284,9 +300,10 @@ $control_command->{command} 1$control_command->{value}
 thanks
 EOF
                                  ;
-     $SD_SIZE_NOW = dirsize($sendmail_dir);
-     ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 1,'control@bugs.something messages appear to have been sent out properly');
-     $SD_SIZE_PREV=$SD_SIZE_NOW;
+     $SD_SIZE =
+        num_messages_sent($SD_SIZE,1,
+                          $sendmail_dir,
+                          'control@bugs.something messages appear to have been sent out properly');
      # now we need to check to make sure the control message was processed without errors
      ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug with $command")) == 0,
        'control@bugs.something'. "$command message was parsed without errors");
@@ -319,9 +336,10 @@ submitter 1 bar@baz.com
 thanks
 EOF
                                  ;
-$SD_SIZE_NOW = dirsize($sendmail_dir);
-ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 1,'control@bugs.something messages appear to have been sent out properly');
-$SD_SIZE_PREV=$SD_SIZE_NOW;
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,1,
+                     $sendmail_dir,
+                     'control@bugs.something messages appear to have been sent out properly');
 # now we need to check to make sure the control message was processed without errors
 ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug with unarchivearchive")) == 0,
    'control@bugs.something'. "unarchive/archive message was parsed without errors");
diff --git a/t/07_control_limit.t b/t/07_control_limit.t
new file mode 100644 (file)
index 0000000..630cc8d
--- /dev/null
@@ -0,0 +1,104 @@
+# -*- mode: cperl; -*-
+
+use Test::More tests => 4;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+use Data::Dumper;
+
+# HTTP::Server:::Simple defines a SIG{CHLD} handler that breaks system; undef it here.
+$SIG{CHLD} = sub {};
+my %config;
+eval {
+     %config = create_debbugs_configuration(debug => exists $ENV{DEBUG}?$ENV{DEBUG}:0);
+};
+if ($@) {
+     BAIL_OUT($@);
+}
+
+my $sendmail_dir = $config{sendmail_dir};
+my $spool_dir = $config{spool_dir};
+my $config_dir = $config{config_dir};
+
+END{
+     if ($ENV{DEBUG}) {
+         diag("spool_dir:   $spool_dir\n");
+         diag("config_dir:   $config_dir\n");
+         diag("sendmail_dir: $sendmail_dir\n");
+     }
+}
+
+# We're going to use create mime message to create these messages, and
+# then just send them to receive.
+
+send_message(to=>'submit@bugs.something',
+            headers => [To   => 'submit@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Submiting a bug',
+                       ],
+            body => <<EOF) or fail('Unable to send message');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+my $SD_SIZE = dirsize($sendmail_dir);
+send_message(to => 'control@bugs.something',
+            headers => [To   => 'control@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => "Munging a bug with limit_package_bar",
+                       ],
+            body => <<EOF) or fail 'message to control@bugs.something failed';
+debug 10
+limit package bar
+severity 1 wishlist
+thanks
+EOF
+
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,1,
+                          $sendmail_dir,
+                     'control@bugs.something messages appear to have been sent out properly');
+
+# make sure this fails
+ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed (with 1 errors): Munging a bug with limit_package_bar")) == 0,
+   'control@bugs.something'. "limit message failed with 1 error");
+
+send_message(to => 'control@bugs.something',
+            headers => [To   => 'control@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => "Munging a bug with limit_package_foo",
+                       ],
+            body => <<EOF) or fail 'message to control@bugs.something failed';
+debug 10
+limit package foo
+severity 1 wishlist
+thanks
+EOF
+
+$SD_SIZE =
+    num_messages_sent($SD_SIZE,1,
+                          $sendmail_dir,
+                     'control@bugs.something messages appear to have been sent out properly');
+
+# make sure this fails
+ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug with limit_package_foo")) == 0,
+   'control@bugs.something'. "limit message succeeded with no errors");
+
index 39a791c79f923bd4c4c91bde438d11758621ca8c..f203668c7c1af29385c9c6a7e51bfffa0a57d0ca 100644 (file)
@@ -31,6 +31,7 @@ use Debbugs::MIME qw(create_mime_message);
 use File::Basename qw(dirname basename);
 use IPC::Open3;
 use IO::Handle;
+use Test::More;
 
 use Params::Validate qw(validate_with :types);
 
@@ -40,9 +41,10 @@ BEGIN{
 
      @EXPORT = ();
      %EXPORT_TAGS = (configuration => [qw(dirsize create_debbugs_configuration send_message)],
+                    mail          => [qw(num_messages_sent)],
                    );
      @EXPORT_OK = ();
-     Exporter::export_ok_tags(qw(configuration));
+     Exporter::export_ok_tags(qw(configuration mail));
      $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
@@ -230,6 +232,25 @@ sub send_message{
      }
 }
 
+=head2 num_messages_sent
+
+     $SD_SIZE = num_messages_sent($SD_SIZE,2,$sendmail_dir,'2 messages have been sent properly');
+
+Tests to make sure that at least a certain number of messages have
+been sent since the last time this command was run. Usefull to test to
+make sure that mail has been sent.
+
+=cut
+
+sub num_messages_sent {
+    my ($prev_size,$num_messages,$sendmail_dir,$test_name) = @_;
+    my $cur_size = dirsize($sendmail_dir);
+    ## print STDERR "sendmail: $sendmail_dir, want: $num_messages,
+    ## size: $cur_size, prev_size: $prev_size\n";
+    ok($cur_size-$prev_size >= $num_messages, $test_name);
+    return $cur_size;
+}
+
 
 1;
 
index cc8d57ddd5282c71fab93b91cb3ccec7e8b293b4..070005abdd1e40517e1449e4269ff029054c993b 100644 (file)
@@ -27,9 +27,11 @@ function toggle_infmessages()
      $output .= sprintf qq(<p><a href="%s">Full log</a></p>),html_escape(bug_links(bug=>$bug_num,links_only=>1));
   }
   else {
-     $output .=  qq(<p><a href="mailto:$bug_num\@$config{email_domain}">Reply</a> ).
-         qq(or <a href="mailto:$bug_num-subscribe\@$config{email_domain}">subscribe</a> ).
+     if (not $status{archived}) {
+       $output .=  qq(<p><a href="mailto:$bug_num\@$config{email_domain}">Reply</a> ).
+          qq(or <a href="mailto:$bug_num-subscribe\@$config{email_domain}">subscribe</a> ).
               qq(to this bug.</p>\n);
+     }
      $output .=  qq(<p><a href="javascript:toggle_infmessages();">Toggle useless messages</a></p>);
      $output .= sprintf qq(<div class="msgreceived"><p>View this report as an <a href="%s">mbox folder</a>, ).
          qq(<a href="%s">status mbox</a>, <a href="%s">maintainer mbox</a></p></div>\n),
index 4a39ba375ddc4a1da94081b8d97baa0ddbfa35c3..872aff44fc0913a75b4c0c0789a79fd2ef169c75 100644 (file)
@@ -1,14 +1,15 @@
 <div class="pkginfo">
   <p>{if (keys %package > 1) { q(Packages)} else {q(Package)}}:
-     {join(q(, ),package_links(package => [map {$_->{package}} values %package],
+     {join(q(, ),package_links(package => [map {$_->{package}} grep {!$_->{is_source}} values %package],
+                               source  => [map {$_->{source} } grep { $_->{is_source}} values %package],
                                class => q(submitter),
                               )
           )};
 {my $output ='';
  for my $package (values %package) {
-     $output .= q(Maintainer for ).package_links(package=>$package->{package}).qq( is ).
+     $output .= q(Maintainer for ).package_links($package->{is_source}?(source=>$package->{source}):(package=>$package->{package})).qq( is ).
                 package_links(maintainer => $package->{maintainer}).qq(; );
-     if (exists $package->{source}) {
+     if (exists $package->{source} and not $package->{is_source}) {
        $output .= q(Source for ).package_links(package=>$package->{package}).qq( is ).
                    package_links(source => $package->{source}).qq(. );
      }
index 102d7fe66cc017dfa20d998ed11ab73306358cf2..685d93f8d4e4068a1edc5fe2482c1fb9b60f0e6f 100644 (file)
      $output .= qq(<abbr title="fixed versions">☺</abbr>);
   }
   if (@{$status{blockedby_array}}) {
-     $output .= qq(<abbr title="blocked by">â\94«</abbr>);
+     $output .= qq(<abbr title="blocked by">â\99\99</abbr>);
   }
   if (@{$status{blocks_array}}) {
-     $output .= qq(<abbr title="blocks">â\94£</abbr>);
+     $output .= qq(<abbr title="blocks">â\99\94</abbr>);
   }
   if (length($status{forwarded})) {
      $output .= qq(<abbr title="forwarded">↝</abbr>);
   if ($status{archived}) {
      $output .= qq(<abbr title="archived">♲</abbr>);
   }
+  if (length $status{affects}){
+     $output .= qq(<abbr title="affects">☣</abbr>);
+  }
   length($output)?$output:'&nbsp;&nbsp;';
   }</span></font>]
-  [{package_links(package=>$status{package},options=>\%options,class=>"submitter")}]
+  [{package_links(package=>[split /,/,$status{package}],options=>\%options,class=>"submitter")}]
   <a href="{html_escape(bug_links(bug=>$status{bug_num},links_only=>1))}">{html_escape($status{subject})}</a>
   <div id="extra_status_{html_escape($status{bug_num})}" class="shortbugstatusextra">
   <span>Reported by: {package_links(submitter=>$status{originator})};</span>
index beeaf3d20fa97b93b4078c5eb530ccd98533fb42..2af273dcf7859bcc0bca947bc0c6bee655128cda 100644 (file)
@@ -1,13 +1,13 @@
 Received: (at fakecontrol) by fakecontrolmessage;
 To: {$request_addr}
-From: $requester
+From: {$requester}
 Subject: Internal Control
 Message-Id: {$action}
 Date: {$date}
 User-Agent: Fakemail v42.6.9
 
 # A New Hope
-# A log time ago, in a galaxy far, far away
+# A long time ago, in a galaxy far, far away
 # something happened.
 #
 # Magically this resulted in the following
index eca577523e8746afd34900bbb0aae9108adf7457..a7394de7429f924850e0a2292790b1c6d102e064 100644 (file)
@@ -3,7 +3,7 @@ This is an automatically generated reply to let you know your message
 has been received.
 { $forwardexplain }{ $resentccexplain }
 If you wish to submit further information on this problem, please
-send it to { $refreplyto }, as before.
+send it to { $refreplyto }.
 
 Please do not send mail to {$config{maintainer_email}} unless you wish
 to report a problem with the {ucfirst($config{bug})}-tracking system.
index 71dd2772970d586e15db4ec3104d779c9ed037cc..92c8b0a3c88baf78989128f6084ffac5d59a6321 100644 (file)
@@ -1,11 +1,12 @@
-Your message didn't have a Package: line at the start (in the
-pseudo-header following the real mail header), or didn't have a
-pseudo-header at all.  Your message has been filed under junk but
-otherwise ignored.
+Your message didn't have a Package: line at the very first line of the
+mail body (part of the pseudo-header), or didn't have a Package: line
+at all. Unfortunatly, this means that your message has been ignored
+completely.
 
-This makes it much harder for us to categorise and deal with your
-problem report. Please _resubmit_ your report to {$baddress}@{$config{email_domain}}
-and tell us which package the report is on. For help, check out
+Without this information we are unable to categorise or otherwise deal
+with your problem report. Please _resubmit_ your report to
+{$baddress}@{$config{email_domain}} and tell us which package the
+report is for. For help, check out
 http://{$config{web_domain}}/Reporting{$config{html_suffix}}.
 
 Your message was dated {$date} and had
@@ -13,4 +14,9 @@ message-id {$messageid}
 and subject {$subject}.
 The complete text of it is attached to this message.
 
-If you need any assistance or explanation please contact {$config{maintainer_email}}.
+If you need any assistance or explanation please contact
+{$config{maintainer_email}} and include the the attached
+message.
+
+If you didn't send the attached message (spam was sent forging your
+from address), we apologize; please disregard this message.
diff --git a/templates/en_US/mail/submitter_changed.tmpl b/templates/en_US/mail/submitter_changed.tmpl
new file mode 100644 (file)
index 0000000..1f0a9c0
--- /dev/null
@@ -0,0 +1,12 @@
+The submitter address recorded for your {$config{bug}} report
+#{$data->{bug_num}}: {$data->{subject}}
+has been changed.
+
+The old submitter address for this report was
+{$old_data->{submitter}}.
+The new submitter address is
+{$data->{submitter}}.
+
+This change was made by
+{$replyto}.
+If it was incorrect, please contact them directly.