]> git.donarmstrong.com Git - debbugs.git/blobdiff - Debbugs/Control.pm
Include the protocol (http://) in gWebDomain and gCGIDomain
[debbugs.git] / Debbugs / Control.pm
index 4d44237aa8a0b93e0b8acc9eab7be892239ac700..aaa8925a0adfedf8f33ab86be49d9e5a35f28d08 100644 (file)
@@ -75,7 +75,7 @@ is true, the above options must be present, and their values are used.
 use warnings;
 use strict;
 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
-use base qw(Exporter);
+use Exporter qw(import);
 
 BEGIN{
      $VERSION = 1.00;
@@ -87,6 +87,7 @@ BEGIN{
                     severity => [qw(set_severity)],
                     affects => [qw(affects)],
                     summary => [qw(summary)],
+                    outlook => [qw(outlook)],
                     owner   => [qw(owner)],
                     title   => [qw(set_title)],
                     forward => [qw(set_forwarded)],
@@ -99,6 +100,7 @@ BEGIN{
                     clone   => [qw(clone_bug)],
                     archive => [qw(bug_archive bug_unarchive),
                                ],
+                    limit   => [qw(check_limit)],
                     log     => [qw(append_action_to_log),
                                ],
                    );
@@ -109,6 +111,7 @@ BEGIN{
 
 use Debbugs::Config qw(:config);
 use Debbugs::Common qw(:lock buglog :misc get_hashname sort_versions);
+use Debbugs::UTF8;
 use Debbugs::Status qw(bug_archiveable :read :hook writebug new_bug splitpackages split_status_fields get_bug_status);
 use Debbugs::CGI qw(html_escape);
 use Debbugs::Log qw(:misc :write);
@@ -123,7 +126,7 @@ use IO::File;
 
 use Debbugs::Text qw(:templates);
 
-use Debbugs::Mail qw(rfc822_date send_mail_message default_headers);
+use Debbugs::Mail qw(rfc822_date send_mail_message default_headers encode_headers);
 use Debbugs::MIME qw(create_mime_message);
 
 use Mail::RFC822::Address qw();
@@ -132,6 +135,7 @@ use POSIX qw(strftime);
 
 use Storable qw(dclone nfreeze);
 use List::Util qw(first max);
+use Encode qw(encode_utf8);
 
 use Carp;
 
@@ -440,7 +444,6 @@ sub set_blocks {
            }
        }
     }
-    my @new_blockers = keys %blockers;
     for my $data (@data) {
        my $old_data = dclone($data);
        # remove blockers and/or add new ones as appropriate
@@ -459,8 +462,7 @@ sub set_blocks {
        push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
        $action = ucfirst(join ('; ',@changed)) if @changed;
        if (not @changed) {
-           print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n"
-               unless __internal_request();
+           print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
            next;
        }
        $data->{blockedby} = join(' ',keys %blockers);
@@ -484,9 +486,7 @@ sub set_blocks {
     $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
     my $new_locks = 0;
     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 ($temp_locks, @blocking_data) =
@@ -625,10 +625,8 @@ sub set_tag {
        __begin_control(%param,
                        command  => 'tag'
                       );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
+    my $transcript = $info{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}) {
@@ -646,11 +644,9 @@ sub set_tag {
        my $action = 'Did not alter tags';
        my %tag_added = ();
        my %tag_removed = ();
-       my %fixed_removed = ();
        my @old_tags = split /\,?\s+/, $data->{keywords};
        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;
@@ -696,8 +692,7 @@ sub set_tag {
        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();
+           print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
            next;
        }
        $action .= '.';
@@ -771,10 +766,8 @@ sub set_severity {
        __begin_control(%param,
                        command  => 'severity'
                       );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
+    my $transcript = $info{transcript};
     my @data = @{$info{data}};
-    my @bugs = @{$info{bugs}};
 
     my $action = '';
     for my $data (@data) {
@@ -876,10 +869,8 @@ sub set_done {
        __begin_control(%param,
                        command  => $param{reopen}?'reopen':'done',
                       );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
+    my $transcript = $info{transcript};
     my @data = @{$info{data}};
-    my @bugs = @{$info{bugs}};
     my $action ='';
 
     if ($param{reopen}) {
@@ -939,7 +930,6 @@ sub set_done {
     }
     else {
        my %submitter_notified;
-       my $requester_notified = 0;
        my $orig_report_set = 0;
        for my $data (@data) {
            if (exists $data->{done} and
@@ -953,8 +943,8 @@ sub set_done {
        for my $data (@data) {
            my $old_data = dclone($data);
            my $hash = get_hashname($data->{bug_num});
-           my $report_fh = IO::File->new("db-h/$hash/$data->{bug_num}.report",'r') or
-               die "Unable to open original report db-h/$hash/$data->{bug_num}.report for reading: $!";
+           my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
+               die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
            my $orig_report;
            {
                local $/;
@@ -998,7 +988,7 @@ sub set_done {
                                                             headers =>
                                                             [To => $data->{submitter},
                                                              Subject => "$config{ubug}#$data->{bug_num} ".
-                                                             "closed by $param{requester} ($param{request_subject})",
+                                                             "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
                                                             ],
                                                            )
                                            ],
@@ -1098,7 +1088,6 @@ sub set_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]) {
@@ -1109,13 +1098,12 @@ sub set_submitter {
              (not defined $data->{originator} or not length $data->{originator})) or
             (defined $param{submitter} and defined $data->{originator} and
              $param{submitter} eq $data->{originator})) {
-           print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n"
-               unless __internal_request();
+           print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
            next;
        }
        else {
            if (defined $data->{originator} and length($data->{originator})) {
-               $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'";
+               $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'.";
                $notify_old_submitter = 1;
            }
            else {
@@ -1214,7 +1202,6 @@ sub set_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);
@@ -1222,8 +1209,7 @@ sub set_forwarded {
        if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
            (not defined $param{forwarded} and
             defined $data->{forwarded} and not length $data->{forwarded})) {
-           print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n"
-               unless __internal_request();
+           print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
            next;
        }
        else {
@@ -1231,7 +1217,7 @@ sub set_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}'";
+               $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'.";
            }
            else {
                $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
@@ -1304,20 +1290,18 @@ sub set_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();
+           print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
            next;
        }
        else {
            if (defined $data->{subject} and length($data->{subject})) {
-               $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'";
+               $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'.";
            } else {
                $action= "Set $config{bug} title to '$param{title}'.";
            }
@@ -1400,7 +1384,6 @@ sub set_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(',',
@@ -1416,8 +1399,7 @@ sub set_package {
        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();
+           print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
            next;
        }
        else {
@@ -1525,7 +1507,6 @@ sub set_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;
@@ -1581,6 +1562,16 @@ sub set_found {
                if (not @svers) {
                    @svers = $version;
                }
+               elsif (not grep {$version eq $_} @svers) {
+                    # The $version was not equal to one of the source
+                    # versions, so it's probably unqualified (or just
+                    # wrong). Delete it, and use the source versions
+                    # instead.
+                   if (exists $found_versions{$version}) {
+                       delete $found_versions{$version};
+                       $found_removed{$version} = 1;
+                   }
+               }
                for my $sver (@svers) {
                    if (not exists $found_versions{$sver}) {
                        $found_versions{$sver} = 1;
@@ -1588,7 +1579,7 @@ sub set_found {
                    }
                    # if the found we are adding matches any fixed
                    # versions, remove them
-                   my @temp = grep m{(^|/)\Q$sver\E}, keys %fixed_versions;
+                   my @temp = grep m{(^|/)\Q$sver\E$}, keys %fixed_versions;
                    delete $fixed_versions{$_} for @temp;
                    $fixed_removed{$_} = 1 for @temp;
                }
@@ -1612,7 +1603,7 @@ sub set_found {
                # 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;
+               my @temp = grep m{(?:^|/)\Q$version\E$}, keys %found_versions;
                delete $found_versions{$_} for @temp;
                $found_removed{$_} = 1 for @temp;
            }
@@ -1649,8 +1640,7 @@ sub set_found {
            $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();
+           print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
            next;
        }
        $action .= '.';
@@ -1736,7 +1726,6 @@ sub set_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;
@@ -1793,6 +1782,12 @@ sub set_fixed {
                if (not @svers) {
                    @svers = $version;
                }
+               else {
+                   if (exists $fixed_versions{$version}) {
+                       $fixed_removed{$version} = 1;
+                       delete $fixed_versions{$version};
+                   }
+               }
                for my $sver (@svers) {
                    if (not exists $fixed_versions{$sver}) {
                        $fixed_versions{$sver} = 1;
@@ -1858,8 +1853,7 @@ sub set_fixed {
            $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();
+           print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
            next;
        }
        $action .= '.';
@@ -1958,7 +1952,6 @@ sub set_merged {
        return;
     }
     my @data = @{$info{data}};
-    my @bugs = @{$info{bugs}};
     my %data;
     my %merged_bugs;
     for my $data (@data) {
@@ -1969,7 +1962,6 @@ sub set_merged {
     # handle unmerging
     my $new_locks = 0;
     if (not exists $param{merge_with}) {
-       my $ok_to_unmerge = 1;
        delete $merged_bugs{$param{bug}};
        if (not keys %merged_bugs) {
            print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
@@ -2003,9 +1995,6 @@ sub set_merged {
        return;
     }
     # lock and load all of the bugs we need
-    my @bugs_to_load = keys %merging;
-    my $bug_to_load;
-    my %merge_added;
     my ($data,$n_locks) =
        __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
                                    data => \@data,
@@ -2015,7 +2004,7 @@ sub set_merged {
     $new_locks += $n_locks;
     %data = %{$data};
     @data = values %data;
-    if (not __check_limit(data => [@data],
+    if (not check_limit(data => [@data],
                          exists $param{limit}?(limit => $param{limit}):(),
                          transcript => $transcript,
                         )) {
@@ -2075,7 +2064,7 @@ sub set_merged {
            # figure out the problems
            print {$transcript} "Unable to merge bugs because:\n";
            for my $change (@{$disallowed_changes}) {
-               print {$transcript} "$change->{field} of #$change->{bug} is '$change->{orig_value}' not '$change->{value}'\n";
+               print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
            }
            if ($attempts > 0) {
                croak "Some bugs were altered while attempting to merge";
@@ -2084,66 +2073,68 @@ sub set_merged {
                croak "Did not alter merged bugs";
            }
        }
-       my ($change_bug) = keys %{$changes};
-       $bug_changed{$change_bug}++;
-       print {$transcript} __bug_info($data{$change_bug}) if
-           $param{show_bug_info} and not __internal_request(1);
-       $bug_info_shown{$change_bug} = 1;
-       __allow_relocking($param{locks},[keys %data]);
-       for my $change (@{$changes->{$change_bug}}) {
-           if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
-               my %target_blockedby;
-               @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
-               my %unhandled_targets = %target_blockedby;
-               my @blocks_to_remove;
-               for my $key (split / /,$change->{orig_value}) {
-                   delete $unhandled_targets{$key};
-                   next if exists $target_blockedby{$key};
-                   set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
-                              block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
-                              remove => 1,
-                              hash_slice(%param,
-                                         keys %common_options,
-                                         keys %append_action_options),
-                             );
+       my @bugs_to_change = keys %{$changes};
+       for my $change_bug (@bugs_to_change) {
+           next unless exists $changes->{$change_bug};
+           $bug_changed{$change_bug}++;
+           print {$transcript} __bug_info($data{$change_bug}) if
+               $param{show_bug_info} and not __internal_request(1);
+           $bug_info_shown{$change_bug} = 1;
+           __allow_relocking($param{locks},[keys %data]);
+           for my $change (@{$changes->{$change_bug}}) {
+               if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
+                   my %target_blockedby;
+                   @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
+                   my %unhandled_targets = %target_blockedby;
+                   for my $key (split / /,$change->{orig_value}) {
+                       delete $unhandled_targets{$key};
+                       next if exists $target_blockedby{$key};
+                       set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
+                                  block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
+                                  remove => 1,
+                                  hash_slice(%param,
+                                             keys %common_options,
+                                             keys %append_action_options),
+                                 );
+                   }
+                   for my $key (keys %unhandled_targets) {
+                       set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
+                                  block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
+                                  add   => 1,
+                                  hash_slice(%param,
+                                             keys %common_options,
+                                             keys %append_action_options),
+                                 );
+                   }
                }
-               for my $key (keys %unhandled_targets) {
-                   set_blocks(bug    => $change->{field} eq 'blocks' ? $key : $change->{bug},
-                              block  => $change->{field} eq 'blocks' ? $change->{bug} : $key,
-                              add   => 1,
-                              hash_slice(%param,
-                                         keys %common_options,
-                                         keys %append_action_options),
-                             );
+               else {
+                   $change->{function}->(bug => $change->{bug},
+                                         $change->{key}, $change->{func_value},
+                                         exists $change->{options}?@{$change->{options}}:(),
+                                         hash_slice(%param,
+                                                    keys %common_options,
+                                                    keys %append_action_options),
+                                        );
                }
            }
-           else {
-               $change->{function}->(bug => $change->{bug},
-                                     $change->{key}, $change->{func_value},
-                                     exists $change->{options}?@{$change->{options}}:(),
-                                     hash_slice(%param,
-                                                keys %common_options,
-                                                keys %append_action_options),
-                                    );
-           }
+           __disallow_relocking($param{locks});
+           my ($data,$n_locks) =
+               __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
+                                           data => \@data,
+                                           locks => $param{locks},
+                                           debug => $debug,
+                                           reload_all => 1,
+                                          );
+           $new_locks += $n_locks;
+           $locks += $n_locks;
+           %data = %{$data};
+           @data = values %data;
+           ($merge_status,$bugs_to_merge) =
+               __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
+           ($disallowed_changes,$changes) = 
+               __calculate_merge_changes(\@data,$merge_status,\%param);
+           $attempts = max(values %bug_changed);
        }
-       __disallow_relocking($param{locks});
-       my ($data,$n_locks) =
-           __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
-                                       data => \@data,
-                                       locks => $param{locks},
-                                       debug => $debug,
-                                       reload_all => 1,
-                                      );
-       $new_locks += $n_locks;
-       $locks += $n_locks;
-       %data = %{$data};
-       @data = values %data;
-       ($merge_status,$bugs_to_merge) =
-           __calculate_merge_status(\@data,\%data,$param{bug});
-       ($disallowed_changes,$changes) = 
-           __calculate_merge_changes(\@data,$merge_status,\%param);
-       $attempts = max(values %bug_changed);
     }
     if ($param{show_bug_info} and not __internal_request(1)) {
        for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
@@ -2152,12 +2143,16 @@ sub set_merged {
        }
     }
     if (keys %{$changes} or @{$disallowed_changes}) {
-       print {$transcript} "Unable to modify bugs so that they could be merged\n";
+       print {$transcript} "After four attempts, the following changes were unable to be made:\n";
        for (1..$new_locks) {
            unfilelock($param{locks});
            $locks--;
        }
        __end_control(%info);
+       for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
+           print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
+       }
+       die "Unable to modify bugs so they could be merged";
        return;
     }
 
@@ -2191,8 +2186,9 @@ sub set_merged {
 sub __allow_relocking{
     my ($locks,$bugs) = @_;
 
-    for my $bug (@{$bugs}) {
-       my @lockfiles = grep {m{/\Q$bug\E$}} keys %{$locks->{locks}};
+    my @locks = (@{$bugs},'merge');
+    for my $lock (@locks) {
+       my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
        next unless @lockfiles;
        $locks->{relockable}{$lockfiles[0]} = 0;
     }
@@ -2273,8 +2269,8 @@ sub __lock_and_load_merged_bugs{
 
 
 sub __calculate_merge_status{
-    my ($data_a,$data_h,$master_bug,$merge) = @_;
-    my %merge_status;
+    my ($data_a,$data_h,$master_bug,$merge_status) = @_;
+    my %merge_status = %{$merge_status // {}};
     my %merged_bugs;
     my $bugs_to_merge = 0;
     for my $data (@{$data_a}) {
@@ -2288,13 +2284,13 @@ sub __calculate_merge_status{
        # look like. However, if merge is set, tags, fixed and found
        # are merged.
        if ($data->{bug_num} == $master_bug) {
-           for (qw(package forwarded severity blocks blockedby done owner summary affects)) {
+           for (qw(package forwarded severity blocks blockedby done owner summary outlook affects)) {
                $merge_status{$_} = $data->{$_}
            }
        }
-       if (not $merge) {
-           next unless $data->{bug_num} == $master_bug;
-       }
+       if (defined $merge_status) {
+           next unless $data->{bug_num} == $master_bug;
+       }
        $merge_status{tag} = {} if not exists $merge_status{tag};
        for my $tag (split /\s+/, $data->{keywords}) {
            $merge_status{tag}{$tag} = 1;
@@ -2304,6 +2300,17 @@ sub __calculate_merge_status{
            @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
        }
     }
+    # if there is a non-source qualified version with a corresponding
+    # source qualified version, we only want to merge the source
+    # qualified version(s)
+    for (qw(fixed found)) {
+       my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
+       for my $unqualified_version (@unqualified_versions) {
+           if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
+               delete $merge_status{"${_}_versions"}{$unqualified_version};
+           }
+       }
+    }
     return (\%merge_status,$bugs_to_merge);
 }
 
@@ -2358,6 +2365,10 @@ sub __calculate_merge_changes{
                           key  => 'summary',
                           options => [],
                          },
+            outlook   => {func => \&outlook,
+                          key  => 'outlook',
+                          options => [],
+                         },
             affects   => {func => \&affects,
                           key  => 'package',
                           options => [],
@@ -2382,7 +2393,7 @@ sub __calculate_merge_changes{
                                allowed => 1,
                               },
            );
-       for my $field (qw(forwarded severity blocks blockedby done owner summary affects package fixed_versions found_versions keywords)) {
+       for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
            # if the ideal bug already has the field set properly, we
            # continue on.
            if ($field eq 'keywords'){
@@ -2393,6 +2404,19 @@ sub __calculate_merge_changes{
                next if join(' ', sort @{$data->{$field}}) eq
                    join(' ',sort keys %{$merge_status->{$field}});
            }
+           elsif ($field eq 'done') {
+               # for done, we only care if the bug is done or not
+               # done, not the value it's set to.
+               if (defined $merge_status->{$field} and length $merge_status->{$field} and
+                   defined $data->{$field}         and length $data->{$field}) {
+                   next;
+               }
+               elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
+                      (not defined $data->{$field}         or not length $data->{$field})
+                     ) {
+                   next;
+               }
+           }
            elsif ($merge_status->{$field} eq $data->{$field}) {
                next;
            }
@@ -2408,10 +2432,12 @@ sub __calculate_merge_changes{
                 function => $force_functions{$field}{func},
                 key      => $force_functions{$field}{key},
                 options  => $force_functions{$field}{options},
-                allowed  => exists $force_functions{$field}{allowed} ? 0 : $force_functions{$field}{allowed},
+                allowed  => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
                };
-           if ($param->{force}) {
-               if ($field ne 'package') {
+           $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
+           $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
+           if ($param->{force} or $change->{allowed}) {
+               if ($field ne 'package' or $change->{allowed}) {
                    push @{$changes{$data->{bug_num}}},$change;
                    next;
                }
@@ -2510,7 +2536,6 @@ sub affects {
     my ($debug,$transcript) =
        @info{qw(debug transcript)};
     my @data = @{$info{data}};
-    my @bugs = @{$info{bugs}};
     my $action = '';
     for my $data (@data) {
        $action = '';
@@ -2565,8 +2590,7 @@ sub affects {
              }
         }
        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();
+           print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
            next;
        }
         my $old_data = dclone($data);
@@ -2614,7 +2638,7 @@ Handles all setting of summary fields
 
 If summary is undef, unsets the summary
 
-If summary is 0, sets the summary to the first paragraph contained in
+If summary is 0 or -1, sets the summary to the first paragraph contained in
 the message passed.
 
 If summary is a positive integer, sets the summary to the message specified.
@@ -2625,53 +2649,95 @@ Otherwise, sets summary to the value passed.
 
 
 sub summary {
-    my %param = validate_with(params => \@_,
+    # outlook and summary are exactly the same, basically
+    return _summary('summary',@_);
+}
+
+=head1 OUTLOOK FUNCTIONS
+
+=head2 outlook
+
+     eval {
+           outlook(bug          => $ref,
+                   transcript   => $transcript,
+                   ($dl > 0 ? (debug => $transcript):()),
+                   requester    => $header{from},
+                   request_addr => $controlrequestaddr,
+                   message      => \@log,
+                    affected_packages => \%affected_packages,
+                   recipients   => \%recipients,
+                   outlook      => undef,
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref with outlook foo: $@";
+       }
+
+Handles all setting of outlook fields
+
+If outlook is undef, unsets the outlook
+
+If outlook is 0, sets the outlook to the first paragraph contained in
+the message passed.
+
+If outlook is a positive integer, sets the outlook to the message specified.
+
+Otherwise, sets outlook to the value passed.
+
+=cut
+
+
+sub outlook {
+    return _summary('outlook',@_);
+}
+
+sub _summary {
+    my ($cmd,@params) = @_;
+    my %param = validate_with(params => \@params,
                              spec   => {bug => {type   => SCALAR,
                                                 regex  => qr/^\d+$/,
                                                },
                                         # specific options here
-                                        summary => {type => SCALAR|UNDEF,
-                                                    default => 0,
-                                                   },
+                                        $cmd , {type => SCALAR|UNDEF,
+                                                default => 0,
+                                               },
                                         %common_options,
                                         %append_action_options,
                                        },
                             );
-# croak "summary must be numeric or undef" if
-#      defined $param{summary} and not $param{summary} =~ /^\d+/;
     my %info =
        __begin_control(%param,
-                       command  => 'summary'
+                       command  => $cmd,
                       );
     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}) {
+    if (not defined $param{$cmd}) {
         # do nothing
-        print {$debug} "Removing summary fields\n";
-        $action = 'Removed summary';
+        print {$debug} "Removing $cmd fields\n";
+        $action = "Removed $cmd";
     }
-    elsif ($param{summary} =~ /^\d+$/) {
+    elsif ($param{$cmd} =~ /^-?\d+$/) {
         my $log = [];
         my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
-        if ($param{summary} == 0) {
+        if ($param{$cmd} == 0 or $param{$cmd} == -1) {
              $log = $param{message};
              $summary_msg = @records + 1;
         }
         else {
-             if (($param{summary} - 1 ) > $#records) {
-                  die "Message number '$param{summary}' exceeds the maximum message '$#records'";
+             if (($param{$cmd} - 1 ) > $#records) {
+                  die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
              }
-             my $record = $records[($param{summary} - 1 )];
+             my $record = $records[($param{$cmd} - 1 )];
              if ($record->{type} !~ /incoming-recv|recips/) {
-                  die "Message number '$param{summary}' is a invalid message type '$record->{type}'";
+                  die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
              }
-             $summary_msg = $param{summary};
+             $summary_msg = $param{$cmd};
              $log = [$record->{text}];
         }
         my $p_o = Debbugs::MIME::parse(join('',@{$log}));
@@ -2693,7 +2759,7 @@ sub summary {
              }
              # skip a paragraph if it looks like it's control or
              # pseudo-headers
-             if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity)\:\s+\S}xi or #pseudo headers
+             if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity|Control)\:\s+\S}xi or #pseudo headers
                  $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
                                 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
                                 debug|(?:not|)forwarded|priority|
@@ -2710,39 +2776,38 @@ sub summary {
              next if $in_pseudoheaders;
              $paragraph .= $line ." \n";
         }
-        print {$debug} "Summary is going to be '$paragraph'\n";
+        print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
         $summary = $paragraph;
         $summary =~ s/[\n\r]/ /g;
         if (not length $summary) {
-             die "Unable to find summary message to use";
+             die "Unable to find $cmd message to use";
         }
         # trim off a trailing spaces
         $summary =~ s/\ *$//;
     }
     else {
-       $summary = $param{summary};
+       $summary = $param{$cmd};
     }
     for my $data (@data) {
-        print {$debug} "Going to change summary\n";
+        print {$debug} "Going to change $cmd\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();
+             (not defined $data->{$cmd} or not length $data->{$cmd})) or
+            $summary eq $data->{$cmd}) {
+            print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
             next;
         }
         if (length $summary) {
-             if (length $data->{summary}) {
-                  $action = "Summary replaced with message bug $param{bug} message $summary_msg";
+             if (length $data->{$cmd}) {
+                  $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
              }
              else {
-                  $action = "Summary recorded from message bug $param{bug} message $summary_msg";
+                  $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
              }
         }
         my $old_data = dclone($data);
-        $data->{summary} = $summary;
+        $data->{$cmd} = $summary;
         append_action_to_log(bug => $data->{bug_num},
-                             command => 'summary',
+                             command => $cmd,
                              old_data => $old_data,
                              new_data => $data,
                              get_lock => 0,
@@ -2803,10 +2868,8 @@ sub clone_bug {
        __begin_control(%param,
                        command  => 'clone'
                       );
-    my ($debug,$transcript) =
-       @info{qw(debug transcript)};
+    my $transcript = $info{transcript};
     my @data = @{$info{data}};
-    my @bugs = @{$info{bugs}};
 
     my $action = '';
     for my $data (@data) {
@@ -2872,19 +2935,21 @@ sub clone_bug {
     # bugs that this bug is blocking are also blocked by the new clone(s)
     for my $bug (split ' ', $data->{blocks}) {
        for my $new_bug (@new_bugs) {
-           set_blocks(bug => $new_bug,
-                      blocks => $bug,
+           set_blocks(bug => $bug,
+                      block => $new_bug,
+                      add => 1,
                       hash_slice(%param,
                                  keys %common_options,
                                  keys %append_action_options),
                      );
        }
     }
-    # bugs that this bug is blocked by are also blocking the new clone(s)
+    # bugs that are blocking this bug are also blocking the new clone(s)
     for my $bug (split ' ', $data->{blockedby}) {
        for my $new_bug (@new_bugs) {
-           set_blocks(bug => $bug,
-                      blocks => $new_bug,
+           set_blocks(bug => $new_bug,
+                      block => $bug,
+                      add => 1,
                       hash_slice(%param,
                                  keys %common_options,
                                  keys %append_action_options),
@@ -2938,15 +3003,13 @@ sub 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}) {
              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();
+                 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
                  next;
              }
              $param{owner} = '';
@@ -3057,7 +3120,6 @@ sub bug_archive {
          print {$transcript} "Bug $param{bug} cannot be archived\n";
          die "Bug $param{bug} cannot be archived";
      }
-     print {$debug} "$param{bug} considering\n";
      if (not $param{archive_unarchived} and
         not exists $data[0]{unarchived}
        ) {
@@ -3153,7 +3215,6 @@ sub bug_unarchive {
                                command=>'unarchive');
      my ($debug,$transcript) =
         @info{qw(debug transcript)};
-     my @data = @{$info{data}};
      my @bugs = @{$info{bugs}};
      my $action = "$config{bug} unarchived.";
      my @files_to_remove;
@@ -3332,7 +3393,7 @@ sub append_action_to_log{
             $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 .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
         $data_diff .= "-->\n";
         $data_diff .= "<!-- old_data:\n";
         my %od;
@@ -3344,30 +3405,30 @@ sub append_action_to_log{
             $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 .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
         $data_diff .= "-->\n";
      }
      my $msg = join('',
                    (exists $param{command} ?
-                    "<!-- command:".html_escape($param{command})." -->\n":""
+                    "<!-- command:".html_escape(encode_utf8_safely($param{command}))." -->\n":""
                    ),
                    (length $param{requester} ?
-                    "<!-- requester: ".html_escape($param{requester})." -->\n":""
+                    "<!-- requester: ".html_escape(encode_utf8_safely($param{requester}))." -->\n":""
                    ),
                    (length $param{request_addr} ?
-                    "<!-- request_addr: ".html_escape($param{request_addr})." -->\n":""
+                    "<!-- request_addr: ".html_escape(encode_utf8_safely($param{request_addr}))." -->\n":""
                    ),
                    "<!-- time:".time()." -->\n",
                    $data_diff,
-                   "<strong>".html_escape($param{action})."</strong>\n");
+                   "<strong>".html_escape(encode_utf8_safely($param{action}))."</strong>\n");
      if (length $param{requester}) {
-          $msg .= "Request was from <code>".html_escape($param{requester})."</code>\n";
+          $msg .= "Request was from <code>".html_escape(encode_utf8_safely($param{requester}))."</code>\n";
      }
      if (length $param{request_addr}) {
-          $msg .= "to <code>".html_escape($param{request_addr})."</code>";
+          $msg .= "to <code>".html_escape(encode_utf8_safely($param{request_addr}))."</code>";
      }
      if (length $param{desc}) {
-         $msg .= ":<br>\n$param{desc}\n";
+         $msg .= ":<br>\n".encode_utf8_safely($param{desc})."\n";
      }
      else {
          $msg .= ".\n";
@@ -3378,7 +3439,7 @@ sub append_action_to_log{
      $msg = '';
      if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
         push @records, {type => exists $param{recips}?'recips':'incoming-recv',
-                        exists $param{recips}?(recips => [make_list($param{recips})]):(),
+                        exists $param{recips}?(recips => [map {encode_utf8_safely($_)} make_list($param{recips})]):(),
                         text => join('',make_list($param{message})),
                        };
      }
@@ -3502,13 +3563,14 @@ sub __return_append_to_log_options{
      }
      if (not exists $param{message}) {
          my $date = rfc822_date();
-         $param{message} = fill_in_template(template  => 'mail/fake_control_message',
-                                            variables => {request_addr => $param{request_addr},
-                                                          requester    => $param{requester},
-                                                          date         => $date,
-                                                          action       => $action
-                                                         },
-                                           );
+         $param{message} =
+              encode_headers(fill_in_template(template  => 'mail/fake_control_message',
+                                              variables => {request_addr => $param{request_addr},
+                                                            requester    => $param{requester},
+                                                            date         => $date,
+                                                            action       => $action
+                                                           },
+                                             ));
      }
      if (not defined $action) {
          carp "Undefined action!";
@@ -3571,7 +3633,8 @@ sub __begin_control {
                             );
     my $new_locks;
     my ($debug,$transcript) = __handle_debug_transcript(@_);
-    print {$debug} "$param{bug} considering\n";
+    print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
+#    print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
     $lockhash = $param{locks} if exists $param{locks};
     my @data = ();
     my $old_die = $SIG{__DIE__};
@@ -3593,7 +3656,7 @@ sub __begin_control {
            }
        }
     }
-    if (not __check_limit(data => \@data,
+    if (not check_limit(data => \@data,
                          exists $param{limit}?(limit => $param{limit}):(),
                          transcript => $transcript,
                         )) {
@@ -3660,9 +3723,9 @@ sub __end_control {
 }
 
 
-=head2 __check_limit
+=head2 check_limit
 
-     __check_limit(data => \@data, limit => $param{limit});
+     check_limit(data => \@data, limit => $param{limit});
 
 
 Checks to make sure that bugs match any limits; each entry of @data
@@ -3679,9 +3742,9 @@ limit to succeed.
 =cut
 
 
-sub __check_limit{
+sub check_limit{
     my %param = validate_with(params => \@_,
-                             spec   => {data  => {type => ARRAYREF|SCALAR,
+                             spec   => {data  => {type => ARRAYREF|HASHREF,
                                                  },
                                         limit => {type => HASHREF|UNDEF,
                                                  },
@@ -3729,7 +3792,7 @@ LIMIT:        for my $limit (make_list($param{limit}{$field})) {
            }
            if (not $match) {
                $going_to_fail = 1;
-               print {$transcript} qq($field: ).join(', ',map{qq("$_")} make_list($data->{$field})).
+               print {$transcript} qq($field: ').join(', ',map{qq("$_")} make_list($data->{$field})).
                    "' does not match at least one of ".
                    join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
            }
@@ -3772,8 +3835,10 @@ sub __message_body_template{
      $extra_var ||={};
      my $hole_var = {'&bugurl' =>
                     sub{"$_[0]: ".
-                            'http://'.$config{cgi_domain}.'/'.
-                                Debbugs::CGI::bug_url($_[0]);
+                            $config{cgi_domain}.'/'.
+                                Debbugs::CGI::bug_links(bug => $_[0],
+                                                        links_only => 1,
+                                                       );
                     }
                    };