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 base qw(Exporter);
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);
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::Util qw(first max);
138 use Encode qw(encode_utf8);
142 # These are a set of options which are common to all of these functions
144 my %common_options = (debug => {type => SCALARREF|HANDLE,
147 transcript => {type => SCALARREF|HANDLE,
150 affected_bugs => {type => HASHREF,
153 affected_packages => {type => HASHREF,
156 recipients => {type => HASHREF,
159 limit => {type => HASHREF,
162 show_bug_info => {type => BOOLEAN,
165 request_subject => {type => SCALAR,
166 default => 'Unknown Subject',
168 request_msgid => {type => SCALAR,
171 request_nn => {type => SCALAR,
174 request_replyto => {type => SCALAR,
177 locks => {type => HASHREF,
183 my %append_action_options =
184 (action => {type => SCALAR,
187 requester => {type => SCALAR,
190 request_addr => {type => SCALAR,
193 location => {type => SCALAR,
196 message => {type => SCALAR|ARRAYREF,
199 append_log => {type => BOOLEAN,
201 depends => [qw(requester request_addr),
205 # locks is both an append_action option, and a common option;
206 # it's ok for it to be in both places.
207 locks => {type => HASHREF,
215 # this is just a generic stub for Debbugs::Control functions.
220 # set_foo(bug => $ref,
221 # transcript => $transcript,
222 # ($dl > 0 ? (debug => $transcript):()),
223 # requester => $header{from},
224 # request_addr => $controlrequestaddr,
226 # affected_packages => \%affected_packages,
227 # recipients => \%recipients,
233 # print {$transcript} "Failed to set foo $ref bar: $@";
241 # my %param = validate_with(params => \@_,
242 # spec => {bug => {type => SCALAR,
243 # regex => qr/^\d+$/,
245 # # specific options here
247 # %append_action_options,
251 # __begin_control(%param,
254 # my ($debug,$transcript) =
255 # @info{qw(debug transcript)};
256 # my @data = @{$info{data}};
257 # my @bugs = @{$info{bugs}};
260 # for my $data (@data) {
261 # append_action_to_log(bug => $data->{bug_num},
263 # __return_append_to_log_options(
268 # if not exists $param{append_log} or $param{append_log};
269 # writebug($data->{bug_num},$data);
270 # print {$transcript} "$action\n";
272 # __end_control(%info);
279 set_block(bug => $ref,
280 transcript => $transcript,
281 ($dl > 0 ? (debug => $transcript):()),
282 requester => $header{from},
283 request_addr => $controlrequestaddr,
285 affected_packages => \%affected_packages,
286 recipients => \%recipients,
292 print {$transcript} "Failed to set blockers of $ref: $@";
295 Alters the set of bugs that block this bug from being fixed
297 This requires altering both this bug (and those it's merged with) as
298 well as the bugs that block this bug from being fixed (and those that
303 =item block -- scalar or arrayref of blocking bugs to set, add or remove
305 =item add -- if true, add blocking bugs
307 =item remove -- if true, remove blocking bugs
314 my %param = validate_with(params => \@_,
315 spec => {bug => {type => SCALAR,
318 # specific options here
319 block => {type => SCALAR|ARRAYREF,
322 add => {type => BOOLEAN,
325 remove => {type => BOOLEAN,
329 %append_action_options,
332 if ($param{add} and $param{remove}) {
333 croak "It's nonsensical to add and remove the same blocking bugs";
335 if (grep {$_ !~ /^\d+$/} make_list($param{block})) {
336 croak "Invalid blocking bug(s):".
337 join(', ',grep {$_ !~ /^\d+$/} make_list($param{block}));
343 elsif ($param{remove}) {
348 __begin_control(%param,
351 my ($debug,$transcript) =
352 @info{qw(debug transcript)};
353 my @data = @{$info{data}};
354 my @bugs = @{$info{bugs}};
357 # The first bit of this code is ugly, and should be cleaned up.
358 # Its purpose is to populate %removed_blockers and %add_blockers
359 # with all of the bugs that should be added or removed as blockers
360 # of all of the bugs which are merged with $param{bug}
363 for my $blocker (make_list($param{block})) {
364 next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
365 my $data = read_bug(bug=>$blocker,
367 if (defined $data and not $data->{archive}) {
368 $data = split_status_fields($data);
369 $ok_blockers{$blocker} = 1;
371 push @merged_bugs, make_list($data->{mergedwith});
372 @ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
375 $bad_blockers{$blocker} = 1;
379 # throw an error if we are setting the blockers and there is a bad
381 if (keys %bad_blockers and $mode eq 'set') {
382 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
383 keys %ok_blockers?'':" and no known blocking bug(s)";
385 # if there are no ok blockers and we are not setting the blockers,
387 if (not keys %ok_blockers and $mode ne 'set') {
388 print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
389 if (keys %bad_blockers) {
390 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
392 __end_control(%info);
396 my @change_blockers = keys %ok_blockers;
398 my %removed_blockers;
401 my @blockers = map {split ' ', $_->{blockedby}} @data;
403 @blockers{@blockers} = (1) x @blockers;
405 # it is nonsensical for a bug to block itself (or a merged
406 # partner); We currently don't allow removal because we'd possibly
410 @bugs{@bugs} = (1) x @bugs;
411 for my $blocker (@change_blockers) {
412 if ($bugs{$blocker}) {
413 croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
416 @blockers = keys %blockers;
418 %removed_blockers = ();
419 for my $blocker (@change_blockers) {
420 next if exists $blockers{$blocker};
421 $blockers{$blocker} = 1;
422 $added_blockers{$blocker} = 1;
425 elsif ($param{remove}) {
426 %added_blockers = ();
427 for my $blocker (@change_blockers) {
428 next if exists $removed_blockers{$blocker};
429 delete $blockers{$blocker};
430 $removed_blockers{$blocker} = 1;
434 @removed_blockers{@blockers} = (1) x @blockers;
436 for my $blocker (@change_blockers) {
437 next if exists $blockers{$blocker};
438 $blockers{$blocker} = 1;
439 if (exists $removed_blockers{$blocker}) {
440 delete $removed_blockers{$blocker};
443 $added_blockers{$blocker} = 1;
447 my @new_blockers = keys %blockers;
448 for my $data (@data) {
449 my $old_data = dclone($data);
450 # remove blockers and/or add new ones as appropriate
451 if ($data->{blockedby} eq '') {
452 print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
454 print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
456 if ($data->{blocks} eq '') {
457 print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
459 print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\n";
462 push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
463 push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
464 $action = ucfirst(join ('; ',@changed)) if @changed;
466 print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
469 $data->{blockedby} = join(' ',keys %blockers);
470 append_action_to_log(bug => $data->{bug_num},
472 old_data => $old_data,
475 __return_append_to_log_options(
480 if not exists $param{append_log} or $param{append_log};
481 writebug($data->{bug_num},$data);
482 print {$transcript} "$action\n";
484 # we do this bit below to avoid code duplication
486 $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
487 $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
489 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 ($debug,$transcript) =
632 @info{qw(debug transcript)};
633 my @data = @{$info{data}};
634 my @bugs = @{$info{bugs}};
635 my @tags = make_list($param{tag});
636 if (not @tags and ($param{remove} or $param{add})) {
637 if ($param{remove}) {
638 print {$transcript} "Requested to remove no tags; doing nothing.\n";
641 print {$transcript} "Requested to add no tags; doing nothing.\n";
643 __end_control(%info);
646 # first things first, make the versions fully qualified source
648 for my $data (@data) {
649 my $action = 'Did not alter tags';
651 my %tag_removed = ();
652 my %fixed_removed = ();
653 my @old_tags = split /\,?\s+/, $data->{keywords};
655 @tags{@old_tags} = (1) x @old_tags;
657 my $old_data = dclone($data);
658 if (not $param{add} and not $param{remove}) {
659 $tag_removed{$_} = 1 for @old_tags;
663 for my $tag (@tags) {
664 if (not $param{remove} and
665 not defined first {$_ eq $tag} @{$config{tags}}) {
666 push @bad_tags, $tag;
670 if (not exists $tags{$tag}) {
672 $tag_added{$tag} = 1;
675 elsif ($param{remove}) {
676 if (exists $tags{$tag}) {
678 $tag_removed{$tag} = 1;
682 if (exists $tag_removed{$tag}) {
683 delete $tag_removed{$tag};
686 $tag_added{$tag} = 1;
691 if (@bad_tags and $param{warn_on_bad_tags}) {
692 print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
693 print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
695 $data->{keywords} = join(' ',keys %tags);
698 push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
699 push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
700 $action = ucfirst(join ('; ',@changed)) if @changed;
702 print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
706 append_action_to_log(bug => $data->{bug_num},
709 old_data => $old_data,
711 __return_append_to_log_options(
716 if not exists $param{append_log} or $param{append_log};
717 writebug($data->{bug_num},$data);
718 print {$transcript} "$action\n";
720 __end_control(%info);
728 set_severity(bug => $ref,
729 transcript => $transcript,
730 ($dl > 0 ? (debug => $transcript):()),
731 requester => $header{from},
732 request_addr => $controlrequestaddr,
734 affected_packages => \%affected_packages,
735 recipients => \%recipients,
736 severity => 'normal',
741 print {$transcript} "Failed to set the severity of bug $ref: $@";
744 Sets the severity of a bug. If severity is not passed, is undefined,
745 or has zero length, sets the severity to the default severity.
750 my %param = validate_with(params => \@_,
751 spec => {bug => {type => SCALAR,
754 # specific options here
755 severity => {type => SCALAR|UNDEF,
756 default => $config{default_severity},
759 %append_action_options,
762 if (not defined $param{severity} or
763 not length $param{severity}
765 $param{severity} = $config{default_severity};
768 # check validity of new severity
769 if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
770 die "Severity '$param{severity}' is not a valid severity level";
773 __begin_control(%param,
774 command => 'severity'
776 my ($debug,$transcript) =
777 @info{qw(debug transcript)};
778 my @data = @{$info{data}};
779 my @bugs = @{$info{bugs}};
782 for my $data (@data) {
783 if (not defined $data->{severity}) {
784 $data->{severity} = $param{severity};
785 $action = "Severity set to '$param{severity}'";
788 if ($data->{severity} eq '') {
789 $data->{severity} = $config{default_severity};
791 if ($data->{severity} eq $param{severity}) {
792 print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
795 $action = "Severity set to '$param{severity}' from '$data->{severity}'";
796 $data->{severity} = $param{severity};
798 append_action_to_log(bug => $data->{bug_num},
800 __return_append_to_log_options(
805 if not exists $param{append_log} or $param{append_log};
806 writebug($data->{bug_num},$data);
807 print {$transcript} "$action\n";
809 __end_control(%info);
816 set_done(bug => $ref,
817 transcript => $transcript,
818 ($dl > 0 ? (debug => $transcript):()),
819 requester => $header{from},
820 request_addr => $controlrequestaddr,
822 affected_packages => \%affected_packages,
823 recipients => \%recipients,
828 print {$transcript} "Failed to set foo $ref bar: $@";
836 my %param = validate_with(params => \@_,
837 spec => {bug => {type => SCALAR,
840 reopen => {type => BOOLEAN,
843 submitter => {type => SCALAR,
846 clear_fixed => {type => BOOLEAN,
849 notify_submitter => {type => BOOLEAN,
852 original_report => {type => SCALARREF,
855 done => {type => SCALAR|UNDEF,
859 %append_action_options,
863 if (exists $param{submitter} and
864 not Mail::RFC822::Address::valid($param{submitter})) {
865 die "New submitter address '$param{submitter}' is not a valid e-mail address";
867 if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
868 $param{done} = $param{requester};
870 if (exists $param{done} and
871 (not defined $param{done} or
872 not length $param{done})) {
878 __begin_control(%param,
879 command => $param{reopen}?'reopen':'done',
881 my ($debug,$transcript) =
882 @info{qw(debug transcript)};
883 my @data = @{$info{data}};
884 my @bugs = @{$info{bugs}};
887 if ($param{reopen}) {
888 # avoid warning multiple times if there are fixed versions
890 for my $data (@data) {
891 if (not exists $data->{done} or
892 not defined $data->{done} or
893 not length $data->{done}) {
894 print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
895 __end_control(%info);
898 if (@{$data->{fixed_versions}} and $warn_fixed) {
899 print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
900 print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
904 $action = "Bug reopened";
905 for my $data (@data) {
906 my $old_data = dclone($data);
908 append_action_to_log(bug => $data->{bug_num},
911 old_data => $old_data,
913 __return_append_to_log_options(
918 if not exists $param{append_log} or $param{append_log};
919 writebug($data->{bug_num},$data);
921 print {$transcript} "$action\n";
922 __end_control(%info);
923 if (exists $param{submitter}) {
924 set_submitter(bug => $param{bug},
925 submitter => $param{submitter},
927 keys %common_options,
928 keys %append_action_options)
931 # clear the fixed revisions
932 if ($param{clear_fixed}) {
933 set_fixed(fixed => [],
937 keys %common_options,
938 keys %append_action_options),
943 my %submitter_notified;
944 my $requester_notified = 0;
945 my $orig_report_set = 0;
946 for my $data (@data) {
947 if (exists $data->{done} and
948 defined $data->{done} and
949 length $data->{done}) {
950 print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
951 __end_control(%info);
955 for my $data (@data) {
956 my $old_data = dclone($data);
957 my $hash = get_hashname($data->{bug_num});
958 my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
959 die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
963 $orig_report= <$report_fh>;
966 if (not $orig_report_set and defined $orig_report and
967 length $orig_report and
968 exists $param{original_report}){
969 ${$param{original_report}} = $orig_report;
970 $orig_report_set = 1;
973 $action = "Marked $config{bug} as done";
975 # set done to the requester
976 $data->{done} = exists $param{done}?$param{done}:$param{requester};
977 append_action_to_log(bug => $data->{bug_num},
980 old_data => $old_data,
982 __return_append_to_log_options(
987 if not exists $param{append_log} or $param{append_log};
988 writebug($data->{bug_num},$data);
989 print {$transcript} "$action\n";
990 # get the original report
991 if ($param{notify_submitter}) {
992 my $submitter_message;
993 if(not exists $submitter_notified{$data->{originator}}) {
995 create_mime_message([default_headers(queue_file => $param{request_nn},
997 msgid => $param{request_msgid},
998 msgtype => 'notifdone',
999 pr_msg => 'they-closed',
1001 [To => $data->{submitter},
1002 Subject => "$config{ubug}#$data->{bug_num} ".
1003 "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
1007 __message_body_template('mail/process_your_bug_done',
1009 replyto => (exists $param{request_replyto} ?
1010 $param{request_replyto} :
1011 $param{requester} || 'Unknown'),
1012 markedby => $param{requester},
1013 subject => $param{request_subject},
1014 messageid => $param{request_msgid},
1017 [join('',make_list($param{message})),$orig_report]
1019 send_mail_message(message => $submitter_message,
1020 recipients => $old_data->{submitter},
1022 $submitter_notified{$data->{originator}} = $submitter_message;
1025 $submitter_message = $submitter_notified{$data->{originator}};
1027 append_action_to_log(bug => $data->{bug_num},
1028 action => "Notification sent",
1030 request_addr => $data->{originator},
1031 desc => "$config{bug} acknowledged by developer.",
1032 recips => [$data->{originator}],
1033 message => $submitter_message,
1038 __end_control(%info);
1039 if (exists $param{fixed}) {
1040 set_fixed(fixed => $param{fixed},
1044 keys %common_options,
1045 keys %append_action_options
1053 =head2 set_submitter
1056 set_submitter(bug => $ref,
1057 transcript => $transcript,
1058 ($dl > 0 ? (debug => $transcript):()),
1059 requester => $header{from},
1060 request_addr => $controlrequestaddr,
1062 affected_packages => \%affected_packages,
1063 recipients => \%recipients,
1064 submitter => $new_submitter,
1065 notify_submitter => 1,
1070 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1073 Sets the submitter of a bug. If notify_submitter is true (the
1074 default), notifies the old submitter of a bug on changes
1079 my %param = validate_with(params => \@_,
1080 spec => {bug => {type => SCALAR,
1083 # specific options here
1084 submitter => {type => SCALAR,
1086 notify_submitter => {type => BOOLEAN,
1090 %append_action_options,
1093 if (not Mail::RFC822::Address::valid($param{submitter})) {
1094 die "New submitter address $param{submitter} is not a valid e-mail address";
1097 __begin_control(%param,
1098 command => 'submitter'
1100 my ($debug,$transcript) =
1101 @info{qw(debug transcript)};
1102 my @data = @{$info{data}};
1103 my @bugs = @{$info{bugs}};
1105 # here we only concern ourselves with the first of the merged bugs
1106 for my $data ($data[0]) {
1107 my $notify_old_submitter = 0;
1108 my $old_data = dclone($data);
1109 print {$debug} "Going to change bug submitter\n";
1110 if (((not defined $param{submitter} or not length $param{submitter}) and
1111 (not defined $data->{originator} or not length $data->{originator})) or
1112 (defined $param{submitter} and defined $data->{originator} and
1113 $param{submitter} eq $data->{originator})) {
1114 print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
1118 if (defined $data->{originator} and length($data->{originator})) {
1119 $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'";
1120 $notify_old_submitter = 1;
1123 $action= "Set $config{bug} submitter to '$param{submitter}'.";
1125 $data->{originator} = $param{submitter};
1127 append_action_to_log(bug => $data->{bug_num},
1128 command => 'submitter',
1130 old_data => $old_data,
1132 __return_append_to_log_options(
1137 if not exists $param{append_log} or $param{append_log};
1138 writebug($data->{bug_num},$data);
1139 print {$transcript} "$action\n";
1140 # notify old submitter
1141 if ($notify_old_submitter and $param{notify_submitter}) {
1142 send_mail_message(message =>
1143 create_mime_message([default_headers(queue_file => $param{request_nn},
1145 msgid => $param{request_msgid},
1147 pr_msg => 'submitter-changed',
1149 [To => $old_data->{submitter},
1150 Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
1154 __message_body_template('mail/submitter_changed',
1155 {old_data => $old_data,
1157 replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
1161 recipients => $old_data->{submitter},
1165 __end_control(%info);
1170 =head2 set_forwarded
1173 set_forwarded(bug => $ref,
1174 transcript => $transcript,
1175 ($dl > 0 ? (debug => $transcript):()),
1176 requester => $header{from},
1177 request_addr => $controlrequestaddr,
1179 affected_packages => \%affected_packages,
1180 recipients => \%recipients,
1181 forwarded => $forward_to,
1186 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1189 Sets the location to which a bug is forwarded. Given an undef
1190 forwarded, unsets forwarded.
1196 my %param = validate_with(params => \@_,
1197 spec => {bug => {type => SCALAR,
1200 # specific options here
1201 forwarded => {type => SCALAR|UNDEF,
1204 %append_action_options,
1207 if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) {
1208 die "Non-printable characters are not allowed in the forwarded field";
1210 $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
1212 __begin_control(%param,
1213 command => 'forwarded'
1215 my ($debug,$transcript) =
1216 @info{qw(debug transcript)};
1217 my @data = @{$info{data}};
1218 my @bugs = @{$info{bugs}};
1220 for my $data (@data) {
1221 my $old_data = dclone($data);
1222 print {$debug} "Going to change bug forwarded\n";
1223 if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
1224 (not defined $param{forwarded} and
1225 defined $data->{forwarded} and not length $data->{forwarded})) {
1226 print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
1230 if (not defined $param{forwarded}) {
1231 $action= "Unset $config{bug} forwarded-to-address";
1233 elsif (defined $data->{forwarded} and length($data->{forwarded})) {
1234 $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'";
1237 $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
1239 $data->{forwarded} = $param{forwarded};
1241 append_action_to_log(bug => $data->{bug_num},
1242 command => 'forwarded',
1244 old_data => $old_data,
1246 __return_append_to_log_options(
1251 if not exists $param{append_log} or $param{append_log};
1252 writebug($data->{bug_num},$data);
1253 print {$transcript} "$action\n";
1255 __end_control(%info);
1264 set_title(bug => $ref,
1265 transcript => $transcript,
1266 ($dl > 0 ? (debug => $transcript):()),
1267 requester => $header{from},
1268 request_addr => $controlrequestaddr,
1270 affected_packages => \%affected_packages,
1271 recipients => \%recipients,
1272 title => $new_title,
1277 print {$transcript} "Failed to set the title of $ref: $@";
1280 Sets the title of a specific bug
1286 my %param = validate_with(params => \@_,
1287 spec => {bug => {type => SCALAR,
1290 # specific options here
1291 title => {type => SCALAR,
1294 %append_action_options,
1297 if ($param{title} =~ /[^[:print:]]/) {
1298 die "Non-printable characters are not allowed in bug titles";
1301 my %info = __begin_control(%param,
1304 my ($debug,$transcript) =
1305 @info{qw(debug transcript)};
1306 my @data = @{$info{data}};
1307 my @bugs = @{$info{bugs}};
1309 for my $data (@data) {
1310 my $old_data = dclone($data);
1311 print {$debug} "Going to change bug title\n";
1312 if (defined $data->{subject} and length($data->{subject}) and
1313 $data->{subject} eq $param{title}) {
1314 print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
1318 if (defined $data->{subject} and length($data->{subject})) {
1319 $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'";
1321 $action= "Set $config{bug} title to '$param{title}'.";
1323 $data->{subject} = $param{title};
1325 append_action_to_log(bug => $data->{bug_num},
1328 old_data => $old_data,
1330 __return_append_to_log_options(
1335 if not exists $param{append_log} or $param{append_log};
1336 writebug($data->{bug_num},$data);
1337 print {$transcript} "$action\n";
1339 __end_control(%info);
1346 set_package(bug => $ref,
1347 transcript => $transcript,
1348 ($dl > 0 ? (debug => $transcript):()),
1349 requester => $header{from},
1350 request_addr => $controlrequestaddr,
1352 affected_packages => \%affected_packages,
1353 recipients => \%recipients,
1354 package => $new_package,
1360 print {$transcript} "Failed to assign or reassign $ref to a package: $@";
1363 Indicates that a bug is in a particular package. If is_source is true,
1364 indicates that the package is a source package. [Internally, this
1365 causes src: to be prepended to the package name.]
1367 The default for is_source is 0. As a special case, if the package
1368 starts with 'src:', it is assumed to be a source package and is_source
1371 The package option must match the package_name_re regex.
1376 my %param = validate_with(params => \@_,
1377 spec => {bug => {type => SCALAR,
1380 # specific options here
1381 package => {type => SCALAR|ARRAYREF,
1383 is_source => {type => BOOLEAN,
1387 %append_action_options,
1390 my @new_packages = map {splitpackages($_)} make_list($param{package});
1391 if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) {
1392 croak "Invalid package name '".
1393 join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages).
1396 my %info = __begin_control(%param,
1397 command => 'package',
1399 my ($debug,$transcript) =
1400 @info{qw(debug transcript)};
1401 my @data = @{$info{data}};
1402 my @bugs = @{$info{bugs}};
1403 # clean up the new package
1407 ($temp =~ s/^src:// or
1408 $param{is_source}) ? 'src:'.$temp:$temp;
1412 my $package_reassigned = 0;
1413 for my $data (@data) {
1414 my $old_data = dclone($data);
1415 print {$debug} "Going to change assigned package\n";
1416 if (defined $data->{package} and length($data->{package}) and
1417 $data->{package} eq $new_package) {
1418 print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
1422 if (defined $data->{package} and length($data->{package})) {
1423 $package_reassigned = 1;
1424 $action= "$config{bug} reassigned from package '$data->{package}'".
1425 " to '$new_package'.";
1427 $action= "$config{bug} assigned to package '$new_package'.";
1429 $data->{package} = $new_package;
1431 append_action_to_log(bug => $data->{bug_num},
1432 command => 'package',
1434 old_data => $old_data,
1436 __return_append_to_log_options(
1441 if not exists $param{append_log} or $param{append_log};
1442 writebug($data->{bug_num},$data);
1443 print {$transcript} "$action\n";
1445 __end_control(%info);
1446 # Only clear the fixed/found versions if the package has been
1448 if ($package_reassigned) {
1449 my @params_for_found_fixed =
1450 map {exists $param{$_}?($_,$param{$_}):()}
1452 keys %common_options,
1453 keys %append_action_options,
1455 set_found(found => [],
1456 @params_for_found_fixed,
1458 set_fixed(fixed => [],
1459 @params_for_found_fixed,
1467 set_found(bug => $ref,
1468 transcript => $transcript,
1469 ($dl > 0 ? (debug => $transcript):()),
1470 requester => $header{from},
1471 request_addr => $controlrequestaddr,
1473 affected_packages => \%affected_packages,
1474 recipients => \%recipients,
1481 print {$transcript} "Failed to set found on $ref: $@";
1485 Sets, adds, or removes the specified found versions of a package
1487 If the version list is empty, and the bug is currently not "done",
1488 causes the done field to be cleared.
1490 If any of the versions added to found are greater than any version in
1491 which the bug is fixed (or when the bug is found and there are no
1492 fixed versions) the done field is cleared.
1497 my %param = validate_with(params => \@_,
1498 spec => {bug => {type => SCALAR,
1501 # specific options here
1502 found => {type => SCALAR|ARRAYREF,
1505 add => {type => BOOLEAN,
1508 remove => {type => BOOLEAN,
1512 %append_action_options,
1515 if ($param{add} and $param{remove}) {
1516 croak "It's nonsensical to add and remove the same versions";
1520 __begin_control(%param,
1523 my ($debug,$transcript) =
1524 @info{qw(debug transcript)};
1525 my @data = @{$info{data}};
1526 my @bugs = @{$info{bugs}};
1528 for my $version (make_list($param{found})) {
1529 next unless defined $version;
1530 $versions{$version} =
1531 [make_source_versions(package => [splitpackages($data[0]{package})],
1532 warnings => $transcript,
1535 versions => $version,
1538 # This is really ugly, but it's what we have to do
1539 if (not @{$versions{$version}}) {
1540 print {$transcript} "Unable to make a source version for version '$version'\n";
1543 if (not keys %versions and ($param{remove} or $param{add})) {
1544 if ($param{remove}) {
1545 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1548 print {$transcript} "Requested to add no versions; doing nothing.\n";
1550 __end_control(%info);
1553 # first things first, make the versions fully qualified source
1555 for my $data (@data) {
1556 # The 'done' field gets a bit weird with version tracking,
1557 # because a bug may be closed by multiple people in different
1558 # branches. Until we have something more flexible, we set it
1559 # every time a bug is fixed, and clear it when a bug is found
1560 # in a version greater than any version in which the bug is
1561 # fixed or when a bug is found and there is no fixed version
1562 my $action = 'Did not alter found versions';
1563 my %found_added = ();
1564 my %found_removed = ();
1565 my %fixed_removed = ();
1567 my $old_data = dclone($data);
1568 if (not $param{add} and not $param{remove}) {
1569 $found_removed{$_} = 1 for @{$data->{found_versions}};
1570 $data->{found_versions} = [];
1573 @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
1575 @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
1576 for my $version (keys %versions) {
1578 my @svers = @{$versions{$version}};
1583 if (exists $found_versions{$version}) {
1584 delete $found_versions{$version};
1585 $found_removed{$version} = 1;
1588 for my $sver (@svers) {
1589 if (not exists $found_versions{$sver}) {
1590 $found_versions{$sver} = 1;
1591 $found_added{$sver} = 1;
1593 # if the found we are adding matches any fixed
1594 # versions, remove them
1595 my @temp = grep m{(^|/)\Q$sver\E$}, keys %fixed_versions;
1596 delete $fixed_versions{$_} for @temp;
1597 $fixed_removed{$_} = 1 for @temp;
1600 # We only care about reopening the bug if the bug is
1602 if (defined $data->{done} and length $data->{done}) {
1603 my @svers_order = sort_versions(map {m{([^/]+)$}; $1;}
1605 # determine if we need to reopen
1606 my @fixed_order = sort_versions(map {m{([^/]+)$}; $1;}
1607 keys %fixed_versions);
1608 if (not @fixed_order or
1609 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1615 elsif ($param{remove}) {
1616 # in the case of removal, we only concern ourself with
1617 # the version passed, not the source version it maps
1619 my @temp = grep m{(?:^|/)\Q$version\E$}, keys %found_versions;
1620 delete $found_versions{$_} for @temp;
1621 $found_removed{$_} = 1 for @temp;
1624 # set the keys to exactly these values
1625 my @svers = @{$versions{$version}};
1629 for my $sver (@svers) {
1630 if (not exists $found_versions{$sver}) {
1631 $found_versions{$sver} = 1;
1632 if (exists $found_removed{$sver}) {
1633 delete $found_removed{$sver};
1636 $found_added{$sver} = 1;
1643 $data->{found_versions} = [keys %found_versions];
1644 $data->{fixed_versions} = [keys %fixed_versions];
1647 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1648 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1649 # push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
1650 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1651 $action = ucfirst(join ('; ',@changed)) if @changed;
1653 $action .= " and reopened"
1655 if (not $reopened and not @changed) {
1656 print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
1660 append_action_to_log(bug => $data->{bug_num},
1663 old_data => $old_data,
1665 __return_append_to_log_options(
1670 if not exists $param{append_log} or $param{append_log};
1671 writebug($data->{bug_num},$data);
1672 print {$transcript} "$action\n";
1674 __end_control(%info);
1680 set_fixed(bug => $ref,
1681 transcript => $transcript,
1682 ($dl > 0 ? (debug => $transcript):()),
1683 requester => $header{from},
1684 request_addr => $controlrequestaddr,
1686 affected_packages => \%affected_packages,
1687 recipients => \%recipients,
1695 print {$transcript} "Failed to set fixed on $ref: $@";
1699 Sets, adds, or removes the specified fixed versions of a package
1701 If the fixed versions are empty (or end up being empty after this
1702 call) or the greatest fixed version is less than the greatest found
1703 version and the reopen option is true, the bug is reopened.
1705 This function is also called by the reopen function, which causes all
1706 of the fixed versions to be cleared.
1711 my %param = validate_with(params => \@_,
1712 spec => {bug => {type => SCALAR,
1715 # specific options here
1716 fixed => {type => SCALAR|ARRAYREF,
1719 add => {type => BOOLEAN,
1722 remove => {type => BOOLEAN,
1725 reopen => {type => BOOLEAN,
1729 %append_action_options,
1732 if ($param{add} and $param{remove}) {
1733 croak "It's nonsensical to add and remove the same versions";
1736 __begin_control(%param,
1739 my ($debug,$transcript) =
1740 @info{qw(debug transcript)};
1741 my @data = @{$info{data}};
1742 my @bugs = @{$info{bugs}};
1744 for my $version (make_list($param{fixed})) {
1745 next unless defined $version;
1746 $versions{$version} =
1747 [make_source_versions(package => [splitpackages($data[0]{package})],
1748 warnings => $transcript,
1751 versions => $version,
1754 # This is really ugly, but it's what we have to do
1755 if (not @{$versions{$version}}) {
1756 print {$transcript} "Unable to make a source version for version '$version'\n";
1759 if (not keys %versions and ($param{remove} or $param{add})) {
1760 if ($param{remove}) {
1761 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1764 print {$transcript} "Requested to add no versions; doing nothing.\n";
1766 __end_control(%info);
1769 # first things first, make the versions fully qualified source
1771 for my $data (@data) {
1772 my $old_data = dclone($data);
1773 # The 'done' field gets a bit weird with version tracking,
1774 # because a bug may be closed by multiple people in different
1775 # branches. Until we have something more flexible, we set it
1776 # every time a bug is fixed, and clear it when a bug is found
1777 # in a version greater than any version in which the bug is
1778 # fixed or when a bug is found and there is no fixed version
1779 my $action = 'Did not alter fixed versions';
1780 my %found_added = ();
1781 my %found_removed = ();
1782 my %fixed_added = ();
1783 my %fixed_removed = ();
1785 if (not $param{add} and not $param{remove}) {
1786 $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
1787 $data->{fixed_versions} = [];
1790 @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
1792 @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
1793 for my $version (keys %versions) {
1795 my @svers = @{$versions{$version}};
1800 if (exists $fixed_versions{$version}) {
1801 $fixed_removed{$version} = 1;
1802 delete $fixed_versions{$version};
1805 for my $sver (@svers) {
1806 if (not exists $fixed_versions{$sver}) {
1807 $fixed_versions{$sver} = 1;
1808 $fixed_added{$sver} = 1;
1812 elsif ($param{remove}) {
1813 # in the case of removal, we only concern ourself with
1814 # the version passed, not the source version it maps
1816 my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
1817 delete $fixed_versions{$_} for @temp;
1818 $fixed_removed{$_} = 1 for @temp;
1821 # set the keys to exactly these values
1822 my @svers = @{$versions{$version}};
1826 for my $sver (@svers) {
1827 if (not exists $fixed_versions{$sver}) {
1828 $fixed_versions{$sver} = 1;
1829 if (exists $fixed_removed{$sver}) {
1830 delete $fixed_removed{$sver};
1833 $fixed_added{$sver} = 1;
1840 $data->{found_versions} = [keys %found_versions];
1841 $data->{fixed_versions} = [keys %fixed_versions];
1843 # If we're supposed to consider reopening, reopen if the
1844 # fixed versions are empty or the greatest found version
1845 # is greater than the greatest fixed version
1846 if ($param{reopen} and defined $data->{done}
1847 and length $data->{done}) {
1848 my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1849 map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
1850 # determine if we need to reopen
1851 my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1852 map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
1853 if (not @fixed_order or
1854 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1861 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1862 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1863 push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
1864 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1865 $action = ucfirst(join ('; ',@changed)) if @changed;
1867 $action .= " and reopened"
1869 if (not $reopened and not @changed) {
1870 print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
1874 append_action_to_log(bug => $data->{bug_num},
1877 old_data => $old_data,
1879 __return_append_to_log_options(
1884 if not exists $param{append_log} or $param{append_log};
1885 writebug($data->{bug_num},$data);
1886 print {$transcript} "$action\n";
1888 __end_control(%info);
1895 set_merged(bug => $ref,
1896 transcript => $transcript,
1897 ($dl > 0 ? (debug => $transcript):()),
1898 requester => $header{from},
1899 request_addr => $controlrequestaddr,
1901 affected_packages => \%affected_packages,
1902 recipients => \%recipients,
1903 merge_with => 12345,
1906 allow_reassign => 1,
1907 reassign_same_source_only => 1,
1912 print {$transcript} "Failed to set merged on $ref: $@";
1916 Sets, adds, or removes the specified merged bugs of a bug
1918 By default, requires
1923 my %param = validate_with(params => \@_,
1924 spec => {bug => {type => SCALAR,
1927 # specific options here
1928 merge_with => {type => ARRAYREF|SCALAR,
1931 remove => {type => BOOLEAN,
1934 force => {type => BOOLEAN,
1937 masterbug => {type => BOOLEAN,
1940 allow_reassign => {type => BOOLEAN,
1943 reassign_different_sources => {type => BOOLEAN,
1947 %append_action_options,
1950 my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
1952 @merging{@merging} = (1) x @merging;
1953 if (grep {$_ !~ /^\d+$/} @merging) {
1954 croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
1956 $param{locks} = {} if not exists $param{locks};
1958 __begin_control(%param,
1961 my ($debug,$transcript) =
1962 @info{qw(debug transcript)};
1963 if (not @merging and exists $param{merge_with}) {
1964 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
1965 __end_control(%info);
1968 my @data = @{$info{data}};
1969 my @bugs = @{$info{bugs}};
1972 for my $data (@data) {
1973 $data{$data->{bug_num}} = $data;
1974 my @merged_bugs = split / /, $data->{mergedwith};
1975 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
1979 if (not exists $param{merge_with}) {
1980 my $ok_to_unmerge = 1;
1981 delete $merged_bugs{$param{bug}};
1982 if (not keys %merged_bugs) {
1983 print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
1984 __end_control(%info);
1987 my $action = "Disconnected #$param{bug} from all other report(s).";
1988 for my $data (@data) {
1989 my $old_data = dclone($data);
1990 if ($data->{bug_num} == $param{bug}) {
1991 $data->{mergedwith} = '';
1994 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
1997 append_action_to_log(bug => $data->{bug_num},
2000 old_data => $old_data,
2002 __return_append_to_log_options(%param,
2006 if not exists $param{append_log} or $param{append_log};
2007 writebug($data->{bug_num},$data);
2009 print {$transcript} "$action\n";
2010 __end_control(%info);
2013 # lock and load all of the bugs we need
2014 my @bugs_to_load = keys %merging;
2017 my ($data,$n_locks) =
2018 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2020 locks => $param{locks},
2023 $new_locks += $n_locks;
2025 @data = values %data;
2026 if (not check_limit(data => [@data],
2027 exists $param{limit}?(limit => $param{limit}):(),
2028 transcript => $transcript,
2030 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
2032 for my $data (@data) {
2033 $data{$data->{bug_num}} = $data;
2034 $merged_bugs{$data->{bug_num}} = 1;
2035 my @merged_bugs = split / /, $data->{mergedwith};
2036 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
2037 if (exists $param{affected_bugs}) {
2038 $param{affected_bugs}{$data->{bug_num}} = 1;
2041 __handle_affected_packages(%param,data => [@data]);
2042 my %bug_info_shown; # which bugs have had information shown
2043 $bug_info_shown{$param{bug}} = 1;
2044 add_recipients(data => [@data],
2045 recipients => $param{recipients},
2046 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
2048 (__internal_request()?(transcript => $transcript):()),
2051 # Figure out what the ideal state is for the bug,
2052 my ($merge_status,$bugs_to_merge) =
2053 __calculate_merge_status(\@data,\%data,$param{bug});
2054 # find out if we actually have any bugs to merge
2055 if (not $bugs_to_merge) {
2056 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
2057 for (1..$new_locks) {
2058 unfilelock($param{locks});
2061 __end_control(%info);
2064 # see what changes need to be made to merge the bugs
2065 # check to make sure that the set of changes we need to make is allowed
2066 my ($disallowed_changes,$changes) =
2067 __calculate_merge_changes(\@data,$merge_status,\%param);
2068 # at this point, stop if there are disallowed changes, otherwise
2069 # make the allowed changes, and then reread the bugs in question
2070 # to get the new data, then recaculate the merges; repeat
2071 # reloading and recalculating until we try too many times or there
2072 # are no changes to make.
2075 # we will allow at most 4 times through this; more than 1
2076 # shouldn't really happen.
2078 while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
2079 if ($attempts > 1) {
2080 print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
2082 if (@{$disallowed_changes}) {
2083 # figure out the problems
2084 print {$transcript} "Unable to merge bugs because:\n";
2085 for my $change (@{$disallowed_changes}) {
2086 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2088 if ($attempts > 0) {
2089 croak "Some bugs were altered while attempting to merge";
2092 croak "Did not alter merged bugs";
2095 my @bugs_to_change = keys %{$changes};
2096 for my $change_bug (@bugs_to_change) {
2097 next unless exists $changes->{$change_bug};
2098 $bug_changed{$change_bug}++;
2099 print {$transcript} __bug_info($data{$change_bug}) if
2100 $param{show_bug_info} and not __internal_request(1);
2101 $bug_info_shown{$change_bug} = 1;
2102 __allow_relocking($param{locks},[keys %data]);
2103 for my $change (@{$changes->{$change_bug}}) {
2104 if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
2105 my %target_blockedby;
2106 @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
2107 my %unhandled_targets = %target_blockedby;
2108 my @blocks_to_remove;
2109 for my $key (split / /,$change->{orig_value}) {
2110 delete $unhandled_targets{$key};
2111 next if exists $target_blockedby{$key};
2112 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2113 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2116 keys %common_options,
2117 keys %append_action_options),
2120 for my $key (keys %unhandled_targets) {
2121 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2122 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2125 keys %common_options,
2126 keys %append_action_options),
2131 $change->{function}->(bug => $change->{bug},
2132 $change->{key}, $change->{func_value},
2133 exists $change->{options}?@{$change->{options}}:(),
2135 keys %common_options,
2136 keys %append_action_options),
2140 __disallow_relocking($param{locks});
2141 my ($data,$n_locks) =
2142 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2144 locks => $param{locks},
2148 $new_locks += $n_locks;
2151 @data = values %data;
2152 ($merge_status,$bugs_to_merge) =
2153 __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
2154 ($disallowed_changes,$changes) =
2155 __calculate_merge_changes(\@data,$merge_status,\%param);
2156 $attempts = max(values %bug_changed);
2159 if ($param{show_bug_info} and not __internal_request(1)) {
2160 for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
2161 next if $bug_info_shown{$data->{bug_num}};
2162 print {$transcript} __bug_info($data);
2165 if (keys %{$changes} or @{$disallowed_changes}) {
2166 print {$transcript} "After four attempts, the following changes were unable to be made:\n";
2167 for (1..$new_locks) {
2168 unfilelock($param{locks});
2171 __end_control(%info);
2172 for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
2173 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2175 die "Unable to modify bugs so they could be merged";
2179 # finally, we can merge the bugs
2180 my $action = "Merged ".join(' ',sort keys %merged_bugs);
2181 for my $data (@data) {
2182 my $old_data = dclone($data);
2183 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
2185 append_action_to_log(bug => $data->{bug_num},
2188 old_data => $old_data,
2190 __return_append_to_log_options(%param,
2194 if not exists $param{append_log} or $param{append_log};
2195 writebug($data->{bug_num},$data);
2197 print {$transcript} "$action\n";
2198 # unlock the extra locks that we got earlier
2199 for (1..$new_locks) {
2200 unfilelock($param{locks});
2203 __end_control(%info);
2206 sub __allow_relocking{
2207 my ($locks,$bugs) = @_;
2209 my @locks = (@{$bugs},'merge');
2210 for my $lock (@locks) {
2211 my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
2212 next unless @lockfiles;
2213 $locks->{relockable}{$lockfiles[0]} = 0;
2217 sub __disallow_relocking{
2219 delete $locks->{relockable};
2222 sub __lock_and_load_merged_bugs{
2224 validate_with(params => \@_,
2226 {bugs_to_load => {type => ARRAYREF,
2227 default => sub {[]},
2229 data => {type => HASHREF|ARRAYREF,
2231 locks => {type => HASHREF,
2232 default => sub {{};},
2234 reload_all => {type => BOOLEAN,
2237 debug => {type => HANDLE,
2243 if (ref($param{data}) eq 'ARRAY') {
2244 for my $data (@{$param{data}}) {
2245 $data{$data->{bug_num}} = dclone($data);
2249 %data = %{dclone($param{data})};
2251 my @bugs_to_load = @{$param{bugs_to_load}};
2252 if ($param{reload_all}) {
2253 push @bugs_to_load, keys %data;
2256 @temp{@bugs_to_load} = (1) x @bugs_to_load;
2257 @bugs_to_load = keys %temp;
2258 my %loaded_this_time;
2260 while ($bug_to_load = shift @bugs_to_load) {
2261 if (not $param{reload_all}) {
2262 next if exists $data{$bug_to_load};
2265 next if $loaded_this_time{$bug_to_load};
2268 if ($param{reload_all}) {
2269 if (exists $data{$bug_to_load}) {
2274 read_bug(bug => $bug_to_load,
2276 locks => $param{locks},
2278 die "Unable to load bug $bug_to_load";
2279 print {$param{debug}} "read bug $bug_to_load\n";
2280 $data{$data->{bug_num}} = $data;
2281 $new_locks += $lock_bug;
2282 $loaded_this_time{$data->{bug_num}} = 1;
2284 grep {not exists $data{$_}}
2285 split / /,$data->{mergedwith};
2287 return (\%data,$new_locks);
2291 sub __calculate_merge_status{
2292 my ($data_a,$data_h,$master_bug,$merge_status) = @_;
2293 my %merge_status = %{$merge_status // {}};
2295 my $bugs_to_merge = 0;
2296 for my $data (@{$data_a}) {
2297 # check to see if this bug is unmerged in the set
2298 if (not length $data->{mergedwith} or
2299 grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
2300 $merged_bugs{$data->{bug_num}} = 1;
2303 # the master_bug is the bug that every other bug is made to
2304 # look like. However, if merge is set, tags, fixed and found
2306 if ($data->{bug_num} == $master_bug) {
2307 for (qw(package forwarded severity blocks blockedby done owner summary outlook affects)) {
2308 $merge_status{$_} = $data->{$_}
2311 if (defined $merge_status) {
2312 next unless $data->{bug_num} == $master_bug;
2314 $merge_status{tag} = {} if not exists $merge_status{tag};
2315 for my $tag (split /\s+/, $data->{keywords}) {
2316 $merge_status{tag}{$tag} = 1;
2318 $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
2319 for (qw(fixed found)) {
2320 @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
2323 # if there is a non-source qualified version with a corresponding
2324 # source qualified version, we only want to merge the source
2325 # qualified version(s)
2326 for (qw(fixed found)) {
2327 my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
2328 for my $unqualified_version (@unqualified_versions) {
2329 if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
2330 delete $merge_status{"${_}_versions"}{$unqualified_version};
2334 return (\%merge_status,$bugs_to_merge);
2339 sub __calculate_merge_changes{
2340 my ($datas,$merge_status,$param) = @_;
2342 my @disallowed_changes;
2343 for my $data (@{$datas}) {
2344 # things that can be forced
2346 # * func is the function to set the new value
2348 # * key is the key of the function to set the value,
2350 # * modify_value is a function which is called to modify the new
2351 # value so that the function will accept it
2353 # * options is an ARRAYREF of options to pass to the function
2355 # * allowed is a BOOLEAN which controls whether this setting
2356 # is allowed to be different by default.
2357 my %force_functions =
2358 (forwarded => {func => \&set_forwarded,
2362 severity => {func => \&set_severity,
2366 blocks => {func => \&set_blocks,
2367 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2371 blockedby => {func => \&set_blocks,
2372 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2376 done => {func => \&set_done,
2380 owner => {func => \&owner,
2384 summary => {func => \&summary,
2388 outlook => {func => \&outlook,
2392 affects => {func => \&affects,
2396 package => {func => \&set_package,
2400 keywords => {func => \&set_tag,
2402 modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
2405 fixed_versions => {func => \&set_fixed,
2407 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2410 found_versions => {func => \&set_found,
2412 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2416 for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
2417 # if the ideal bug already has the field set properly, we
2419 if ($field eq 'keywords'){
2420 next if join(' ',sort split /\s+/,$data->{keywords}) eq
2421 join(' ',sort keys %{$merge_status->{tag}});
2423 elsif ($field =~ /^(?:fixed|found)_versions$/) {
2424 next if join(' ', sort @{$data->{$field}}) eq
2425 join(' ',sort keys %{$merge_status->{$field}});
2427 elsif ($field eq 'done') {
2428 # for done, we only care if the bug is done or not
2429 # done, not the value it's set to.
2430 if (defined $merge_status->{$field} and length $merge_status->{$field} and
2431 defined $data->{$field} and length $data->{$field}) {
2434 elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
2435 (not defined $data->{$field} or not length $data->{$field})
2440 elsif ($merge_status->{$field} eq $data->{$field}) {
2445 bug => $data->{bug_num},
2446 orig_value => $data->{$field},
2448 (exists $force_functions{$field}{modify_value} ?
2449 $force_functions{$field}{modify_value}->($merge_status->{$field}):
2450 $merge_status->{$field}),
2451 value => $merge_status->{$field},
2452 function => $force_functions{$field}{func},
2453 key => $force_functions{$field}{key},
2454 options => $force_functions{$field}{options},
2455 allowed => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
2457 $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
2458 $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
2459 if ($param->{force} or $change->{allowed}) {
2460 if ($field ne 'package' or $change->{allowed}) {
2461 push @{$changes{$data->{bug_num}}},$change;
2464 if ($param->{allow_reassign}) {
2465 if ($param->{reassign_different_sources}) {
2466 push @{$changes{$data->{bug_num}}},$change;
2469 # allow reassigning if binary_to_source returns at
2470 # least one of the same source packages
2471 my @merge_status_source =
2472 binary_to_source(package => $merge_status->{package},
2475 my @other_bug_source =
2476 binary_to_source(package => $data->{package},
2479 my %merge_status_sources;
2480 @merge_status_sources{@merge_status_source} =
2481 (1) x @merge_status_source;
2482 if (grep {$merge_status_sources{$_}} @other_bug_source) {
2483 push @{$changes{$data->{bug_num}}},$change;
2488 push @disallowed_changes,$change;
2490 # blocks and blocked by are weird; we have to go through and
2491 # set blocks to the other half of the merged bugs
2493 return (\@disallowed_changes,\%changes);
2499 affects(bug => $ref,
2500 transcript => $transcript,
2501 ($dl > 0 ? (debug => $transcript):()),
2502 requester => $header{from},
2503 request_addr => $controlrequestaddr,
2505 affected_packages => \%affected_packages,
2506 recipients => \%recipients,
2514 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
2517 This marks a bug as affecting packages which the bug is not actually
2518 in. This should only be used in cases where fixing the bug instantly
2519 resolves the problem in the other packages.
2521 By default, the packages are set to the list of packages passed.
2522 However, if you pass add => 1 or remove => 1, the list of packages
2523 passed are added or removed from the affects list, respectively.
2528 my %param = validate_with(params => \@_,
2529 spec => {bug => {type => SCALAR,
2532 # specific options here
2533 package => {type => SCALAR|ARRAYREF|UNDEF,
2536 add => {type => BOOLEAN,
2539 remove => {type => BOOLEAN,
2543 %append_action_options,
2546 if ($param{add} and $param{remove}) {
2547 croak "Asking to both add and remove affects is nonsensical";
2549 if (not defined $param{package}) {
2550 $param{package} = [];
2553 __begin_control(%param,
2554 command => 'affects'
2556 my ($debug,$transcript) =
2557 @info{qw(debug transcript)};
2558 my @data = @{$info{data}};
2559 my @bugs = @{$info{bugs}};
2561 for my $data (@data) {
2563 print {$debug} "Going to change affects\n";
2564 my @packages = splitpackages($data->{affects});
2566 @packages{@packages} = (1) x @packages;
2569 for my $package (make_list($param{package})) {
2570 next unless defined $package and length $package;
2571 if (not $packages{$package}) {
2572 $packages{$package} = 1;
2573 push @added,$package;
2577 $action = "Added indication that $data->{bug_num} affects ".
2578 english_join(\@added);
2581 elsif ($param{remove}) {
2583 for my $package (make_list($param{package})) {
2584 if ($packages{$package}) {
2585 next unless defined $package and length $package;
2586 delete $packages{$package};
2587 push @removed,$package;
2590 $action = "Removed indication that $data->{bug_num} affects " .
2591 english_join(\@removed);
2594 my %added_packages = ();
2595 my %removed_packages = %packages;
2597 for my $package (make_list($param{package})) {
2598 next unless defined $package and length $package;
2599 $packages{$package} = 1;
2600 delete $removed_packages{$package};
2601 $added_packages{$package} = 1;
2603 if (keys %removed_packages) {
2604 $action = "Removed indication that $data->{bug_num} affects ".
2605 english_join([keys %removed_packages]);
2606 $action .= "\n" if keys %added_packages;
2608 if (keys %added_packages) {
2609 $action .= "Added indication that $data->{bug_num} affects " .
2610 english_join([keys %added_packages]);
2613 if (not length $action) {
2614 print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
2617 my $old_data = dclone($data);
2618 $data->{affects} = join(',',keys %packages);
2619 append_action_to_log(bug => $data->{bug_num},
2621 command => 'affects',
2623 old_data => $old_data,
2624 __return_append_to_log_options(
2629 if not exists $param{append_log} or $param{append_log};
2630 writebug($data->{bug_num},$data);
2631 print {$transcript} "$action\n";
2633 __end_control(%info);
2637 =head1 SUMMARY FUNCTIONS
2642 summary(bug => $ref,
2643 transcript => $transcript,
2644 ($dl > 0 ? (debug => $transcript):()),
2645 requester => $header{from},
2646 request_addr => $controlrequestaddr,
2648 affected_packages => \%affected_packages,
2649 recipients => \%recipients,
2655 print {$transcript} "Failed to mark $ref with summary foo: $@";
2658 Handles all setting of summary fields
2660 If summary is undef, unsets the summary
2662 If summary is 0, sets the summary to the first paragraph contained in
2665 If summary is a positive integer, sets the summary to the message specified.
2667 Otherwise, sets summary to the value passed.
2673 # outlook and summary are exactly the same, basically
2674 return _summary('summary',@_);
2677 =head1 OUTLOOK FUNCTIONS
2682 outlook(bug => $ref,
2683 transcript => $transcript,
2684 ($dl > 0 ? (debug => $transcript):()),
2685 requester => $header{from},
2686 request_addr => $controlrequestaddr,
2688 affected_packages => \%affected_packages,
2689 recipients => \%recipients,
2695 print {$transcript} "Failed to mark $ref with outlook foo: $@";
2698 Handles all setting of outlook fields
2700 If outlook is undef, unsets the outlook
2702 If outlook is 0, sets the outlook to the first paragraph contained in
2705 If outlook is a positive integer, sets the outlook to the message specified.
2707 Otherwise, sets outlook to the value passed.
2713 return _summary('outlook',@_);
2717 my ($cmd,@params) = @_;
2718 my %param = validate_with(params => \@params,
2719 spec => {bug => {type => SCALAR,
2722 # specific options here
2723 $cmd , {type => SCALAR|UNDEF,
2727 %append_action_options,
2731 __begin_control(%param,
2734 my ($debug,$transcript) =
2735 @info{qw(debug transcript)};
2736 my @data = @{$info{data}};
2737 my @bugs = @{$info{bugs}};
2738 # figure out the log that we're going to use
2740 my $summary_msg = '';
2742 if (not defined $param{$cmd}) {
2744 print {$debug} "Removing $cmd fields\n";
2745 $action = "Removed $cmd";
2747 elsif ($param{$cmd} =~ /^\d+$/) {
2749 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
2750 if ($param{$cmd} == 0) {
2751 $log = $param{message};
2752 $summary_msg = @records + 1;
2755 if (($param{$cmd} - 1 ) > $#records) {
2756 die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
2758 my $record = $records[($param{$cmd} - 1 )];
2759 if ($record->{type} !~ /incoming-recv|recips/) {
2760 die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
2762 $summary_msg = $param{$cmd};
2763 $log = [$record->{text}];
2765 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
2766 my $body = $p_o->{body};
2767 my $in_pseudoheaders = 0;
2769 # walk through body until we get non-blank lines
2770 for my $line (@{$body}) {
2771 if ($line =~ /^\s*$/) {
2772 if (length $paragraph) {
2773 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
2779 $in_pseudoheaders = 0;
2782 # skip a paragraph if it looks like it's control or
2784 if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity)\:\s+\S}xi or #pseudo headers
2785 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
2786 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
2787 debug|(?:not|)forwarded|priority|
2788 (?:un|)block|limit|(?:un|)archive|
2789 reassign|retitle|affects|wrongpackage
2790 (?:un|force|)merge|user(?:category|tags?|)
2792 if (not length $paragraph) {
2793 print {$debug} "Found control/pseudo-headers and skiping them\n";
2794 $in_pseudoheaders = 1;
2798 next if $in_pseudoheaders;
2799 $paragraph .= $line ." \n";
2801 print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
2802 $summary = $paragraph;
2803 $summary =~ s/[\n\r]/ /g;
2804 if (not length $summary) {
2805 die "Unable to find $cmd message to use";
2807 # trim off a trailing spaces
2808 $summary =~ s/\ *$//;
2811 $summary = $param{$cmd};
2813 for my $data (@data) {
2814 print {$debug} "Going to change $cmd\n";
2815 if (((not defined $summary or not length $summary) and
2816 (not defined $data->{$cmd} or not length $data->{$cmd})) or
2817 $summary eq $data->{$cmd}) {
2818 print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
2821 if (length $summary) {
2822 if (length $data->{$cmd}) {
2823 $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
2826 $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
2829 my $old_data = dclone($data);
2830 $data->{$cmd} = $summary;
2831 append_action_to_log(bug => $data->{bug_num},
2833 old_data => $old_data,
2836 __return_append_to_log_options(
2841 if not exists $param{append_log} or $param{append_log};
2842 writebug($data->{bug_num},$data);
2843 print {$transcript} "$action\n";
2845 __end_control(%info);
2853 clone_bug(bug => $ref,
2854 transcript => $transcript,
2855 ($dl > 0 ? (debug => $transcript):()),
2856 requester => $header{from},
2857 request_addr => $controlrequestaddr,
2859 affected_packages => \%affected_packages,
2860 recipients => \%recipients,
2865 print {$transcript} "Failed to clone bug $ref bar: $@";
2868 Clones the given bug.
2870 We currently don't support cloning merged bugs, but this could be
2871 handled by internally unmerging, cloning, then remerging the bugs.
2876 my %param = validate_with(params => \@_,
2877 spec => {bug => {type => SCALAR,
2880 new_bugs => {type => ARRAYREF,
2882 new_clones => {type => HASHREF,
2886 %append_action_options,
2890 __begin_control(%param,
2893 my ($debug,$transcript) =
2894 @info{qw(debug transcript)};
2895 my @data = @{$info{data}};
2896 my @bugs = @{$info{bugs}};
2899 for my $data (@data) {
2900 if (length($data->{mergedwith})) {
2901 die "Bug is marked as being merged with others. Use an existing clone.\n";
2905 die "Not exactly one bug‽ This shouldn't happen.";
2907 my $data = $data[0];
2909 for my $newclone_id (@{$param{new_bugs}}) {
2910 my $new_bug_num = new_bug(copy => $data->{bug_num});
2911 $param{new_clones}{$newclone_id} = $new_bug_num;
2912 $clones{$newclone_id} = $new_bug_num;
2914 my @new_bugs = sort values %clones;
2916 for my $new_bug (@new_bugs) {
2917 # no collapsed ids or the higher collapsed id is not one less
2918 # than the next highest new bug
2919 if (not @collapsed_ids or
2920 $collapsed_ids[-1][1]+1 != $new_bug) {
2921 push @collapsed_ids,[$new_bug,$new_bug];
2924 $collapsed_ids[-1][1] = $new_bug;
2928 for my $ci (@collapsed_ids) {
2929 if ($ci->[0] == $ci->[1]) {
2930 push @collapsed,$ci->[0];
2933 push @collapsed,$ci->[0].'-'.$ci->[1]
2936 my $collapsed_str = english_join(\@collapsed);
2937 $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
2938 for my $new_bug (@new_bugs) {
2939 append_action_to_log(bug => $new_bug,
2941 __return_append_to_log_options(
2946 if not exists $param{append_log} or $param{append_log};
2948 append_action_to_log(bug => $data->{bug_num},
2950 __return_append_to_log_options(
2955 if not exists $param{append_log} or $param{append_log};
2956 writebug($data->{bug_num},$data);
2957 print {$transcript} "$action\n";
2958 __end_control(%info);
2959 # bugs that this bug is blocking are also blocked by the new clone(s)
2960 for my $bug (split ' ', $data->{blocks}) {
2961 for my $new_bug (@new_bugs) {
2962 set_blocks(bug => $new_bug,
2965 keys %common_options,
2966 keys %append_action_options),
2970 # bugs that this bug is blocked by are also blocking the new clone(s)
2971 for my $bug (split ' ', $data->{blockedby}) {
2972 for my $new_bug (@new_bugs) {
2973 set_blocks(bug => $bug,
2976 keys %common_options,
2977 keys %append_action_options),
2985 =head1 OWNER FUNCTIONS
2991 transcript => $transcript,
2992 ($dl > 0 ? (debug => $transcript):()),
2993 requester => $header{from},
2994 request_addr => $controlrequestaddr,
2996 recipients => \%recipients,
3002 print {$transcript} "Failed to mark $ref as having an owner: $@";
3005 Handles all setting of the owner field; given an owner of undef or of
3006 no length, indicates that a bug is not owned by anyone.
3011 my %param = validate_with(params => \@_,
3012 spec => {bug => {type => SCALAR,
3015 owner => {type => SCALAR|UNDEF,
3018 %append_action_options,
3022 __begin_control(%param,
3025 my ($debug,$transcript) =
3026 @info{qw(debug transcript)};
3027 my @data = @{$info{data}};
3028 my @bugs = @{$info{bugs}};
3030 for my $data (@data) {
3031 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
3032 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
3033 if (not defined $param{owner} or not length $param{owner}) {
3034 if (not defined $data->{owner} or not length $data->{owner}) {
3035 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
3039 $action = "Removed annotation that $config{bug} was owned by " .
3043 if ($data->{owner} eq $param{owner}) {
3044 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
3047 if (length $data->{owner}) {
3048 $action = "Owner changed from $data->{owner} to $param{owner}.";
3051 $action = "Owner recorded as $param{owner}."
3054 my $old_data = dclone($data);
3055 $data->{owner} = $param{owner};
3056 append_action_to_log(bug => $data->{bug_num},
3059 old_data => $old_data,
3061 __return_append_to_log_options(
3066 if not exists $param{append_log} or $param{append_log};
3067 writebug($data->{bug_num},$data);
3068 print {$transcript} "$action\n";
3070 __end_control(%info);
3074 =head1 ARCHIVE FUNCTIONS
3081 bug_archive(bug => $bug_num,
3083 transcript => \$transcript,
3088 transcript("Unable to archive $bug_num\n");
3091 transcript($transcript);
3094 This routine archives a bug
3098 =item bug -- bug number
3100 =item check_archiveable -- check wether a bug is archiveable before
3101 archiving; defaults to 1
3103 =item archive_unarchived -- whether to archive bugs which have not
3104 previously been archived; defaults to 1. [Set to 0 when used from
3107 =item ignore_time -- whether to ignore time constraints when archiving
3108 a bug; defaults to 0.
3115 my %param = validate_with(params => \@_,
3116 spec => {bug => {type => SCALAR,
3119 check_archiveable => {type => BOOLEAN,
3122 archive_unarchived => {type => BOOLEAN,
3125 ignore_time => {type => BOOLEAN,
3129 %append_action_options,
3132 my %info = __begin_control(%param,
3133 command => 'archive',
3135 my ($debug,$transcript) = @info{qw(debug transcript)};
3136 my @data = @{$info{data}};
3137 my @bugs = @{$info{bugs}};
3138 my $action = "$config{bug} archived.";
3139 if ($param{check_archiveable} and
3140 not bug_archiveable(bug=>$param{bug},
3141 ignore_time => $param{ignore_time},
3143 print {$transcript} "Bug $param{bug} cannot be archived\n";
3144 die "Bug $param{bug} cannot be archived";
3146 if (not $param{archive_unarchived} and
3147 not exists $data[0]{unarchived}
3149 print {$transcript} "$param{bug} has not been archived previously\n";
3150 die "$param{bug} has not been archived previously";
3152 add_recipients(recipients => $param{recipients},
3155 transcript => $transcript,
3157 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3158 for my $bug (@bugs) {
3159 if ($param{check_archiveable}) {
3160 die "Bug $bug cannot be archived (but $param{bug} can?)"
3161 unless bug_archiveable(bug=>$bug,
3162 ignore_time => $param{ignore_time},
3166 # If we get here, we can archive/remove this bug
3167 print {$debug} "$param{bug} removing\n";
3168 for my $bug (@bugs) {
3169 #print "$param{bug} removing $bug\n" if $debug;
3170 my $dir = get_hashname($bug);
3171 # First indicate that this bug is being archived
3172 append_action_to_log(bug => $bug,
3174 command => 'archive',
3175 # we didn't actually change the data
3176 # when we archived, so we don't pass
3177 # a real new_data or old_data
3180 __return_append_to_log_options(
3185 if not exists $param{append_log} or $param{append_log};
3186 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
3187 if ($config{save_old_bugs}) {
3188 mkpath("$config{spool_dir}/archive/$dir");
3189 foreach my $file (@files_to_remove) {
3190 link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3191 copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3192 # we need to bail out here if things have
3193 # gone horribly wrong to avoid removing a
3195 die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
3198 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
3200 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
3201 print {$debug} "deleted $bug (from $param{bug})\n";
3203 bughook_archive(@bugs);
3204 __end_control(%info);
3207 =head2 bug_unarchive
3211 bug_unarchive(bug => $bug_num,
3213 transcript => \$transcript,
3218 transcript("Unable to archive bug: $bug_num");
3220 transcript($transcript);
3222 This routine unarchives a bug
3227 my %param = validate_with(params => \@_,
3228 spec => {bug => {type => SCALAR,
3232 %append_action_options,
3236 my %info = __begin_control(%param,
3238 command=>'unarchive');
3239 my ($debug,$transcript) =
3240 @info{qw(debug transcript)};
3241 my @data = @{$info{data}};
3242 my @bugs = @{$info{bugs}};
3243 my $action = "$config{bug} unarchived.";
3244 my @files_to_remove;
3245 for my $bug (@bugs) {
3246 print {$debug} "$param{bug} removing $bug\n";
3247 my $dir = get_hashname($bug);
3248 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
3249 mkpath("archive/$dir");
3250 foreach my $file (@files_to_copy) {
3251 # die'ing here sucks
3252 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3253 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3254 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
3256 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
3257 print {$transcript} "Unarchived $config{bug} $bug\n";
3259 unlink(@files_to_remove) or die "Unable to unlink bugs";
3260 # Indicate that this bug has been archived previously
3261 for my $bug (@bugs) {
3262 my $newdata = readbug($bug);
3263 my $old_data = dclone($newdata);
3264 if (not defined $newdata) {
3265 print {$transcript} "$config{bug} $bug disappeared!\n";
3266 die "Bug $bug disappeared!";
3268 $newdata->{unarchived} = time;
3269 append_action_to_log(bug => $bug,
3271 command => 'unarchive',
3272 new_data => $newdata,
3273 old_data => $old_data,
3274 __return_append_to_log_options(
3279 if not exists $param{append_log} or $param{append_log};
3280 writebug($bug,$newdata);
3282 __end_control(%info);
3285 =head2 append_action_to_log
3287 append_action_to_log
3289 This should probably be moved to Debbugs::Log; have to think that out
3294 sub append_action_to_log{
3295 my %param = validate_with(params => \@_,
3296 spec => {bug => {type => SCALAR,
3299 new_data => {type => HASHREF,
3302 old_data => {type => HASHREF,
3305 command => {type => SCALAR,
3308 action => {type => SCALAR,
3310 requester => {type => SCALAR,
3313 request_addr => {type => SCALAR,
3316 location => {type => SCALAR,
3319 message => {type => SCALAR|ARRAYREF,
3322 recips => {type => SCALAR|ARRAYREF,
3325 desc => {type => SCALAR,
3328 get_lock => {type => BOOLEAN,
3331 locks => {type => HASHREF,
3335 # append_action_options here
3336 # because some of these
3337 # options aren't actually
3338 # optional, even though the
3339 # original function doesn't
3343 # Fix this to use $param{location}
3344 my $log_location = buglog($param{bug});
3345 die "Unable to find .log for $param{bug}"
3346 if not defined $log_location;
3347 if ($param{get_lock}) {
3348 filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
3352 my $logfh = IO::File->new(">>$log_location") or
3353 die "Unable to open $log_location for appending: $!";
3354 # determine difference between old and new
3356 if (exists $param{old_data} and exists $param{new_data}) {
3357 my $old_data = dclone($param{old_data});
3358 my $new_data = dclone($param{new_data});
3359 for my $key (keys %{$old_data}) {
3360 if (not exists $Debbugs::Status::fields{$key}) {
3361 delete $old_data->{$key};
3364 next unless exists $new_data->{$key};
3365 next unless defined $new_data->{$key};
3366 if (not defined $old_data->{$key}) {
3367 delete $old_data->{$key};
3370 if (ref($new_data->{$key}) and
3371 ref($old_data->{$key}) and
3372 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3373 local $Storable::canonical = 1;
3374 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3375 delete $new_data->{$key};
3376 delete $old_data->{$key};
3379 elsif ($new_data->{$key} eq $old_data->{$key}) {
3380 delete $new_data->{$key};
3381 delete $old_data->{$key};
3384 for my $key (keys %{$new_data}) {
3385 if (not exists $Debbugs::Status::fields{$key}) {
3386 delete $new_data->{$key};
3389 next unless exists $old_data->{$key};
3390 next unless defined $old_data->{$key};
3391 if (not defined $new_data->{$key} or
3392 not exists $Debbugs::Status::fields{$key}) {
3393 delete $new_data->{$key};
3396 if (ref($new_data->{$key}) and
3397 ref($old_data->{$key}) and
3398 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3399 local $Storable::canonical = 1;
3400 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3401 delete $new_data->{$key};
3402 delete $old_data->{$key};
3405 elsif ($new_data->{$key} eq $old_data->{$key}) {
3406 delete $new_data->{$key};
3407 delete $old_data->{$key};
3410 $data_diff .= "<!-- new_data:\n";
3412 for my $key (keys %{$new_data}) {
3413 if (not exists $Debbugs::Status::fields{$key}) {
3414 warn "No such field $key";
3417 $nd{$key} = $new_data->{$key};
3418 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
3420 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
3421 $data_diff .= "-->\n";
3422 $data_diff .= "<!-- old_data:\n";
3424 for my $key (keys %{$old_data}) {
3425 if (not exists $Debbugs::Status::fields{$key}) {
3426 warn "No such field $key";
3429 $od{$key} = $old_data->{$key};
3430 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
3432 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
3433 $data_diff .= "-->\n";
3436 (exists $param{command} ?
3437 "<!-- command:".html_escape(encode_utf8_safely($param{command}))." -->\n":""
3439 (length $param{requester} ?
3440 "<!-- requester: ".html_escape(encode_utf8_safely($param{requester}))." -->\n":""
3442 (length $param{request_addr} ?
3443 "<!-- request_addr: ".html_escape(encode_utf8_safely($param{request_addr}))." -->\n":""
3445 "<!-- time:".time()." -->\n",
3447 "<strong>".html_escape(encode_utf8_safely($param{action}))."</strong>\n");
3448 if (length $param{requester}) {
3449 $msg .= "Request was from <code>".html_escape(encode_utf8_safely($param{requester}))."</code>\n";
3451 if (length $param{request_addr}) {
3452 $msg .= "to <code>".html_escape(encode_utf8_safely($param{request_addr}))."</code>";
3454 if (length $param{desc}) {
3455 $msg .= ":<br>\n".encode_utf8_safely($param{desc})."\n";
3460 push @records, {type => 'html',
3464 if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
3465 push @records, {type => exists $param{recips}?'recips':'incoming-recv',
3466 exists $param{recips}?(recips => [make_list($param{recips})]):(),
3467 text => join('',make_list($param{message})),
3470 write_log_records(logfh=>$logfh,
3471 records => \@records,
3473 close $logfh or die "Unable to close $log_location: $!";
3474 if ($param{get_lock}) {
3475 unfilelock(exists $param{locks}?$param{locks}:());
3483 =head1 PRIVATE FUNCTIONS
3485 =head2 __handle_affected_packages
3487 __handle_affected_packages(affected_packages => {},
3495 sub __handle_affected_packages{
3496 my %param = validate_with(params => \@_,
3497 spec => {%common_options,
3498 data => {type => ARRAYREF|HASHREF
3503 for my $data (make_list($param{data})) {
3504 next unless exists $data->{package} and defined $data->{package};
3505 my @packages = split /\s*,\s*/,$data->{package};
3506 @{$param{affected_packages}}{@packages} = (1) x @packages;
3510 =head2 __handle_debug_transcript
3512 my ($debug,$transcript) = __handle_debug_transcript(%param);
3514 Returns a debug and transcript filehandle
3519 sub __handle_debug_transcript{
3520 my %param = validate_with(params => \@_,
3521 spec => {%common_options},
3524 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
3525 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3526 return ($debug,$transcript);
3533 Produces a small bit of bug information to kick out to the transcript
3540 next unless defined $data and exists $data->{bug_num};
3541 $return .= "Bug #".($data->{bug_num}||'').
3542 ((defined $data->{done} and length $data->{done})?
3543 " {Done: $data->{done}}":''
3545 " [".($data->{package}||'(no package)'). "] ".
3546 ($data->{subject}||'(no subject)')."\n";
3552 =head2 __internal_request
3554 __internal_request()
3555 __internal_request($level)
3557 Returns true if the caller of the function calling __internal_request
3558 belongs to __PACKAGE__
3560 This allows us to be magical, and don't bother to print bug info if
3561 the second caller is from this package, amongst other things.
3563 An optional level is allowed, which increments the number of levels to
3564 check by the given value. [This is basically for use by internal
3565 functions like __begin_control which are always called by
3570 sub __internal_request{
3572 $l = 0 if not defined $l;
3573 if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
3579 sub __return_append_to_log_options{
3581 my $action = $param{action} if exists $param{action};
3582 if (not exists $param{requester}) {
3583 $param{requester} = $config{control_internal_requester};
3585 if (not exists $param{request_addr}) {
3586 $param{request_addr} = $config{control_internal_request_addr};
3588 if (not exists $param{message}) {
3589 my $date = rfc822_date();
3590 $param{message} = fill_in_template(template => 'mail/fake_control_message',
3591 variables => {request_addr => $param{request_addr},
3592 requester => $param{requester},
3598 if (not defined $action) {
3599 carp "Undefined action!";
3600 $action = "unknown action";
3602 return (action => $action,
3603 hash_slice(%param,keys %append_action_options),
3607 =head2 __begin_control
3609 my %info = __begin_control(%param,
3611 command=>'unarchive');
3612 my ($debug,$transcript) = @info{qw(debug transcript)};
3613 my @data = @{$info{data}};
3614 my @bugs = @{$info{bugs}};
3617 Starts the process of modifying a bug; handles all of the generic
3618 things that almost every control request needs
3620 Returns a hash containing
3624 =item new_locks -- number of new locks taken out by this call
3626 =item debug -- the debug file handle
3628 =item transcript -- the transcript file handle
3630 =item data -- an arrayref containing the data of the bugs
3631 corresponding to this request
3633 =item bugs -- an arrayref containing the bug numbers of the bugs
3634 corresponding to this request
3642 sub __begin_control {
3643 my %param = validate_with(params => \@_,
3644 spec => {bug => {type => SCALAR,
3647 archived => {type => BOOLEAN,
3650 command => {type => SCALAR,
3658 my ($debug,$transcript) = __handle_debug_transcript(@_);
3659 print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
3660 # print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
3661 $lockhash = $param{locks} if exists $param{locks};
3663 my $old_die = $SIG{__DIE__};
3664 $SIG{__DIE__} = *sig_die{CODE};
3666 ($new_locks, @data) =
3667 lock_read_all_merged_bugs(bug => $param{bug},
3668 $param{archived}?(location => 'archive'):(),
3669 exists $param{locks} ? (locks => $param{locks}):(),
3671 $locks += $new_locks;
3673 die "Unable to read any bugs successfully.";
3675 if (not $param{archived}) {
3676 for my $data (@data) {
3677 if ($data->{archived}) {
3678 die "Not altering archived bugs; see unarchive.";
3682 if (not check_limit(data => \@data,
3683 exists $param{limit}?(limit => $param{limit}):(),
3684 transcript => $transcript,
3686 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
3689 __handle_affected_packages(%param,data => \@data);
3690 print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
3691 print {$debug} "$param{bug} read $locks locks\n";
3692 if (not @data or not defined $data[0]) {
3693 print {$transcript} "No bug found for $param{bug}\n";
3694 die "No bug found for $param{bug}";
3697 add_recipients(data => \@data,
3698 recipients => $param{recipients},
3699 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
3701 (__internal_request()?(transcript => $transcript):()),
3704 print {$debug} "$param{bug} read done\n";
3705 my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
3706 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3707 return (data => \@data,
3709 old_die => $old_die,
3710 new_locks => $new_locks,
3712 transcript => $transcript,
3714 exists $param{locks}?(locks => $param{locks}):(),
3718 =head2 __end_control
3720 __end_control(%info);
3722 Handles tearing down from a control request
3728 if (exists $info{new_locks} and $info{new_locks} > 0) {
3729 print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
3730 for (1..$info{new_locks}) {
3731 unfilelock(exists $info{locks}?$info{locks}:());
3735 $SIG{__DIE__} = $info{old_die};
3736 if (exists $info{param}{affected_bugs}) {
3737 @{$info{param}{affected_bugs}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
3739 add_recipients(recipients => $info{param}{recipients},
3740 (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
3741 data => $info{data},
3742 debug => $info{debug},
3743 transcript => $info{transcript},
3745 __handle_affected_packages(%{$info{param}},data=>$info{data});
3751 check_limit(data => \@data, limit => $param{limit});
3754 Checks to make sure that bugs match any limits; each entry of @data
3755 much satisfy the limit.
3757 Returns true if there are no entries in data, or there are no keys in
3758 limit; returns false (0) if there are any entries which do not match.
3760 The limit hashref elements can contain an arrayref of scalars to
3761 match; regexes are also acccepted. At least one of the entries in each
3762 element needs to match the corresponding field in all data for the
3769 my %param = validate_with(params => \@_,
3770 spec => {data => {type => ARRAYREF|HASHREF,
3772 limit => {type => HASHREF|UNDEF,
3774 transcript => {type => SCALARREF|HANDLE,
3779 my @data = make_list($param{data});
3781 not defined $param{limit} or
3782 not keys %{$param{limit}}) {
3785 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3786 my $going_to_fail = 0;
3787 for my $data (@data) {
3788 $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
3789 status => dclone($data),
3791 for my $field (keys %{$param{limit}}) {
3792 next unless exists $param{limit}{$field};
3794 my @data_fields = make_list($data->{$field});
3795 LIMIT: for my $limit (make_list($param{limit}{$field})) {
3796 if (not ref $limit) {
3797 for my $data_field (@data_fields) {
3798 if ($data_field eq $limit) {
3804 elsif (ref($limit) eq 'Regexp') {
3805 for my $data_field (@data_fields) {
3806 if ($data_field =~ $limit) {
3813 warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
3818 print {$transcript} qq($field: ).join(', ',map{qq("$_")} make_list($data->{$field})).
3819 "' does not match at least one of ".
3820 join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
3824 return $going_to_fail?0:1;
3832 We override die to specially handle unlocking files in the cases where
3833 we are called via eval. [If we're not called via eval, it doesn't
3839 if ($^S) { # in eval
3841 for (1..$locks) { unfilelock(defined $lockhash?$lockhash:()); }
3848 # =head2 __message_body_template
3850 # message_body_template('mail/ack',{ref=>'foo'});
3852 # Creates a message body using a template
3856 sub __message_body_template{
3857 my ($template,$extra_var) = @_;
3859 my $hole_var = {'&bugurl' =>
3861 'http://'.$config{cgi_domain}.'/'.
3862 Debbugs::CGI::bug_links(bug => $_[0],
3868 my $body = fill_in_template(template => $template,
3869 variables => {config => \%config,
3872 hole_var => $hole_var,
3874 return fill_in_template(template => 'mail/message_body',
3875 variables => {config => \%config,
3879 hole_var => $hole_var,
3883 sub __all_undef_or_equal {
3885 return 1 if @values == 1 or @values == 0;
3886 my $not_def = grep {not defined $_} @values;
3887 if ($not_def == @values) {
3890 if ($not_def > 0 and $not_def != @values) {
3893 my $first_val = shift @values;
3894 for my $val (@values) {
3895 if ($first_val ne $val) {