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 __end_control(%info);
383 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
384 keys %ok_blockers?'':" and no known blocking bug(s)";
386 # if there are no ok blockers and we are not setting the blockers,
388 if (not keys %ok_blockers and $mode ne 'set') {
389 print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
390 if (keys %bad_blockers) {
391 __end_control(%info);
392 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
394 __end_control(%info);
398 my @change_blockers = keys %ok_blockers;
400 my %removed_blockers;
403 my @blockers = map {split ' ', $_->{blockedby}} @data;
405 @blockers{@blockers} = (1) x @blockers;
407 # it is nonsensical for a bug to block itself (or a merged
408 # partner); We currently don't allow removal because we'd possibly
412 @bugs{@bugs} = (1) x @bugs;
413 for my $blocker (@change_blockers) {
414 if ($bugs{$blocker}) {
415 __end_control(%info);
416 croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
419 @blockers = keys %blockers;
421 %removed_blockers = ();
422 for my $blocker (@change_blockers) {
423 next if exists $blockers{$blocker};
424 $blockers{$blocker} = 1;
425 $added_blockers{$blocker} = 1;
428 elsif ($param{remove}) {
429 %added_blockers = ();
430 for my $blocker (@change_blockers) {
431 next if exists $removed_blockers{$blocker};
432 delete $blockers{$blocker};
433 $removed_blockers{$blocker} = 1;
437 @removed_blockers{@blockers} = (1) x @blockers;
439 for my $blocker (@change_blockers) {
440 next if exists $blockers{$blocker};
441 $blockers{$blocker} = 1;
442 if (exists $removed_blockers{$blocker}) {
443 delete $removed_blockers{$blocker};
446 $added_blockers{$blocker} = 1;
450 for my $data (@data) {
451 my $old_data = dclone($data);
452 # remove blockers and/or add new ones as appropriate
453 if ($data->{blockedby} eq '') {
454 print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
456 print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
458 if ($data->{blocks} eq '') {
459 print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
461 print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\n";
464 push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
465 push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
466 $action = ucfirst(join ('; ',@changed)) if @changed;
468 print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
471 $data->{blockedby} = join(' ',keys %blockers);
472 append_action_to_log(bug => $data->{bug_num},
474 old_data => $old_data,
477 __return_append_to_log_options(
482 if not exists $param{append_log} or $param{append_log};
483 writebug($data->{bug_num},$data);
484 print {$transcript} "$action\n";
486 # we do this bit below to avoid code duplication
488 $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
489 $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
491 for my $add_remove (keys %mungable_blocks) {
493 for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
494 next if $munge_blockers{$blocker};
495 my ($temp_locks, @blocking_data) =
496 lock_read_all_merged_bugs(bug => $blocker,
497 ($param{archived}?(location => 'archive'):()),
498 exists $param{locks}?(locks => $param{locks}):(),
500 $locks+= $temp_locks;
501 $new_locks+=$temp_locks;
502 if (not @blocking_data) {
503 for (1..$new_locks) {
504 unfilelock(exists $param{locks}?$param{locks}:());
507 die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
509 for (map {$_->{bug_num}} @blocking_data) {
510 $munge_blockers{$_} = 1;
512 for my $data (@blocking_data) {
513 my $old_data = dclone($data);
515 my @blocks = split ' ', $data->{blocks};
516 @blocks{@blocks} = (1) x @blocks;
518 for my $bug (@bugs) {
519 if ($add_remove eq 'remove') {
520 next unless exists $blocks{$bug};
521 delete $blocks{$bug};
524 next if exists $blocks{$bug};
529 $data->{blocks} = join(' ',sort keys %blocks);
530 my $action = ($add_remove eq 'add'?'Added':'Removed').
531 " indication that bug $data->{bug_num} blocks ".
533 append_action_to_log(bug => $data->{bug_num},
535 old_data => $old_data,
538 __return_append_to_log_options(%param,
542 writebug($data->{bug_num},$data);
544 __handle_affected_packages(%param,data=>\@blocking_data);
545 add_recipients(recipients => $param{recipients},
546 actions_taken => {blocks => 1},
547 data => \@blocking_data,
549 transcript => $transcript,
552 for (1..$new_locks) {
553 unfilelock(exists $param{locks}?$param{locks}:());
558 __end_control(%info);
567 transcript => $transcript,
568 ($dl > 0 ? (debug => $transcript):()),
569 requester => $header{from},
570 request_addr => $controlrequestaddr,
572 affected_packages => \%affected_packages,
573 recipients => \%recipients,
580 print {$transcript} "Failed to set tag on $ref: $@";
584 Sets, adds, or removes the specified tags on a bug
588 =item tag -- scalar or arrayref of tags to set, add or remove
590 =item add -- if true, add tags
592 =item remove -- if true, remove tags
594 =item warn_on_bad_tags -- if true (the default) warn if bad tags are
602 my %param = validate_with(params => \@_,
603 spec => {bug => {type => SCALAR,
606 # specific options here
607 tag => {type => SCALAR|ARRAYREF,
610 add => {type => BOOLEAN,
613 remove => {type => BOOLEAN,
616 warn_on_bad_tags => {type => BOOLEAN,
620 %append_action_options,
623 if ($param{add} and $param{remove}) {
624 croak "It's nonsensical to add and remove the same tags";
628 __begin_control(%param,
631 my $transcript = $info{transcript};
632 my @data = @{$info{data}};
633 my @tags = make_list($param{tag});
634 if (not @tags and ($param{remove} or $param{add})) {
635 if ($param{remove}) {
636 print {$transcript} "Requested to remove no tags; doing nothing.\n";
639 print {$transcript} "Requested to add no tags; doing nothing.\n";
641 __end_control(%info);
644 # first things first, make the versions fully qualified source
646 for my $data (@data) {
647 my $action = 'Did not alter tags';
649 my %tag_removed = ();
650 my @old_tags = split /\,?\s+/, $data->{keywords};
652 @tags{@old_tags} = (1) x @old_tags;
653 my $old_data = dclone($data);
654 if (not $param{add} and not $param{remove}) {
655 $tag_removed{$_} = 1 for @old_tags;
659 for my $tag (@tags) {
660 if (not $param{remove} and
661 not defined first {$_ eq $tag} @{$config{tags}}) {
662 push @bad_tags, $tag;
666 if (not exists $tags{$tag}) {
668 $tag_added{$tag} = 1;
671 elsif ($param{remove}) {
672 if (exists $tags{$tag}) {
674 $tag_removed{$tag} = 1;
678 if (exists $tag_removed{$tag}) {
679 delete $tag_removed{$tag};
682 $tag_added{$tag} = 1;
687 if (@bad_tags and $param{warn_on_bad_tags}) {
688 print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
689 print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
691 $data->{keywords} = join(' ',keys %tags);
694 push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
695 push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
696 $action = ucfirst(join ('; ',@changed)) if @changed;
698 print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
702 append_action_to_log(bug => $data->{bug_num},
705 old_data => $old_data,
707 __return_append_to_log_options(
712 if not exists $param{append_log} or $param{append_log};
713 writebug($data->{bug_num},$data);
714 print {$transcript} "$action\n";
716 __end_control(%info);
724 set_severity(bug => $ref,
725 transcript => $transcript,
726 ($dl > 0 ? (debug => $transcript):()),
727 requester => $header{from},
728 request_addr => $controlrequestaddr,
730 affected_packages => \%affected_packages,
731 recipients => \%recipients,
732 severity => 'normal',
737 print {$transcript} "Failed to set the severity of bug $ref: $@";
740 Sets the severity of a bug. If severity is not passed, is undefined,
741 or has zero length, sets the severity to the default severity.
746 my %param = validate_with(params => \@_,
747 spec => {bug => {type => SCALAR,
750 # specific options here
751 severity => {type => SCALAR|UNDEF,
752 default => $config{default_severity},
755 %append_action_options,
758 if (not defined $param{severity} or
759 not length $param{severity}
761 $param{severity} = $config{default_severity};
764 # check validity of new severity
765 if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
766 die "Severity '$param{severity}' is not a valid severity level";
769 __begin_control(%param,
770 command => 'severity'
772 my $transcript = $info{transcript};
773 my @data = @{$info{data}};
776 for my $data (@data) {
777 if (not defined $data->{severity}) {
778 $data->{severity} = $param{severity};
779 $action = "Severity set to '$param{severity}'";
782 if ($data->{severity} eq '') {
783 $data->{severity} = $config{default_severity};
785 if ($data->{severity} eq $param{severity}) {
786 print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
789 $action = "Severity set to '$param{severity}' from '$data->{severity}'";
790 $data->{severity} = $param{severity};
792 append_action_to_log(bug => $data->{bug_num},
794 __return_append_to_log_options(
799 if not exists $param{append_log} or $param{append_log};
800 writebug($data->{bug_num},$data);
801 print {$transcript} "$action\n";
803 __end_control(%info);
810 set_done(bug => $ref,
811 transcript => $transcript,
812 ($dl > 0 ? (debug => $transcript):()),
813 requester => $header{from},
814 request_addr => $controlrequestaddr,
816 affected_packages => \%affected_packages,
817 recipients => \%recipients,
822 print {$transcript} "Failed to set foo $ref bar: $@";
830 my %param = validate_with(params => \@_,
831 spec => {bug => {type => SCALAR,
834 reopen => {type => BOOLEAN,
837 submitter => {type => SCALAR,
840 clear_fixed => {type => BOOLEAN,
843 notify_submitter => {type => BOOLEAN,
846 original_report => {type => SCALARREF,
849 done => {type => SCALAR|UNDEF,
853 %append_action_options,
857 if (exists $param{submitter} and
858 not Mail::RFC822::Address::valid($param{submitter})) {
859 die "New submitter address '$param{submitter}' is not a valid e-mail address";
861 if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
862 $param{done} = $param{requester};
864 if (exists $param{done} and
865 (not defined $param{done} or
866 not length $param{done})) {
872 __begin_control(%param,
873 command => $param{reopen}?'reopen':'done',
875 my $transcript = $info{transcript};
876 my @data = @{$info{data}};
879 if ($param{reopen}) {
880 # avoid warning multiple times if there are fixed versions
882 for my $data (@data) {
883 if (not exists $data->{done} or
884 not defined $data->{done} or
885 not length $data->{done}) {
886 print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
887 __end_control(%info);
890 if (@{$data->{fixed_versions}} and $warn_fixed) {
891 print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
892 print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
896 $action = "Bug reopened";
897 for my $data (@data) {
898 my $old_data = dclone($data);
900 append_action_to_log(bug => $data->{bug_num},
903 old_data => $old_data,
905 __return_append_to_log_options(
910 if not exists $param{append_log} or $param{append_log};
911 writebug($data->{bug_num},$data);
913 print {$transcript} "$action\n";
914 __end_control(%info);
915 if (exists $param{submitter}) {
916 set_submitter(bug => $param{bug},
917 submitter => $param{submitter},
919 keys %common_options,
920 keys %append_action_options)
923 # clear the fixed revisions
924 if ($param{clear_fixed}) {
925 set_fixed(fixed => [],
929 keys %common_options,
930 keys %append_action_options),
935 my %submitter_notified;
936 my $orig_report_set = 0;
937 for my $data (@data) {
938 if (exists $data->{done} and
939 defined $data->{done} and
940 length $data->{done}) {
941 print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
942 __end_control(%info);
946 for my $data (@data) {
947 my $old_data = dclone($data);
948 my $hash = get_hashname($data->{bug_num});
949 my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
950 die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
954 $orig_report= <$report_fh>;
957 if (not $orig_report_set and defined $orig_report and
958 length $orig_report and
959 exists $param{original_report}){
960 ${$param{original_report}} = $orig_report;
961 $orig_report_set = 1;
964 $action = "Marked $config{bug} as done";
966 # set done to the requester
967 $data->{done} = exists $param{done}?$param{done}:$param{requester};
968 append_action_to_log(bug => $data->{bug_num},
971 old_data => $old_data,
973 __return_append_to_log_options(
978 if not exists $param{append_log} or $param{append_log};
979 writebug($data->{bug_num},$data);
980 print {$transcript} "$action\n";
981 # get the original report
982 if ($param{notify_submitter}) {
983 my $submitter_message;
984 if(not exists $submitter_notified{$data->{originator}}) {
986 create_mime_message([default_headers(queue_file => $param{request_nn},
988 msgid => $param{request_msgid},
989 msgtype => 'notifdone',
990 pr_msg => 'they-closed',
992 [To => $data->{submitter},
993 Subject => "$config{ubug}#$data->{bug_num} ".
994 "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
998 __message_body_template('mail/process_your_bug_done',
1000 replyto => (exists $param{request_replyto} ?
1001 $param{request_replyto} :
1002 $param{requester} || 'Unknown'),
1003 markedby => $param{requester},
1004 subject => $param{request_subject},
1005 messageid => $param{request_msgid},
1008 [join('',make_list($param{message})),$orig_report]
1010 send_mail_message(message => $submitter_message,
1011 recipients => $old_data->{submitter},
1013 $submitter_notified{$data->{originator}} = $submitter_message;
1016 $submitter_message = $submitter_notified{$data->{originator}};
1018 append_action_to_log(bug => $data->{bug_num},
1019 action => "Notification sent",
1021 request_addr => $data->{originator},
1022 desc => "$config{bug} acknowledged by developer.",
1023 recips => [$data->{originator}],
1024 message => $submitter_message,
1029 __end_control(%info);
1030 if (exists $param{fixed}) {
1031 set_fixed(fixed => $param{fixed},
1035 keys %common_options,
1036 keys %append_action_options
1044 =head2 set_submitter
1047 set_submitter(bug => $ref,
1048 transcript => $transcript,
1049 ($dl > 0 ? (debug => $transcript):()),
1050 requester => $header{from},
1051 request_addr => $controlrequestaddr,
1053 affected_packages => \%affected_packages,
1054 recipients => \%recipients,
1055 submitter => $new_submitter,
1056 notify_submitter => 1,
1061 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1064 Sets the submitter of a bug. If notify_submitter is true (the
1065 default), notifies the old submitter of a bug on changes
1070 my %param = validate_with(params => \@_,
1071 spec => {bug => {type => SCALAR,
1074 # specific options here
1075 submitter => {type => SCALAR,
1077 notify_submitter => {type => BOOLEAN,
1081 %append_action_options,
1084 if (not Mail::RFC822::Address::valid($param{submitter})) {
1085 die "New submitter address $param{submitter} is not a valid e-mail address";
1088 __begin_control(%param,
1089 command => 'submitter'
1091 my ($debug,$transcript) =
1092 @info{qw(debug transcript)};
1093 my @data = @{$info{data}};
1095 # here we only concern ourselves with the first of the merged bugs
1096 for my $data ($data[0]) {
1097 my $notify_old_submitter = 0;
1098 my $old_data = dclone($data);
1099 print {$debug} "Going to change bug submitter\n";
1100 if (((not defined $param{submitter} or not length $param{submitter}) and
1101 (not defined $data->{originator} or not length $data->{originator})) or
1102 (defined $param{submitter} and defined $data->{originator} and
1103 $param{submitter} eq $data->{originator})) {
1104 print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
1108 if (defined $data->{originator} and length($data->{originator})) {
1109 $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'.";
1110 $notify_old_submitter = 1;
1113 $action= "Set $config{bug} submitter to '$param{submitter}'.";
1115 $data->{originator} = $param{submitter};
1117 append_action_to_log(bug => $data->{bug_num},
1118 command => 'submitter',
1120 old_data => $old_data,
1122 __return_append_to_log_options(
1127 if not exists $param{append_log} or $param{append_log};
1128 writebug($data->{bug_num},$data);
1129 print {$transcript} "$action\n";
1130 # notify old submitter
1131 if ($notify_old_submitter and $param{notify_submitter}) {
1132 send_mail_message(message =>
1133 create_mime_message([default_headers(queue_file => $param{request_nn},
1135 msgid => $param{request_msgid},
1137 pr_msg => 'submitter-changed',
1139 [To => $old_data->{submitter},
1140 Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
1144 __message_body_template('mail/submitter_changed',
1145 {old_data => $old_data,
1147 replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
1151 recipients => $old_data->{submitter},
1155 __end_control(%info);
1160 =head2 set_forwarded
1163 set_forwarded(bug => $ref,
1164 transcript => $transcript,
1165 ($dl > 0 ? (debug => $transcript):()),
1166 requester => $header{from},
1167 request_addr => $controlrequestaddr,
1169 affected_packages => \%affected_packages,
1170 recipients => \%recipients,
1171 forwarded => $forward_to,
1176 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1179 Sets the location to which a bug is forwarded. Given an undef
1180 forwarded, unsets forwarded.
1186 my %param = validate_with(params => \@_,
1187 spec => {bug => {type => SCALAR,
1190 # specific options here
1191 forwarded => {type => SCALAR|UNDEF,
1194 %append_action_options,
1197 if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) {
1198 die "Non-printable characters are not allowed in the forwarded field";
1200 $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
1202 __begin_control(%param,
1203 command => 'forwarded'
1205 my ($debug,$transcript) =
1206 @info{qw(debug transcript)};
1207 my @data = @{$info{data}};
1209 for my $data (@data) {
1210 my $old_data = dclone($data);
1211 print {$debug} "Going to change bug forwarded\n";
1212 if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
1213 (not defined $param{forwarded} and
1214 defined $data->{forwarded} and not length $data->{forwarded})) {
1215 print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
1219 if (not defined $param{forwarded}) {
1220 $action= "Unset $config{bug} forwarded-to-address";
1222 elsif (defined $data->{forwarded} and length($data->{forwarded})) {
1223 $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'.";
1226 $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
1228 $data->{forwarded} = $param{forwarded};
1230 append_action_to_log(bug => $data->{bug_num},
1231 command => 'forwarded',
1233 old_data => $old_data,
1235 __return_append_to_log_options(
1240 if not exists $param{append_log} or $param{append_log};
1241 writebug($data->{bug_num},$data);
1242 print {$transcript} "$action\n";
1244 __end_control(%info);
1253 set_title(bug => $ref,
1254 transcript => $transcript,
1255 ($dl > 0 ? (debug => $transcript):()),
1256 requester => $header{from},
1257 request_addr => $controlrequestaddr,
1259 affected_packages => \%affected_packages,
1260 recipients => \%recipients,
1261 title => $new_title,
1266 print {$transcript} "Failed to set the title of $ref: $@";
1269 Sets the title of a specific bug
1275 my %param = validate_with(params => \@_,
1276 spec => {bug => {type => SCALAR,
1279 # specific options here
1280 title => {type => SCALAR,
1283 %append_action_options,
1286 if ($param{title} =~ /[^[:print:]]/) {
1287 die "Non-printable characters are not allowed in bug titles";
1290 my %info = __begin_control(%param,
1293 my ($debug,$transcript) =
1294 @info{qw(debug transcript)};
1295 my @data = @{$info{data}};
1297 for my $data (@data) {
1298 my $old_data = dclone($data);
1299 print {$debug} "Going to change bug title\n";
1300 if (defined $data->{subject} and length($data->{subject}) and
1301 $data->{subject} eq $param{title}) {
1302 print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
1306 if (defined $data->{subject} and length($data->{subject})) {
1307 $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'.";
1309 $action= "Set $config{bug} title to '$param{title}'.";
1311 $data->{subject} = $param{title};
1313 append_action_to_log(bug => $data->{bug_num},
1316 old_data => $old_data,
1318 __return_append_to_log_options(
1323 if not exists $param{append_log} or $param{append_log};
1324 writebug($data->{bug_num},$data);
1325 print {$transcript} "$action\n";
1327 __end_control(%info);
1334 set_package(bug => $ref,
1335 transcript => $transcript,
1336 ($dl > 0 ? (debug => $transcript):()),
1337 requester => $header{from},
1338 request_addr => $controlrequestaddr,
1340 affected_packages => \%affected_packages,
1341 recipients => \%recipients,
1342 package => $new_package,
1348 print {$transcript} "Failed to assign or reassign $ref to a package: $@";
1351 Indicates that a bug is in a particular package. If is_source is true,
1352 indicates that the package is a source package. [Internally, this
1353 causes src: to be prepended to the package name.]
1355 The default for is_source is 0. As a special case, if the package
1356 starts with 'src:', it is assumed to be a source package and is_source
1359 The package option must match the package_name_re regex.
1364 my %param = validate_with(params => \@_,
1365 spec => {bug => {type => SCALAR,
1368 # specific options here
1369 package => {type => SCALAR|ARRAYREF,
1371 is_source => {type => BOOLEAN,
1375 %append_action_options,
1378 my @new_packages = map {splitpackages($_)} make_list($param{package});
1379 if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) {
1380 croak "Invalid package name '".
1381 join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages).
1384 my %info = __begin_control(%param,
1385 command => 'package',
1387 my ($debug,$transcript) =
1388 @info{qw(debug transcript)};
1389 my @data = @{$info{data}};
1390 # clean up the new package
1394 ($temp =~ s/^src:// or
1395 $param{is_source}) ? 'src:'.$temp:$temp;
1399 my $package_reassigned = 0;
1400 for my $data (@data) {
1401 my $old_data = dclone($data);
1402 print {$debug} "Going to change assigned package\n";
1403 if (defined $data->{package} and length($data->{package}) and
1404 $data->{package} eq $new_package) {
1405 print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
1409 if (defined $data->{package} and length($data->{package})) {
1410 $package_reassigned = 1;
1411 $action= "$config{bug} reassigned from package '$data->{package}'".
1412 " to '$new_package'.";
1414 $action= "$config{bug} assigned to package '$new_package'.";
1416 $data->{package} = $new_package;
1418 append_action_to_log(bug => $data->{bug_num},
1419 command => 'package',
1421 old_data => $old_data,
1423 __return_append_to_log_options(
1428 if not exists $param{append_log} or $param{append_log};
1429 writebug($data->{bug_num},$data);
1430 print {$transcript} "$action\n";
1432 __end_control(%info);
1433 # Only clear the fixed/found versions if the package has been
1435 if ($package_reassigned) {
1436 my @params_for_found_fixed =
1437 map {exists $param{$_}?($_,$param{$_}):()}
1439 keys %common_options,
1440 keys %append_action_options,
1442 set_found(found => [],
1443 @params_for_found_fixed,
1445 set_fixed(fixed => [],
1446 @params_for_found_fixed,
1454 set_found(bug => $ref,
1455 transcript => $transcript,
1456 ($dl > 0 ? (debug => $transcript):()),
1457 requester => $header{from},
1458 request_addr => $controlrequestaddr,
1460 affected_packages => \%affected_packages,
1461 recipients => \%recipients,
1468 print {$transcript} "Failed to set found on $ref: $@";
1472 Sets, adds, or removes the specified found versions of a package
1474 If the version list is empty, and the bug is currently not "done",
1475 causes the done field to be cleared.
1477 If any of the versions added to found are greater than any version in
1478 which the bug is fixed (or when the bug is found and there are no
1479 fixed versions) the done field is cleared.
1484 my %param = validate_with(params => \@_,
1485 spec => {bug => {type => SCALAR,
1488 # specific options here
1489 found => {type => SCALAR|ARRAYREF,
1492 add => {type => BOOLEAN,
1495 remove => {type => BOOLEAN,
1499 %append_action_options,
1502 if ($param{add} and $param{remove}) {
1503 croak "It's nonsensical to add and remove the same versions";
1507 __begin_control(%param,
1510 my ($debug,$transcript) =
1511 @info{qw(debug transcript)};
1512 my @data = @{$info{data}};
1514 for my $version (make_list($param{found})) {
1515 next unless defined $version;
1516 $versions{$version} =
1517 [make_source_versions(package => [splitpackages($data[0]{package})],
1518 warnings => $transcript,
1521 versions => $version,
1524 # This is really ugly, but it's what we have to do
1525 if (not @{$versions{$version}}) {
1526 print {$transcript} "Unable to make a source version for version '$version'\n";
1529 if (not keys %versions and ($param{remove} or $param{add})) {
1530 if ($param{remove}) {
1531 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1534 print {$transcript} "Requested to add no versions; doing nothing.\n";
1536 __end_control(%info);
1539 # first things first, make the versions fully qualified source
1541 for my $data (@data) {
1542 # The 'done' field gets a bit weird with version tracking,
1543 # because a bug may be closed by multiple people in different
1544 # branches. Until we have something more flexible, we set it
1545 # every time a bug is fixed, and clear it when a bug is found
1546 # in a version greater than any version in which the bug is
1547 # fixed or when a bug is found and there is no fixed version
1548 my $action = 'Did not alter found versions';
1549 my %found_added = ();
1550 my %found_removed = ();
1551 my %fixed_removed = ();
1553 my $old_data = dclone($data);
1554 if (not $param{add} and not $param{remove}) {
1555 $found_removed{$_} = 1 for @{$data->{found_versions}};
1556 $data->{found_versions} = [];
1559 @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
1561 @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
1562 for my $version (keys %versions) {
1564 my @svers = @{$versions{$version}};
1568 elsif (not grep {$version eq $_} @svers) {
1569 # The $version was not equal to one of the source
1570 # versions, so it's probably unqualified (or just
1571 # wrong). Delete it, and use the source versions
1573 if (exists $found_versions{$version}) {
1574 delete $found_versions{$version};
1575 $found_removed{$version} = 1;
1578 for my $sver (@svers) {
1579 if (not exists $found_versions{$sver}) {
1580 $found_versions{$sver} = 1;
1581 $found_added{$sver} = 1;
1583 # if the found we are adding matches any fixed
1584 # versions, remove them
1585 my @temp = grep m{(^|/)\Q$sver\E$}, keys %fixed_versions;
1586 delete $fixed_versions{$_} for @temp;
1587 $fixed_removed{$_} = 1 for @temp;
1590 # We only care about reopening the bug if the bug is
1592 if (defined $data->{done} and length $data->{done}) {
1593 my @svers_order = sort_versions(map {m{([^/]+)$}; $1;}
1595 # determine if we need to reopen
1596 my @fixed_order = sort_versions(map {m{([^/]+)$}; $1;}
1597 keys %fixed_versions);
1598 if (not @fixed_order or
1599 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1605 elsif ($param{remove}) {
1606 # in the case of removal, we only concern ourself with
1607 # the version passed, not the source version it maps
1609 my @temp = grep m{(?:^|/)\Q$version\E$}, keys %found_versions;
1610 delete $found_versions{$_} for @temp;
1611 $found_removed{$_} = 1 for @temp;
1614 # set the keys to exactly these values
1615 my @svers = @{$versions{$version}};
1619 for my $sver (@svers) {
1620 if (not exists $found_versions{$sver}) {
1621 $found_versions{$sver} = 1;
1622 if (exists $found_removed{$sver}) {
1623 delete $found_removed{$sver};
1626 $found_added{$sver} = 1;
1633 $data->{found_versions} = [keys %found_versions];
1634 $data->{fixed_versions} = [keys %fixed_versions];
1637 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1638 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1639 # push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
1640 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1641 $action = ucfirst(join ('; ',@changed)) if @changed;
1643 $action .= " and reopened"
1645 if (not $reopened and not @changed) {
1646 print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
1650 append_action_to_log(bug => $data->{bug_num},
1653 old_data => $old_data,
1655 __return_append_to_log_options(
1660 if not exists $param{append_log} or $param{append_log};
1661 writebug($data->{bug_num},$data);
1662 print {$transcript} "$action\n";
1664 __end_control(%info);
1670 set_fixed(bug => $ref,
1671 transcript => $transcript,
1672 ($dl > 0 ? (debug => $transcript):()),
1673 requester => $header{from},
1674 request_addr => $controlrequestaddr,
1676 affected_packages => \%affected_packages,
1677 recipients => \%recipients,
1685 print {$transcript} "Failed to set fixed on $ref: $@";
1689 Sets, adds, or removes the specified fixed versions of a package
1691 If the fixed versions are empty (or end up being empty after this
1692 call) or the greatest fixed version is less than the greatest found
1693 version and the reopen option is true, the bug is reopened.
1695 This function is also called by the reopen function, which causes all
1696 of the fixed versions to be cleared.
1701 my %param = validate_with(params => \@_,
1702 spec => {bug => {type => SCALAR,
1705 # specific options here
1706 fixed => {type => SCALAR|ARRAYREF,
1709 add => {type => BOOLEAN,
1712 remove => {type => BOOLEAN,
1715 reopen => {type => BOOLEAN,
1719 %append_action_options,
1722 if ($param{add} and $param{remove}) {
1723 croak "It's nonsensical to add and remove the same versions";
1726 __begin_control(%param,
1729 my ($debug,$transcript) =
1730 @info{qw(debug transcript)};
1731 my @data = @{$info{data}};
1733 for my $version (make_list($param{fixed})) {
1734 next unless defined $version;
1735 $versions{$version} =
1736 [make_source_versions(package => [splitpackages($data[0]{package})],
1737 warnings => $transcript,
1740 versions => $version,
1743 # This is really ugly, but it's what we have to do
1744 if (not @{$versions{$version}}) {
1745 print {$transcript} "Unable to make a source version for version '$version'\n";
1748 if (not keys %versions and ($param{remove} or $param{add})) {
1749 if ($param{remove}) {
1750 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1753 print {$transcript} "Requested to add no versions; doing nothing.\n";
1755 __end_control(%info);
1758 # first things first, make the versions fully qualified source
1760 for my $data (@data) {
1761 my $old_data = dclone($data);
1762 # The 'done' field gets a bit weird with version tracking,
1763 # because a bug may be closed by multiple people in different
1764 # branches. Until we have something more flexible, we set it
1765 # every time a bug is fixed, and clear it when a bug is found
1766 # in a version greater than any version in which the bug is
1767 # fixed or when a bug is found and there is no fixed version
1768 my $action = 'Did not alter fixed versions';
1769 my %found_added = ();
1770 my %found_removed = ();
1771 my %fixed_added = ();
1772 my %fixed_removed = ();
1774 if (not $param{add} and not $param{remove}) {
1775 $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
1776 $data->{fixed_versions} = [];
1779 @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
1781 @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
1782 for my $version (keys %versions) {
1784 my @svers = @{$versions{$version}};
1789 if (exists $fixed_versions{$version}) {
1790 $fixed_removed{$version} = 1;
1791 delete $fixed_versions{$version};
1794 for my $sver (@svers) {
1795 if (not exists $fixed_versions{$sver}) {
1796 $fixed_versions{$sver} = 1;
1797 $fixed_added{$sver} = 1;
1801 elsif ($param{remove}) {
1802 # in the case of removal, we only concern ourself with
1803 # the version passed, not the source version it maps
1805 my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
1806 delete $fixed_versions{$_} for @temp;
1807 $fixed_removed{$_} = 1 for @temp;
1810 # set the keys to exactly these values
1811 my @svers = @{$versions{$version}};
1815 for my $sver (@svers) {
1816 if (not exists $fixed_versions{$sver}) {
1817 $fixed_versions{$sver} = 1;
1818 if (exists $fixed_removed{$sver}) {
1819 delete $fixed_removed{$sver};
1822 $fixed_added{$sver} = 1;
1829 $data->{found_versions} = [keys %found_versions];
1830 $data->{fixed_versions} = [keys %fixed_versions];
1832 # If we're supposed to consider reopening, reopen if the
1833 # fixed versions are empty or the greatest found version
1834 # is greater than the greatest fixed version
1835 if ($param{reopen} and defined $data->{done}
1836 and length $data->{done}) {
1837 my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1838 map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
1839 # determine if we need to reopen
1840 my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1841 map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
1842 if (not @fixed_order or
1843 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1850 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1851 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1852 push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
1853 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1854 $action = ucfirst(join ('; ',@changed)) if @changed;
1856 $action .= " and reopened"
1858 if (not $reopened and not @changed) {
1859 print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
1863 append_action_to_log(bug => $data->{bug_num},
1866 old_data => $old_data,
1868 __return_append_to_log_options(
1873 if not exists $param{append_log} or $param{append_log};
1874 writebug($data->{bug_num},$data);
1875 print {$transcript} "$action\n";
1877 __end_control(%info);
1884 set_merged(bug => $ref,
1885 transcript => $transcript,
1886 ($dl > 0 ? (debug => $transcript):()),
1887 requester => $header{from},
1888 request_addr => $controlrequestaddr,
1890 affected_packages => \%affected_packages,
1891 recipients => \%recipients,
1892 merge_with => 12345,
1895 allow_reassign => 1,
1896 reassign_same_source_only => 1,
1901 print {$transcript} "Failed to set merged on $ref: $@";
1905 Sets, adds, or removes the specified merged bugs of a bug
1907 By default, requires
1912 my %param = validate_with(params => \@_,
1913 spec => {bug => {type => SCALAR,
1916 # specific options here
1917 merge_with => {type => ARRAYREF|SCALAR,
1920 remove => {type => BOOLEAN,
1923 force => {type => BOOLEAN,
1926 masterbug => {type => BOOLEAN,
1929 allow_reassign => {type => BOOLEAN,
1932 reassign_different_sources => {type => BOOLEAN,
1936 %append_action_options,
1939 my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
1941 @merging{@merging} = (1) x @merging;
1942 if (grep {$_ !~ /^\d+$/} @merging) {
1943 croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
1945 $param{locks} = {} if not exists $param{locks};
1947 __begin_control(%param,
1950 my ($debug,$transcript) =
1951 @info{qw(debug transcript)};
1952 if (not @merging and exists $param{merge_with}) {
1953 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
1954 __end_control(%info);
1957 my @data = @{$info{data}};
1960 for my $data (@data) {
1961 $data{$data->{bug_num}} = $data;
1962 my @merged_bugs = split / /, $data->{mergedwith};
1963 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
1967 if (not exists $param{merge_with}) {
1968 delete $merged_bugs{$param{bug}};
1969 if (not keys %merged_bugs) {
1970 print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
1971 __end_control(%info);
1974 my $action = "Disconnected #$param{bug} from all other report(s).";
1975 for my $data (@data) {
1976 my $old_data = dclone($data);
1977 if ($data->{bug_num} == $param{bug}) {
1978 $data->{mergedwith} = '';
1981 $data->{mergedwith} =
1984 grep {$_ != $data->{bug_num}}
1987 append_action_to_log(bug => $data->{bug_num},
1990 old_data => $old_data,
1992 __return_append_to_log_options(%param,
1996 if not exists $param{append_log} or $param{append_log};
1997 writebug($data->{bug_num},$data);
1999 print {$transcript} "$action\n";
2000 __end_control(%info);
2003 # lock and load all of the bugs we need
2004 my ($data,$n_locks) =
2005 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2007 locks => $param{locks},
2010 $new_locks += $n_locks;
2012 @data = values %data;
2013 if (not check_limit(data => [@data],
2014 exists $param{limit}?(limit => $param{limit}):(),
2015 transcript => $transcript,
2017 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
2019 for my $data (@data) {
2020 $data{$data->{bug_num}} = $data;
2021 $merged_bugs{$data->{bug_num}} = 1;
2022 my @merged_bugs = split / /, $data->{mergedwith};
2023 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
2024 if (exists $param{affected_bugs}) {
2025 $param{affected_bugs}{$data->{bug_num}} = 1;
2028 __handle_affected_packages(%param,data => [@data]);
2029 my %bug_info_shown; # which bugs have had information shown
2030 $bug_info_shown{$param{bug}} = 1;
2031 add_recipients(data => [@data],
2032 recipients => $param{recipients},
2033 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
2035 (__internal_request()?(transcript => $transcript):()),
2038 # Figure out what the ideal state is for the bug,
2039 my ($merge_status,$bugs_to_merge) =
2040 __calculate_merge_status(\@data,\%data,$param{bug});
2041 # find out if we actually have any bugs to merge
2042 if (not $bugs_to_merge) {
2043 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
2044 for (1..$new_locks) {
2045 unfilelock($param{locks});
2048 __end_control(%info);
2051 # see what changes need to be made to merge the bugs
2052 # check to make sure that the set of changes we need to make is allowed
2053 my ($disallowed_changes,$changes) =
2054 __calculate_merge_changes(\@data,$merge_status,\%param);
2055 # at this point, stop if there are disallowed changes, otherwise
2056 # make the allowed changes, and then reread the bugs in question
2057 # to get the new data, then recaculate the merges; repeat
2058 # reloading and recalculating until we try too many times or there
2059 # are no changes to make.
2062 # we will allow at most 4 times through this; more than 1
2063 # shouldn't really happen.
2065 while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
2066 if ($attempts > 1) {
2067 print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
2069 if (@{$disallowed_changes}) {
2070 # figure out the problems
2071 print {$transcript} "Unable to merge bugs because:\n";
2072 for my $change (@{$disallowed_changes}) {
2073 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2075 if ($attempts > 0) {
2076 __end_control(%info);
2077 croak "Some bugs were altered while attempting to merge";
2080 __end_control(%info);
2081 croak "Did not alter merged bugs";
2084 my @bugs_to_change = keys %{$changes};
2085 for my $change_bug (@bugs_to_change) {
2086 next unless exists $changes->{$change_bug};
2087 $bug_changed{$change_bug}++;
2088 print {$transcript} __bug_info($data{$change_bug}) if
2089 $param{show_bug_info} and not __internal_request(1);
2090 $bug_info_shown{$change_bug} = 1;
2091 __allow_relocking($param{locks},[keys %data]);
2092 for my $change (@{$changes->{$change_bug}}) {
2093 if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
2094 my %target_blockedby;
2095 @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
2096 my %unhandled_targets = %target_blockedby;
2097 for my $key (split / /,$change->{orig_value}) {
2098 delete $unhandled_targets{$key};
2099 next if exists $target_blockedby{$key};
2100 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2101 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2104 keys %common_options,
2105 keys %append_action_options),
2108 for my $key (keys %unhandled_targets) {
2109 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2110 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2113 keys %common_options,
2114 keys %append_action_options),
2119 $change->{function}->(bug => $change->{bug},
2120 $change->{key}, $change->{func_value},
2121 exists $change->{options}?@{$change->{options}}:(),
2123 keys %common_options,
2124 keys %append_action_options),
2128 __disallow_relocking($param{locks});
2129 my ($data,$n_locks) =
2130 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2132 locks => $param{locks},
2136 $new_locks += $n_locks;
2139 @data = values %data;
2140 ($merge_status,$bugs_to_merge) =
2141 __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
2142 ($disallowed_changes,$changes) =
2143 __calculate_merge_changes(\@data,$merge_status,\%param);
2144 $attempts = max(values %bug_changed);
2147 if ($param{show_bug_info} and not __internal_request(1)) {
2148 for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
2149 next if $bug_info_shown{$data->{bug_num}};
2150 print {$transcript} __bug_info($data);
2153 if (keys %{$changes} or @{$disallowed_changes}) {
2154 print {$transcript} "After four attempts, the following changes were unable to be made:\n";
2155 for (1..$new_locks) {
2156 unfilelock($param{locks});
2159 __end_control(%info);
2160 for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
2161 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2163 die "Unable to modify bugs so they could be merged";
2167 # finally, we can merge the bugs
2168 my $action = "Merged ".join(' ',sort { $a <=> $b } keys %merged_bugs);
2169 for my $data (@data) {
2170 my $old_data = dclone($data);
2171 $data->{mergedwith} =
2174 grep {$_ != $data->{bug_num}}
2176 append_action_to_log(bug => $data->{bug_num},
2179 old_data => $old_data,
2181 __return_append_to_log_options(%param,
2185 if not exists $param{append_log} or $param{append_log};
2186 writebug($data->{bug_num},$data);
2188 print {$transcript} "$action\n";
2189 # unlock the extra locks that we got earlier
2190 for (1..$new_locks) {
2191 unfilelock($param{locks});
2194 __end_control(%info);
2197 sub __allow_relocking{
2198 my ($locks,$bugs) = @_;
2200 my @locks = (@{$bugs},'merge');
2201 for my $lock (@locks) {
2202 my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
2203 next unless @lockfiles;
2204 $locks->{relockable}{$lockfiles[0]} = 0;
2208 sub __disallow_relocking{
2210 delete $locks->{relockable};
2213 sub __lock_and_load_merged_bugs{
2215 validate_with(params => \@_,
2217 {bugs_to_load => {type => ARRAYREF,
2218 default => sub {[]},
2220 data => {type => HASHREF|ARRAYREF,
2222 locks => {type => HASHREF,
2223 default => sub {{};},
2225 reload_all => {type => BOOLEAN,
2228 debug => {type => HANDLE,
2234 if (ref($param{data}) eq 'ARRAY') {
2235 for my $data (@{$param{data}}) {
2236 $data{$data->{bug_num}} = dclone($data);
2240 %data = %{dclone($param{data})};
2242 my @bugs_to_load = @{$param{bugs_to_load}};
2243 if ($param{reload_all}) {
2244 push @bugs_to_load, keys %data;
2247 @temp{@bugs_to_load} = (1) x @bugs_to_load;
2248 @bugs_to_load = keys %temp;
2249 my %loaded_this_time;
2251 while ($bug_to_load = shift @bugs_to_load) {
2252 if (not $param{reload_all}) {
2253 next if exists $data{$bug_to_load};
2256 next if $loaded_this_time{$bug_to_load};
2259 if ($param{reload_all}) {
2260 if (exists $data{$bug_to_load}) {
2265 read_bug(bug => $bug_to_load,
2267 locks => $param{locks},
2269 die "Unable to load bug $bug_to_load";
2270 print {$param{debug}} "read bug $bug_to_load\n";
2271 $data{$data->{bug_num}} = $data;
2272 $new_locks += $lock_bug;
2273 $loaded_this_time{$data->{bug_num}} = 1;
2275 grep {not exists $data{$_}}
2276 split / /,$data->{mergedwith};
2278 return (\%data,$new_locks);
2282 sub __calculate_merge_status{
2283 my ($data_a,$data_h,$master_bug,$merge_status) = @_;
2284 my %merge_status = %{$merge_status // {}};
2286 my $bugs_to_merge = 0;
2287 for my $data (@{$data_a}) {
2288 # check to see if this bug is unmerged in the set
2289 if (not length $data->{mergedwith} or
2290 grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
2291 $merged_bugs{$data->{bug_num}} = 1;
2294 # the master_bug is the bug that every other bug is made to
2295 # look like. However, if merge is set, tags, fixed and found
2297 if ($data->{bug_num} == $master_bug) {
2298 for (qw(package forwarded severity blocks blockedby done owner summary outlook affects)) {
2299 $merge_status{$_} = $data->{$_}
2302 if (defined $merge_status) {
2303 next unless $data->{bug_num} == $master_bug;
2305 $merge_status{tag} = {} if not exists $merge_status{tag};
2306 for my $tag (split /\s+/, $data->{keywords}) {
2307 $merge_status{tag}{$tag} = 1;
2309 $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
2310 for (qw(fixed found)) {
2311 @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
2314 # if there is a non-source qualified version with a corresponding
2315 # source qualified version, we only want to merge the source
2316 # qualified version(s)
2317 for (qw(fixed found)) {
2318 my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
2319 for my $unqualified_version (@unqualified_versions) {
2320 if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
2321 delete $merge_status{"${_}_versions"}{$unqualified_version};
2325 return (\%merge_status,$bugs_to_merge);
2330 sub __calculate_merge_changes{
2331 my ($datas,$merge_status,$param) = @_;
2333 my @disallowed_changes;
2334 for my $data (@{$datas}) {
2335 # things that can be forced
2337 # * func is the function to set the new value
2339 # * key is the key of the function to set the value,
2341 # * modify_value is a function which is called to modify the new
2342 # value so that the function will accept it
2344 # * options is an ARRAYREF of options to pass to the function
2346 # * allowed is a BOOLEAN which controls whether this setting
2347 # is allowed to be different by default.
2348 my %force_functions =
2349 (forwarded => {func => \&set_forwarded,
2353 severity => {func => \&set_severity,
2357 blocks => {func => \&set_blocks,
2358 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2362 blockedby => {func => \&set_blocks,
2363 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2367 done => {func => \&set_done,
2371 owner => {func => \&owner,
2375 summary => {func => \&summary,
2379 outlook => {func => \&outlook,
2383 affects => {func => \&affects,
2387 package => {func => \&set_package,
2391 keywords => {func => \&set_tag,
2393 modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
2396 fixed_versions => {func => \&set_fixed,
2398 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2401 found_versions => {func => \&set_found,
2403 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2407 for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
2408 # if the ideal bug already has the field set properly, we
2410 if ($field eq 'keywords'){
2411 next if join(' ',sort split /\s+/,$data->{keywords}) eq
2412 join(' ',sort keys %{$merge_status->{tag}});
2414 elsif ($field =~ /^(?:fixed|found)_versions$/) {
2415 next if join(' ', sort @{$data->{$field}}) eq
2416 join(' ',sort keys %{$merge_status->{$field}});
2418 elsif ($field eq 'done') {
2419 # for done, we only care if the bug is done or not
2420 # done, not the value it's set to.
2421 if (defined $merge_status->{$field} and length $merge_status->{$field} and
2422 defined $data->{$field} and length $data->{$field}) {
2425 elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
2426 (not defined $data->{$field} or not length $data->{$field})
2431 elsif ($merge_status->{$field} eq $data->{$field}) {
2436 bug => $data->{bug_num},
2437 orig_value => $data->{$field},
2439 (exists $force_functions{$field}{modify_value} ?
2440 $force_functions{$field}{modify_value}->($merge_status->{$field}):
2441 $merge_status->{$field}),
2442 value => $merge_status->{$field},
2443 function => $force_functions{$field}{func},
2444 key => $force_functions{$field}{key},
2445 options => $force_functions{$field}{options},
2446 allowed => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
2448 $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
2449 $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
2450 if ($param->{force} or $change->{allowed}) {
2451 if ($field ne 'package' or $change->{allowed}) {
2452 push @{$changes{$data->{bug_num}}},$change;
2455 if ($param->{allow_reassign}) {
2456 if ($param->{reassign_different_sources}) {
2457 push @{$changes{$data->{bug_num}}},$change;
2460 # allow reassigning if binary_to_source returns at
2461 # least one of the same source packages
2462 my @merge_status_source =
2463 binary_to_source(package => $merge_status->{package},
2466 my @other_bug_source =
2467 binary_to_source(package => $data->{package},
2470 my %merge_status_sources;
2471 @merge_status_sources{@merge_status_source} =
2472 (1) x @merge_status_source;
2473 if (grep {$merge_status_sources{$_}} @other_bug_source) {
2474 push @{$changes{$data->{bug_num}}},$change;
2479 push @disallowed_changes,$change;
2481 # blocks and blocked by are weird; we have to go through and
2482 # set blocks to the other half of the merged bugs
2484 return (\@disallowed_changes,\%changes);
2490 affects(bug => $ref,
2491 transcript => $transcript,
2492 ($dl > 0 ? (debug => $transcript):()),
2493 requester => $header{from},
2494 request_addr => $controlrequestaddr,
2496 affected_packages => \%affected_packages,
2497 recipients => \%recipients,
2505 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
2508 This marks a bug as affecting packages which the bug is not actually
2509 in. This should only be used in cases where fixing the bug instantly
2510 resolves the problem in the other packages.
2512 By default, the packages are set to the list of packages passed.
2513 However, if you pass add => 1 or remove => 1, the list of packages
2514 passed are added or removed from the affects list, respectively.
2519 my %param = validate_with(params => \@_,
2520 spec => {bug => {type => SCALAR,
2523 # specific options here
2524 package => {type => SCALAR|ARRAYREF|UNDEF,
2527 add => {type => BOOLEAN,
2530 remove => {type => BOOLEAN,
2534 %append_action_options,
2537 if ($param{add} and $param{remove}) {
2538 croak "Asking to both add and remove affects is nonsensical";
2540 if (not defined $param{package}) {
2541 $param{package} = [];
2544 __begin_control(%param,
2545 command => 'affects'
2547 my ($debug,$transcript) =
2548 @info{qw(debug transcript)};
2549 my @data = @{$info{data}};
2551 for my $data (@data) {
2553 print {$debug} "Going to change affects\n";
2554 my @packages = splitpackages($data->{affects});
2556 @packages{@packages} = (1) x @packages;
2559 for my $package (make_list($param{package})) {
2560 next unless defined $package and length $package;
2561 if (not $packages{$package}) {
2562 $packages{$package} = 1;
2563 push @added,$package;
2567 $action = "Added indication that $data->{bug_num} affects ".
2568 english_join(\@added);
2571 elsif ($param{remove}) {
2573 for my $package (make_list($param{package})) {
2574 if ($packages{$package}) {
2575 next unless defined $package and length $package;
2576 delete $packages{$package};
2577 push @removed,$package;
2580 $action = "Removed indication that $data->{bug_num} affects " .
2581 english_join(\@removed);
2584 my %added_packages = ();
2585 my %removed_packages = %packages;
2587 for my $package (make_list($param{package})) {
2588 next unless defined $package and length $package;
2589 $packages{$package} = 1;
2590 delete $removed_packages{$package};
2591 $added_packages{$package} = 1;
2593 if (keys %removed_packages) {
2594 $action = "Removed indication that $data->{bug_num} affects ".
2595 english_join([keys %removed_packages]);
2596 $action .= "\n" if keys %added_packages;
2598 if (keys %added_packages) {
2599 $action .= "Added indication that $data->{bug_num} affects " .
2600 english_join([keys %added_packages]);
2603 if (not length $action) {
2604 print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
2607 my $old_data = dclone($data);
2608 $data->{affects} = join(',',keys %packages);
2609 append_action_to_log(bug => $data->{bug_num},
2611 command => 'affects',
2613 old_data => $old_data,
2614 __return_append_to_log_options(
2619 if not exists $param{append_log} or $param{append_log};
2620 writebug($data->{bug_num},$data);
2621 print {$transcript} "$action\n";
2623 __end_control(%info);
2627 =head1 SUMMARY FUNCTIONS
2632 summary(bug => $ref,
2633 transcript => $transcript,
2634 ($dl > 0 ? (debug => $transcript):()),
2635 requester => $header{from},
2636 request_addr => $controlrequestaddr,
2638 affected_packages => \%affected_packages,
2639 recipients => \%recipients,
2645 print {$transcript} "Failed to mark $ref with summary foo: $@";
2648 Handles all setting of summary fields
2650 If summary is undef, unsets the summary
2652 If summary is 0 or -1, sets the summary to the first paragraph contained in
2655 If summary is a positive integer, sets the summary to the message specified.
2657 Otherwise, sets summary to the value passed.
2663 # outlook and summary are exactly the same, basically
2664 return _summary('summary',@_);
2667 =head1 OUTLOOK FUNCTIONS
2672 outlook(bug => $ref,
2673 transcript => $transcript,
2674 ($dl > 0 ? (debug => $transcript):()),
2675 requester => $header{from},
2676 request_addr => $controlrequestaddr,
2678 affected_packages => \%affected_packages,
2679 recipients => \%recipients,
2685 print {$transcript} "Failed to mark $ref with outlook foo: $@";
2688 Handles all setting of outlook fields
2690 If outlook is undef, unsets the outlook
2692 If outlook is 0, sets the outlook to the first paragraph contained in
2695 If outlook is a positive integer, sets the outlook to the message specified.
2697 Otherwise, sets outlook to the value passed.
2703 return _summary('outlook',@_);
2707 my ($cmd,@params) = @_;
2708 my %param = validate_with(params => \@params,
2709 spec => {bug => {type => SCALAR,
2712 # specific options here
2713 $cmd , {type => SCALAR|UNDEF,
2717 %append_action_options,
2721 __begin_control(%param,
2724 my ($debug,$transcript) =
2725 @info{qw(debug transcript)};
2726 my @data = @{$info{data}};
2727 # figure out the log that we're going to use
2729 my $summary_msg = '';
2731 if (not defined $param{$cmd}) {
2733 print {$debug} "Removing $cmd fields\n";
2734 $action = "Removed $cmd";
2736 elsif ($param{$cmd} =~ /^-?\d+$/) {
2738 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
2739 if ($param{$cmd} == 0 or $param{$cmd} == -1) {
2740 $log = $param{message};
2741 $summary_msg = @records + 1;
2744 if (($param{$cmd} - 1 ) > $#records) {
2745 die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
2747 my $record = $records[($param{$cmd} - 1 )];
2748 if ($record->{type} !~ /incoming-recv|recips/) {
2749 die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
2751 $summary_msg = $param{$cmd};
2752 $log = [$record->{text}];
2754 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
2755 my $body = $p_o->{body};
2756 my $in_pseudoheaders = 0;
2758 # walk through body until we get non-blank lines
2759 for my $line (@{$body}) {
2760 if ($line =~ /^\s*$/) {
2761 if (length $paragraph) {
2762 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
2768 $in_pseudoheaders = 0;
2771 # skip a paragraph if it looks like it's control or
2773 if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity|Control)\:\s+\S}xi or #pseudo headers
2774 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
2775 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
2776 debug|(?:not|)forwarded|priority|
2777 (?:un|)block|limit|(?:un|)archive|
2778 reassign|retitle|affects|wrongpackage
2779 (?:un|force|)merge|user(?:category|tags?|)
2781 if (not length $paragraph) {
2782 print {$debug} "Found control/pseudo-headers and skiping them\n";
2783 $in_pseudoheaders = 1;
2787 next if $in_pseudoheaders;
2788 $paragraph .= $line ." \n";
2790 print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
2791 $summary = $paragraph;
2792 $summary =~ s/[\n\r]/ /g;
2793 if (not length $summary) {
2794 die "Unable to find $cmd message to use";
2796 # trim off a trailing spaces
2797 $summary =~ s/\ *$//;
2800 $summary = $param{$cmd};
2802 for my $data (@data) {
2803 print {$debug} "Going to change $cmd\n";
2804 if (((not defined $summary or not length $summary) and
2805 (not defined $data->{$cmd} or not length $data->{$cmd})) or
2806 $summary eq $data->{$cmd}) {
2807 print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
2810 if (length $summary) {
2811 if (length $data->{$cmd}) {
2812 $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
2815 $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
2818 my $old_data = dclone($data);
2819 $data->{$cmd} = $summary;
2820 append_action_to_log(bug => $data->{bug_num},
2822 old_data => $old_data,
2825 __return_append_to_log_options(
2830 if not exists $param{append_log} or $param{append_log};
2831 writebug($data->{bug_num},$data);
2832 print {$transcript} "$action\n";
2834 __end_control(%info);
2842 clone_bug(bug => $ref,
2843 transcript => $transcript,
2844 ($dl > 0 ? (debug => $transcript):()),
2845 requester => $header{from},
2846 request_addr => $controlrequestaddr,
2848 affected_packages => \%affected_packages,
2849 recipients => \%recipients,
2854 print {$transcript} "Failed to clone bug $ref bar: $@";
2857 Clones the given bug.
2859 We currently don't support cloning merged bugs, but this could be
2860 handled by internally unmerging, cloning, then remerging the bugs.
2865 my %param = validate_with(params => \@_,
2866 spec => {bug => {type => SCALAR,
2869 new_bugs => {type => ARRAYREF,
2871 new_clones => {type => HASHREF,
2875 %append_action_options,
2879 __begin_control(%param,
2882 my $transcript = $info{transcript};
2883 my @data = @{$info{data}};
2886 for my $data (@data) {
2887 if (length($data->{mergedwith})) {
2888 die "Bug is marked as being merged with others. Use an existing clone.\n";
2892 die "Not exactly one bug‽ This shouldn't happen.";
2894 my $data = $data[0];
2896 for my $newclone_id (@{$param{new_bugs}}) {
2897 my $new_bug_num = new_bug(copy => $data->{bug_num});
2898 $param{new_clones}{$newclone_id} = $new_bug_num;
2899 $clones{$newclone_id} = $new_bug_num;
2901 my @new_bugs = sort values %clones;
2903 for my $new_bug (@new_bugs) {
2904 # no collapsed ids or the higher collapsed id is not one less
2905 # than the next highest new bug
2906 if (not @collapsed_ids or
2907 $collapsed_ids[-1][1]+1 != $new_bug) {
2908 push @collapsed_ids,[$new_bug,$new_bug];
2911 $collapsed_ids[-1][1] = $new_bug;
2915 for my $ci (@collapsed_ids) {
2916 if ($ci->[0] == $ci->[1]) {
2917 push @collapsed,$ci->[0];
2920 push @collapsed,$ci->[0].'-'.$ci->[1]
2923 my $collapsed_str = english_join(\@collapsed);
2924 $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
2925 for my $new_bug (@new_bugs) {
2926 append_action_to_log(bug => $new_bug,
2928 __return_append_to_log_options(
2933 if not exists $param{append_log} or $param{append_log};
2935 append_action_to_log(bug => $data->{bug_num},
2937 __return_append_to_log_options(
2942 if not exists $param{append_log} or $param{append_log};
2943 writebug($data->{bug_num},$data);
2944 print {$transcript} "$action\n";
2945 __end_control(%info);
2946 # bugs that this bug is blocking are also blocked by the new clone(s)
2947 for my $bug (split ' ', $data->{blocks}) {
2948 for my $new_bug (@new_bugs) {
2949 set_blocks(bug => $bug,
2953 keys %common_options,
2954 keys %append_action_options),
2958 # bugs that are blocking this bug are also blocking the new clone(s)
2959 for my $bug (split ' ', $data->{blockedby}) {
2960 for my $new_bug (@new_bugs) {
2961 set_blocks(bug => $new_bug,
2965 keys %common_options,
2966 keys %append_action_options),
2974 =head1 OWNER FUNCTIONS
2980 transcript => $transcript,
2981 ($dl > 0 ? (debug => $transcript):()),
2982 requester => $header{from},
2983 request_addr => $controlrequestaddr,
2985 recipients => \%recipients,
2991 print {$transcript} "Failed to mark $ref as having an owner: $@";
2994 Handles all setting of the owner field; given an owner of undef or of
2995 no length, indicates that a bug is not owned by anyone.
3000 my %param = validate_with(params => \@_,
3001 spec => {bug => {type => SCALAR,
3004 owner => {type => SCALAR|UNDEF,
3007 %append_action_options,
3011 __begin_control(%param,
3014 my ($debug,$transcript) =
3015 @info{qw(debug transcript)};
3016 my @data = @{$info{data}};
3018 for my $data (@data) {
3019 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
3020 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
3021 if (not defined $param{owner} or not length $param{owner}) {
3022 if (not defined $data->{owner} or not length $data->{owner}) {
3023 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
3027 $action = "Removed annotation that $config{bug} was owned by " .
3031 if ($data->{owner} eq $param{owner}) {
3032 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
3035 if (length $data->{owner}) {
3036 $action = "Owner changed from $data->{owner} to $param{owner}.";
3039 $action = "Owner recorded as $param{owner}."
3042 my $old_data = dclone($data);
3043 $data->{owner} = $param{owner};
3044 append_action_to_log(bug => $data->{bug_num},
3047 old_data => $old_data,
3049 __return_append_to_log_options(
3054 if not exists $param{append_log} or $param{append_log};
3055 writebug($data->{bug_num},$data);
3056 print {$transcript} "$action\n";
3058 __end_control(%info);
3062 =head1 ARCHIVE FUNCTIONS
3069 bug_archive(bug => $bug_num,
3071 transcript => \$transcript,
3076 transcript("Unable to archive $bug_num\n");
3079 transcript($transcript);
3082 This routine archives a bug
3086 =item bug -- bug number
3088 =item check_archiveable -- check wether a bug is archiveable before
3089 archiving; defaults to 1
3091 =item archive_unarchived -- whether to archive bugs which have not
3092 previously been archived; defaults to 1. [Set to 0 when used from
3095 =item ignore_time -- whether to ignore time constraints when archiving
3096 a bug; defaults to 0.
3103 my %param = validate_with(params => \@_,
3104 spec => {bug => {type => SCALAR,
3107 check_archiveable => {type => BOOLEAN,
3110 archive_unarchived => {type => BOOLEAN,
3113 ignore_time => {type => BOOLEAN,
3117 %append_action_options,
3120 my %info = __begin_control(%param,
3121 command => 'archive',
3123 my ($debug,$transcript) = @info{qw(debug transcript)};
3124 my @data = @{$info{data}};
3125 my @bugs = @{$info{bugs}};
3126 my $action = "$config{bug} archived.";
3127 if ($param{check_archiveable} and
3128 not bug_archiveable(bug=>$param{bug},
3129 ignore_time => $param{ignore_time},
3131 print {$transcript} "Bug $param{bug} cannot be archived\n";
3132 die "Bug $param{bug} cannot be archived";
3134 if (not $param{archive_unarchived} and
3135 not exists $data[0]{unarchived}
3137 print {$transcript} "$param{bug} has not been archived previously\n";
3138 die "$param{bug} has not been archived previously";
3140 add_recipients(recipients => $param{recipients},
3143 transcript => $transcript,
3145 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3146 for my $bug (@bugs) {
3147 if ($param{check_archiveable}) {
3148 die "Bug $bug cannot be archived (but $param{bug} can?)"
3149 unless bug_archiveable(bug=>$bug,
3150 ignore_time => $param{ignore_time},
3154 # If we get here, we can archive/remove this bug
3155 print {$debug} "$param{bug} removing\n";
3156 for my $bug (@bugs) {
3157 #print "$param{bug} removing $bug\n" if $debug;
3158 my $dir = get_hashname($bug);
3159 # First indicate that this bug is being archived
3160 append_action_to_log(bug => $bug,
3162 command => 'archive',
3163 # we didn't actually change the data
3164 # when we archived, so we don't pass
3165 # a real new_data or old_data
3168 __return_append_to_log_options(
3173 if not exists $param{append_log} or $param{append_log};
3174 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
3175 if ($config{save_old_bugs}) {
3176 mkpath("$config{spool_dir}/archive/$dir");
3177 foreach my $file (@files_to_remove) {
3178 link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3179 copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3180 # we need to bail out here if things have
3181 # gone horribly wrong to avoid removing a
3183 die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
3186 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
3188 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
3189 print {$debug} "deleted $bug (from $param{bug})\n";
3191 bughook_archive(@bugs);
3192 __end_control(%info);
3195 =head2 bug_unarchive
3199 bug_unarchive(bug => $bug_num,
3201 transcript => \$transcript,
3206 transcript("Unable to archive bug: $bug_num");
3208 transcript($transcript);
3210 This routine unarchives a bug
3215 my %param = validate_with(params => \@_,
3216 spec => {bug => {type => SCALAR,
3220 %append_action_options,
3224 my %info = __begin_control(%param,
3226 command=>'unarchive');
3227 my ($debug,$transcript) =
3228 @info{qw(debug transcript)};
3229 my @bugs = @{$info{bugs}};
3230 my $action = "$config{bug} unarchived.";
3231 my @files_to_remove;
3232 for my $bug (@bugs) {
3233 print {$debug} "$param{bug} removing $bug\n";
3234 my $dir = get_hashname($bug);
3235 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
3236 mkpath("archive/$dir");
3237 foreach my $file (@files_to_copy) {
3238 # die'ing here sucks
3239 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3240 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3241 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
3243 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
3244 print {$transcript} "Unarchived $config{bug} $bug\n";
3246 unlink(@files_to_remove) or die "Unable to unlink bugs";
3247 # Indicate that this bug has been archived previously
3248 for my $bug (@bugs) {
3249 my $newdata = readbug($bug);
3250 my $old_data = dclone($newdata);
3251 if (not defined $newdata) {
3252 print {$transcript} "$config{bug} $bug disappeared!\n";
3253 die "Bug $bug disappeared!";
3255 $newdata->{unarchived} = time;
3256 append_action_to_log(bug => $bug,
3258 command => 'unarchive',
3259 new_data => $newdata,
3260 old_data => $old_data,
3261 __return_append_to_log_options(
3266 if not exists $param{append_log} or $param{append_log};
3267 writebug($bug,$newdata);
3269 __end_control(%info);
3272 =head2 append_action_to_log
3274 append_action_to_log
3276 This should probably be moved to Debbugs::Log; have to think that out
3281 sub append_action_to_log{
3282 my %param = validate_with(params => \@_,
3283 spec => {bug => {type => SCALAR,
3286 new_data => {type => HASHREF,
3289 old_data => {type => HASHREF,
3292 command => {type => SCALAR,
3295 action => {type => SCALAR,
3297 requester => {type => SCALAR,
3300 request_addr => {type => SCALAR,
3303 location => {type => SCALAR,
3306 message => {type => SCALAR|ARRAYREF,
3309 recips => {type => SCALAR|ARRAYREF,
3312 desc => {type => SCALAR,
3315 get_lock => {type => BOOLEAN,
3318 locks => {type => HASHREF,
3322 # append_action_options here
3323 # because some of these
3324 # options aren't actually
3325 # optional, even though the
3326 # original function doesn't
3330 # Fix this to use $param{location}
3331 my $log_location = buglog($param{bug});
3332 die "Unable to find .log for $param{bug}"
3333 if not defined $log_location;
3334 if ($param{get_lock}) {
3335 filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
3339 my $logfh = IO::File->new(">>$log_location") or
3340 die "Unable to open $log_location for appending: $!";
3341 # determine difference between old and new
3343 if (exists $param{old_data} and exists $param{new_data}) {
3344 my $old_data = dclone($param{old_data});
3345 my $new_data = dclone($param{new_data});
3346 for my $key (keys %{$old_data}) {
3347 if (not exists $Debbugs::Status::fields{$key}) {
3348 delete $old_data->{$key};
3351 next unless exists $new_data->{$key};
3352 next unless defined $new_data->{$key};
3353 if (not defined $old_data->{$key}) {
3354 delete $old_data->{$key};
3357 if (ref($new_data->{$key}) and
3358 ref($old_data->{$key}) and
3359 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3360 local $Storable::canonical = 1;
3361 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3362 delete $new_data->{$key};
3363 delete $old_data->{$key};
3366 elsif ($new_data->{$key} eq $old_data->{$key}) {
3367 delete $new_data->{$key};
3368 delete $old_data->{$key};
3371 for my $key (keys %{$new_data}) {
3372 if (not exists $Debbugs::Status::fields{$key}) {
3373 delete $new_data->{$key};
3376 next unless exists $old_data->{$key};
3377 next unless defined $old_data->{$key};
3378 if (not defined $new_data->{$key} or
3379 not exists $Debbugs::Status::fields{$key}) {
3380 delete $new_data->{$key};
3383 if (ref($new_data->{$key}) and
3384 ref($old_data->{$key}) and
3385 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3386 local $Storable::canonical = 1;
3387 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3388 delete $new_data->{$key};
3389 delete $old_data->{$key};
3392 elsif ($new_data->{$key} eq $old_data->{$key}) {
3393 delete $new_data->{$key};
3394 delete $old_data->{$key};
3397 $data_diff .= "<!-- new_data:\n";
3399 for my $key (keys %{$new_data}) {
3400 if (not exists $Debbugs::Status::fields{$key}) {
3401 warn "No such field $key";
3404 $nd{$key} = $new_data->{$key};
3405 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
3407 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
3408 $data_diff .= "-->\n";
3409 $data_diff .= "<!-- old_data:\n";
3411 for my $key (keys %{$old_data}) {
3412 if (not exists $Debbugs::Status::fields{$key}) {
3413 warn "No such field $key";
3416 $od{$key} = $old_data->{$key};
3417 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
3419 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
3420 $data_diff .= "-->\n";
3423 (exists $param{command} ?
3424 "<!-- command:".html_escape(encode_utf8_safely($param{command}))." -->\n":""
3426 (length $param{requester} ?
3427 "<!-- requester: ".html_escape(encode_utf8_safely($param{requester}))." -->\n":""
3429 (length $param{request_addr} ?
3430 "<!-- request_addr: ".html_escape(encode_utf8_safely($param{request_addr}))." -->\n":""
3432 "<!-- time:".time()." -->\n",
3434 "<strong>".html_escape(encode_utf8_safely($param{action}))."</strong>\n");
3435 if (length $param{requester}) {
3436 $msg .= "Request was from <code>".html_escape(encode_utf8_safely($param{requester}))."</code>\n";
3438 if (length $param{request_addr}) {
3439 $msg .= "to <code>".html_escape(encode_utf8_safely($param{request_addr}))."</code>";
3441 if (length $param{desc}) {
3442 $msg .= ":<br>\n".encode_utf8_safely($param{desc})."\n";
3447 push @records, {type => 'html',
3451 if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
3452 push @records, {type => exists $param{recips}?'recips':'incoming-recv',
3453 exists $param{recips}?(recips => [map {encode_utf8_safely($_)} make_list($param{recips})]):(),
3454 text => join('',make_list($param{message})),
3457 write_log_records(logfh=>$logfh,
3458 records => \@records,
3460 close $logfh or die "Unable to close $log_location: $!";
3461 if ($param{get_lock}) {
3462 unfilelock(exists $param{locks}?$param{locks}:());
3470 =head1 PRIVATE FUNCTIONS
3472 =head2 __handle_affected_packages
3474 __handle_affected_packages(affected_packages => {},
3482 sub __handle_affected_packages{
3483 my %param = validate_with(params => \@_,
3484 spec => {%common_options,
3485 data => {type => ARRAYREF|HASHREF
3490 for my $data (make_list($param{data})) {
3491 next unless exists $data->{package} and defined $data->{package};
3492 my @packages = split /\s*,\s*/,$data->{package};
3493 @{$param{affected_packages}}{@packages} = (1) x @packages;
3497 =head2 __handle_debug_transcript
3499 my ($debug,$transcript) = __handle_debug_transcript(%param);
3501 Returns a debug and transcript filehandle
3506 sub __handle_debug_transcript{
3507 my %param = validate_with(params => \@_,
3508 spec => {%common_options},
3511 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
3512 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3513 return ($debug,$transcript);
3520 Produces a small bit of bug information to kick out to the transcript
3527 next unless defined $data and exists $data->{bug_num};
3528 $return .= "Bug #".($data->{bug_num}||'').
3529 ((defined $data->{done} and length $data->{done})?
3530 " {Done: $data->{done}}":''
3532 " [".($data->{package}||'(no package)'). "] ".
3533 ($data->{subject}||'(no subject)')."\n";
3539 =head2 __internal_request
3541 __internal_request()
3542 __internal_request($level)
3544 Returns true if the caller of the function calling __internal_request
3545 belongs to __PACKAGE__
3547 This allows us to be magical, and don't bother to print bug info if
3548 the second caller is from this package, amongst other things.
3550 An optional level is allowed, which increments the number of levels to
3551 check by the given value. [This is basically for use by internal
3552 functions like __begin_control which are always called by
3557 sub __internal_request{
3559 $l = 0 if not defined $l;
3560 if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
3566 sub __return_append_to_log_options{
3568 my $action = $param{action} if exists $param{action};
3569 if (not exists $param{requester}) {
3570 $param{requester} = $config{control_internal_requester};
3572 if (not exists $param{request_addr}) {
3573 $param{request_addr} = $config{control_internal_request_addr};
3575 if (not exists $param{message}) {
3576 my $date = rfc822_date();
3578 encode_headers(fill_in_template(template => 'mail/fake_control_message',
3579 variables => {request_addr => $param{request_addr},
3580 requester => $param{requester},
3586 if (not defined $action) {
3587 carp "Undefined action!";
3588 $action = "unknown action";
3590 return (action => $action,
3591 hash_slice(%param,keys %append_action_options),
3595 =head2 __begin_control
3597 my %info = __begin_control(%param,
3599 command=>'unarchive');
3600 my ($debug,$transcript) = @info{qw(debug transcript)};
3601 my @data = @{$info{data}};
3602 my @bugs = @{$info{bugs}};
3605 Starts the process of modifying a bug; handles all of the generic
3606 things that almost every control request needs
3608 Returns a hash containing
3612 =item new_locks -- number of new locks taken out by this call
3614 =item debug -- the debug file handle
3616 =item transcript -- the transcript file handle
3618 =item data -- an arrayref containing the data of the bugs
3619 corresponding to this request
3621 =item bugs -- an arrayref containing the bug numbers of the bugs
3622 corresponding to this request
3630 sub __begin_control {
3631 my %param = validate_with(params => \@_,
3632 spec => {bug => {type => SCALAR,
3635 archived => {type => BOOLEAN,
3638 command => {type => SCALAR,
3646 my ($debug,$transcript) = __handle_debug_transcript(@_);
3647 print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
3648 # print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
3649 $lockhash = $param{locks} if exists $param{locks};
3651 my $old_die = $SIG{__DIE__};
3652 $SIG{__DIE__} = *sig_die{CODE};
3654 ($new_locks, @data) =
3655 lock_read_all_merged_bugs(bug => $param{bug},
3656 $param{archived}?(location => 'archive'):(),
3657 exists $param{locks} ? (locks => $param{locks}):(),
3659 $locks += $new_locks;
3661 die "Unable to read any bugs successfully.";
3663 if (not $param{archived}) {
3664 for my $data (@data) {
3665 if ($data->{archived}) {
3666 die "Not altering archived bugs; see unarchive.";
3670 if (not check_limit(data => \@data,
3671 exists $param{limit}?(limit => $param{limit}):(),
3672 transcript => $transcript,
3674 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
3677 __handle_affected_packages(%param,data => \@data);
3678 print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
3679 print {$debug} "$param{bug} read $locks locks\n";
3680 if (not @data or not defined $data[0]) {
3681 print {$transcript} "No bug found for $param{bug}\n";
3682 die "No bug found for $param{bug}";
3685 add_recipients(data => \@data,
3686 recipients => $param{recipients},
3687 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
3689 (__internal_request()?(transcript => $transcript):()),
3692 print {$debug} "$param{bug} read done\n";
3693 my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
3694 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3695 return (data => \@data,
3697 old_die => $old_die,
3698 new_locks => $new_locks,
3700 transcript => $transcript,
3702 exists $param{locks}?(locks => $param{locks}):(),
3706 =head2 __end_control
3708 __end_control(%info);
3710 Handles tearing down from a control request
3716 if (exists $info{new_locks} and $info{new_locks} > 0) {
3717 print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
3718 for (1..$info{new_locks}) {
3719 unfilelock(exists $info{locks}?$info{locks}:());
3723 $SIG{__DIE__} = $info{old_die};
3724 if (exists $info{param}{affected_bugs}) {
3725 @{$info{param}{affected_bugs}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
3727 add_recipients(recipients => $info{param}{recipients},
3728 (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
3729 data => $info{data},
3730 debug => $info{debug},
3731 transcript => $info{transcript},
3733 __handle_affected_packages(%{$info{param}},data=>$info{data});
3739 check_limit(data => \@data, limit => $param{limit});
3742 Checks to make sure that bugs match any limits; each entry of @data
3743 much satisfy the limit.
3745 Returns true if there are no entries in data, or there are no keys in
3746 limit; returns false (0) if there are any entries which do not match.
3748 The limit hashref elements can contain an arrayref of scalars to
3749 match; regexes are also acccepted. At least one of the entries in each
3750 element needs to match the corresponding field in all data for the
3757 my %param = validate_with(params => \@_,
3758 spec => {data => {type => ARRAYREF|HASHREF,
3760 limit => {type => HASHREF|UNDEF,
3762 transcript => {type => SCALARREF|HANDLE,
3767 my @data = make_list($param{data});
3769 not defined $param{limit} or
3770 not keys %{$param{limit}}) {
3773 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3774 my $going_to_fail = 0;
3775 for my $data (@data) {
3776 $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
3777 status => dclone($data),
3779 for my $field (keys %{$param{limit}}) {
3780 next unless exists $param{limit}{$field};
3782 my @data_fields = make_list($data->{$field});
3783 LIMIT: for my $limit (make_list($param{limit}{$field})) {
3784 if (not ref $limit) {
3785 for my $data_field (@data_fields) {
3786 if ($data_field eq $limit) {
3792 elsif (ref($limit) eq 'Regexp') {
3793 for my $data_field (@data_fields) {
3794 if ($data_field =~ $limit) {
3801 warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
3806 print {$transcript} qq($field: ').join(', ',map{qq("$_")} make_list($data->{$field})).
3807 "' does not match at least one of ".
3808 join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
3812 return $going_to_fail?0:1;
3820 We override die to specially handle unlocking files in the cases where
3821 we are called via eval. [If we're not called via eval, it doesn't
3827 if ($^S) { # in eval
3829 for (1..$locks) { unfilelock(defined $lockhash?$lockhash:()); }
3836 # =head2 __message_body_template
3838 # message_body_template('mail/ack',{ref=>'foo'});
3840 # Creates a message body using a template
3844 sub __message_body_template{
3845 my ($template,$extra_var) = @_;
3847 my $hole_var = {'&bugurl' =>
3849 $config{cgi_domain}.'/'.
3850 Debbugs::CGI::bug_links(bug => $_[0],
3856 my $body = fill_in_template(template => $template,
3857 variables => {config => \%config,
3860 hole_var => $hole_var,
3862 return fill_in_template(template => 'mail/message_body',
3863 variables => {config => \%config,
3867 hole_var => $hole_var,
3871 sub __all_undef_or_equal {
3873 return 1 if @values == 1 or @values == 0;
3874 my $not_def = grep {not defined $_} @values;
3875 if ($not_def == @values) {
3878 if ($not_def > 0 and $not_def != @values) {
3881 my $first_val = shift @values;
3882 for my $val (@values) {
3883 if ($first_val ne $val) {