1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # [Other people have contributed to this file; their copyrights should
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
10 package Debbugs::Control;
14 Debbugs::Control -- Routines for modifying the state of bugs
23 This module is an abstraction of a lot of functions which originally
24 were only present in service.in, but as time has gone on needed to be
25 called from elsewhere.
27 All of the public functions take the following options:
31 =item debug -- scalar reference to which debbuging information is
34 =item transcript -- scalar reference to which transcript information
37 =item affected_bugs -- hashref which is updated with bugs affected by
43 Functions which should (probably) append to the .log file take the
48 =item requester -- Email address of the individual who requested the change
50 =item request_addr -- Address to which the request was sent
52 =item location -- Optional location; currently ignored but may be
53 supported in the future for updating archived bugs upon archival
55 =item message -- The original message which caused the action to be taken
57 =item append_log -- Whether or not to append information to the log.
61 B<append_log> (for most functions) is a special option. When set to
62 false, no appending to the log is done at all. When it is not present,
63 the above information is faked, and appended to the log file. When it
64 is true, the above options must be present, and their values are used.
67 =head1 GENERAL FUNCTIONS
73 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
74 use base qw(Exporter);
78 $DEBUG = 0 unless defined $DEBUG;
81 %EXPORT_TAGS = (affects => [qw(affects)],
82 summary => [qw(summary)],
84 archive => [qw(bug_archive bug_unarchive),
86 log => [qw(append_action_to_log),
90 Exporter::export_ok_tags(keys %EXPORT_TAGS);
91 $EXPORT_TAGS{all} = [@EXPORT_OK];
94 use Debbugs::Config qw(:config);
95 use Debbugs::Common qw(:lock buglog :misc get_hashname);
96 use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages);
97 use Debbugs::CGI qw(html_escape);
98 use Debbugs::Log qw(:misc);
99 use Debbugs::Recipients qw(:add);
101 use Params::Validate qw(validate_with :types);
102 use File::Path qw(mkpath);
105 use Debbugs::Text qw(:templates);
107 use Debbugs::Mail qw(rfc822_date);
109 use POSIX qw(strftime);
113 # These are a set of options which are common to all of these functions
115 my %common_options = (debug => {type => SCALARREF|HANDLE,
118 transcript => {type => SCALARREF|HANDLE,
121 affected_bugs => {type => HASHREF,
124 affected_packages => {type => HASHREF,
127 recipients => {type => HASHREF,
130 limit => {type => HASHREF,
136 my %append_action_options =
137 (action => {type => SCALAR,
140 requester => {type => SCALAR,
143 request_addr => {type => SCALAR,
146 location => {type => SCALAR,
149 message => {type => SCALAR|ARRAYREF,
152 append_log => {type => BOOLEAN,
154 depends => [qw(requester request_addr),
161 # this is just a generic stub for Debbugs::Control functions.
167 # transcript => $transcript,
168 # ($dl > 0 ? (debug => $transcript):()),
169 # requester => $header{from},
170 # request_addr => $controlrequestaddr,
172 # affected_packages => \%affected_packages,
173 # recipients => \%recipients,
179 # print {$transcript} "Failed to foo $ref bar: $@";
187 # my %param = validate_with(params => \@_,
188 # spec => {bug => {type => SCALAR,
189 # regex => qr/^\d+$/,
191 # # specific options here
193 # %append_action_options,
198 # local $SIG{__DIE__} = sub {
200 # for (1..$locks) { unfilelock(); }
204 # my ($debug,$transcript) = __handle_debug_transcript(%param);
206 # ($locks, @data) = lock_read_all_merged_bugs($param{bug});
207 # __handle_affected_packages(data => \@data,%param);
208 # print {$transcript} __bug_info(@data);
209 # add_recipients(data => \@data,
210 # recipients => $param{recipients}
212 # transcript => $transcript,
214 # for my $data (@data) {
215 # append_action_to_log(bug => $data->{bug_num},
217 # __return_append_to_log_options(
222 # if not exists $param{append_log} or $param{append_log};
223 # writebug($data->{bug_num},$data);
224 # print {$transcript} "$action\n";
225 # add_recipients(data => $data,
226 # recipients => $param{recipients},
228 # transcript => $transcript,
232 # for (1..$locks) { unfilelock(); }
241 transcript => $transcript,
242 ($dl > 0 ? (debug => $transcript):()),
243 requester => $header{from},
244 request_addr => $controlrequestaddr,
246 affected_packages => \%affected_packages,
247 recipients => \%recipients,
255 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
258 This marks a bug as affecting packages which the bug is not actually
259 in. This should only be used in cases where fixing the bug instantly
260 resolves the problem in the other packages.
262 By default, the packages are set to the list of packages passed.
263 However, if you pass add => 1 or remove => 1, the list of packages
264 passed are added or removed from the affects list, respectively.
269 my %param = validate_with(params => \@_,
270 spec => {bug => {type => SCALAR,
273 # specific options here
274 packages => {type => SCALAR|ARRAYREF,
277 add => {type => BOOLEAN,
280 remove => {type => BOOLEAN,
284 %append_action_options,
287 if ($param{add} and $param{remove}) {
288 croak "Asking to both add and remove affects is nonsensical";
292 local $SIG{__DIE__} = sub {
294 for (1..$locks) { unfilelock(); }
298 my ($debug,$transcript) = __handle_debug_transcript(%param);
300 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
301 __handle_affected_packages(data => \@data,%param);
302 print {$transcript} __bug_info(@data);
303 add_recipients(data => \@data,
304 recipients => $param{recipients},
306 transcript => $transcript,
308 my $action = 'Did not alter affected packages';
309 for my $data (@data) {
310 print {$debug} "Going to change affects\n";
311 my @packages = splitpackages($data->{affects});
313 @packages{@packages} = (1) x @packages;
316 for my $package (make_list($param{packages})) {
317 if (not $packages{$package}) {
318 $packages{$package} = 1;
319 push @added,$package;
323 $action = "Added indication that $data->{bug_num} affects ".
324 english_join(', ',' and ',@added);
327 elsif ($param{remove}) {
329 for my $package (make_list($param{packages})) {
330 if ($packages{$package}) {
331 delete $packages{$package};
332 push @removed,$package;
335 $action = "Removed indication that $data->{bug_num} affects " .
336 english_join(', ',' and ',@removed);
340 for my $package (make_list($param{packages})) {
341 $packages{$package} = 1;
343 $action = "Noted that $data->{bug_num} affects ".
344 english_join(', ',' and ', keys %packages);
346 $data->{affects} = join(',',keys %packages);
347 append_action_to_log(bug => $data->{bug_num},
349 __return_append_to_log_options(
354 if not exists $param{append_log} or $param{append_log};
355 writebug($data->{bug_num},$data);
356 print {$transcript} "$action\n";
357 add_recipients(data => $data,
358 recipients => $param{recipients},
360 transcript => $transcript,
364 for (1..$locks) { unfilelock(); }
370 =head1 SUMMARY FUNCTIONS
376 transcript => $transcript,
377 ($dl > 0 ? (debug => $transcript):()),
378 requester => $header{from},
379 request_addr => $controlrequestaddr,
381 affected_packages => \%affected_packages,
382 recipients => \%recipients,
388 print {$transcript} "Failed to mark $ref with summary foo: $@";
391 Handles all setting of summary fields
393 If summary is undef, unsets the summary
395 If summary is 0, sets the summary to the first paragraph contained in
398 If summary is numeric, sets the summary to the message specified.
405 my %param = validate_with(params => \@_,
406 spec => {bug => {type => SCALAR,
409 # specific options here
410 summary => {type => SCALAR|UNDEF,
414 %append_action_options,
417 croak "summary must be numeric or undef" if
418 defined $param{summary} and not $param{summary} =~ /^\d+$/;
421 local $SIG{__DIE__} = sub {
423 for (1..$locks) { unfilelock(); }
427 my ($debug,$transcript) = __handle_debug_transcript(%param);
429 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
430 __handle_affected_packages(data => \@data,%param);
431 print {$transcript} __bug_info(@data);
432 add_recipients(data => \@data,
433 recipients => $param{recipients},
435 transcript => $transcript,
437 # figure out the log that we're going to use
439 my $summary_msg = '';
441 if (not defined $param{summary}) {
443 print {$debug} "Removing summary fields";
444 $action = 'Removed summary';
448 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
449 if ($param{summary} == 0) {
451 $summary_msg = @records + 1;
454 if (($param{summary} - 1 ) > $#records) {
455 die "Message number '$param{summary}' exceeds the maximum message '$#records'";
457 my $record = $records[($param{summary} - 1 )];
458 if ($record->{type} !~ /incoming-recv|recips/) {
459 die "Message number '$param{summary}' is a invalid message type '$record->{type}'";
461 $summary_msg = $param{summary};
462 $log = [$record->{text}];
464 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
465 my $body = $p_o->{body};
466 my $in_pseudoheaders = 0;
468 # walk through body until we get non-blank lines
469 for my $line (@{$body}) {
470 if ($line =~ /^\s*$/) {
471 if (length $paragraph) {
472 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
478 $in_pseudoheaders = 0;
481 # skip a paragraph if it looks like it's control or
483 if ($line =~ m{^\s*(?:(?:Package|Source|Version)\:| #pseudo headers
484 (?:package|(?:no|)owner|severity|tag|summary| #control
485 reopen|close|(?:not|)(?:fixed|found)|clone|
486 (?:force|)merge|user(?:category|tag|)
489 if (not length $paragraph) {
490 print {$debug} "Found control/pseudo-headers and skiping them\n";
491 $in_pseudoheaders = 1;
495 next if $in_pseudoheaders;
498 print {$debug} "Summary is going to be '$paragraph'\n";
499 $summary = $paragraph;
500 $summary =~ s/[\n\r]//g;
501 if (not length $summary) {
502 die "Unable to find summary message to use";
505 for my $data (@data) {
506 print {$debug} "Going to change summary";
507 if (length $summary) {
508 if (length $data->{summary}) {
509 $action = "Summary replaced with message bug $param{bug} message $summary_msg";
512 $action = "Summary recorded from message bug $param{bug} message $summary_msg";
515 $data->{summary} = $summary;
516 append_action_to_log(bug => $data->{bug_num},
518 __return_append_to_log_options(
523 if not exists $param{append_log} or $param{append_log};
524 writebug($data->{bug_num},$data);
525 print {$transcript} "$action\n";
526 add_recipients(data => $data,
527 recipients => $param{recipients},
529 transcript => $transcript,
533 for (1..$locks) { unfilelock(); }
541 =head1 OWNER FUNCTIONS
547 transcript => $transcript,
548 ($dl > 0 ? (debug => $transcript):()),
549 requester => $header{from},
550 request_addr => $controlrequestaddr,
552 recipients => \%recipients,
558 print {$transcript} "Failed to mark $ref as having an owner: $@";
561 Handles all setting of the owner field; given an owner of undef or of
562 no length, indicates that a bug is not owned by anyone.
567 my %param = validate_with(params => \@_,
568 spec => {bug => {type => SCALAR,
571 owner => {type => SCALAR|UNDEF,
574 %append_action_options,
579 local $SIG{__DIE__} = sub {
581 for (1..$locks) { unfilelock(); }
585 my ($debug,$transcript) = __handle_debug_transcript(%param);
587 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
588 __handle_affected_packages(data => \@data,%param);
589 print {$transcript} __bug_info(@data);
590 @data and defined $data[0] or die "No bug found for $param{bug}";
591 add_recipients(data => \@data,
592 recipients => $param{recipients},
594 transcript => $transcript,
597 for my $data (@data) {
598 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
599 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
600 if (not defined $param{owner} or not length $param{owner}) {
602 $action = "Removed annotation that $config{bug} was owned by " .
606 if (length $data->{owner}) {
607 $action = "Owner changed from $data->{owner} to $param{owner}.";
610 $action = "Owner recorded as $param{owner}."
613 $data->{owner} = $param{owner};
614 append_action_to_log(bug => $data->{bug_num},
616 __return_append_to_log_options(
621 if not exists $param{append_log} or $param{append_log};
622 writebug($data->{bug_num},$data);
623 print {$transcript} "$action\n";
624 add_recipients(data => $data,
625 recipients => $param{recipients},
627 transcript => $transcript,
631 for (1..$locks) { unfilelock(); }
636 =head1 ARCHIVE FUNCTIONS
643 bug_archive(bug => $bug_num,
645 transcript => \$transcript,
650 transcript("Unable to archive $bug_num\n");
653 transcript($transcript);
656 This routine archives a bug
660 =item bug -- bug number
662 =item check_archiveable -- check wether a bug is archiveable before
663 archiving; defaults to 1
665 =item archive_unarchived -- whether to archive bugs which have not
666 previously been archived; defaults to 1. [Set to 0 when used from
669 =item ignore_time -- whether to ignore time constraints when archiving
670 a bug; defaults to 0.
677 my %param = validate_with(params => \@_,
678 spec => {bug => {type => SCALAR,
681 check_archiveable => {type => BOOLEAN,
684 archive_unarchived => {type => BOOLEAN,
687 ignore_time => {type => BOOLEAN,
691 %append_action_options,
696 local $SIG{__DIE__} = sub {
698 for (1..$locks) { unfilelock(); }
702 my $action = "$config{bug} archived.";
703 my ($debug,$transcript) = __handle_debug_transcript(%param);
704 if ($param{check_archiveable} and
705 not bug_archiveable(bug=>$param{bug},
706 ignore_time => $param{ignore_time},
708 print {$transcript} "Bug $param{bug} cannot be archived\n";
709 die "Bug $param{bug} cannot be archived";
711 print {$debug} "$param{bug} considering\n";
713 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
714 __handle_affected_packages(data => \@data,%param);
715 print {$transcript} __bug_info(@data);
716 print {$debug} "$param{bug} read $locks\n";
717 @data and defined $data[0] or die "No bug found for $param{bug}";
718 print {$debug} "$param{bug} read done\n";
720 if (not $param{archive_unarchived} and
721 not exists $data[0]{unarchived}
723 print {$transcript} "$param{bug} has not been archived previously\n";
724 die "$param{bug} has not been archived previously";
726 add_recipients(recipients => $param{recipients},
729 transcript => $transcript,
731 my @bugs = map {$_->{bug_num}} @data;
732 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
733 for my $bug (@bugs) {
734 if ($param{check_archiveable}) {
735 die "Bug $bug cannot be archived (but $param{bug} can?)"
736 unless bug_archiveable(bug=>$bug,
737 ignore_time => $param{ignore_time},
741 # If we get here, we can archive/remove this bug
742 print {$debug} "$param{bug} removing\n";
743 for my $bug (@bugs) {
744 #print "$param{bug} removing $bug\n" if $debug;
745 my $dir = get_hashname($bug);
746 # First indicate that this bug is being archived
747 append_action_to_log(bug => $bug,
749 __return_append_to_log_options(
754 if not exists $param{append_log} or $param{append_log};
755 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
756 if ($config{save_old_bugs}) {
757 mkpath("$config{spool_dir}/archive/$dir");
758 foreach my $file (@files_to_remove) {
759 link( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" ) or
760 copy( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" );
763 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
765 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
766 print {$transcript} "deleted $bug (from $param{bug})\n";
768 bughook_archive(@bugs);
769 if (exists $param{bugs_affected}) {
770 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
772 print {$debug} "$param{bug} unlocking $locks\n";
774 for (1..$locks) { unfilelock(); }
776 print {$debug} "$param{bug} unlocking done\n";
783 bug_unarchive(bug => $bug_num,
785 transcript => \$transcript,
790 transcript("Unable to archive bug: $bug_num");
792 transcript($transcript);
794 This routine unarchives a bug
799 my %param = validate_with(params => \@_,
800 spec => {bug => {type => SCALAR,
804 %append_action_options,
808 local $SIG{__DIE__} = sub {
810 for (1..$locks) { unfilelock(); }
814 my $action = "$config{bug} unarchived.";
815 my ($debug,$transcript) = __handle_debug_transcript(%param);
816 print {$debug} "$param{bug} considering\n";
818 ($locks, @data) = lock_read_all_merged_bugs($param{bug},'archive');
819 __handle_affected_packages(data => \@data,%param);
820 print {$transcript} __bug_info(@data);
821 print {$debug} "$param{bug} read $locks\n";
822 if (not @data or not defined $data[0]) {
823 print {$transcript} "No bug found for $param{bug}\n";
824 die "No bug found for $param{bug}";
826 print {$debug} "$param{bug} read done\n";
827 my @bugs = map {$_->{bug_num}} @data;
828 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
829 print {$debug} "$param{bug} unarchiving\n";
831 for my $bug (@bugs) {
832 print {$debug} "$param{bug} removing $bug\n";
833 my $dir = get_hashname($bug);
834 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
835 mkpath("archive/$dir");
836 foreach my $file (@files_to_copy) {
838 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
839 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
840 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
842 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
843 print {$transcript} "Unarchived $config{bug} $bug\n";
845 unlink(@files_to_remove) or die "Unable to unlink bugs";
846 # Indicate that this bug has been archived previously
847 for my $bug (@bugs) {
848 my $newdata = readbug($bug);
849 if (not defined $newdata) {
850 print {$transcript} "$config{bug} $bug disappeared!\n";
851 die "Bug $bug disappeared!";
853 $newdata->{unarchived} = time;
854 append_action_to_log(bug => $bug,
856 __return_append_to_log_options(
861 if not exists $param{append_log} or $param{append_log};
862 writebug($bug,$newdata);
863 add_recipients(recipients => $param{recipients},
866 transcript => $transcript,
869 print {$debug} "$param{bug} unlocking $locks\n";
871 for (1..$locks) { unfilelock(); };
873 if (exists $param{bugs_affected}) {
874 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
876 print {$debug} "$param{bug} unlocking done\n";
879 =head2 append_action_to_log
883 This should probably be moved to Debbugs::Log; have to think that out
888 sub append_action_to_log{
889 my %param = validate_with(params => \@_,
890 spec => {bug => {type => SCALAR,
893 action => {type => SCALAR,
895 requester => {type => SCALAR,
897 request_addr => {type => SCALAR,
899 location => {type => SCALAR,
902 message => {type => SCALAR|ARRAYREF,
904 get_lock => {type => BOOLEAN,
909 # Fix this to use $param{location}
910 my $log_location = buglog($param{bug});
911 die "Unable to find .log for $param{bug}"
912 if not defined $log_location;
913 if ($param{get_lock}) {
914 filelock("lock/$param{bug}");
916 my $log = IO::File->new(">>$log_location") or
917 die "Unable to open $log_location for appending: $!";
919 "<!-- time:".time." -->\n".
920 "<strong>".html_escape($param{action})."</strong>\n".
921 "Request was from <code>".html_escape($param{requester})."</code>\n".
922 "to <code>".html_escape($param{request_addr})."</code>. \n".
924 "\7\n",escape_log(make_list($param{message})),"\n\3\n"
925 or die "Unable to append to $log_location: $!";
926 close $log or die "Unable to close $log_location: $!";
927 if ($param{get_lock}) {
935 =head1 PRIVATE FUNCTIONS
937 =head2 __handle_affected_packages
939 __handle_affected_packages(affected_packages => {},
947 sub __handle_affected_packages{
948 my %param = validate_with(params => \@_,
949 spec => {%common_options,
950 data => {type => ARRAYREF|HASHREF
955 for my $data (make_list($param{data})) {
956 $param{affected_packages}{$data->{package}} = 1;
960 =head2 __handle_debug_transcript
962 my ($debug,$transcript) = __handle_debug_transcript(%param);
964 Returns a debug and transcript filehandle
969 sub __handle_debug_transcript{
970 my %param = validate_with(params => \@_,
971 spec => {%common_options},
974 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
975 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
976 return ($debug,$transcript);
983 Produces a small bit of bug information to kick out to the transcript
990 $return .= "Bug ".($data->{bug_num}||'').
991 " [".($data->{package}||''). "] ".
992 ($data->{subject}||'')."\n";
998 sub __return_append_to_log_options{
1000 my $action = $param{action} if exists $param{action};
1001 if (not exists $param{requester}) {
1002 $param{requester} = $config{control_internal_requester};
1004 if (not exists $param{request_addr}) {
1005 $param{request_addr} = $config{control_internal_request_addr};
1007 if (not exists $param{message}) {
1008 my $date = rfc822_date();
1009 $param{message} = fill_in_template(template => 'mail/fake_control_message',
1010 variables => {request_addr => $param{request_addr},
1011 requester => $param{requester},
1017 if (not defined $action) {
1018 carp "Undefined action!";
1019 $action = "unknown action";
1021 return (action => $action,
1022 (map {exists $append_action_options{$_}?($_,$param{$_}):()}