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,2008,2009 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 request_nn -- Name of queue file which caused this request
54 =item request_msgid -- Message id of message which caused this request
56 =item location -- Optional location; currently ignored but may be
57 supported in the future for updating archived bugs upon archival
59 =item message -- The original message which caused the action to be taken
61 =item append_log -- Whether or not to append information to the log.
65 B<append_log> (for most functions) is a special option. When set to
66 false, no appending to the log is done at all. When it is not present,
67 the above information is faked, and appended to the log file. When it
68 is true, the above options must be present, and their values are used.
71 =head1 GENERAL FUNCTIONS
77 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
78 use Exporter qw(import);
82 $DEBUG = 0 unless defined $DEBUG;
85 %EXPORT_TAGS = (done => [qw(set_done)],
86 submitter => [qw(set_submitter)],
87 severity => [qw(set_severity)],
88 affects => [qw(affects)],
89 summary => [qw(summary)],
90 outlook => [qw(outlook)],
92 title => [qw(set_title)],
93 forward => [qw(set_forwarded)],
94 found => [qw(set_found set_fixed)],
95 fixed => [qw(set_found set_fixed)],
96 package => [qw(set_package)],
97 block => [qw(set_blocks)],
98 merge => [qw(set_merged)],
100 clone => [qw(clone_bug)],
101 archive => [qw(bug_archive bug_unarchive),
103 limit => [qw(check_limit)],
104 log => [qw(append_action_to_log),
108 Exporter::export_ok_tags(keys %EXPORT_TAGS);
109 $EXPORT_TAGS{all} = [@EXPORT_OK];
112 use Debbugs::Config qw(:config);
113 use Debbugs::Common qw(:lock buglog :misc get_hashname sort_versions);
115 use Debbugs::Status qw(bug_archiveable :read :hook writebug new_bug splitpackages split_status_fields get_bug_status);
116 use Debbugs::CGI qw(html_escape);
117 use Debbugs::Log qw(:misc :write);
118 use Debbugs::Recipients qw(:add);
119 use Debbugs::Packages qw(:versions :mapping);
121 use Data::Dumper qw();
122 use Params::Validate qw(validate_with :types);
123 use File::Path qw(mkpath);
124 use File::Copy qw(copy);
127 use Debbugs::Text qw(:templates);
129 use Debbugs::Mail qw(rfc822_date send_mail_message default_headers encode_headers);
130 use Debbugs::MIME qw(create_mime_message);
132 use Mail::RFC822::Address qw();
134 use POSIX qw(strftime);
136 use Storable qw(dclone nfreeze);
137 use List::AllUtils qw(first max);
138 use Encode qw(encode_utf8);
142 # These are a set of options which are common to all of these functions
144 my %common_options = (debug => {type => SCALARREF|HANDLE,
147 transcript => {type => SCALARREF|HANDLE,
150 affected_bugs => {type => HASHREF,
153 affected_packages => {type => HASHREF,
156 recipients => {type => HASHREF,
159 limit => {type => HASHREF,
162 show_bug_info => {type => BOOLEAN,
165 request_subject => {type => SCALAR,
166 default => 'Unknown Subject',
168 request_msgid => {type => SCALAR,
171 request_nn => {type => SCALAR,
174 request_replyto => {type => SCALAR,
177 locks => {type => HASHREF,
183 my %append_action_options =
184 (action => {type => SCALAR,
187 requester => {type => SCALAR,
190 request_addr => {type => SCALAR,
193 location => {type => SCALAR,
196 message => {type => SCALAR|ARRAYREF,
199 append_log => {type => BOOLEAN,
201 depends => [qw(requester request_addr),
205 # locks is both an append_action option, and a common option;
206 # it's ok for it to be in both places.
207 locks => {type => HASHREF,
215 # this is just a generic stub for Debbugs::Control functions.
220 # set_foo(bug => $ref,
221 # transcript => $transcript,
222 # ($dl > 0 ? (debug => $transcript):()),
223 # requester => $header{from},
224 # request_addr => $controlrequestaddr,
226 # affected_packages => \%affected_packages,
227 # recipients => \%recipients,
233 # print {$transcript} "Failed to set foo $ref bar: $@";
241 # my %param = validate_with(params => \@_,
242 # spec => {bug => {type => SCALAR,
243 # regex => qr/^\d+$/,
245 # # specific options here
247 # %append_action_options,
251 # __begin_control(%param,
254 # my ($debug,$transcript) =
255 # @info{qw(debug transcript)};
256 # my @data = @{$info{data}};
257 # my @bugs = @{$info{bugs}};
260 # for my $data (@data) {
261 # append_action_to_log(bug => $data->{bug_num},
263 # __return_append_to_log_options(
268 # if not exists $param{append_log} or $param{append_log};
269 # writebug($data->{bug_num},$data);
270 # print {$transcript} "$action\n";
272 # __end_control(%info);
279 set_block(bug => $ref,
280 transcript => $transcript,
281 ($dl > 0 ? (debug => $transcript):()),
282 requester => $header{from},
283 request_addr => $controlrequestaddr,
285 affected_packages => \%affected_packages,
286 recipients => \%recipients,
292 print {$transcript} "Failed to set blockers of $ref: $@";
295 Alters the set of bugs that block this bug from being fixed
297 This requires altering both this bug (and those it's merged with) as
298 well as the bugs that block this bug from being fixed (and those that
303 =item block -- scalar or arrayref of blocking bugs to set, add or remove
305 =item add -- if true, add blocking bugs
307 =item remove -- if true, remove blocking bugs
314 my %param = validate_with(params => \@_,
315 spec => {bug => {type => SCALAR,
318 # specific options here
319 block => {type => SCALAR|ARRAYREF,
322 add => {type => BOOLEAN,
325 remove => {type => BOOLEAN,
329 %append_action_options,
332 if ($param{add} and $param{remove}) {
333 croak "It's nonsensical to add and remove the same blocking bugs";
335 if (grep {$_ !~ /^\d+$/} make_list($param{block})) {
336 croak "Invalid blocking bug(s):".
337 join(', ',grep {$_ !~ /^\d+$/} make_list($param{block}));
343 elsif ($param{remove}) {
348 __begin_control(%param,
351 my ($debug,$transcript) =
352 @info{qw(debug transcript)};
353 my @data = @{$info{data}};
354 my @bugs = @{$info{bugs}};
357 # The first bit of this code is ugly, and should be cleaned up.
358 # Its purpose is to populate %removed_blockers and %add_blockers
359 # with all of the bugs that should be added or removed as blockers
360 # of all of the bugs which are merged with $param{bug}
363 for my $blocker (make_list($param{block})) {
364 next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
365 my $data = read_bug(bug=>$blocker,
367 if (defined $data and not $data->{archive}) {
368 $data = split_status_fields($data);
369 $ok_blockers{$blocker} = 1;
371 push @merged_bugs, make_list($data->{mergedwith});
372 @ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
375 $bad_blockers{$blocker} = 1;
379 # throw an error if we are setting the blockers and there is a bad
381 if (keys %bad_blockers and $mode eq 'set') {
382 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
383 keys %ok_blockers?'':" and no known blocking bug(s)";
385 # if there are no ok blockers and we are not setting the blockers,
387 if (not keys %ok_blockers and $mode ne 'set') {
388 print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
389 if (keys %bad_blockers) {
390 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
392 __end_control(%info);
396 my @change_blockers = keys %ok_blockers;
398 my %removed_blockers;
401 my @blockers = map {split ' ', $_->{blockedby}} @data;
403 @blockers{@blockers} = (1) x @blockers;
405 # it is nonsensical for a bug to block itself (or a merged
406 # partner); We currently don't allow removal because we'd possibly
410 @bugs{@bugs} = (1) x @bugs;
411 for my $blocker (@change_blockers) {
412 if ($bugs{$blocker}) {
413 croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
416 @blockers = keys %blockers;
418 %removed_blockers = ();
419 for my $blocker (@change_blockers) {
420 next if exists $blockers{$blocker};
421 $blockers{$blocker} = 1;
422 $added_blockers{$blocker} = 1;
425 elsif ($param{remove}) {
426 %added_blockers = ();
427 for my $blocker (@change_blockers) {
428 next if exists $removed_blockers{$blocker};
429 delete $blockers{$blocker};
430 $removed_blockers{$blocker} = 1;
434 @removed_blockers{@blockers} = (1) x @blockers;
436 for my $blocker (@change_blockers) {
437 next if exists $blockers{$blocker};
438 $blockers{$blocker} = 1;
439 if (exists $removed_blockers{$blocker}) {
440 delete $removed_blockers{$blocker};
443 $added_blockers{$blocker} = 1;
447 for my $data (@data) {
448 my $old_data = dclone($data);
449 # remove blockers and/or add new ones as appropriate
450 if ($data->{blockedby} eq '') {
451 print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
453 print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
455 if ($data->{blocks} eq '') {
456 print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
458 print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\n";
461 push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
462 push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
463 $action = ucfirst(join ('; ',@changed)) if @changed;
465 print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
468 $data->{blockedby} = join(' ',keys %blockers);
469 append_action_to_log(bug => $data->{bug_num},
471 old_data => $old_data,
474 __return_append_to_log_options(
479 if not exists $param{append_log} or $param{append_log};
480 writebug($data->{bug_num},$data);
481 print {$transcript} "$action\n";
483 # we do this bit below to avoid code duplication
485 $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
486 $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
488 for my $add_remove (keys %mungable_blocks) {
490 for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
491 next if $munge_blockers{$blocker};
492 my ($temp_locks, @blocking_data) =
493 lock_read_all_merged_bugs(bug => $blocker,
494 ($param{archived}?(location => 'archive'):()),
495 exists $param{locks}?(locks => $param{locks}):(),
497 $locks+= $temp_locks;
498 $new_locks+=$temp_locks;
499 if (not @blocking_data) {
500 for (1..$new_locks) {
501 unfilelock(exists $param{locks}?$param{locks}:());
504 die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
506 for (map {$_->{bug_num}} @blocking_data) {
507 $munge_blockers{$_} = 1;
509 for my $data (@blocking_data) {
510 my $old_data = dclone($data);
512 my @blocks = split ' ', $data->{blocks};
513 @blocks{@blocks} = (1) x @blocks;
515 for my $bug (@bugs) {
516 if ($add_remove eq 'remove') {
517 next unless exists $blocks{$bug};
518 delete $blocks{$bug};
521 next if exists $blocks{$bug};
526 $data->{blocks} = join(' ',sort keys %blocks);
527 my $action = ($add_remove eq 'add'?'Added':'Removed').
528 " indication that bug $data->{bug_num} blocks ".
530 append_action_to_log(bug => $data->{bug_num},
532 old_data => $old_data,
535 __return_append_to_log_options(%param,
539 writebug($data->{bug_num},$data);
541 __handle_affected_packages(%param,data=>\@blocking_data);
542 add_recipients(recipients => $param{recipients},
543 actions_taken => {blocks => 1},
544 data => \@blocking_data,
546 transcript => $transcript,
549 for (1..$new_locks) {
550 unfilelock(exists $param{locks}?$param{locks}:());
555 __end_control(%info);
564 transcript => $transcript,
565 ($dl > 0 ? (debug => $transcript):()),
566 requester => $header{from},
567 request_addr => $controlrequestaddr,
569 affected_packages => \%affected_packages,
570 recipients => \%recipients,
577 print {$transcript} "Failed to set tag on $ref: $@";
581 Sets, adds, or removes the specified tags on a bug
585 =item tag -- scalar or arrayref of tags to set, add or remove
587 =item add -- if true, add tags
589 =item remove -- if true, remove tags
591 =item warn_on_bad_tags -- if true (the default) warn if bad tags are
599 my %param = validate_with(params => \@_,
600 spec => {bug => {type => SCALAR,
603 # specific options here
604 tag => {type => SCALAR|ARRAYREF,
607 add => {type => BOOLEAN,
610 remove => {type => BOOLEAN,
613 warn_on_bad_tags => {type => BOOLEAN,
617 %append_action_options,
620 if ($param{add} and $param{remove}) {
621 croak "It's nonsensical to add and remove the same tags";
625 __begin_control(%param,
628 my $transcript = $info{transcript};
629 my @data = @{$info{data}};
630 my @tags = make_list($param{tag});
631 if (not @tags and ($param{remove} or $param{add})) {
632 if ($param{remove}) {
633 print {$transcript} "Requested to remove no tags; doing nothing.\n";
636 print {$transcript} "Requested to add no tags; doing nothing.\n";
638 __end_control(%info);
641 # first things first, make the versions fully qualified source
643 for my $data (@data) {
644 my $action = 'Did not alter tags';
646 my %tag_removed = ();
647 my @old_tags = split /\,?\s+/, $data->{keywords};
649 @tags{@old_tags} = (1) x @old_tags;
650 my $old_data = dclone($data);
651 if (not $param{add} and not $param{remove}) {
652 $tag_removed{$_} = 1 for @old_tags;
656 for my $tag (@tags) {
657 if (not $param{remove} and
658 not defined first {$_ eq $tag} @{$config{tags}}) {
659 push @bad_tags, $tag;
663 if (not exists $tags{$tag}) {
665 $tag_added{$tag} = 1;
668 elsif ($param{remove}) {
669 if (exists $tags{$tag}) {
671 $tag_removed{$tag} = 1;
675 if (exists $tag_removed{$tag}) {
676 delete $tag_removed{$tag};
679 $tag_added{$tag} = 1;
684 if (@bad_tags and $param{warn_on_bad_tags}) {
685 print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
686 print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
688 $data->{keywords} = join(' ',keys %tags);
691 push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
692 push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
693 $action = ucfirst(join ('; ',@changed)) if @changed;
695 print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
699 append_action_to_log(bug => $data->{bug_num},
702 old_data => $old_data,
704 __return_append_to_log_options(
709 if not exists $param{append_log} or $param{append_log};
710 writebug($data->{bug_num},$data);
711 print {$transcript} "$action\n";
713 __end_control(%info);
721 set_severity(bug => $ref,
722 transcript => $transcript,
723 ($dl > 0 ? (debug => $transcript):()),
724 requester => $header{from},
725 request_addr => $controlrequestaddr,
727 affected_packages => \%affected_packages,
728 recipients => \%recipients,
729 severity => 'normal',
734 print {$transcript} "Failed to set the severity of bug $ref: $@";
737 Sets the severity of a bug. If severity is not passed, is undefined,
738 or has zero length, sets the severity to the default severity.
743 my %param = validate_with(params => \@_,
744 spec => {bug => {type => SCALAR,
747 # specific options here
748 severity => {type => SCALAR|UNDEF,
749 default => $config{default_severity},
752 %append_action_options,
755 if (not defined $param{severity} or
756 not length $param{severity}
758 $param{severity} = $config{default_severity};
761 # check validity of new severity
762 if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
763 die "Severity '$param{severity}' is not a valid severity level";
766 __begin_control(%param,
767 command => 'severity'
769 my $transcript = $info{transcript};
770 my @data = @{$info{data}};
773 for my $data (@data) {
774 if (not defined $data->{severity}) {
775 $data->{severity} = $param{severity};
776 $action = "Severity set to '$param{severity}'";
779 if ($data->{severity} eq '') {
780 $data->{severity} = $config{default_severity};
782 if ($data->{severity} eq $param{severity}) {
783 print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
786 $action = "Severity set to '$param{severity}' from '$data->{severity}'";
787 $data->{severity} = $param{severity};
789 append_action_to_log(bug => $data->{bug_num},
791 __return_append_to_log_options(
796 if not exists $param{append_log} or $param{append_log};
797 writebug($data->{bug_num},$data);
798 print {$transcript} "$action\n";
800 __end_control(%info);
807 set_done(bug => $ref,
808 transcript => $transcript,
809 ($dl > 0 ? (debug => $transcript):()),
810 requester => $header{from},
811 request_addr => $controlrequestaddr,
813 affected_packages => \%affected_packages,
814 recipients => \%recipients,
819 print {$transcript} "Failed to set foo $ref bar: $@";
827 my %param = validate_with(params => \@_,
828 spec => {bug => {type => SCALAR,
831 reopen => {type => BOOLEAN,
834 submitter => {type => SCALAR,
837 clear_fixed => {type => BOOLEAN,
840 notify_submitter => {type => BOOLEAN,
843 original_report => {type => SCALARREF,
846 done => {type => SCALAR|UNDEF,
850 %append_action_options,
854 if (exists $param{submitter} and
855 not Mail::RFC822::Address::valid($param{submitter})) {
856 die "New submitter address '$param{submitter}' is not a valid e-mail address";
858 if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
859 $param{done} = $param{requester};
861 if (exists $param{done} and
862 (not defined $param{done} or
863 not length $param{done})) {
869 __begin_control(%param,
870 command => $param{reopen}?'reopen':'done',
872 my $transcript = $info{transcript};
873 my @data = @{$info{data}};
876 if ($param{reopen}) {
877 # avoid warning multiple times if there are fixed versions
879 for my $data (@data) {
880 if (not exists $data->{done} or
881 not defined $data->{done} or
882 not length $data->{done}) {
883 print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
884 __end_control(%info);
887 if (@{$data->{fixed_versions}} and $warn_fixed) {
888 print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
889 print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
893 $action = "Bug reopened";
894 for my $data (@data) {
895 my $old_data = dclone($data);
897 append_action_to_log(bug => $data->{bug_num},
900 old_data => $old_data,
902 __return_append_to_log_options(
907 if not exists $param{append_log} or $param{append_log};
908 writebug($data->{bug_num},$data);
910 print {$transcript} "$action\n";
911 __end_control(%info);
912 if (exists $param{submitter}) {
913 set_submitter(bug => $param{bug},
914 submitter => $param{submitter},
916 keys %common_options,
917 keys %append_action_options)
920 # clear the fixed revisions
921 if ($param{clear_fixed}) {
922 set_fixed(fixed => [],
926 keys %common_options,
927 keys %append_action_options),
932 my %submitter_notified;
933 my $orig_report_set = 0;
934 for my $data (@data) {
935 if (exists $data->{done} and
936 defined $data->{done} and
937 length $data->{done}) {
938 print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
939 __end_control(%info);
943 for my $data (@data) {
944 my $old_data = dclone($data);
945 my $hash = get_hashname($data->{bug_num});
946 my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
947 die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
951 $orig_report= <$report_fh>;
954 if (not $orig_report_set and defined $orig_report and
955 length $orig_report and
956 exists $param{original_report}){
957 ${$param{original_report}} = $orig_report;
958 $orig_report_set = 1;
961 $action = "Marked $config{bug} as done";
963 # set done to the requester
964 $data->{done} = exists $param{done}?$param{done}:$param{requester};
965 append_action_to_log(bug => $data->{bug_num},
968 old_data => $old_data,
970 __return_append_to_log_options(
975 if not exists $param{append_log} or $param{append_log};
976 writebug($data->{bug_num},$data);
977 print {$transcript} "$action\n";
978 # get the original report
979 if ($param{notify_submitter}) {
980 my $submitter_message;
981 if(not exists $submitter_notified{$data->{originator}}) {
983 create_mime_message([default_headers(queue_file => $param{request_nn},
985 msgid => $param{request_msgid},
986 msgtype => 'notifdone',
987 pr_msg => 'they-closed',
989 [To => $data->{submitter},
990 Subject => "$config{ubug}#$data->{bug_num} ".
991 "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
995 __message_body_template('mail/process_your_bug_done',
997 replyto => (exists $param{request_replyto} ?
998 $param{request_replyto} :
999 $param{requester} || 'Unknown'),
1000 markedby => $param{requester},
1001 subject => $param{request_subject},
1002 messageid => $param{request_msgid},
1005 [join('',make_list($param{message})),$orig_report]
1007 send_mail_message(message => $submitter_message,
1008 recipients => $old_data->{submitter},
1010 $submitter_notified{$data->{originator}} = $submitter_message;
1013 $submitter_message = $submitter_notified{$data->{originator}};
1015 append_action_to_log(bug => $data->{bug_num},
1016 action => "Notification sent",
1018 request_addr => $data->{originator},
1019 desc => "$config{bug} acknowledged by developer.",
1020 recips => [$data->{originator}],
1021 message => $submitter_message,
1026 __end_control(%info);
1027 if (exists $param{fixed}) {
1028 set_fixed(fixed => $param{fixed},
1032 keys %common_options,
1033 keys %append_action_options
1041 =head2 set_submitter
1044 set_submitter(bug => $ref,
1045 transcript => $transcript,
1046 ($dl > 0 ? (debug => $transcript):()),
1047 requester => $header{from},
1048 request_addr => $controlrequestaddr,
1050 affected_packages => \%affected_packages,
1051 recipients => \%recipients,
1052 submitter => $new_submitter,
1053 notify_submitter => 1,
1058 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1061 Sets the submitter of a bug. If notify_submitter is true (the
1062 default), notifies the old submitter of a bug on changes
1067 my %param = validate_with(params => \@_,
1068 spec => {bug => {type => SCALAR,
1071 # specific options here
1072 submitter => {type => SCALAR,
1074 notify_submitter => {type => BOOLEAN,
1078 %append_action_options,
1081 if (not Mail::RFC822::Address::valid($param{submitter})) {
1082 die "New submitter address $param{submitter} is not a valid e-mail address";
1085 __begin_control(%param,
1086 command => 'submitter'
1088 my ($debug,$transcript) =
1089 @info{qw(debug transcript)};
1090 my @data = @{$info{data}};
1092 # here we only concern ourselves with the first of the merged bugs
1093 for my $data ($data[0]) {
1094 my $notify_old_submitter = 0;
1095 my $old_data = dclone($data);
1096 print {$debug} "Going to change bug submitter\n";
1097 if (((not defined $param{submitter} or not length $param{submitter}) and
1098 (not defined $data->{originator} or not length $data->{originator})) or
1099 (defined $param{submitter} and defined $data->{originator} and
1100 $param{submitter} eq $data->{originator})) {
1101 print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
1105 if (defined $data->{originator} and length($data->{originator})) {
1106 $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'.";
1107 $notify_old_submitter = 1;
1110 $action= "Set $config{bug} submitter to '$param{submitter}'.";
1112 $data->{originator} = $param{submitter};
1114 append_action_to_log(bug => $data->{bug_num},
1115 command => 'submitter',
1117 old_data => $old_data,
1119 __return_append_to_log_options(
1124 if not exists $param{append_log} or $param{append_log};
1125 writebug($data->{bug_num},$data);
1126 print {$transcript} "$action\n";
1127 # notify old submitter
1128 if ($notify_old_submitter and $param{notify_submitter}) {
1129 send_mail_message(message =>
1130 create_mime_message([default_headers(queue_file => $param{request_nn},
1132 msgid => $param{request_msgid},
1134 pr_msg => 'submitter-changed',
1136 [To => $old_data->{submitter},
1137 Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
1141 __message_body_template('mail/submitter_changed',
1142 {old_data => $old_data,
1144 replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
1148 recipients => $old_data->{submitter},
1152 __end_control(%info);
1157 =head2 set_forwarded
1160 set_forwarded(bug => $ref,
1161 transcript => $transcript,
1162 ($dl > 0 ? (debug => $transcript):()),
1163 requester => $header{from},
1164 request_addr => $controlrequestaddr,
1166 affected_packages => \%affected_packages,
1167 recipients => \%recipients,
1168 forwarded => $forward_to,
1173 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1176 Sets the location to which a bug is forwarded. Given an undef
1177 forwarded, unsets forwarded.
1183 my %param = validate_with(params => \@_,
1184 spec => {bug => {type => SCALAR,
1187 # specific options here
1188 forwarded => {type => SCALAR|UNDEF,
1191 %append_action_options,
1194 if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) {
1195 die "Non-printable characters are not allowed in the forwarded field";
1197 $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
1199 __begin_control(%param,
1200 command => 'forwarded'
1202 my ($debug,$transcript) =
1203 @info{qw(debug transcript)};
1204 my @data = @{$info{data}};
1206 for my $data (@data) {
1207 my $old_data = dclone($data);
1208 print {$debug} "Going to change bug forwarded\n";
1209 if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
1210 (not defined $param{forwarded} and
1211 defined $data->{forwarded} and not length $data->{forwarded})) {
1212 print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
1216 if (not defined $param{forwarded}) {
1217 $action= "Unset $config{bug} forwarded-to-address";
1219 elsif (defined $data->{forwarded} and length($data->{forwarded})) {
1220 $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'.";
1223 $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
1225 $data->{forwarded} = $param{forwarded};
1227 append_action_to_log(bug => $data->{bug_num},
1228 command => 'forwarded',
1230 old_data => $old_data,
1232 __return_append_to_log_options(
1237 if not exists $param{append_log} or $param{append_log};
1238 writebug($data->{bug_num},$data);
1239 print {$transcript} "$action\n";
1241 __end_control(%info);
1250 set_title(bug => $ref,
1251 transcript => $transcript,
1252 ($dl > 0 ? (debug => $transcript):()),
1253 requester => $header{from},
1254 request_addr => $controlrequestaddr,
1256 affected_packages => \%affected_packages,
1257 recipients => \%recipients,
1258 title => $new_title,
1263 print {$transcript} "Failed to set the title of $ref: $@";
1266 Sets the title of a specific bug
1272 my %param = validate_with(params => \@_,
1273 spec => {bug => {type => SCALAR,
1276 # specific options here
1277 title => {type => SCALAR,
1280 %append_action_options,
1283 if ($param{title} =~ /[^[:print:]]/) {
1284 die "Non-printable characters are not allowed in bug titles";
1287 my %info = __begin_control(%param,
1290 my ($debug,$transcript) =
1291 @info{qw(debug transcript)};
1292 my @data = @{$info{data}};
1294 for my $data (@data) {
1295 my $old_data = dclone($data);
1296 print {$debug} "Going to change bug title\n";
1297 if (defined $data->{subject} and length($data->{subject}) and
1298 $data->{subject} eq $param{title}) {
1299 print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
1303 if (defined $data->{subject} and length($data->{subject})) {
1304 $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'.";
1306 $action= "Set $config{bug} title to '$param{title}'.";
1308 $data->{subject} = $param{title};
1310 append_action_to_log(bug => $data->{bug_num},
1313 old_data => $old_data,
1315 __return_append_to_log_options(
1320 if not exists $param{append_log} or $param{append_log};
1321 writebug($data->{bug_num},$data);
1322 print {$transcript} "$action\n";
1324 __end_control(%info);
1331 set_package(bug => $ref,
1332 transcript => $transcript,
1333 ($dl > 0 ? (debug => $transcript):()),
1334 requester => $header{from},
1335 request_addr => $controlrequestaddr,
1337 affected_packages => \%affected_packages,
1338 recipients => \%recipients,
1339 package => $new_package,
1345 print {$transcript} "Failed to assign or reassign $ref to a package: $@";
1348 Indicates that a bug is in a particular package. If is_source is true,
1349 indicates that the package is a source package. [Internally, this
1350 causes src: to be prepended to the package name.]
1352 The default for is_source is 0. As a special case, if the package
1353 starts with 'src:', it is assumed to be a source package and is_source
1356 The package option must match the package_name_re regex.
1361 my %param = validate_with(params => \@_,
1362 spec => {bug => {type => SCALAR,
1365 # specific options here
1366 package => {type => SCALAR|ARRAYREF,
1368 is_source => {type => BOOLEAN,
1372 %append_action_options,
1375 my @new_packages = map {splitpackages($_)} make_list($param{package});
1376 if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) {
1377 croak "Invalid package name '".
1378 join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages).
1381 my %info = __begin_control(%param,
1382 command => 'package',
1384 my ($debug,$transcript) =
1385 @info{qw(debug transcript)};
1386 my @data = @{$info{data}};
1387 # clean up the new package
1391 ($temp =~ s/^src:// or
1392 $param{is_source}) ? 'src:'.$temp:$temp;
1396 my $package_reassigned = 0;
1397 for my $data (@data) {
1398 my $old_data = dclone($data);
1399 print {$debug} "Going to change assigned package\n";
1400 if (defined $data->{package} and length($data->{package}) and
1401 $data->{package} eq $new_package) {
1402 print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
1406 if (defined $data->{package} and length($data->{package})) {
1407 $package_reassigned = 1;
1408 $action= "$config{bug} reassigned from package '$data->{package}'".
1409 " to '$new_package'.";
1411 $action= "$config{bug} assigned to package '$new_package'.";
1413 $data->{package} = $new_package;
1415 append_action_to_log(bug => $data->{bug_num},
1416 command => 'package',
1418 old_data => $old_data,
1420 __return_append_to_log_options(
1425 if not exists $param{append_log} or $param{append_log};
1426 writebug($data->{bug_num},$data);
1427 print {$transcript} "$action\n";
1429 __end_control(%info);
1430 # Only clear the fixed/found versions if the package has been
1432 if ($package_reassigned) {
1433 my @params_for_found_fixed =
1434 map {exists $param{$_}?($_,$param{$_}):()}
1436 keys %common_options,
1437 keys %append_action_options,
1439 set_found(found => [],
1440 @params_for_found_fixed,
1442 set_fixed(fixed => [],
1443 @params_for_found_fixed,
1451 set_found(bug => $ref,
1452 transcript => $transcript,
1453 ($dl > 0 ? (debug => $transcript):()),
1454 requester => $header{from},
1455 request_addr => $controlrequestaddr,
1457 affected_packages => \%affected_packages,
1458 recipients => \%recipients,
1465 print {$transcript} "Failed to set found on $ref: $@";
1469 Sets, adds, or removes the specified found versions of a package
1471 If the version list is empty, and the bug is currently not "done",
1472 causes the done field to be cleared.
1474 If any of the versions added to found are greater than any version in
1475 which the bug is fixed (or when the bug is found and there are no
1476 fixed versions) the done field is cleared.
1481 my %param = validate_with(params => \@_,
1482 spec => {bug => {type => SCALAR,
1485 # specific options here
1486 found => {type => SCALAR|ARRAYREF,
1489 add => {type => BOOLEAN,
1492 remove => {type => BOOLEAN,
1496 %append_action_options,
1499 if ($param{add} and $param{remove}) {
1500 croak "It's nonsensical to add and remove the same versions";
1504 __begin_control(%param,
1507 my ($debug,$transcript) =
1508 @info{qw(debug transcript)};
1509 my @data = @{$info{data}};
1511 for my $version (make_list($param{found})) {
1512 next unless defined $version;
1513 $versions{$version} =
1514 [make_source_versions(package => [splitpackages($data[0]{package})],
1515 warnings => $transcript,
1518 versions => $version,
1521 # This is really ugly, but it's what we have to do
1522 if (not @{$versions{$version}}) {
1523 print {$transcript} "Unable to make a source version for version '$version'\n";
1526 if (not keys %versions and ($param{remove} or $param{add})) {
1527 if ($param{remove}) {
1528 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1531 print {$transcript} "Requested to add no versions; doing nothing.\n";
1533 __end_control(%info);
1536 # first things first, make the versions fully qualified source
1538 for my $data (@data) {
1539 # The 'done' field gets a bit weird with version tracking,
1540 # because a bug may be closed by multiple people in different
1541 # branches. Until we have something more flexible, we set it
1542 # every time a bug is fixed, and clear it when a bug is found
1543 # in a version greater than any version in which the bug is
1544 # fixed or when a bug is found and there is no fixed version
1545 my $action = 'Did not alter found versions';
1546 my %found_added = ();
1547 my %found_removed = ();
1548 my %fixed_removed = ();
1550 my $old_data = dclone($data);
1551 if (not $param{add} and not $param{remove}) {
1552 $found_removed{$_} = 1 for @{$data->{found_versions}};
1553 $data->{found_versions} = [];
1556 @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
1558 @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
1559 for my $version (keys %versions) {
1561 my @svers = @{$versions{$version}};
1565 elsif (not grep {$version eq $_} @svers) {
1566 # The $version was not equal to one of the source
1567 # versions, so it's probably unqualified (or just
1568 # wrong). Delete it, and use the source versions
1570 if (exists $found_versions{$version}) {
1571 delete $found_versions{$version};
1572 $found_removed{$version} = 1;
1575 for my $sver (@svers) {
1576 if (not exists $found_versions{$sver}) {
1577 $found_versions{$sver} = 1;
1578 $found_added{$sver} = 1;
1580 # if the found we are adding matches any fixed
1581 # versions, remove them
1582 my @temp = grep m{(^|/)\Q$sver\E$}, keys %fixed_versions;
1583 delete $fixed_versions{$_} for @temp;
1584 $fixed_removed{$_} = 1 for @temp;
1587 # We only care about reopening the bug if the bug is
1589 if (defined $data->{done} and length $data->{done}) {
1590 my @svers_order = sort_versions(map {m{([^/]+)$}; $1;}
1592 # determine if we need to reopen
1593 my @fixed_order = sort_versions(map {m{([^/]+)$}; $1;}
1594 keys %fixed_versions);
1595 if (not @fixed_order or
1596 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1602 elsif ($param{remove}) {
1603 # in the case of removal, we only concern ourself with
1604 # the version passed, not the source version it maps
1606 my @temp = grep m{(?:^|/)\Q$version\E$}, keys %found_versions;
1607 delete $found_versions{$_} for @temp;
1608 $found_removed{$_} = 1 for @temp;
1611 # set the keys to exactly these values
1612 my @svers = @{$versions{$version}};
1616 for my $sver (@svers) {
1617 if (not exists $found_versions{$sver}) {
1618 $found_versions{$sver} = 1;
1619 if (exists $found_removed{$sver}) {
1620 delete $found_removed{$sver};
1623 $found_added{$sver} = 1;
1630 $data->{found_versions} = [keys %found_versions];
1631 $data->{fixed_versions} = [keys %fixed_versions];
1634 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1635 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1636 # push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
1637 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1638 $action = ucfirst(join ('; ',@changed)) if @changed;
1640 $action .= " and reopened"
1642 if (not $reopened and not @changed) {
1643 print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
1647 append_action_to_log(bug => $data->{bug_num},
1650 old_data => $old_data,
1652 __return_append_to_log_options(
1657 if not exists $param{append_log} or $param{append_log};
1658 writebug($data->{bug_num},$data);
1659 print {$transcript} "$action\n";
1661 __end_control(%info);
1667 set_fixed(bug => $ref,
1668 transcript => $transcript,
1669 ($dl > 0 ? (debug => $transcript):()),
1670 requester => $header{from},
1671 request_addr => $controlrequestaddr,
1673 affected_packages => \%affected_packages,
1674 recipients => \%recipients,
1682 print {$transcript} "Failed to set fixed on $ref: $@";
1686 Sets, adds, or removes the specified fixed versions of a package
1688 If the fixed versions are empty (or end up being empty after this
1689 call) or the greatest fixed version is less than the greatest found
1690 version and the reopen option is true, the bug is reopened.
1692 This function is also called by the reopen function, which causes all
1693 of the fixed versions to be cleared.
1698 my %param = validate_with(params => \@_,
1699 spec => {bug => {type => SCALAR,
1702 # specific options here
1703 fixed => {type => SCALAR|ARRAYREF,
1706 add => {type => BOOLEAN,
1709 remove => {type => BOOLEAN,
1712 reopen => {type => BOOLEAN,
1716 %append_action_options,
1719 if ($param{add} and $param{remove}) {
1720 croak "It's nonsensical to add and remove the same versions";
1723 __begin_control(%param,
1726 my ($debug,$transcript) =
1727 @info{qw(debug transcript)};
1728 my @data = @{$info{data}};
1730 for my $version (make_list($param{fixed})) {
1731 next unless defined $version;
1732 $versions{$version} =
1733 [make_source_versions(package => [splitpackages($data[0]{package})],
1734 warnings => $transcript,
1737 versions => $version,
1740 # This is really ugly, but it's what we have to do
1741 if (not @{$versions{$version}}) {
1742 print {$transcript} "Unable to make a source version for version '$version'\n";
1745 if (not keys %versions and ($param{remove} or $param{add})) {
1746 if ($param{remove}) {
1747 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1750 print {$transcript} "Requested to add no versions; doing nothing.\n";
1752 __end_control(%info);
1755 # first things first, make the versions fully qualified source
1757 for my $data (@data) {
1758 my $old_data = dclone($data);
1759 # The 'done' field gets a bit weird with version tracking,
1760 # because a bug may be closed by multiple people in different
1761 # branches. Until we have something more flexible, we set it
1762 # every time a bug is fixed, and clear it when a bug is found
1763 # in a version greater than any version in which the bug is
1764 # fixed or when a bug is found and there is no fixed version
1765 my $action = 'Did not alter fixed versions';
1766 my %found_added = ();
1767 my %found_removed = ();
1768 my %fixed_added = ();
1769 my %fixed_removed = ();
1771 if (not $param{add} and not $param{remove}) {
1772 $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
1773 $data->{fixed_versions} = [];
1776 @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
1778 @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
1779 for my $version (keys %versions) {
1781 my @svers = @{$versions{$version}};
1786 if (exists $fixed_versions{$version}) {
1787 $fixed_removed{$version} = 1;
1788 delete $fixed_versions{$version};
1791 for my $sver (@svers) {
1792 if (not exists $fixed_versions{$sver}) {
1793 $fixed_versions{$sver} = 1;
1794 $fixed_added{$sver} = 1;
1798 elsif ($param{remove}) {
1799 # in the case of removal, we only concern ourself with
1800 # the version passed, not the source version it maps
1802 my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
1803 delete $fixed_versions{$_} for @temp;
1804 $fixed_removed{$_} = 1 for @temp;
1807 # set the keys to exactly these values
1808 my @svers = @{$versions{$version}};
1812 for my $sver (@svers) {
1813 if (not exists $fixed_versions{$sver}) {
1814 $fixed_versions{$sver} = 1;
1815 if (exists $fixed_removed{$sver}) {
1816 delete $fixed_removed{$sver};
1819 $fixed_added{$sver} = 1;
1826 $data->{found_versions} = [keys %found_versions];
1827 $data->{fixed_versions} = [keys %fixed_versions];
1829 # If we're supposed to consider reopening, reopen if the
1830 # fixed versions are empty or the greatest found version
1831 # is greater than the greatest fixed version
1832 if ($param{reopen} and defined $data->{done}
1833 and length $data->{done}) {
1834 my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1835 map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
1836 # determine if we need to reopen
1837 my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1838 map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
1839 if (not @fixed_order or
1840 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1847 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1848 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1849 push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
1850 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1851 $action = ucfirst(join ('; ',@changed)) if @changed;
1853 $action .= " and reopened"
1855 if (not $reopened and not @changed) {
1856 print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
1860 append_action_to_log(bug => $data->{bug_num},
1863 old_data => $old_data,
1865 __return_append_to_log_options(
1870 if not exists $param{append_log} or $param{append_log};
1871 writebug($data->{bug_num},$data);
1872 print {$transcript} "$action\n";
1874 __end_control(%info);
1881 set_merged(bug => $ref,
1882 transcript => $transcript,
1883 ($dl > 0 ? (debug => $transcript):()),
1884 requester => $header{from},
1885 request_addr => $controlrequestaddr,
1887 affected_packages => \%affected_packages,
1888 recipients => \%recipients,
1889 merge_with => 12345,
1892 allow_reassign => 1,
1893 reassign_same_source_only => 1,
1898 print {$transcript} "Failed to set merged on $ref: $@";
1902 Sets, adds, or removes the specified merged bugs of a bug
1904 By default, requires
1909 my %param = validate_with(params => \@_,
1910 spec => {bug => {type => SCALAR,
1913 # specific options here
1914 merge_with => {type => ARRAYREF|SCALAR,
1917 remove => {type => BOOLEAN,
1920 force => {type => BOOLEAN,
1923 masterbug => {type => BOOLEAN,
1926 allow_reassign => {type => BOOLEAN,
1929 reassign_different_sources => {type => BOOLEAN,
1933 %append_action_options,
1936 my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
1938 @merging{@merging} = (1) x @merging;
1939 if (grep {$_ !~ /^\d+$/} @merging) {
1940 croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
1942 $param{locks} = {} if not exists $param{locks};
1944 __begin_control(%param,
1947 my ($debug,$transcript) =
1948 @info{qw(debug transcript)};
1949 if (not @merging and exists $param{merge_with}) {
1950 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
1951 __end_control(%info);
1954 my @data = @{$info{data}};
1957 for my $data (@data) {
1958 $data{$data->{bug_num}} = $data;
1959 my @merged_bugs = split / /, $data->{mergedwith};
1960 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
1964 if (not exists $param{merge_with}) {
1965 delete $merged_bugs{$param{bug}};
1966 if (not keys %merged_bugs) {
1967 print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
1968 __end_control(%info);
1971 my $action = "Disconnected #$param{bug} from all other report(s).";
1972 for my $data (@data) {
1973 my $old_data = dclone($data);
1974 if ($data->{bug_num} == $param{bug}) {
1975 $data->{mergedwith} = '';
1978 $data->{mergedwith} =
1981 grep {$_ != $data->{bug_num}}
1984 append_action_to_log(bug => $data->{bug_num},
1987 old_data => $old_data,
1989 __return_append_to_log_options(%param,
1993 if not exists $param{append_log} or $param{append_log};
1994 writebug($data->{bug_num},$data);
1996 print {$transcript} "$action\n";
1997 __end_control(%info);
2000 # lock and load all of the bugs we need
2001 my ($data,$n_locks) =
2002 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2004 locks => $param{locks},
2007 $new_locks += $n_locks;
2009 @data = values %data;
2010 if (not check_limit(data => [@data],
2011 exists $param{limit}?(limit => $param{limit}):(),
2012 transcript => $transcript,
2014 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
2016 for my $data (@data) {
2017 $data{$data->{bug_num}} = $data;
2018 $merged_bugs{$data->{bug_num}} = 1;
2019 my @merged_bugs = split / /, $data->{mergedwith};
2020 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
2021 if (exists $param{affected_bugs}) {
2022 $param{affected_bugs}{$data->{bug_num}} = 1;
2025 __handle_affected_packages(%param,data => [@data]);
2026 my %bug_info_shown; # which bugs have had information shown
2027 $bug_info_shown{$param{bug}} = 1;
2028 add_recipients(data => [@data],
2029 recipients => $param{recipients},
2030 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
2032 (__internal_request()?(transcript => $transcript):()),
2035 # Figure out what the ideal state is for the bug,
2036 my ($merge_status,$bugs_to_merge) =
2037 __calculate_merge_status(\@data,\%data,$param{bug});
2038 # find out if we actually have any bugs to merge
2039 if (not $bugs_to_merge) {
2040 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
2041 for (1..$new_locks) {
2042 unfilelock($param{locks});
2045 __end_control(%info);
2048 # see what changes need to be made to merge the bugs
2049 # check to make sure that the set of changes we need to make is allowed
2050 my ($disallowed_changes,$changes) =
2051 __calculate_merge_changes(\@data,$merge_status,\%param);
2052 # at this point, stop if there are disallowed changes, otherwise
2053 # make the allowed changes, and then reread the bugs in question
2054 # to get the new data, then recaculate the merges; repeat
2055 # reloading and recalculating until we try too many times or there
2056 # are no changes to make.
2059 # we will allow at most 4 times through this; more than 1
2060 # shouldn't really happen.
2062 while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
2063 if ($attempts > 1) {
2064 print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
2066 if (@{$disallowed_changes}) {
2067 # figure out the problems
2068 print {$transcript} "Unable to merge bugs because:\n";
2069 for my $change (@{$disallowed_changes}) {
2070 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2072 if ($attempts > 0) {
2073 croak "Some bugs were altered while attempting to merge";
2076 croak "Did not alter merged bugs";
2079 my @bugs_to_change = keys %{$changes};
2080 for my $change_bug (@bugs_to_change) {
2081 next unless exists $changes->{$change_bug};
2082 $bug_changed{$change_bug}++;
2083 print {$transcript} __bug_info($data{$change_bug}) if
2084 $param{show_bug_info} and not __internal_request(1);
2085 $bug_info_shown{$change_bug} = 1;
2086 __allow_relocking($param{locks},[keys %data]);
2087 for my $change (@{$changes->{$change_bug}}) {
2088 if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
2089 my %target_blockedby;
2090 @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
2091 my %unhandled_targets = %target_blockedby;
2092 for my $key (split / /,$change->{orig_value}) {
2093 delete $unhandled_targets{$key};
2094 next if exists $target_blockedby{$key};
2095 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2096 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2099 keys %common_options,
2100 keys %append_action_options),
2103 for my $key (keys %unhandled_targets) {
2104 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2105 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2108 keys %common_options,
2109 keys %append_action_options),
2114 $change->{function}->(bug => $change->{bug},
2115 $change->{key}, $change->{func_value},
2116 exists $change->{options}?@{$change->{options}}:(),
2118 keys %common_options,
2119 keys %append_action_options),
2123 __disallow_relocking($param{locks});
2124 my ($data,$n_locks) =
2125 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2127 locks => $param{locks},
2131 $new_locks += $n_locks;
2134 @data = values %data;
2135 ($merge_status,$bugs_to_merge) =
2136 __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
2137 ($disallowed_changes,$changes) =
2138 __calculate_merge_changes(\@data,$merge_status,\%param);
2139 $attempts = max(values %bug_changed);
2142 if ($param{show_bug_info} and not __internal_request(1)) {
2143 for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
2144 next if $bug_info_shown{$data->{bug_num}};
2145 print {$transcript} __bug_info($data);
2148 if (keys %{$changes} or @{$disallowed_changes}) {
2149 print {$transcript} "After four attempts, the following changes were unable to be made:\n";
2150 for (1..$new_locks) {
2151 unfilelock($param{locks});
2154 __end_control(%info);
2155 for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
2156 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2158 die "Unable to modify bugs so they could be merged";
2162 # finally, we can merge the bugs
2163 my $action = "Merged ".join(' ',sort { $a <=> $b } keys %merged_bugs);
2164 for my $data (@data) {
2165 my $old_data = dclone($data);
2166 $data->{mergedwith} =
2169 grep {$_ != $data->{bug_num}}
2171 append_action_to_log(bug => $data->{bug_num},
2174 old_data => $old_data,
2176 __return_append_to_log_options(%param,
2180 if not exists $param{append_log} or $param{append_log};
2181 writebug($data->{bug_num},$data);
2183 print {$transcript} "$action\n";
2184 # unlock the extra locks that we got earlier
2185 for (1..$new_locks) {
2186 unfilelock($param{locks});
2189 __end_control(%info);
2192 sub __allow_relocking{
2193 my ($locks,$bugs) = @_;
2195 my @locks = (@{$bugs},'merge');
2196 for my $lock (@locks) {
2197 my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
2198 next unless @lockfiles;
2199 $locks->{relockable}{$lockfiles[0]} = 0;
2203 sub __disallow_relocking{
2205 delete $locks->{relockable};
2208 sub __lock_and_load_merged_bugs{
2210 validate_with(params => \@_,
2212 {bugs_to_load => {type => ARRAYREF,
2213 default => sub {[]},
2215 data => {type => HASHREF|ARRAYREF,
2217 locks => {type => HASHREF,
2218 default => sub {{};},
2220 reload_all => {type => BOOLEAN,
2223 debug => {type => HANDLE,
2229 if (ref($param{data}) eq 'ARRAY') {
2230 for my $data (@{$param{data}}) {
2231 $data{$data->{bug_num}} = dclone($data);
2235 %data = %{dclone($param{data})};
2237 my @bugs_to_load = @{$param{bugs_to_load}};
2238 if ($param{reload_all}) {
2239 push @bugs_to_load, keys %data;
2242 @temp{@bugs_to_load} = (1) x @bugs_to_load;
2243 @bugs_to_load = keys %temp;
2244 my %loaded_this_time;
2246 while ($bug_to_load = shift @bugs_to_load) {
2247 if (not $param{reload_all}) {
2248 next if exists $data{$bug_to_load};
2251 next if $loaded_this_time{$bug_to_load};
2254 if ($param{reload_all}) {
2255 if (exists $data{$bug_to_load}) {
2260 read_bug(bug => $bug_to_load,
2262 locks => $param{locks},
2264 die "Unable to load bug $bug_to_load";
2265 print {$param{debug}} "read bug $bug_to_load\n";
2266 $data{$data->{bug_num}} = $data;
2267 $new_locks += $lock_bug;
2268 $loaded_this_time{$data->{bug_num}} = 1;
2270 grep {not exists $data{$_}}
2271 split / /,$data->{mergedwith};
2273 return (\%data,$new_locks);
2277 sub __calculate_merge_status{
2278 my ($data_a,$data_h,$master_bug,$merge_status) = @_;
2279 my %merge_status = %{$merge_status // {}};
2281 my $bugs_to_merge = 0;
2282 for my $data (@{$data_a}) {
2283 # check to see if this bug is unmerged in the set
2284 if (not length $data->{mergedwith} or
2285 grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
2286 $merged_bugs{$data->{bug_num}} = 1;
2289 # the master_bug is the bug that every other bug is made to
2290 # look like. However, if merge is set, tags, fixed and found
2292 if ($data->{bug_num} == $master_bug) {
2293 for (qw(package forwarded severity blocks blockedby done owner summary outlook affects)) {
2294 $merge_status{$_} = $data->{$_}
2297 if (defined $merge_status) {
2298 next unless $data->{bug_num} == $master_bug;
2300 $merge_status{tag} = {} if not exists $merge_status{tag};
2301 for my $tag (split /\s+/, $data->{keywords}) {
2302 $merge_status{tag}{$tag} = 1;
2304 $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
2305 for (qw(fixed found)) {
2306 @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
2309 # if there is a non-source qualified version with a corresponding
2310 # source qualified version, we only want to merge the source
2311 # qualified version(s)
2312 for (qw(fixed found)) {
2313 my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
2314 for my $unqualified_version (@unqualified_versions) {
2315 if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
2316 delete $merge_status{"${_}_versions"}{$unqualified_version};
2320 return (\%merge_status,$bugs_to_merge);
2325 sub __calculate_merge_changes{
2326 my ($datas,$merge_status,$param) = @_;
2328 my @disallowed_changes;
2329 for my $data (@{$datas}) {
2330 # things that can be forced
2332 # * func is the function to set the new value
2334 # * key is the key of the function to set the value,
2336 # * modify_value is a function which is called to modify the new
2337 # value so that the function will accept it
2339 # * options is an ARRAYREF of options to pass to the function
2341 # * allowed is a BOOLEAN which controls whether this setting
2342 # is allowed to be different by default.
2343 my %force_functions =
2344 (forwarded => {func => \&set_forwarded,
2348 severity => {func => \&set_severity,
2352 blocks => {func => \&set_blocks,
2353 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2357 blockedby => {func => \&set_blocks,
2358 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2362 done => {func => \&set_done,
2366 owner => {func => \&owner,
2370 summary => {func => \&summary,
2374 outlook => {func => \&outlook,
2378 affects => {func => \&affects,
2382 package => {func => \&set_package,
2386 keywords => {func => \&set_tag,
2388 modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
2391 fixed_versions => {func => \&set_fixed,
2393 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2396 found_versions => {func => \&set_found,
2398 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2402 for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
2403 # if the ideal bug already has the field set properly, we
2405 if ($field eq 'keywords'){
2406 next if join(' ',sort split /\s+/,$data->{keywords}) eq
2407 join(' ',sort keys %{$merge_status->{tag}});
2409 elsif ($field =~ /^(?:fixed|found)_versions$/) {
2410 next if join(' ', sort @{$data->{$field}}) eq
2411 join(' ',sort keys %{$merge_status->{$field}});
2413 elsif ($field eq 'done') {
2414 # for done, we only care if the bug is done or not
2415 # done, not the value it's set to.
2416 if (defined $merge_status->{$field} and length $merge_status->{$field} and
2417 defined $data->{$field} and length $data->{$field}) {
2420 elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
2421 (not defined $data->{$field} or not length $data->{$field})
2426 elsif ($merge_status->{$field} eq $data->{$field}) {
2431 bug => $data->{bug_num},
2432 orig_value => $data->{$field},
2434 (exists $force_functions{$field}{modify_value} ?
2435 $force_functions{$field}{modify_value}->($merge_status->{$field}):
2436 $merge_status->{$field}),
2437 value => $merge_status->{$field},
2438 function => $force_functions{$field}{func},
2439 key => $force_functions{$field}{key},
2440 options => $force_functions{$field}{options},
2441 allowed => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
2443 $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
2444 $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
2445 if ($param->{force} or $change->{allowed}) {
2446 if ($field ne 'package' or $change->{allowed}) {
2447 push @{$changes{$data->{bug_num}}},$change;
2450 if ($param->{allow_reassign}) {
2451 if ($param->{reassign_different_sources}) {
2452 push @{$changes{$data->{bug_num}}},$change;
2455 # allow reassigning if binary_to_source returns at
2456 # least one of the same source packages
2457 my @merge_status_source =
2458 binary_to_source(package => $merge_status->{package},
2461 my @other_bug_source =
2462 binary_to_source(package => $data->{package},
2465 my %merge_status_sources;
2466 @merge_status_sources{@merge_status_source} =
2467 (1) x @merge_status_source;
2468 if (grep {$merge_status_sources{$_}} @other_bug_source) {
2469 push @{$changes{$data->{bug_num}}},$change;
2474 push @disallowed_changes,$change;
2476 # blocks and blocked by are weird; we have to go through and
2477 # set blocks to the other half of the merged bugs
2479 return (\@disallowed_changes,\%changes);
2485 affects(bug => $ref,
2486 transcript => $transcript,
2487 ($dl > 0 ? (debug => $transcript):()),
2488 requester => $header{from},
2489 request_addr => $controlrequestaddr,
2491 affected_packages => \%affected_packages,
2492 recipients => \%recipients,
2500 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
2503 This marks a bug as affecting packages which the bug is not actually
2504 in. This should only be used in cases where fixing the bug instantly
2505 resolves the problem in the other packages.
2507 By default, the packages are set to the list of packages passed.
2508 However, if you pass add => 1 or remove => 1, the list of packages
2509 passed are added or removed from the affects list, respectively.
2514 my %param = validate_with(params => \@_,
2515 spec => {bug => {type => SCALAR,
2518 # specific options here
2519 package => {type => SCALAR|ARRAYREF|UNDEF,
2522 add => {type => BOOLEAN,
2525 remove => {type => BOOLEAN,
2529 %append_action_options,
2532 if ($param{add} and $param{remove}) {
2533 croak "Asking to both add and remove affects is nonsensical";
2535 if (not defined $param{package}) {
2536 $param{package} = [];
2539 __begin_control(%param,
2540 command => 'affects'
2542 my ($debug,$transcript) =
2543 @info{qw(debug transcript)};
2544 my @data = @{$info{data}};
2546 for my $data (@data) {
2548 print {$debug} "Going to change affects\n";
2549 my @packages = splitpackages($data->{affects});
2551 @packages{@packages} = (1) x @packages;
2554 for my $package (make_list($param{package})) {
2555 next unless defined $package and length $package;
2556 if (not $packages{$package}) {
2557 $packages{$package} = 1;
2558 push @added,$package;
2562 $action = "Added indication that $data->{bug_num} affects ".
2563 english_join(\@added);
2566 elsif ($param{remove}) {
2568 for my $package (make_list($param{package})) {
2569 if ($packages{$package}) {
2570 next unless defined $package and length $package;
2571 delete $packages{$package};
2572 push @removed,$package;
2575 $action = "Removed indication that $data->{bug_num} affects " .
2576 english_join(\@removed);
2579 my %added_packages = ();
2580 my %removed_packages = %packages;
2582 for my $package (make_list($param{package})) {
2583 next unless defined $package and length $package;
2584 $packages{$package} = 1;
2585 delete $removed_packages{$package};
2586 $added_packages{$package} = 1;
2588 if (keys %removed_packages) {
2589 $action = "Removed indication that $data->{bug_num} affects ".
2590 english_join([keys %removed_packages]);
2591 $action .= "\n" if keys %added_packages;
2593 if (keys %added_packages) {
2594 $action .= "Added indication that $data->{bug_num} affects " .
2595 english_join([keys %added_packages]);
2598 if (not length $action) {
2599 print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
2602 my $old_data = dclone($data);
2603 $data->{affects} = join(',',keys %packages);
2604 append_action_to_log(bug => $data->{bug_num},
2606 command => 'affects',
2608 old_data => $old_data,
2609 __return_append_to_log_options(
2614 if not exists $param{append_log} or $param{append_log};
2615 writebug($data->{bug_num},$data);
2616 print {$transcript} "$action\n";
2618 __end_control(%info);
2622 =head1 SUMMARY FUNCTIONS
2627 summary(bug => $ref,
2628 transcript => $transcript,
2629 ($dl > 0 ? (debug => $transcript):()),
2630 requester => $header{from},
2631 request_addr => $controlrequestaddr,
2633 affected_packages => \%affected_packages,
2634 recipients => \%recipients,
2640 print {$transcript} "Failed to mark $ref with summary foo: $@";
2643 Handles all setting of summary fields
2645 If summary is undef, unsets the summary
2647 If summary is 0 or -1, sets the summary to the first paragraph contained in
2650 If summary is a positive integer, sets the summary to the message specified.
2652 Otherwise, sets summary to the value passed.
2658 # outlook and summary are exactly the same, basically
2659 return _summary('summary',@_);
2662 =head1 OUTLOOK FUNCTIONS
2667 outlook(bug => $ref,
2668 transcript => $transcript,
2669 ($dl > 0 ? (debug => $transcript):()),
2670 requester => $header{from},
2671 request_addr => $controlrequestaddr,
2673 affected_packages => \%affected_packages,
2674 recipients => \%recipients,
2680 print {$transcript} "Failed to mark $ref with outlook foo: $@";
2683 Handles all setting of outlook fields
2685 If outlook is undef, unsets the outlook
2687 If outlook is 0, sets the outlook to the first paragraph contained in
2690 If outlook is a positive integer, sets the outlook to the message specified.
2692 Otherwise, sets outlook to the value passed.
2698 return _summary('outlook',@_);
2702 my ($cmd,@params) = @_;
2703 my %param = validate_with(params => \@params,
2704 spec => {bug => {type => SCALAR,
2707 # specific options here
2708 $cmd , {type => SCALAR|UNDEF,
2712 %append_action_options,
2716 __begin_control(%param,
2719 my ($debug,$transcript) =
2720 @info{qw(debug transcript)};
2721 my @data = @{$info{data}};
2722 # figure out the log that we're going to use
2724 my $summary_msg = '';
2726 if (not defined $param{$cmd}) {
2728 print {$debug} "Removing $cmd fields\n";
2729 $action = "Removed $cmd";
2731 elsif ($param{$cmd} =~ /^-?\d+$/) {
2733 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
2734 if ($param{$cmd} == 0 or $param{$cmd} == -1) {
2735 $log = $param{message};
2736 $summary_msg = @records + 1;
2739 if (($param{$cmd} - 1 ) > $#records) {
2740 die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
2742 my $record = $records[($param{$cmd} - 1 )];
2743 if ($record->{type} !~ /incoming-recv|recips/) {
2744 die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
2746 $summary_msg = $param{$cmd};
2747 $log = [$record->{text}];
2749 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
2750 my $body = $p_o->{body};
2751 my $in_pseudoheaders = 0;
2753 # walk through body until we get non-blank lines
2754 for my $line (@{$body}) {
2755 if ($line =~ /^\s*$/) {
2756 if (length $paragraph) {
2757 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
2763 $in_pseudoheaders = 0;
2766 # skip a paragraph if it looks like it's control or
2768 if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity|Control)\:\s+\S}xi or #pseudo headers
2769 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
2770 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
2771 debug|(?:not|)forwarded|priority|
2772 (?:un|)block|limit|(?:un|)archive|
2773 reassign|retitle|affects|wrongpackage
2774 (?:un|force|)merge|user(?:category|tags?|)
2776 if (not length $paragraph) {
2777 print {$debug} "Found control/pseudo-headers and skiping them\n";
2778 $in_pseudoheaders = 1;
2782 next if $in_pseudoheaders;
2783 $paragraph .= $line ." \n";
2785 print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
2786 $summary = $paragraph;
2787 $summary =~ s/[\n\r]/ /g;
2788 if (not length $summary) {
2789 die "Unable to find $cmd message to use";
2791 # trim off a trailing spaces
2792 $summary =~ s/\ *$//;
2795 $summary = $param{$cmd};
2797 for my $data (@data) {
2798 print {$debug} "Going to change $cmd\n";
2799 if (((not defined $summary or not length $summary) and
2800 (not defined $data->{$cmd} or not length $data->{$cmd})) or
2801 $summary eq $data->{$cmd}) {
2802 print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
2805 if (length $summary) {
2806 if (length $data->{$cmd}) {
2807 $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
2810 $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
2813 my $old_data = dclone($data);
2814 $data->{$cmd} = $summary;
2815 append_action_to_log(bug => $data->{bug_num},
2817 old_data => $old_data,
2820 __return_append_to_log_options(
2825 if not exists $param{append_log} or $param{append_log};
2826 writebug($data->{bug_num},$data);
2827 print {$transcript} "$action\n";
2829 __end_control(%info);
2837 clone_bug(bug => $ref,
2838 transcript => $transcript,
2839 ($dl > 0 ? (debug => $transcript):()),
2840 requester => $header{from},
2841 request_addr => $controlrequestaddr,
2843 affected_packages => \%affected_packages,
2844 recipients => \%recipients,
2849 print {$transcript} "Failed to clone bug $ref bar: $@";
2852 Clones the given bug.
2854 We currently don't support cloning merged bugs, but this could be
2855 handled by internally unmerging, cloning, then remerging the bugs.
2860 my %param = validate_with(params => \@_,
2861 spec => {bug => {type => SCALAR,
2864 new_bugs => {type => ARRAYREF,
2866 new_clones => {type => HASHREF,
2870 %append_action_options,
2874 __begin_control(%param,
2877 my $transcript = $info{transcript};
2878 my @data = @{$info{data}};
2881 for my $data (@data) {
2882 if (length($data->{mergedwith})) {
2883 die "Bug is marked as being merged with others. Use an existing clone.\n";
2887 die "Not exactly one bug‽ This shouldn't happen.";
2889 my $data = $data[0];
2891 for my $newclone_id (@{$param{new_bugs}}) {
2892 my $new_bug_num = new_bug(copy => $data->{bug_num});
2893 $param{new_clones}{$newclone_id} = $new_bug_num;
2894 $clones{$newclone_id} = $new_bug_num;
2896 my @new_bugs = sort values %clones;
2898 for my $new_bug (@new_bugs) {
2899 # no collapsed ids or the higher collapsed id is not one less
2900 # than the next highest new bug
2901 if (not @collapsed_ids or
2902 $collapsed_ids[-1][1]+1 != $new_bug) {
2903 push @collapsed_ids,[$new_bug,$new_bug];
2906 $collapsed_ids[-1][1] = $new_bug;
2910 for my $ci (@collapsed_ids) {
2911 if ($ci->[0] == $ci->[1]) {
2912 push @collapsed,$ci->[0];
2915 push @collapsed,$ci->[0].'-'.$ci->[1]
2918 my $collapsed_str = english_join(\@collapsed);
2919 $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
2920 for my $new_bug (@new_bugs) {
2921 append_action_to_log(bug => $new_bug,
2923 __return_append_to_log_options(
2928 if not exists $param{append_log} or $param{append_log};
2930 append_action_to_log(bug => $data->{bug_num},
2932 __return_append_to_log_options(
2937 if not exists $param{append_log} or $param{append_log};
2938 writebug($data->{bug_num},$data);
2939 print {$transcript} "$action\n";
2940 __end_control(%info);
2941 # bugs that this bug is blocking are also blocked by the new clone(s)
2942 for my $bug (split ' ', $data->{blocks}) {
2943 for my $new_bug (@new_bugs) {
2944 set_blocks(bug => $bug,
2948 keys %common_options,
2949 keys %append_action_options),
2953 # bugs that are blocking this bug are also blocking the new clone(s)
2954 for my $bug (split ' ', $data->{blockedby}) {
2955 for my $new_bug (@new_bugs) {
2956 set_blocks(bug => $new_bug,
2960 keys %common_options,
2961 keys %append_action_options),
2969 =head1 OWNER FUNCTIONS
2975 transcript => $transcript,
2976 ($dl > 0 ? (debug => $transcript):()),
2977 requester => $header{from},
2978 request_addr => $controlrequestaddr,
2980 recipients => \%recipients,
2986 print {$transcript} "Failed to mark $ref as having an owner: $@";
2989 Handles all setting of the owner field; given an owner of undef or of
2990 no length, indicates that a bug is not owned by anyone.
2995 my %param = validate_with(params => \@_,
2996 spec => {bug => {type => SCALAR,
2999 owner => {type => SCALAR|UNDEF,
3002 %append_action_options,
3006 __begin_control(%param,
3009 my ($debug,$transcript) =
3010 @info{qw(debug transcript)};
3011 my @data = @{$info{data}};
3013 for my $data (@data) {
3014 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
3015 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
3016 if (not defined $param{owner} or not length $param{owner}) {
3017 if (not defined $data->{owner} or not length $data->{owner}) {
3018 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
3022 $action = "Removed annotation that $config{bug} was owned by " .
3026 if ($data->{owner} eq $param{owner}) {
3027 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
3030 if (length $data->{owner}) {
3031 $action = "Owner changed from $data->{owner} to $param{owner}.";
3034 $action = "Owner recorded as $param{owner}."
3037 my $old_data = dclone($data);
3038 $data->{owner} = $param{owner};
3039 append_action_to_log(bug => $data->{bug_num},
3042 old_data => $old_data,
3044 __return_append_to_log_options(
3049 if not exists $param{append_log} or $param{append_log};
3050 writebug($data->{bug_num},$data);
3051 print {$transcript} "$action\n";
3053 __end_control(%info);
3057 =head1 ARCHIVE FUNCTIONS
3064 bug_archive(bug => $bug_num,
3066 transcript => \$transcript,
3071 transcript("Unable to archive $bug_num\n");
3074 transcript($transcript);
3077 This routine archives a bug
3081 =item bug -- bug number
3083 =item check_archiveable -- check wether a bug is archiveable before
3084 archiving; defaults to 1
3086 =item archive_unarchived -- whether to archive bugs which have not
3087 previously been archived; defaults to 1. [Set to 0 when used from
3090 =item ignore_time -- whether to ignore time constraints when archiving
3091 a bug; defaults to 0.
3098 my %param = validate_with(params => \@_,
3099 spec => {bug => {type => SCALAR,
3102 check_archiveable => {type => BOOLEAN,
3105 archive_unarchived => {type => BOOLEAN,
3108 ignore_time => {type => BOOLEAN,
3112 %append_action_options,
3115 my %info = __begin_control(%param,
3116 command => 'archive',
3118 my ($debug,$transcript) = @info{qw(debug transcript)};
3119 my @data = @{$info{data}};
3120 my @bugs = @{$info{bugs}};
3121 my $action = "$config{bug} archived.";
3122 if ($param{check_archiveable} and
3123 not bug_archiveable(bug=>$param{bug},
3124 ignore_time => $param{ignore_time},
3126 print {$transcript} "Bug $param{bug} cannot be archived\n";
3127 die "Bug $param{bug} cannot be archived";
3129 if (not $param{archive_unarchived} and
3130 not exists $data[0]{unarchived}
3132 print {$transcript} "$param{bug} has not been archived previously\n";
3133 die "$param{bug} has not been archived previously";
3135 add_recipients(recipients => $param{recipients},
3138 transcript => $transcript,
3140 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3141 for my $bug (@bugs) {
3142 if ($param{check_archiveable}) {
3143 die "Bug $bug cannot be archived (but $param{bug} can?)"
3144 unless bug_archiveable(bug=>$bug,
3145 ignore_time => $param{ignore_time},
3149 # If we get here, we can archive/remove this bug
3150 print {$debug} "$param{bug} removing\n";
3151 for my $bug (@bugs) {
3152 #print "$param{bug} removing $bug\n" if $debug;
3153 my $dir = get_hashname($bug);
3154 # First indicate that this bug is being archived
3155 append_action_to_log(bug => $bug,
3157 command => 'archive',
3158 # we didn't actually change the data
3159 # when we archived, so we don't pass
3160 # a real new_data or old_data
3163 __return_append_to_log_options(
3168 if not exists $param{append_log} or $param{append_log};
3169 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
3170 if ($config{save_old_bugs}) {
3171 mkpath("$config{spool_dir}/archive/$dir");
3172 foreach my $file (@files_to_remove) {
3173 link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3174 copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3175 # we need to bail out here if things have
3176 # gone horribly wrong to avoid removing a
3178 die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
3181 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
3183 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
3184 print {$debug} "deleted $bug (from $param{bug})\n";
3186 bughook_archive(@bugs);
3187 __end_control(%info);
3190 =head2 bug_unarchive
3194 bug_unarchive(bug => $bug_num,
3196 transcript => \$transcript,
3201 transcript("Unable to archive bug: $bug_num");
3203 transcript($transcript);
3205 This routine unarchives a bug
3210 my %param = validate_with(params => \@_,
3211 spec => {bug => {type => SCALAR,
3215 %append_action_options,
3219 my %info = __begin_control(%param,
3221 command=>'unarchive');
3222 my ($debug,$transcript) =
3223 @info{qw(debug transcript)};
3224 my @bugs = @{$info{bugs}};
3225 my $action = "$config{bug} unarchived.";
3226 my @files_to_remove;
3227 for my $bug (@bugs) {
3228 print {$debug} "$param{bug} removing $bug\n";
3229 my $dir = get_hashname($bug);
3230 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
3231 mkpath("archive/$dir");
3232 foreach my $file (@files_to_copy) {
3233 # die'ing here sucks
3234 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3235 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3236 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
3238 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
3239 print {$transcript} "Unarchived $config{bug} $bug\n";
3241 unlink(@files_to_remove) or die "Unable to unlink bugs";
3242 # Indicate that this bug has been archived previously
3243 for my $bug (@bugs) {
3244 my $newdata = readbug($bug);
3245 my $old_data = dclone($newdata);
3246 if (not defined $newdata) {
3247 print {$transcript} "$config{bug} $bug disappeared!\n";
3248 die "Bug $bug disappeared!";
3250 $newdata->{unarchived} = time;
3251 append_action_to_log(bug => $bug,
3253 command => 'unarchive',
3254 new_data => $newdata,
3255 old_data => $old_data,
3256 __return_append_to_log_options(
3261 if not exists $param{append_log} or $param{append_log};
3262 writebug($bug,$newdata);
3264 __end_control(%info);
3267 =head2 append_action_to_log
3269 append_action_to_log
3271 This should probably be moved to Debbugs::Log; have to think that out
3276 sub append_action_to_log{
3277 my %param = validate_with(params => \@_,
3278 spec => {bug => {type => SCALAR,
3281 new_data => {type => HASHREF,
3284 old_data => {type => HASHREF,
3287 command => {type => SCALAR,
3290 action => {type => SCALAR,
3292 requester => {type => SCALAR,
3295 request_addr => {type => SCALAR,
3298 location => {type => SCALAR,
3301 message => {type => SCALAR|ARRAYREF,
3304 recips => {type => SCALAR|ARRAYREF,
3307 desc => {type => SCALAR,
3310 get_lock => {type => BOOLEAN,
3313 locks => {type => HASHREF,
3317 # append_action_options here
3318 # because some of these
3319 # options aren't actually
3320 # optional, even though the
3321 # original function doesn't
3325 # Fix this to use $param{location}
3326 my $log_location = buglog($param{bug});
3327 die "Unable to find .log for $param{bug}"
3328 if not defined $log_location;
3329 if ($param{get_lock}) {
3330 filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
3334 my $logfh = IO::File->new(">>$log_location") or
3335 die "Unable to open $log_location for appending: $!";
3336 # determine difference between old and new
3338 if (exists $param{old_data} and exists $param{new_data}) {
3339 my $old_data = dclone($param{old_data});
3340 my $new_data = dclone($param{new_data});
3341 for my $key (keys %{$old_data}) {
3342 if (not exists $Debbugs::Status::fields{$key}) {
3343 delete $old_data->{$key};
3346 next unless exists $new_data->{$key};
3347 next unless defined $new_data->{$key};
3348 if (not defined $old_data->{$key}) {
3349 delete $old_data->{$key};
3352 if (ref($new_data->{$key}) and
3353 ref($old_data->{$key}) and
3354 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3355 local $Storable::canonical = 1;
3356 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3357 delete $new_data->{$key};
3358 delete $old_data->{$key};
3361 elsif ($new_data->{$key} eq $old_data->{$key}) {
3362 delete $new_data->{$key};
3363 delete $old_data->{$key};
3366 for my $key (keys %{$new_data}) {
3367 if (not exists $Debbugs::Status::fields{$key}) {
3368 delete $new_data->{$key};
3371 next unless exists $old_data->{$key};
3372 next unless defined $old_data->{$key};
3373 if (not defined $new_data->{$key} or
3374 not exists $Debbugs::Status::fields{$key}) {
3375 delete $new_data->{$key};
3378 if (ref($new_data->{$key}) and
3379 ref($old_data->{$key}) and
3380 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3381 local $Storable::canonical = 1;
3382 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3383 delete $new_data->{$key};
3384 delete $old_data->{$key};
3387 elsif ($new_data->{$key} eq $old_data->{$key}) {
3388 delete $new_data->{$key};
3389 delete $old_data->{$key};
3392 $data_diff .= "<!-- new_data:\n";
3394 for my $key (keys %{$new_data}) {
3395 if (not exists $Debbugs::Status::fields{$key}) {
3396 warn "No such field $key";
3399 $nd{$key} = $new_data->{$key};
3400 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
3402 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
3403 $data_diff .= "-->\n";
3404 $data_diff .= "<!-- old_data:\n";
3406 for my $key (keys %{$old_data}) {
3407 if (not exists $Debbugs::Status::fields{$key}) {
3408 warn "No such field $key";
3411 $od{$key} = $old_data->{$key};
3412 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
3414 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
3415 $data_diff .= "-->\n";
3418 (exists $param{command} ?
3419 "<!-- command:".html_escape(encode_utf8_safely($param{command}))." -->\n":""
3421 (length $param{requester} ?
3422 "<!-- requester: ".html_escape(encode_utf8_safely($param{requester}))." -->\n":""
3424 (length $param{request_addr} ?
3425 "<!-- request_addr: ".html_escape(encode_utf8_safely($param{request_addr}))." -->\n":""
3427 "<!-- time:".time()." -->\n",
3429 "<strong>".html_escape(encode_utf8_safely($param{action}))."</strong>\n");
3430 if (length $param{requester}) {
3431 $msg .= "Request was from <code>".html_escape(encode_utf8_safely($param{requester}))."</code>\n";
3433 if (length $param{request_addr}) {
3434 $msg .= "to <code>".html_escape(encode_utf8_safely($param{request_addr}))."</code>";
3436 if (length $param{desc}) {
3437 $msg .= ":<br>\n".encode_utf8_safely($param{desc})."\n";
3442 push @records, {type => 'html',
3446 if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
3447 push @records, {type => exists $param{recips}?'recips':'incoming-recv',
3448 exists $param{recips}?(recips => [map {encode_utf8_safely($_)} make_list($param{recips})]):(),
3449 text => join('',make_list($param{message})),
3452 write_log_records(logfh=>$logfh,
3453 records => \@records,
3455 close $logfh or die "Unable to close $log_location: $!";
3456 if ($param{get_lock}) {
3457 unfilelock(exists $param{locks}?$param{locks}:());
3465 =head1 PRIVATE FUNCTIONS
3467 =head2 __handle_affected_packages
3469 __handle_affected_packages(affected_packages => {},
3477 sub __handle_affected_packages{
3478 my %param = validate_with(params => \@_,
3479 spec => {%common_options,
3480 data => {type => ARRAYREF|HASHREF
3485 for my $data (make_list($param{data})) {
3486 next unless exists $data->{package} and defined $data->{package};
3487 my @packages = split /\s*,\s*/,$data->{package};
3488 @{$param{affected_packages}}{@packages} = (1) x @packages;
3492 =head2 __handle_debug_transcript
3494 my ($debug,$transcript) = __handle_debug_transcript(%param);
3496 Returns a debug and transcript filehandle
3501 sub __handle_debug_transcript{
3502 my %param = validate_with(params => \@_,
3503 spec => {%common_options},
3506 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
3507 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3508 return ($debug,$transcript);
3515 Produces a small bit of bug information to kick out to the transcript
3522 next unless defined $data and exists $data->{bug_num};
3523 $return .= "Bug #".($data->{bug_num}||'').
3524 ((defined $data->{done} and length $data->{done})?
3525 " {Done: $data->{done}}":''
3527 " [".($data->{package}||'(no package)'). "] ".
3528 ($data->{subject}||'(no subject)')."\n";
3534 =head2 __internal_request
3536 __internal_request()
3537 __internal_request($level)
3539 Returns true if the caller of the function calling __internal_request
3540 belongs to __PACKAGE__
3542 This allows us to be magical, and don't bother to print bug info if
3543 the second caller is from this package, amongst other things.
3545 An optional level is allowed, which increments the number of levels to
3546 check by the given value. [This is basically for use by internal
3547 functions like __begin_control which are always called by
3552 sub __internal_request{
3554 $l = 0 if not defined $l;
3555 if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
3561 sub __return_append_to_log_options{
3563 my $action = $param{action} if exists $param{action};
3564 if (not exists $param{requester}) {
3565 $param{requester} = $config{control_internal_requester};
3567 if (not exists $param{request_addr}) {
3568 $param{request_addr} = $config{control_internal_request_addr};
3570 if (not exists $param{message}) {
3571 my $date = rfc822_date();
3573 encode_headers(fill_in_template(template => 'mail/fake_control_message',
3574 variables => {request_addr => $param{request_addr},
3575 requester => $param{requester},
3581 if (not defined $action) {
3582 carp "Undefined action!";
3583 $action = "unknown action";
3585 return (action => $action,
3586 hash_slice(%param,keys %append_action_options),
3590 =head2 __begin_control
3592 my %info = __begin_control(%param,
3594 command=>'unarchive');
3595 my ($debug,$transcript) = @info{qw(debug transcript)};
3596 my @data = @{$info{data}};
3597 my @bugs = @{$info{bugs}};
3600 Starts the process of modifying a bug; handles all of the generic
3601 things that almost every control request needs
3603 Returns a hash containing
3607 =item new_locks -- number of new locks taken out by this call
3609 =item debug -- the debug file handle
3611 =item transcript -- the transcript file handle
3613 =item data -- an arrayref containing the data of the bugs
3614 corresponding to this request
3616 =item bugs -- an arrayref containing the bug numbers of the bugs
3617 corresponding to this request
3625 sub __begin_control {
3626 my %param = validate_with(params => \@_,
3627 spec => {bug => {type => SCALAR,
3630 archived => {type => BOOLEAN,
3633 command => {type => SCALAR,
3641 my ($debug,$transcript) = __handle_debug_transcript(@_);
3642 print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
3643 # print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
3644 $lockhash = $param{locks} if exists $param{locks};
3646 my $old_die = $SIG{__DIE__};
3647 $SIG{__DIE__} = *sig_die{CODE};
3649 ($new_locks, @data) =
3650 lock_read_all_merged_bugs(bug => $param{bug},
3651 $param{archived}?(location => 'archive'):(),
3652 exists $param{locks} ? (locks => $param{locks}):(),
3654 $locks += $new_locks;
3656 die "Unable to read any bugs successfully.";
3658 if (not $param{archived}) {
3659 for my $data (@data) {
3660 if ($data->{archived}) {
3661 die "Not altering archived bugs; see unarchive.";
3665 if (not check_limit(data => \@data,
3666 exists $param{limit}?(limit => $param{limit}):(),
3667 transcript => $transcript,
3669 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
3672 __handle_affected_packages(%param,data => \@data);
3673 print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
3674 print {$debug} "$param{bug} read $locks locks\n";
3675 if (not @data or not defined $data[0]) {
3676 print {$transcript} "No bug found for $param{bug}\n";
3677 die "No bug found for $param{bug}";
3680 add_recipients(data => \@data,
3681 recipients => $param{recipients},
3682 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
3684 (__internal_request()?(transcript => $transcript):()),
3687 print {$debug} "$param{bug} read done\n";
3688 my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
3689 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3690 return (data => \@data,
3692 old_die => $old_die,
3693 new_locks => $new_locks,
3695 transcript => $transcript,
3697 exists $param{locks}?(locks => $param{locks}):(),
3701 =head2 __end_control
3703 __end_control(%info);
3705 Handles tearing down from a control request
3711 if (exists $info{new_locks} and $info{new_locks} > 0) {
3712 print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
3713 for (1..$info{new_locks}) {
3714 unfilelock(exists $info{locks}?$info{locks}:());
3718 $SIG{__DIE__} = $info{old_die};
3719 if (exists $info{param}{affected_bugs}) {
3720 @{$info{param}{affected_bugs}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
3722 add_recipients(recipients => $info{param}{recipients},
3723 (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
3724 data => $info{data},
3725 debug => $info{debug},
3726 transcript => $info{transcript},
3728 __handle_affected_packages(%{$info{param}},data=>$info{data});
3734 check_limit(data => \@data, limit => $param{limit});
3737 Checks to make sure that bugs match any limits; each entry of @data
3738 much satisfy the limit.
3740 Returns true if there are no entries in data, or there are no keys in
3741 limit; returns false (0) if there are any entries which do not match.
3743 The limit hashref elements can contain an arrayref of scalars to
3744 match; regexes are also acccepted. At least one of the entries in each
3745 element needs to match the corresponding field in all data for the
3752 my %param = validate_with(params => \@_,
3753 spec => {data => {type => ARRAYREF|HASHREF,
3755 limit => {type => HASHREF|UNDEF,
3757 transcript => {type => SCALARREF|HANDLE,
3762 my @data = make_list($param{data});
3764 not defined $param{limit} or
3765 not keys %{$param{limit}}) {
3768 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3769 my $going_to_fail = 0;
3770 for my $data (@data) {
3771 $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
3772 status => dclone($data),
3774 for my $field (keys %{$param{limit}}) {
3775 next unless exists $param{limit}{$field};
3777 my @data_fields = make_list($data->{$field});
3778 LIMIT: for my $limit (make_list($param{limit}{$field})) {
3779 if (not ref $limit) {
3780 for my $data_field (@data_fields) {
3781 if ($data_field eq $limit) {
3787 elsif (ref($limit) eq 'Regexp') {
3788 for my $data_field (@data_fields) {
3789 if ($data_field =~ $limit) {
3796 warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
3801 print {$transcript} qq($field: ').join(', ',map{qq("$_")} make_list($data->{$field})).
3802 "' does not match at least one of ".
3803 join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
3807 return $going_to_fail?0:1;
3815 We override die to specially handle unlocking files in the cases where
3816 we are called via eval. [If we're not called via eval, it doesn't
3822 if ($^S) { # in eval
3824 for (1..$locks) { unfilelock(defined $lockhash?$lockhash:()); }
3831 # =head2 __message_body_template
3833 # message_body_template('mail/ack',{ref=>'foo'});
3835 # Creates a message body using a template
3839 sub __message_body_template{
3840 my ($template,$extra_var) = @_;
3842 my $hole_var = {'&bugurl' =>
3844 $config{cgi_domain}.'/'.
3845 Debbugs::CGI::bug_links(bug => $_[0],
3851 my $body = fill_in_template(template => $template,
3852 variables => {config => \%config,
3855 hole_var => $hole_var,
3857 return fill_in_template(template => 'mail/message_body',
3858 variables => {config => \%config,
3862 hole_var => $hole_var,
3866 sub __all_undef_or_equal {
3868 return 1 if @values == 1 or @values == 0;
3869 my $not_def = grep {not defined $_} @values;
3870 if ($not_def == @values) {
3873 if ($not_def > 0 and $not_def != @values) {
3876 my $first_val = shift @values;
3877 for my $val (@values) {
3878 if ($first_val ne $val) {