]> git.donarmstrong.com Git - debbugs.git/blobdiff - Debbugs/Control.pm
merge changes from don
[debbugs.git] / Debbugs / Control.pm
index 493125102e242d267ab06d1b094e9acbe3f2036e..ee743bb874f45bd3bd4ba8b113397f0c04d34f02 100644 (file)
@@ -64,7 +64,7 @@ the above information is faked, and appended to the log file. When it
 is true, the above options must be present, and their values are used.
 
 
-=head1 FUNCTIONS
+=head1 GENERAL FUNCTIONS
 
 =cut
 
@@ -78,21 +78,25 @@ BEGIN{
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
-     %EXPORT_TAGS = (archive => [qw(bug_archive bug_unarchive),
+     %EXPORT_TAGS = (affects => [qw(affects)],
+                    summary => [qw(summary)],
+                    owner   => [qw(owner)],
+                    archive => [qw(bug_archive bug_unarchive),
                                ],
                     log     => [qw(append_action_to_log),
                                ],
                    );
      @EXPORT_OK = ();
-     Exporter::export_ok_tags(qw(archive log));
+     Exporter::export_ok_tags(keys %EXPORT_TAGS);
      $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
 use Debbugs::Config qw(:config);
 use Debbugs::Common qw(:lock buglog :misc get_hashname);
-use Debbugs::Status qw(bug_archiveable :read :hook writebug);
+use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages);
 use Debbugs::CGI qw(html_escape);
 use Debbugs::Log qw(:misc);
+use Debbugs::Recipients qw(:add);
 
 use Params::Validate qw(validate_with :types);
 use File::Path qw(mkpath);
@@ -117,8 +121,14 @@ my %common_options = (debug       => {type => SCALARREF|HANDLE,
                      affected_bugs => {type => HASHREF,
                                        optional => 1,
                                       },
+                     affected_packages => {type => HASHREF,
+                                           optional => 1,
+                                          },
                      recipients    => {type => HASHREF,
-                                       optional => 1,
+                                       default => {},
+                                      },
+                     limit         => {type => HASHREF,
+                                       default => {},
                                       },
                     );
 
@@ -148,6 +158,480 @@ my %append_action_options =
      );
 
 
+# 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
+
+     eval {
+           affects(bug          => $ref,
+                   transcript   => $transcript,
+                   ($dl > 0 ? (debug => $transcript):()),
+                   requester    => $header{from},
+                   request_addr => $controlrequestaddr,
+                   message      => \@log,
+                    affected_packages => \%affected_packages,
+                   recipients   => \%recipients,
+                   packages     => undef,
+                    add          => 1,
+                    remove       => 0,
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as affecting $packages: $@";
+       }
+
+This marks a bug as affecting packages which the bug is not actually
+in. This should only be used in cases where fixing the bug instantly
+resolves the problem in the other packages.
+
+By default, the packages are set to the list of packages passed.
+However, if you pass add => 1 or remove => 1, the list of packages
+passed are added or removed from the affects list, respectively.
+
+=cut
+
+sub affects {
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type   => SCALAR,
+                                                regex  => qr/^\d+$/,
+                                               },
+                                        # specific options here
+                                        packages => {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 "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';
+    for my $data (@data) {
+        print {$debug} "Going to change affects\n";
+        my @packages = splitpackages($data->{affects});
+        my %packages;
+        @packages{@packages} = (1) x @packages;
+        if ($param{add}) {
+             my @added = ();
+             for my $package (make_list($param{packages})) {
+                  if (not $packages{$package}) {
+                       $packages{$package} = 1;
+                       push @added,$package;
+                  }
+             }
+             if (@added) {
+                  $action = "Added indication that $data->{bug_num} affects ".
+                       english_join(', ',' and ',@added);
+             }
+        }
+        elsif ($param{remove}) {
+             my @removed = ();
+             for my $package (make_list($param{packages})) {
+                  if ($packages{$package}) {
+                       delete $packages{$package};
+                       push @removed,$package;
+                  }
+             }
+             $action = "Removed indication that $data->{bug_num} affects " .
+                  english_join(', ',' and ',@removed);
+        }
+        else {
+             %packages = ();
+             for my $package (make_list($param{packages})) {
+                  $packages{$package} = 1;
+             }
+             $action = "Noted that $data->{bug_num} affects ".
+                  english_join(', ',' and ', keys %packages);
+        }
+        $data->{affects} = join(',',keys %packages);
+        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(); }
+     }
+
+}
+
+
+=head1 SUMMARY FUNCTIONS
+
+=head2 summary
+
+     eval {
+           summary(bug          => $ref,
+                   transcript   => $transcript,
+                   ($dl > 0 ? (debug => $transcript):()),
+                   requester    => $header{from},
+                   request_addr => $controlrequestaddr,
+                   message      => \@log,
+                    affected_packages => \%affected_packages,
+                   recipients   => \%recipients,
+                   summary      => undef,
+                   );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref with summary foo: $@";
+       }
+
+Handles all setting of summary fields
+
+If summary is undef, unsets the summary
+
+If summary is 0, sets the summary to the first paragraph contained in
+the message passed.
+
+If summary is numeric, sets the summary to the message specified.
+
+
+=cut
+
+
+sub summary {
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type   => SCALAR,
+                                                regex  => qr/^\d+$/,
+                                               },
+                                        # specific options here
+                                        summary => {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+$/;
+    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,
+                 );
+    # 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";
+        $action = 'Removed summary';
+    }
+    else {
+        my $log = [];
+        my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
+        if ($param{summary} == 0) {
+             $log = $param{log};
+             $summary_msg = @records + 1;
+        }
+        else {
+             if (($param{summary} - 1 ) > $#records) {
+                  die "Message number '$param{summary}' exceeds the maximum message '$#records'";
+             }
+             my $record = $records[($param{summary} - 1 )];
+             if ($record->{type} !~ /incoming-recv|recips/) {
+                  die "Message number '$param{summary}' is a invalid message type '$record->{type}'";
+             }
+             $summary_msg = $param{summary};
+             $log = [$record->{text}];
+        }
+        my $p_o = Debbugs::MIME::parse(join('',@{$log}));
+        my $body = $p_o->{body};
+        my $in_pseudoheaders = 0;
+        my $paragraph = '';
+        # walk through body until we get non-blank lines
+        for my $line (@{$body}) {
+             if ($line =~ /^\s*$/) {
+                  if (length $paragraph) {
+                       last;
+                  }
+                  $in_pseudoheaders = 0;
+                  next;
+             }
+             # skip a paragraph if it looks like it's control or
+             # pseudo-headers
+             if ($line =~ m{^\s*(?:(?:Package|Source|Version)\:| #pseudo headers
+                                (?:package|(?:no|)owner|severity|tag|summary| #control
+                                     reopen|close|(?:not|)(?:fixed|found)|clone|
+                                     (?:force|)merge|user(?:category|tag|)
+                                )
+                           )\s+\S}x) {
+                  if (not length $paragraph) {
+                       print {$debug} "Found control/pseudo-headers and skiping them\n";
+                       $in_pseudoheaders = 1;
+                       next;
+                  }
+             }
+             next if $in_pseudoheaders;
+             $paragraph .= $line;
+        }
+        print {$debug} "Summary 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";
+        }
+    }
+    for my $data (@data) {
+        print {$debug} "Going to change summary";
+        if (length $summary) {
+             if (length $data->{summary}) {
+                  $action = "Summary replaced with message bug $param{bug} message $summary_msg";
+             }
+             else {
+                  $action = "Summary recorded from message bug $param{bug} message $summary_msg";
+             }
+        }
+        $data->{summary} = $summary;
+        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(); }
+     }
+
+}
+
+
+
+
+=head1 OWNER FUNCTIONS
+
+=head2 owner
+
+     eval {
+           owner(bug          => $ref,
+                 transcript   => $transcript,
+                 ($dl > 0 ? (debug => $transcript):()),
+                 requester    => $header{from},
+                 request_addr => $controlrequestaddr,
+                 message      => \@log,
+                 recipients   => \%recipients,
+                 owner        => undef,
+                );
+       };
+       if ($@) {
+           $errors++;
+           print {$transcript} "Failed to mark $ref as having an owner: $@";
+       }
+
+Handles all setting of the owner field; given an owner of undef or of
+no length, indicates that a bug is not owned by anyone.
+
+=cut
+
+sub owner {
+     my %param = validate_with(params => \@_,
+                              spec   => {bug => {type   => SCALAR,
+                                                 regex  => qr/^\d+$/,
+                                                },
+                                         owner => {type => SCALAR|UNDEF,
+                                                  },
+                                         %common_options,
+                                         %append_action_options,
+                                        },
+                             );
+     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 $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}.";
+         }
+         else {
+              if (length $data->{owner}) {
+                   $action = "Owner changed from $data->{owner} to $param{owner}.";
+              }
+              else {
+                   $action = "Owner recorded as $param{owner}."
+              }
+         }
+         $data->{owner} = $param{owner};
+         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(); }
+     }
+}
+
+
+=head1 ARCHIVE FUNCTIONS
+
+
 =head2 bug_archive
 
      my $error = '';
@@ -204,6 +688,7 @@ sub bug_archive {
                                         },
                              );
      our $locks = 0;
+     $locks = 0;
      local $SIG{__DIE__} = sub {
          if ($locks) {
               for (1..$locks) { unfilelock(); }
@@ -220,49 +705,34 @@ sub bug_archive {
          die "Bug $param{bug} cannot be archived";
      }
      print {$debug} "$param{bug} considering\n";
-     my ($data);
-     ($locks, $data) = lockreadbugmerge($param{bug});
+     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";
-     defined $data or die "No bug found for $param{bug}";
-     print {$debug} "$param{bug} read ok (done $data->{done})\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->{unarchived}
+        not exists $data[0]{unarchived}
        ) {
          print {$transcript} "$param{bug} has not been archived previously\n";
          die "$param{bug} has not been archived previously";
      }
-
-     my @bugs = ($param{bug});
-     # my %bugs;
-     # @bugs{@bugs} = (1) x @bugs;
-     if (length($data->{mergedwith})) {
-         push(@bugs,split / /,$data->{mergedwith});
-     }
+     add_recipients(recipients => $param{recipients},
+                   data => \@data,
+                   debug      => $debug,
+                   transcript => $transcript,
+                  );
+     my @bugs = map {$_->{bug_num}} @data;
      print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
      for my $bug (@bugs) {
-         my $newdata;
-         print {$debug} "$param{bug} $bug check\n";
-         if ($bug != $param{bug}) {
-              print {$debug} "$param{bug} $bug reading\n";
-              $newdata = lockreadbug($bug) || die "huh $bug ?";
-              print {$debug} "$param{bug} $bug read ok\n";
-              $locks++;
-         } else {
-              $newdata = $data;
-         }
-         print {$debug} "$param{bug} $bug read/not\n";
-         my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
-         $newdata->{mergedwith} eq $expectmerge ||
-              die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
-         print {$debug} "$param{bug} $bug merge-ok\n";
-         if ($param{check_archiveable}) {
-              die "Bug $bug cannot be archived (but $param{bug} can?)"
-                   unless bug_archiveable(bug=>$bug,
-                                          ignore_time => $param{ignore_time},
-                                         );
-         }
+        if ($param{check_archiveable}) {
+            die "Bug $bug cannot be archived (but $param{bug} can?)"
+                unless bug_archiveable(bug=>$bug,
+                                       ignore_time => $param{ignore_time},
+                                      );
+        }
      }
      # If we get here, we can archive/remove this bug
      print {$debug} "$param{bug} removing\n";
@@ -330,45 +800,29 @@ sub bug_unarchive {
                                          %append_action_options,
                                         },
                              );
+     our $locks = 0;
+     local $SIG{__DIE__} = sub {
+         if ($locks) {
+              for (1..$locks) { unfilelock(); }
+              $locks = 0;
+         }
+     };
      my $action = "$config{bug} unarchived.";
      my ($debug,$transcript) = __handle_debug_transcript(%param);
      print {$debug} "$param{bug} considering\n";
-     my ($locks, $data) = lockreadbugmerge($param{bug},'archive');
+     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 defined $data) {
-         print {$transcript} "No bug found for $param{bug}\n";
-         die "No bug found for $param{bug}";
+     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 ok (done $data->{done})\n";
      print {$debug} "$param{bug} read done\n";
-     my @bugs = ($param{bug});
-     # my %bugs;
-     # @bugs{@bugs} = (1) x @bugs;
-     if (length($data->{mergedwith})) {
-         push(@bugs,split / /,$data->{mergedwith});
-     }
+     my @bugs = map {$_->{bug_num}} @data;
      print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
-     for my $bug (@bugs) {
-         my $newdata;
-         print {$debug} "$param{bug} $bug check\n";
-         if ($bug != $param{bug}) {
-              print {$debug} "$param{bug} $bug reading\n";
-              $newdata = lockreadbug($bug,'archive') or die "huh $bug ?";
-              print {$debug} "$param{bug} $bug read ok\n";
-              $locks++;
-         } else {
-              $newdata = $data;
-         }
-         print {$debug} "$param{bug} $bug read/not\n";
-         my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
-         if ($newdata->{mergedwith} ne $expectmerge ) {
-              print {$transcript} "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
-              die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
-         }
-         print {$debug} "$param{bug} $bug merge-ok\n";
-     }
-     # If we get here, we can archive/remove this bug
-     print {$debug} "$param{bug} removing\n";
+     print {$debug} "$param{bug} unarchiving\n";
      my @files_to_remove;
      for my $bug (@bugs) {
          print {$debug} "$param{bug} removing $bug\n";
@@ -402,6 +856,11 @@ 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) {
@@ -471,6 +930,29 @@ sub append_action_to_log{
 
 =head1 PRIVATE FUNCTIONS
 
+=head2 __handle_affected_packages
+
+     __handle_affected_packages(affected_packages => {},
+                                data => [@data],
+                               )
+
+
+
+=cut
+
+sub __handle_affected_packages{
+     my %param = validate_with(params => \@_,
+                              spec   => {%common_options,
+                                         data => {type => ARRAYREF|HASHREF
+                                                 },
+                                        },
+                              allow_extra => 1,
+                             );
+     for my $data (make_list($param{data})) {
+         $param{affected_packages}{$data->{package}} = 1;
+     }
+}
+
 =head2 __handle_debug_transcript
 
      my ($debug,$transcript) = __handle_debug_transcript(%param);
@@ -490,6 +972,25 @@ sub __handle_debug_transcript{
      return ($debug,$transcript);
 }
 
+=head2 __bug_info
+
+     __bug_info($data)
+
+Produces a small bit of bug information to kick out to the transcript
+
+=cut
+
+sub __bug_info{
+     my $return = '';
+     for my $data (@_) {
+         $return .= "Bug ".($data->{bug_num}||'').
+              " [".($data->{package}||''). "] ".
+                   ($data->{subject}||'')."\n";
+     }
+     return $return;
+}
+
+
 sub __return_append_to_log_options{
      my %param = @_;
      my $action = $param{action} if exists $param{action};