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 :utf8);
114 use Debbugs::Status qw(bug_archiveable :read :hook writebug new_bug splitpackages split_status_fields get_bug_status);
115 use Debbugs::CGI qw(html_escape);
116 use Debbugs::Log qw(:misc :write);
117 use Debbugs::Recipients qw(:add);
118 use Debbugs::Packages qw(:versions :mapping);
120 use Data::Dumper qw();
121 use Params::Validate qw(validate_with :types);
122 use File::Path qw(mkpath);
123 use File::Copy qw(copy);
126 use Debbugs::Text qw(:templates);
128 use Debbugs::Mail qw(rfc822_date send_mail_message default_headers);
129 use Debbugs::MIME qw(create_mime_message);
131 use Mail::RFC822::Address qw();
133 use POSIX qw(strftime);
135 use Storable qw(dclone nfreeze);
136 use List::Util qw(first max);
137 use Encode qw(encode_utf8);
141 # These are a set of options which are common to all of these functions
143 my %common_options = (debug => {type => SCALARREF|HANDLE,
146 transcript => {type => SCALARREF|HANDLE,
149 affected_bugs => {type => HASHREF,
152 affected_packages => {type => HASHREF,
155 recipients => {type => HASHREF,
158 limit => {type => HASHREF,
161 show_bug_info => {type => BOOLEAN,
164 request_subject => {type => SCALAR,
165 default => 'Unknown Subject',
167 request_msgid => {type => SCALAR,
170 request_nn => {type => SCALAR,
173 request_replyto => {type => SCALAR,
176 locks => {type => HASHREF,
182 my %append_action_options =
183 (action => {type => SCALAR,
186 requester => {type => SCALAR,
189 request_addr => {type => SCALAR,
192 location => {type => SCALAR,
195 message => {type => SCALAR|ARRAYREF,
198 append_log => {type => BOOLEAN,
200 depends => [qw(requester request_addr),
204 # locks is both an append_action option, and a common option;
205 # it's ok for it to be in both places.
206 locks => {type => HASHREF,
214 # this is just a generic stub for Debbugs::Control functions.
219 # set_foo(bug => $ref,
220 # transcript => $transcript,
221 # ($dl > 0 ? (debug => $transcript):()),
222 # requester => $header{from},
223 # request_addr => $controlrequestaddr,
225 # affected_packages => \%affected_packages,
226 # recipients => \%recipients,
232 # print {$transcript} "Failed to set foo $ref bar: $@";
240 # my %param = validate_with(params => \@_,
241 # spec => {bug => {type => SCALAR,
242 # regex => qr/^\d+$/,
244 # # specific options here
246 # %append_action_options,
250 # __begin_control(%param,
253 # my ($debug,$transcript) =
254 # @info{qw(debug transcript)};
255 # my @data = @{$info{data}};
256 # my @bugs = @{$info{bugs}};
259 # for my $data (@data) {
260 # append_action_to_log(bug => $data->{bug_num},
262 # __return_append_to_log_options(
267 # if not exists $param{append_log} or $param{append_log};
268 # writebug($data->{bug_num},$data);
269 # print {$transcript} "$action\n";
271 # __end_control(%info);
278 set_block(bug => $ref,
279 transcript => $transcript,
280 ($dl > 0 ? (debug => $transcript):()),
281 requester => $header{from},
282 request_addr => $controlrequestaddr,
284 affected_packages => \%affected_packages,
285 recipients => \%recipients,
291 print {$transcript} "Failed to set blockers of $ref: $@";
294 Alters the set of bugs that block this bug from being fixed
296 This requires altering both this bug (and those it's merged with) as
297 well as the bugs that block this bug from being fixed (and those that
302 =item block -- scalar or arrayref of blocking bugs to set, add or remove
304 =item add -- if true, add blocking bugs
306 =item remove -- if true, remove blocking bugs
313 my %param = validate_with(params => \@_,
314 spec => {bug => {type => SCALAR,
317 # specific options here
318 block => {type => SCALAR|ARRAYREF,
321 add => {type => BOOLEAN,
324 remove => {type => BOOLEAN,
328 %append_action_options,
331 if ($param{add} and $param{remove}) {
332 croak "It's nonsensical to add and remove the same blocking bugs";
334 if (grep {$_ !~ /^\d+$/} make_list($param{block})) {
335 croak "Invalid blocking bug(s):".
336 join(', ',grep {$_ !~ /^\d+$/} make_list($param{block}));
342 elsif ($param{remove}) {
347 __begin_control(%param,
350 my ($debug,$transcript) =
351 @info{qw(debug transcript)};
352 my @data = @{$info{data}};
353 my @bugs = @{$info{bugs}};
356 # The first bit of this code is ugly, and should be cleaned up.
357 # Its purpose is to populate %removed_blockers and %add_blockers
358 # with all of the bugs that should be added or removed as blockers
359 # of all of the bugs which are merged with $param{bug}
362 for my $blocker (make_list($param{block})) {
363 next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
364 my $data = read_bug(bug=>$blocker,
366 if (defined $data and not $data->{archive}) {
367 $data = split_status_fields($data);
368 $ok_blockers{$blocker} = 1;
370 push @merged_bugs, make_list($data->{mergedwith});
371 @ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
374 $bad_blockers{$blocker} = 1;
378 # throw an error if we are setting the blockers and there is a bad
380 if (keys %bad_blockers and $mode eq 'set') {
381 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
382 keys %ok_blockers?'':" and no known blocking bug(s)";
384 # if there are no ok blockers and we are not setting the blockers,
386 if (not keys %ok_blockers and $mode ne 'set') {
387 print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
388 if (keys %bad_blockers) {
389 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
391 __end_control(%info);
395 my @change_blockers = keys %ok_blockers;
397 my %removed_blockers;
400 my @blockers = map {split ' ', $_->{blockedby}} @data;
402 @blockers{@blockers} = (1) x @blockers;
404 # it is nonsensical for a bug to block itself (or a merged
405 # partner); We currently don't allow removal because we'd possibly
409 @bugs{@bugs} = (1) x @bugs;
410 for my $blocker (@change_blockers) {
411 if ($bugs{$blocker}) {
412 croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
415 @blockers = keys %blockers;
417 %removed_blockers = ();
418 for my $blocker (@change_blockers) {
419 next if exists $blockers{$blocker};
420 $blockers{$blocker} = 1;
421 $added_blockers{$blocker} = 1;
424 elsif ($param{remove}) {
425 %added_blockers = ();
426 for my $blocker (@change_blockers) {
427 next if exists $removed_blockers{$blocker};
428 delete $blockers{$blocker};
429 $removed_blockers{$blocker} = 1;
433 @removed_blockers{@blockers} = (1) x @blockers;
435 for my $blocker (@change_blockers) {
436 next if exists $blockers{$blocker};
437 $blockers{$blocker} = 1;
438 if (exists $removed_blockers{$blocker}) {
439 delete $removed_blockers{$blocker};
442 $added_blockers{$blocker} = 1;
446 my @new_blockers = keys %blockers;
447 for my $data (@data) {
448 my $old_data = dclone($data);
449 # remove blockers and/or add new ones as appropriate
450 if ($data->{blockedby} eq '') {
451 print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
453 print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
455 if ($data->{blocks} eq '') {
456 print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
458 print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\n";
461 push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
462 push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
463 $action = ucfirst(join ('; ',@changed)) if @changed;
465 print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
468 $data->{blockedby} = join(' ',keys %blockers);
469 append_action_to_log(bug => $data->{bug_num},
471 old_data => $old_data,
474 __return_append_to_log_options(
479 if not exists $param{append_log} or $param{append_log};
480 writebug($data->{bug_num},$data);
481 print {$transcript} "$action\n";
483 # we do this bit below to avoid code duplication
485 $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
486 $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
488 for my $add_remove (keys %mungable_blocks) {
492 for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
493 next if $munge_blockers{$blocker};
494 my ($temp_locks, @blocking_data) =
495 lock_read_all_merged_bugs(bug => $blocker,
496 ($param{archived}?(location => 'archive'):()),
497 exists $param{locks}?(locks => $param{locks}):(),
499 $locks+= $temp_locks;
500 $new_locks+=$temp_locks;
501 if (not @blocking_data) {
502 for (1..$new_locks) {
503 unfilelock(exists $param{locks}?$param{locks}:());
506 die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
508 for (map {$_->{bug_num}} @blocking_data) {
509 $munge_blockers{$_} = 1;
511 for my $data (@blocking_data) {
512 my $old_data = dclone($data);
514 my @blocks = split ' ', $data->{blocks};
515 @blocks{@blocks} = (1) x @blocks;
517 for my $bug (@bugs) {
518 if ($add_remove eq 'remove') {
519 next unless exists $blocks{$bug};
520 delete $blocks{$bug};
523 next if exists $blocks{$bug};
528 $data->{blocks} = join(' ',sort keys %blocks);
529 my $action = ($add_remove eq 'add'?'Added':'Removed').
530 " indication that bug $data->{bug_num} blocks ".
532 append_action_to_log(bug => $data->{bug_num},
534 old_data => $old_data,
537 __return_append_to_log_options(%param,
541 writebug($data->{bug_num},$data);
543 __handle_affected_packages(%param,data=>\@blocking_data);
544 add_recipients(recipients => $param{recipients},
545 actions_taken => {blocks => 1},
546 data => \@blocking_data,
548 transcript => $transcript,
551 for (1..$new_locks) {
552 unfilelock(exists $param{locks}?$param{locks}:());
557 __end_control(%info);
566 transcript => $transcript,
567 ($dl > 0 ? (debug => $transcript):()),
568 requester => $header{from},
569 request_addr => $controlrequestaddr,
571 affected_packages => \%affected_packages,
572 recipients => \%recipients,
579 print {$transcript} "Failed to set tag on $ref: $@";
583 Sets, adds, or removes the specified tags on a bug
587 =item tag -- scalar or arrayref of tags to set, add or remove
589 =item add -- if true, add tags
591 =item remove -- if true, remove tags
593 =item warn_on_bad_tags -- if true (the default) warn if bad tags are
601 my %param = validate_with(params => \@_,
602 spec => {bug => {type => SCALAR,
605 # specific options here
606 tag => {type => SCALAR|ARRAYREF,
609 add => {type => BOOLEAN,
612 remove => {type => BOOLEAN,
615 warn_on_bad_tags => {type => BOOLEAN,
619 %append_action_options,
622 if ($param{add} and $param{remove}) {
623 croak "It's nonsensical to add and remove the same tags";
627 __begin_control(%param,
630 my ($debug,$transcript) =
631 @info{qw(debug transcript)};
632 my @data = @{$info{data}};
633 my @bugs = @{$info{bugs}};
634 my @tags = make_list($param{tag});
635 if (not @tags and ($param{remove} or $param{add})) {
636 if ($param{remove}) {
637 print {$transcript} "Requested to remove no tags; doing nothing.\n";
640 print {$transcript} "Requested to add no tags; doing nothing.\n";
642 __end_control(%info);
645 # first things first, make the versions fully qualified source
647 for my $data (@data) {
648 my $action = 'Did not alter tags';
650 my %tag_removed = ();
651 my %fixed_removed = ();
652 my @old_tags = split /\,?\s+/, $data->{keywords};
654 @tags{@old_tags} = (1) x @old_tags;
656 my $old_data = dclone($data);
657 if (not $param{add} and not $param{remove}) {
658 $tag_removed{$_} = 1 for @old_tags;
662 for my $tag (@tags) {
663 if (not $param{remove} and
664 not defined first {$_ eq $tag} @{$config{tags}}) {
665 push @bad_tags, $tag;
669 if (not exists $tags{$tag}) {
671 $tag_added{$tag} = 1;
674 elsif ($param{remove}) {
675 if (exists $tags{$tag}) {
677 $tag_removed{$tag} = 1;
681 if (exists $tag_removed{$tag}) {
682 delete $tag_removed{$tag};
685 $tag_added{$tag} = 1;
690 if (@bad_tags and $param{warn_on_bad_tags}) {
691 print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
692 print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
694 $data->{keywords} = join(' ',keys %tags);
697 push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
698 push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
699 $action = ucfirst(join ('; ',@changed)) if @changed;
701 print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
705 append_action_to_log(bug => $data->{bug_num},
708 old_data => $old_data,
710 __return_append_to_log_options(
715 if not exists $param{append_log} or $param{append_log};
716 writebug($data->{bug_num},$data);
717 print {$transcript} "$action\n";
719 __end_control(%info);
727 set_severity(bug => $ref,
728 transcript => $transcript,
729 ($dl > 0 ? (debug => $transcript):()),
730 requester => $header{from},
731 request_addr => $controlrequestaddr,
733 affected_packages => \%affected_packages,
734 recipients => \%recipients,
735 severity => 'normal',
740 print {$transcript} "Failed to set the severity of bug $ref: $@";
743 Sets the severity of a bug. If severity is not passed, is undefined,
744 or has zero length, sets the severity to the default severity.
749 my %param = validate_with(params => \@_,
750 spec => {bug => {type => SCALAR,
753 # specific options here
754 severity => {type => SCALAR|UNDEF,
755 default => $config{default_severity},
758 %append_action_options,
761 if (not defined $param{severity} or
762 not length $param{severity}
764 $param{severity} = $config{default_severity};
767 # check validity of new severity
768 if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
769 die "Severity '$param{severity}' is not a valid severity level";
772 __begin_control(%param,
773 command => 'severity'
775 my ($debug,$transcript) =
776 @info{qw(debug transcript)};
777 my @data = @{$info{data}};
778 my @bugs = @{$info{bugs}};
781 for my $data (@data) {
782 if (not defined $data->{severity}) {
783 $data->{severity} = $param{severity};
784 $action = "Severity set to '$param{severity}'";
787 if ($data->{severity} eq '') {
788 $data->{severity} = $config{default_severity};
790 if ($data->{severity} eq $param{severity}) {
791 print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
794 $action = "Severity set to '$param{severity}' from '$data->{severity}'";
795 $data->{severity} = $param{severity};
797 append_action_to_log(bug => $data->{bug_num},
799 __return_append_to_log_options(
804 if not exists $param{append_log} or $param{append_log};
805 writebug($data->{bug_num},$data);
806 print {$transcript} "$action\n";
808 __end_control(%info);
815 set_done(bug => $ref,
816 transcript => $transcript,
817 ($dl > 0 ? (debug => $transcript):()),
818 requester => $header{from},
819 request_addr => $controlrequestaddr,
821 affected_packages => \%affected_packages,
822 recipients => \%recipients,
827 print {$transcript} "Failed to set foo $ref bar: $@";
835 my %param = validate_with(params => \@_,
836 spec => {bug => {type => SCALAR,
839 reopen => {type => BOOLEAN,
842 submitter => {type => SCALAR,
845 clear_fixed => {type => BOOLEAN,
848 notify_submitter => {type => BOOLEAN,
851 original_report => {type => SCALARREF,
854 done => {type => SCALAR|UNDEF,
858 %append_action_options,
862 if (exists $param{submitter} and
863 not Mail::RFC822::Address::valid($param{submitter})) {
864 die "New submitter address '$param{submitter}' is not a valid e-mail address";
866 if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
867 $param{done} = $param{requester};
869 if (exists $param{done} and
870 (not defined $param{done} or
871 not length $param{done})) {
877 __begin_control(%param,
878 command => $param{reopen}?'reopen':'done',
880 my ($debug,$transcript) =
881 @info{qw(debug transcript)};
882 my @data = @{$info{data}};
883 my @bugs = @{$info{bugs}};
886 if ($param{reopen}) {
887 # avoid warning multiple times if there are fixed versions
889 for my $data (@data) {
890 if (not exists $data->{done} or
891 not defined $data->{done} or
892 not length $data->{done}) {
893 print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
894 __end_control(%info);
897 if (@{$data->{fixed_versions}} and $warn_fixed) {
898 print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
899 print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
903 $action = "Bug reopened";
904 for my $data (@data) {
905 my $old_data = dclone($data);
907 append_action_to_log(bug => $data->{bug_num},
910 old_data => $old_data,
912 __return_append_to_log_options(
917 if not exists $param{append_log} or $param{append_log};
918 writebug($data->{bug_num},$data);
920 print {$transcript} "$action\n";
921 __end_control(%info);
922 if (exists $param{submitter}) {
923 set_submitter(bug => $param{bug},
924 submitter => $param{submitter},
926 keys %common_options,
927 keys %append_action_options)
930 # clear the fixed revisions
931 if ($param{clear_fixed}) {
932 set_fixed(fixed => [],
936 keys %common_options,
937 keys %append_action_options),
942 my %submitter_notified;
943 my $requester_notified = 0;
944 my $orig_report_set = 0;
945 for my $data (@data) {
946 if (exists $data->{done} and
947 defined $data->{done} and
948 length $data->{done}) {
949 print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
950 __end_control(%info);
954 for my $data (@data) {
955 my $old_data = dclone($data);
956 my $hash = get_hashname($data->{bug_num});
957 my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
958 die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
962 $orig_report= <$report_fh>;
965 if (not $orig_report_set and defined $orig_report and
966 length $orig_report and
967 exists $param{original_report}){
968 ${$param{original_report}} = $orig_report;
969 $orig_report_set = 1;
972 $action = "Marked $config{bug} as done";
974 # set done to the requester
975 $data->{done} = exists $param{done}?$param{done}:$param{requester};
976 append_action_to_log(bug => $data->{bug_num},
979 old_data => $old_data,
981 __return_append_to_log_options(
986 if not exists $param{append_log} or $param{append_log};
987 writebug($data->{bug_num},$data);
988 print {$transcript} "$action\n";
989 # get the original report
990 if ($param{notify_submitter}) {
991 my $submitter_message;
992 if(not exists $submitter_notified{$data->{originator}}) {
994 create_mime_message([default_headers(queue_file => $param{request_nn},
996 msgid => $param{request_msgid},
997 msgtype => 'notifdone',
998 pr_msg => 'they-closed',
1000 [To => $data->{submitter},
1001 Subject => "$config{ubug}#$data->{bug_num} ".
1002 "closed by $param{requester} ".(defined $param{request_subject}?"($param{request_subject})":""),
1006 __message_body_template('mail/process_your_bug_done',
1008 replyto => (exists $param{request_replyto} ?
1009 $param{request_replyto} :
1010 $param{requester} || 'Unknown'),
1011 markedby => $param{requester},
1012 subject => $param{request_subject},
1013 messageid => $param{request_msgid},
1016 [join('',make_list($param{message})),$orig_report]
1018 send_mail_message(message => $submitter_message,
1019 recipients => $old_data->{submitter},
1021 $submitter_notified{$data->{originator}} = $submitter_message;
1024 $submitter_message = $submitter_notified{$data->{originator}};
1026 append_action_to_log(bug => $data->{bug_num},
1027 action => "Notification sent",
1029 request_addr => $data->{originator},
1030 desc => "$config{bug} acknowledged by developer.",
1031 recips => [$data->{originator}],
1032 message => $submitter_message,
1037 __end_control(%info);
1038 if (exists $param{fixed}) {
1039 set_fixed(fixed => $param{fixed},
1043 keys %common_options,
1044 keys %append_action_options
1052 =head2 set_submitter
1055 set_submitter(bug => $ref,
1056 transcript => $transcript,
1057 ($dl > 0 ? (debug => $transcript):()),
1058 requester => $header{from},
1059 request_addr => $controlrequestaddr,
1061 affected_packages => \%affected_packages,
1062 recipients => \%recipients,
1063 submitter => $new_submitter,
1064 notify_submitter => 1,
1069 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1072 Sets the submitter of a bug. If notify_submitter is true (the
1073 default), notifies the old submitter of a bug on changes
1078 my %param = validate_with(params => \@_,
1079 spec => {bug => {type => SCALAR,
1082 # specific options here
1083 submitter => {type => SCALAR,
1085 notify_submitter => {type => BOOLEAN,
1089 %append_action_options,
1092 if (not Mail::RFC822::Address::valid($param{submitter})) {
1093 die "New submitter address $param{submitter} is not a valid e-mail address";
1096 __begin_control(%param,
1097 command => 'submitter'
1099 my ($debug,$transcript) =
1100 @info{qw(debug transcript)};
1101 my @data = @{$info{data}};
1102 my @bugs = @{$info{bugs}};
1104 # here we only concern ourselves with the first of the merged bugs
1105 for my $data ($data[0]) {
1106 my $notify_old_submitter = 0;
1107 my $old_data = dclone($data);
1108 print {$debug} "Going to change bug submitter\n";
1109 if (((not defined $param{submitter} or not length $param{submitter}) and
1110 (not defined $data->{originator} or not length $data->{originator})) or
1111 (defined $param{submitter} and defined $data->{originator} and
1112 $param{submitter} eq $data->{originator})) {
1113 print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
1117 if (defined $data->{originator} and length($data->{originator})) {
1118 $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'";
1119 $notify_old_submitter = 1;
1122 $action= "Set $config{bug} submitter to '$param{submitter}'.";
1124 $data->{originator} = $param{submitter};
1126 append_action_to_log(bug => $data->{bug_num},
1127 command => 'submitter',
1129 old_data => $old_data,
1131 __return_append_to_log_options(
1136 if not exists $param{append_log} or $param{append_log};
1137 writebug($data->{bug_num},$data);
1138 print {$transcript} "$action\n";
1139 # notify old submitter
1140 if ($notify_old_submitter and $param{notify_submitter}) {
1141 send_mail_message(message =>
1142 create_mime_message([default_headers(queue_file => $param{request_nn},
1144 msgid => $param{request_msgid},
1146 pr_msg => 'submitter-changed',
1148 [To => $old_data->{submitter},
1149 Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
1153 __message_body_template('mail/submitter_changed',
1154 {old_data => $old_data,
1156 replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
1160 recipients => $old_data->{submitter},
1164 __end_control(%info);
1169 =head2 set_forwarded
1172 set_forwarded(bug => $ref,
1173 transcript => $transcript,
1174 ($dl > 0 ? (debug => $transcript):()),
1175 requester => $header{from},
1176 request_addr => $controlrequestaddr,
1178 affected_packages => \%affected_packages,
1179 recipients => \%recipients,
1180 forwarded => $forward_to,
1185 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1188 Sets the location to which a bug is forwarded. Given an undef
1189 forwarded, unsets forwarded.
1195 my %param = validate_with(params => \@_,
1196 spec => {bug => {type => SCALAR,
1199 # specific options here
1200 forwarded => {type => SCALAR|UNDEF,
1203 %append_action_options,
1206 if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) {
1207 die "Non-printable characters are not allowed in the forwarded field";
1209 $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
1211 __begin_control(%param,
1212 command => 'forwarded'
1214 my ($debug,$transcript) =
1215 @info{qw(debug transcript)};
1216 my @data = @{$info{data}};
1217 my @bugs = @{$info{bugs}};
1219 for my $data (@data) {
1220 my $old_data = dclone($data);
1221 print {$debug} "Going to change bug forwarded\n";
1222 if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
1223 (not defined $param{forwarded} and
1224 defined $data->{forwarded} and not length $data->{forwarded})) {
1225 print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
1229 if (not defined $param{forwarded}) {
1230 $action= "Unset $config{bug} forwarded-to-address";
1232 elsif (defined $data->{forwarded} and length($data->{forwarded})) {
1233 $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'";
1236 $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
1238 $data->{forwarded} = $param{forwarded};
1240 append_action_to_log(bug => $data->{bug_num},
1241 command => 'forwarded',
1243 old_data => $old_data,
1245 __return_append_to_log_options(
1250 if not exists $param{append_log} or $param{append_log};
1251 writebug($data->{bug_num},$data);
1252 print {$transcript} "$action\n";
1254 __end_control(%info);
1263 set_title(bug => $ref,
1264 transcript => $transcript,
1265 ($dl > 0 ? (debug => $transcript):()),
1266 requester => $header{from},
1267 request_addr => $controlrequestaddr,
1269 affected_packages => \%affected_packages,
1270 recipients => \%recipients,
1271 title => $new_title,
1276 print {$transcript} "Failed to set the title of $ref: $@";
1279 Sets the title of a specific bug
1285 my %param = validate_with(params => \@_,
1286 spec => {bug => {type => SCALAR,
1289 # specific options here
1290 title => {type => SCALAR,
1293 %append_action_options,
1296 if ($param{title} =~ /[^[:print:]]/) {
1297 die "Non-printable characters are not allowed in bug titles";
1300 my %info = __begin_control(%param,
1303 my ($debug,$transcript) =
1304 @info{qw(debug transcript)};
1305 my @data = @{$info{data}};
1306 my @bugs = @{$info{bugs}};
1308 for my $data (@data) {
1309 my $old_data = dclone($data);
1310 print {$debug} "Going to change bug title\n";
1311 if (defined $data->{subject} and length($data->{subject}) and
1312 $data->{subject} eq $param{title}) {
1313 print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
1317 if (defined $data->{subject} and length($data->{subject})) {
1318 $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'";
1320 $action= "Set $config{bug} title to '$param{title}'.";
1322 $data->{subject} = $param{title};
1324 append_action_to_log(bug => $data->{bug_num},
1327 old_data => $old_data,
1329 __return_append_to_log_options(
1334 if not exists $param{append_log} or $param{append_log};
1335 writebug($data->{bug_num},$data);
1336 print {$transcript} "$action\n";
1338 __end_control(%info);
1345 set_package(bug => $ref,
1346 transcript => $transcript,
1347 ($dl > 0 ? (debug => $transcript):()),
1348 requester => $header{from},
1349 request_addr => $controlrequestaddr,
1351 affected_packages => \%affected_packages,
1352 recipients => \%recipients,
1353 package => $new_package,
1359 print {$transcript} "Failed to assign or reassign $ref to a package: $@";
1362 Indicates that a bug is in a particular package. If is_source is true,
1363 indicates that the package is a source package. [Internally, this
1364 causes src: to be prepended to the package name.]
1366 The default for is_source is 0. As a special case, if the package
1367 starts with 'src:', it is assumed to be a source package and is_source
1370 The package option must match the package_name_re regex.
1375 my %param = validate_with(params => \@_,
1376 spec => {bug => {type => SCALAR,
1379 # specific options here
1380 package => {type => SCALAR|ARRAYREF,
1382 is_source => {type => BOOLEAN,
1386 %append_action_options,
1389 my @new_packages = map {splitpackages($_)} make_list($param{package});
1390 if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) {
1391 croak "Invalid package name '".
1392 join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages).
1395 my %info = __begin_control(%param,
1396 command => 'package',
1398 my ($debug,$transcript) =
1399 @info{qw(debug transcript)};
1400 my @data = @{$info{data}};
1401 my @bugs = @{$info{bugs}};
1402 # clean up the new package
1406 ($temp =~ s/^src:// or
1407 $param{is_source}) ? 'src:'.$temp:$temp;
1411 my $package_reassigned = 0;
1412 for my $data (@data) {
1413 my $old_data = dclone($data);
1414 print {$debug} "Going to change assigned package\n";
1415 if (defined $data->{package} and length($data->{package}) and
1416 $data->{package} eq $new_package) {
1417 print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
1421 if (defined $data->{package} and length($data->{package})) {
1422 $package_reassigned = 1;
1423 $action= "$config{bug} reassigned from package '$data->{package}'".
1424 " to '$new_package'.";
1426 $action= "$config{bug} assigned to package '$new_package'.";
1428 $data->{package} = $new_package;
1430 append_action_to_log(bug => $data->{bug_num},
1431 command => 'package',
1433 old_data => $old_data,
1435 __return_append_to_log_options(
1440 if not exists $param{append_log} or $param{append_log};
1441 writebug($data->{bug_num},$data);
1442 print {$transcript} "$action\n";
1444 __end_control(%info);
1445 # Only clear the fixed/found versions if the package has been
1447 if ($package_reassigned) {
1448 my @params_for_found_fixed =
1449 map {exists $param{$_}?($_,$param{$_}):()}
1451 keys %common_options,
1452 keys %append_action_options,
1454 set_found(found => [],
1455 @params_for_found_fixed,
1457 set_fixed(fixed => [],
1458 @params_for_found_fixed,
1466 set_found(bug => $ref,
1467 transcript => $transcript,
1468 ($dl > 0 ? (debug => $transcript):()),
1469 requester => $header{from},
1470 request_addr => $controlrequestaddr,
1472 affected_packages => \%affected_packages,
1473 recipients => \%recipients,
1480 print {$transcript} "Failed to set found on $ref: $@";
1484 Sets, adds, or removes the specified found versions of a package
1486 If the version list is empty, and the bug is currently not "done",
1487 causes the done field to be cleared.
1489 If any of the versions added to found are greater than any version in
1490 which the bug is fixed (or when the bug is found and there are no
1491 fixed versions) the done field is cleared.
1496 my %param = validate_with(params => \@_,
1497 spec => {bug => {type => SCALAR,
1500 # specific options here
1501 found => {type => SCALAR|ARRAYREF,
1504 add => {type => BOOLEAN,
1507 remove => {type => BOOLEAN,
1511 %append_action_options,
1514 if ($param{add} and $param{remove}) {
1515 croak "It's nonsensical to add and remove the same versions";
1519 __begin_control(%param,
1522 my ($debug,$transcript) =
1523 @info{qw(debug transcript)};
1524 my @data = @{$info{data}};
1525 my @bugs = @{$info{bugs}};
1527 for my $version (make_list($param{found})) {
1528 next unless defined $version;
1529 $versions{$version} =
1530 [make_source_versions(package => [splitpackages($data[0]{package})],
1531 warnings => $transcript,
1534 versions => $version,
1537 # This is really ugly, but it's what we have to do
1538 if (not @{$versions{$version}}) {
1539 print {$transcript} "Unable to make a source version for version '$version'\n";
1542 if (not keys %versions and ($param{remove} or $param{add})) {
1543 if ($param{remove}) {
1544 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1547 print {$transcript} "Requested to add no versions; doing nothing.\n";
1549 __end_control(%info);
1552 # first things first, make the versions fully qualified source
1554 for my $data (@data) {
1555 # The 'done' field gets a bit weird with version tracking,
1556 # because a bug may be closed by multiple people in different
1557 # branches. Until we have something more flexible, we set it
1558 # every time a bug is fixed, and clear it when a bug is found
1559 # in a version greater than any version in which the bug is
1560 # fixed or when a bug is found and there is no fixed version
1561 my $action = 'Did not alter found versions';
1562 my %found_added = ();
1563 my %found_removed = ();
1564 my %fixed_removed = ();
1566 my $old_data = dclone($data);
1567 if (not $param{add} and not $param{remove}) {
1568 $found_removed{$_} = 1 for @{$data->{found_versions}};
1569 $data->{found_versions} = [];
1572 @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
1574 @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
1575 for my $version (keys %versions) {
1577 my @svers = @{$versions{$version}};
1581 elsif (not grep {$version eq $_} @svers) {
1582 # The $version was not equal to one of the source
1583 # versions, so it's probably unqualified (or just
1584 # wrong). Delete it, and use the source versions
1586 if (exists $found_versions{$version}) {
1587 delete $found_versions{$version};
1588 $found_removed{$version} = 1;
1591 for my $sver (@svers) {
1592 if (not exists $found_versions{$sver}) {
1593 $found_versions{$sver} = 1;
1594 $found_added{$sver} = 1;
1596 # if the found we are adding matches any fixed
1597 # versions, remove them
1598 my @temp = grep m{(^|/)\Q$sver\E$}, keys %fixed_versions;
1599 delete $fixed_versions{$_} for @temp;
1600 $fixed_removed{$_} = 1 for @temp;
1603 # We only care about reopening the bug if the bug is
1605 if (defined $data->{done} and length $data->{done}) {
1606 my @svers_order = sort_versions(map {m{([^/]+)$}; $1;}
1608 # determine if we need to reopen
1609 my @fixed_order = sort_versions(map {m{([^/]+)$}; $1;}
1610 keys %fixed_versions);
1611 if (not @fixed_order or
1612 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1618 elsif ($param{remove}) {
1619 # in the case of removal, we only concern ourself with
1620 # the version passed, not the source version it maps
1622 my @temp = grep m{(?:^|/)\Q$version\E$}, keys %found_versions;
1623 delete $found_versions{$_} for @temp;
1624 $found_removed{$_} = 1 for @temp;
1627 # set the keys to exactly these values
1628 my @svers = @{$versions{$version}};
1632 for my $sver (@svers) {
1633 if (not exists $found_versions{$sver}) {
1634 $found_versions{$sver} = 1;
1635 if (exists $found_removed{$sver}) {
1636 delete $found_removed{$sver};
1639 $found_added{$sver} = 1;
1646 $data->{found_versions} = [keys %found_versions];
1647 $data->{fixed_versions} = [keys %fixed_versions];
1650 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1651 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1652 # push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
1653 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1654 $action = ucfirst(join ('; ',@changed)) if @changed;
1656 $action .= " and reopened"
1658 if (not $reopened and not @changed) {
1659 print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
1663 append_action_to_log(bug => $data->{bug_num},
1666 old_data => $old_data,
1668 __return_append_to_log_options(
1673 if not exists $param{append_log} or $param{append_log};
1674 writebug($data->{bug_num},$data);
1675 print {$transcript} "$action\n";
1677 __end_control(%info);
1683 set_fixed(bug => $ref,
1684 transcript => $transcript,
1685 ($dl > 0 ? (debug => $transcript):()),
1686 requester => $header{from},
1687 request_addr => $controlrequestaddr,
1689 affected_packages => \%affected_packages,
1690 recipients => \%recipients,
1698 print {$transcript} "Failed to set fixed on $ref: $@";
1702 Sets, adds, or removes the specified fixed versions of a package
1704 If the fixed versions are empty (or end up being empty after this
1705 call) or the greatest fixed version is less than the greatest found
1706 version and the reopen option is true, the bug is reopened.
1708 This function is also called by the reopen function, which causes all
1709 of the fixed versions to be cleared.
1714 my %param = validate_with(params => \@_,
1715 spec => {bug => {type => SCALAR,
1718 # specific options here
1719 fixed => {type => SCALAR|ARRAYREF,
1722 add => {type => BOOLEAN,
1725 remove => {type => BOOLEAN,
1728 reopen => {type => BOOLEAN,
1732 %append_action_options,
1735 if ($param{add} and $param{remove}) {
1736 croak "It's nonsensical to add and remove the same versions";
1739 __begin_control(%param,
1742 my ($debug,$transcript) =
1743 @info{qw(debug transcript)};
1744 my @data = @{$info{data}};
1745 my @bugs = @{$info{bugs}};
1747 for my $version (make_list($param{fixed})) {
1748 next unless defined $version;
1749 $versions{$version} =
1750 [make_source_versions(package => [splitpackages($data[0]{package})],
1751 warnings => $transcript,
1754 versions => $version,
1757 # This is really ugly, but it's what we have to do
1758 if (not @{$versions{$version}}) {
1759 print {$transcript} "Unable to make a source version for version '$version'\n";
1762 if (not keys %versions and ($param{remove} or $param{add})) {
1763 if ($param{remove}) {
1764 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1767 print {$transcript} "Requested to add no versions; doing nothing.\n";
1769 __end_control(%info);
1772 # first things first, make the versions fully qualified source
1774 for my $data (@data) {
1775 my $old_data = dclone($data);
1776 # The 'done' field gets a bit weird with version tracking,
1777 # because a bug may be closed by multiple people in different
1778 # branches. Until we have something more flexible, we set it
1779 # every time a bug is fixed, and clear it when a bug is found
1780 # in a version greater than any version in which the bug is
1781 # fixed or when a bug is found and there is no fixed version
1782 my $action = 'Did not alter fixed versions';
1783 my %found_added = ();
1784 my %found_removed = ();
1785 my %fixed_added = ();
1786 my %fixed_removed = ();
1788 if (not $param{add} and not $param{remove}) {
1789 $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
1790 $data->{fixed_versions} = [];
1793 @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
1795 @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
1796 for my $version (keys %versions) {
1798 my @svers = @{$versions{$version}};
1803 if (exists $fixed_versions{$version}) {
1804 $fixed_removed{$version} = 1;
1805 delete $fixed_versions{$version};
1808 for my $sver (@svers) {
1809 if (not exists $fixed_versions{$sver}) {
1810 $fixed_versions{$sver} = 1;
1811 $fixed_added{$sver} = 1;
1815 elsif ($param{remove}) {
1816 # in the case of removal, we only concern ourself with
1817 # the version passed, not the source version it maps
1819 my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
1820 delete $fixed_versions{$_} for @temp;
1821 $fixed_removed{$_} = 1 for @temp;
1824 # set the keys to exactly these values
1825 my @svers = @{$versions{$version}};
1829 for my $sver (@svers) {
1830 if (not exists $fixed_versions{$sver}) {
1831 $fixed_versions{$sver} = 1;
1832 if (exists $fixed_removed{$sver}) {
1833 delete $fixed_removed{$sver};
1836 $fixed_added{$sver} = 1;
1843 $data->{found_versions} = [keys %found_versions];
1844 $data->{fixed_versions} = [keys %fixed_versions];
1846 # If we're supposed to consider reopening, reopen if the
1847 # fixed versions are empty or the greatest found version
1848 # is greater than the greatest fixed version
1849 if ($param{reopen} and defined $data->{done}
1850 and length $data->{done}) {
1851 my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1852 map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
1853 # determine if we need to reopen
1854 my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1855 map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
1856 if (not @fixed_order or
1857 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1864 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1865 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1866 push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
1867 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1868 $action = ucfirst(join ('; ',@changed)) if @changed;
1870 $action .= " and reopened"
1872 if (not $reopened and not @changed) {
1873 print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
1877 append_action_to_log(bug => $data->{bug_num},
1880 old_data => $old_data,
1882 __return_append_to_log_options(
1887 if not exists $param{append_log} or $param{append_log};
1888 writebug($data->{bug_num},$data);
1889 print {$transcript} "$action\n";
1891 __end_control(%info);
1898 set_merged(bug => $ref,
1899 transcript => $transcript,
1900 ($dl > 0 ? (debug => $transcript):()),
1901 requester => $header{from},
1902 request_addr => $controlrequestaddr,
1904 affected_packages => \%affected_packages,
1905 recipients => \%recipients,
1906 merge_with => 12345,
1909 allow_reassign => 1,
1910 reassign_same_source_only => 1,
1915 print {$transcript} "Failed to set merged on $ref: $@";
1919 Sets, adds, or removes the specified merged bugs of a bug
1921 By default, requires
1926 my %param = validate_with(params => \@_,
1927 spec => {bug => {type => SCALAR,
1930 # specific options here
1931 merge_with => {type => ARRAYREF|SCALAR,
1934 remove => {type => BOOLEAN,
1937 force => {type => BOOLEAN,
1940 masterbug => {type => BOOLEAN,
1943 allow_reassign => {type => BOOLEAN,
1946 reassign_different_sources => {type => BOOLEAN,
1950 %append_action_options,
1953 my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
1955 @merging{@merging} = (1) x @merging;
1956 if (grep {$_ !~ /^\d+$/} @merging) {
1957 croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
1959 $param{locks} = {} if not exists $param{locks};
1961 __begin_control(%param,
1964 my ($debug,$transcript) =
1965 @info{qw(debug transcript)};
1966 if (not @merging and exists $param{merge_with}) {
1967 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
1968 __end_control(%info);
1971 my @data = @{$info{data}};
1972 my @bugs = @{$info{bugs}};
1975 for my $data (@data) {
1976 $data{$data->{bug_num}} = $data;
1977 my @merged_bugs = split / /, $data->{mergedwith};
1978 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
1982 if (not exists $param{merge_with}) {
1983 my $ok_to_unmerge = 1;
1984 delete $merged_bugs{$param{bug}};
1985 if (not keys %merged_bugs) {
1986 print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
1987 __end_control(%info);
1990 my $action = "Disconnected #$param{bug} from all other report(s).";
1991 for my $data (@data) {
1992 my $old_data = dclone($data);
1993 if ($data->{bug_num} == $param{bug}) {
1994 $data->{mergedwith} = '';
1997 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
2000 append_action_to_log(bug => $data->{bug_num},
2003 old_data => $old_data,
2005 __return_append_to_log_options(%param,
2009 if not exists $param{append_log} or $param{append_log};
2010 writebug($data->{bug_num},$data);
2012 print {$transcript} "$action\n";
2013 __end_control(%info);
2016 # lock and load all of the bugs we need
2017 my @bugs_to_load = keys %merging;
2020 my ($data,$n_locks) =
2021 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2023 locks => $param{locks},
2026 $new_locks += $n_locks;
2028 @data = values %data;
2029 if (not check_limit(data => [@data],
2030 exists $param{limit}?(limit => $param{limit}):(),
2031 transcript => $transcript,
2033 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
2035 for my $data (@data) {
2036 $data{$data->{bug_num}} = $data;
2037 $merged_bugs{$data->{bug_num}} = 1;
2038 my @merged_bugs = split / /, $data->{mergedwith};
2039 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
2040 if (exists $param{affected_bugs}) {
2041 $param{affected_bugs}{$data->{bug_num}} = 1;
2044 __handle_affected_packages(%param,data => [@data]);
2045 my %bug_info_shown; # which bugs have had information shown
2046 $bug_info_shown{$param{bug}} = 1;
2047 add_recipients(data => [@data],
2048 recipients => $param{recipients},
2049 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
2051 (__internal_request()?(transcript => $transcript):()),
2054 # Figure out what the ideal state is for the bug,
2055 my ($merge_status,$bugs_to_merge) =
2056 __calculate_merge_status(\@data,\%data,$param{bug});
2057 # find out if we actually have any bugs to merge
2058 if (not $bugs_to_merge) {
2059 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
2060 for (1..$new_locks) {
2061 unfilelock($param{locks});
2064 __end_control(%info);
2067 # see what changes need to be made to merge the bugs
2068 # check to make sure that the set of changes we need to make is allowed
2069 my ($disallowed_changes,$changes) =
2070 __calculate_merge_changes(\@data,$merge_status,\%param);
2071 # at this point, stop if there are disallowed changes, otherwise
2072 # make the allowed changes, and then reread the bugs in question
2073 # to get the new data, then recaculate the merges; repeat
2074 # reloading and recalculating until we try too many times or there
2075 # are no changes to make.
2078 # we will allow at most 4 times through this; more than 1
2079 # shouldn't really happen.
2081 while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
2082 if ($attempts > 1) {
2083 print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
2085 if (@{$disallowed_changes}) {
2086 # figure out the problems
2087 print {$transcript} "Unable to merge bugs because:\n";
2088 for my $change (@{$disallowed_changes}) {
2089 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2091 if ($attempts > 0) {
2092 croak "Some bugs were altered while attempting to merge";
2095 croak "Did not alter merged bugs";
2098 my @bugs_to_change = keys %{$changes};
2099 for my $change_bug (@bugs_to_change) {
2100 next unless exists $changes->{$change_bug};
2101 $bug_changed{$change_bug}++;
2102 print {$transcript} __bug_info($data{$change_bug}) if
2103 $param{show_bug_info} and not __internal_request(1);
2104 $bug_info_shown{$change_bug} = 1;
2105 __allow_relocking($param{locks},[keys %data]);
2106 for my $change (@{$changes->{$change_bug}}) {
2107 if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
2108 my %target_blockedby;
2109 @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
2110 my %unhandled_targets = %target_blockedby;
2111 my @blocks_to_remove;
2112 for my $key (split / /,$change->{orig_value}) {
2113 delete $unhandled_targets{$key};
2114 next if exists $target_blockedby{$key};
2115 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2116 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2119 keys %common_options,
2120 keys %append_action_options),
2123 for my $key (keys %unhandled_targets) {
2124 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2125 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2128 keys %common_options,
2129 keys %append_action_options),
2134 $change->{function}->(bug => $change->{bug},
2135 $change->{key}, $change->{func_value},
2136 exists $change->{options}?@{$change->{options}}:(),
2138 keys %common_options,
2139 keys %append_action_options),
2143 __disallow_relocking($param{locks});
2144 my ($data,$n_locks) =
2145 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2147 locks => $param{locks},
2151 $new_locks += $n_locks;
2154 @data = values %data;
2155 ($merge_status,$bugs_to_merge) =
2156 __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
2157 ($disallowed_changes,$changes) =
2158 __calculate_merge_changes(\@data,$merge_status,\%param);
2159 $attempts = max(values %bug_changed);
2162 if ($param{show_bug_info} and not __internal_request(1)) {
2163 for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
2164 next if $bug_info_shown{$data->{bug_num}};
2165 print {$transcript} __bug_info($data);
2168 if (keys %{$changes} or @{$disallowed_changes}) {
2169 print {$transcript} "After four attempts, the following changes were unable to be made:\n";
2170 for (1..$new_locks) {
2171 unfilelock($param{locks});
2174 __end_control(%info);
2175 for my $change ((map {@{$_}} values %{$changes}), @{$disallowed_changes}) {
2176 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
2178 die "Unable to modify bugs so they could be merged";
2182 # finally, we can merge the bugs
2183 my $action = "Merged ".join(' ',sort keys %merged_bugs);
2184 for my $data (@data) {
2185 my $old_data = dclone($data);
2186 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
2188 append_action_to_log(bug => $data->{bug_num},
2191 old_data => $old_data,
2193 __return_append_to_log_options(%param,
2197 if not exists $param{append_log} or $param{append_log};
2198 writebug($data->{bug_num},$data);
2200 print {$transcript} "$action\n";
2201 # unlock the extra locks that we got earlier
2202 for (1..$new_locks) {
2203 unfilelock($param{locks});
2206 __end_control(%info);
2209 sub __allow_relocking{
2210 my ($locks,$bugs) = @_;
2212 my @locks = (@{$bugs},'merge');
2213 for my $lock (@locks) {
2214 my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
2215 next unless @lockfiles;
2216 $locks->{relockable}{$lockfiles[0]} = 0;
2220 sub __disallow_relocking{
2222 delete $locks->{relockable};
2225 sub __lock_and_load_merged_bugs{
2227 validate_with(params => \@_,
2229 {bugs_to_load => {type => ARRAYREF,
2230 default => sub {[]},
2232 data => {type => HASHREF|ARRAYREF,
2234 locks => {type => HASHREF,
2235 default => sub {{};},
2237 reload_all => {type => BOOLEAN,
2240 debug => {type => HANDLE,
2246 if (ref($param{data}) eq 'ARRAY') {
2247 for my $data (@{$param{data}}) {
2248 $data{$data->{bug_num}} = dclone($data);
2252 %data = %{dclone($param{data})};
2254 my @bugs_to_load = @{$param{bugs_to_load}};
2255 if ($param{reload_all}) {
2256 push @bugs_to_load, keys %data;
2259 @temp{@bugs_to_load} = (1) x @bugs_to_load;
2260 @bugs_to_load = keys %temp;
2261 my %loaded_this_time;
2263 while ($bug_to_load = shift @bugs_to_load) {
2264 if (not $param{reload_all}) {
2265 next if exists $data{$bug_to_load};
2268 next if $loaded_this_time{$bug_to_load};
2271 if ($param{reload_all}) {
2272 if (exists $data{$bug_to_load}) {
2277 read_bug(bug => $bug_to_load,
2279 locks => $param{locks},
2281 die "Unable to load bug $bug_to_load";
2282 print {$param{debug}} "read bug $bug_to_load\n";
2283 $data{$data->{bug_num}} = $data;
2284 $new_locks += $lock_bug;
2285 $loaded_this_time{$data->{bug_num}} = 1;
2287 grep {not exists $data{$_}}
2288 split / /,$data->{mergedwith};
2290 return (\%data,$new_locks);
2294 sub __calculate_merge_status{
2295 my ($data_a,$data_h,$master_bug,$merge_status) = @_;
2296 my %merge_status = %{$merge_status // {}};
2298 my $bugs_to_merge = 0;
2299 for my $data (@{$data_a}) {
2300 # check to see if this bug is unmerged in the set
2301 if (not length $data->{mergedwith} or
2302 grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
2303 $merged_bugs{$data->{bug_num}} = 1;
2306 # the master_bug is the bug that every other bug is made to
2307 # look like. However, if merge is set, tags, fixed and found
2309 if ($data->{bug_num} == $master_bug) {
2310 for (qw(package forwarded severity blocks blockedby done owner summary outlook affects)) {
2311 $merge_status{$_} = $data->{$_}
2314 if (defined $merge_status) {
2315 next unless $data->{bug_num} == $master_bug;
2317 $merge_status{tag} = {} if not exists $merge_status{tag};
2318 for my $tag (split /\s+/, $data->{keywords}) {
2319 $merge_status{tag}{$tag} = 1;
2321 $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
2322 for (qw(fixed found)) {
2323 @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
2326 # if there is a non-source qualified version with a corresponding
2327 # source qualified version, we only want to merge the source
2328 # qualified version(s)
2329 for (qw(fixed found)) {
2330 my @unqualified_versions = grep {m{/}?0:1} keys %{$merge_status{"${_}_versions"}};
2331 for my $unqualified_version (@unqualified_versions) {
2332 if (grep {m{/\Q$unqualified_version\E}} keys %{$merge_status{"${_}_versions"}}) {
2333 delete $merge_status{"${_}_versions"}{$unqualified_version};
2337 return (\%merge_status,$bugs_to_merge);
2342 sub __calculate_merge_changes{
2343 my ($datas,$merge_status,$param) = @_;
2345 my @disallowed_changes;
2346 for my $data (@{$datas}) {
2347 # things that can be forced
2349 # * func is the function to set the new value
2351 # * key is the key of the function to set the value,
2353 # * modify_value is a function which is called to modify the new
2354 # value so that the function will accept it
2356 # * options is an ARRAYREF of options to pass to the function
2358 # * allowed is a BOOLEAN which controls whether this setting
2359 # is allowed to be different by default.
2360 my %force_functions =
2361 (forwarded => {func => \&set_forwarded,
2365 severity => {func => \&set_severity,
2369 blocks => {func => \&set_blocks,
2370 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2374 blockedby => {func => \&set_blocks,
2375 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2379 done => {func => \&set_done,
2383 owner => {func => \&owner,
2387 summary => {func => \&summary,
2391 outlook => {func => \&outlook,
2395 affects => {func => \&affects,
2399 package => {func => \&set_package,
2403 keywords => {func => \&set_tag,
2405 modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
2408 fixed_versions => {func => \&set_fixed,
2410 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2413 found_versions => {func => \&set_found,
2415 modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
2419 for my $field (qw(forwarded severity blocks blockedby done owner summary outlook affects package fixed_versions found_versions keywords)) {
2420 # if the ideal bug already has the field set properly, we
2422 if ($field eq 'keywords'){
2423 next if join(' ',sort split /\s+/,$data->{keywords}) eq
2424 join(' ',sort keys %{$merge_status->{tag}});
2426 elsif ($field =~ /^(?:fixed|found)_versions$/) {
2427 next if join(' ', sort @{$data->{$field}}) eq
2428 join(' ',sort keys %{$merge_status->{$field}});
2430 elsif ($field eq 'done') {
2431 # for done, we only care if the bug is done or not
2432 # done, not the value it's set to.
2433 if (defined $merge_status->{$field} and length $merge_status->{$field} and
2434 defined $data->{$field} and length $data->{$field}) {
2437 elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
2438 (not defined $data->{$field} or not length $data->{$field})
2443 elsif ($merge_status->{$field} eq $data->{$field}) {
2448 bug => $data->{bug_num},
2449 orig_value => $data->{$field},
2451 (exists $force_functions{$field}{modify_value} ?
2452 $force_functions{$field}{modify_value}->($merge_status->{$field}):
2453 $merge_status->{$field}),
2454 value => $merge_status->{$field},
2455 function => $force_functions{$field}{func},
2456 key => $force_functions{$field}{key},
2457 options => $force_functions{$field}{options},
2458 allowed => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
2460 $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
2461 $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
2462 if ($param->{force} or $change->{allowed}) {
2463 if ($field ne 'package' or $change->{allowed}) {
2464 push @{$changes{$data->{bug_num}}},$change;
2467 if ($param->{allow_reassign}) {
2468 if ($param->{reassign_different_sources}) {
2469 push @{$changes{$data->{bug_num}}},$change;
2472 # allow reassigning if binary_to_source returns at
2473 # least one of the same source packages
2474 my @merge_status_source =
2475 binary_to_source(package => $merge_status->{package},
2478 my @other_bug_source =
2479 binary_to_source(package => $data->{package},
2482 my %merge_status_sources;
2483 @merge_status_sources{@merge_status_source} =
2484 (1) x @merge_status_source;
2485 if (grep {$merge_status_sources{$_}} @other_bug_source) {
2486 push @{$changes{$data->{bug_num}}},$change;
2491 push @disallowed_changes,$change;
2493 # blocks and blocked by are weird; we have to go through and
2494 # set blocks to the other half of the merged bugs
2496 return (\@disallowed_changes,\%changes);
2502 affects(bug => $ref,
2503 transcript => $transcript,
2504 ($dl > 0 ? (debug => $transcript):()),
2505 requester => $header{from},
2506 request_addr => $controlrequestaddr,
2508 affected_packages => \%affected_packages,
2509 recipients => \%recipients,
2517 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
2520 This marks a bug as affecting packages which the bug is not actually
2521 in. This should only be used in cases where fixing the bug instantly
2522 resolves the problem in the other packages.
2524 By default, the packages are set to the list of packages passed.
2525 However, if you pass add => 1 or remove => 1, the list of packages
2526 passed are added or removed from the affects list, respectively.
2531 my %param = validate_with(params => \@_,
2532 spec => {bug => {type => SCALAR,
2535 # specific options here
2536 package => {type => SCALAR|ARRAYREF|UNDEF,
2539 add => {type => BOOLEAN,
2542 remove => {type => BOOLEAN,
2546 %append_action_options,
2549 if ($param{add} and $param{remove}) {
2550 croak "Asking to both add and remove affects is nonsensical";
2552 if (not defined $param{package}) {
2553 $param{package} = [];
2556 __begin_control(%param,
2557 command => 'affects'
2559 my ($debug,$transcript) =
2560 @info{qw(debug transcript)};
2561 my @data = @{$info{data}};
2562 my @bugs = @{$info{bugs}};
2564 for my $data (@data) {
2566 print {$debug} "Going to change affects\n";
2567 my @packages = splitpackages($data->{affects});
2569 @packages{@packages} = (1) x @packages;
2572 for my $package (make_list($param{package})) {
2573 next unless defined $package and length $package;
2574 if (not $packages{$package}) {
2575 $packages{$package} = 1;
2576 push @added,$package;
2580 $action = "Added indication that $data->{bug_num} affects ".
2581 english_join(\@added);
2584 elsif ($param{remove}) {
2586 for my $package (make_list($param{package})) {
2587 if ($packages{$package}) {
2588 next unless defined $package and length $package;
2589 delete $packages{$package};
2590 push @removed,$package;
2593 $action = "Removed indication that $data->{bug_num} affects " .
2594 english_join(\@removed);
2597 my %added_packages = ();
2598 my %removed_packages = %packages;
2600 for my $package (make_list($param{package})) {
2601 next unless defined $package and length $package;
2602 $packages{$package} = 1;
2603 delete $removed_packages{$package};
2604 $added_packages{$package} = 1;
2606 if (keys %removed_packages) {
2607 $action = "Removed indication that $data->{bug_num} affects ".
2608 english_join([keys %removed_packages]);
2609 $action .= "\n" if keys %added_packages;
2611 if (keys %added_packages) {
2612 $action .= "Added indication that $data->{bug_num} affects " .
2613 english_join([keys %added_packages]);
2616 if (not length $action) {
2617 print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
2620 my $old_data = dclone($data);
2621 $data->{affects} = join(',',keys %packages);
2622 append_action_to_log(bug => $data->{bug_num},
2624 command => 'affects',
2626 old_data => $old_data,
2627 __return_append_to_log_options(
2632 if not exists $param{append_log} or $param{append_log};
2633 writebug($data->{bug_num},$data);
2634 print {$transcript} "$action\n";
2636 __end_control(%info);
2640 =head1 SUMMARY FUNCTIONS
2645 summary(bug => $ref,
2646 transcript => $transcript,
2647 ($dl > 0 ? (debug => $transcript):()),
2648 requester => $header{from},
2649 request_addr => $controlrequestaddr,
2651 affected_packages => \%affected_packages,
2652 recipients => \%recipients,
2658 print {$transcript} "Failed to mark $ref with summary foo: $@";
2661 Handles all setting of summary fields
2663 If summary is undef, unsets the summary
2665 If summary is 0, sets the summary to the first paragraph contained in
2668 If summary is a positive integer, sets the summary to the message specified.
2670 Otherwise, sets summary to the value passed.
2676 # outlook and summary are exactly the same, basically
2677 return _summary('summary',@_);
2680 =head1 OUTLOOK FUNCTIONS
2685 outlook(bug => $ref,
2686 transcript => $transcript,
2687 ($dl > 0 ? (debug => $transcript):()),
2688 requester => $header{from},
2689 request_addr => $controlrequestaddr,
2691 affected_packages => \%affected_packages,
2692 recipients => \%recipients,
2698 print {$transcript} "Failed to mark $ref with outlook foo: $@";
2701 Handles all setting of outlook fields
2703 If outlook is undef, unsets the outlook
2705 If outlook is 0, sets the outlook to the first paragraph contained in
2708 If outlook is a positive integer, sets the outlook to the message specified.
2710 Otherwise, sets outlook to the value passed.
2716 return _summary('outlook',@_);
2720 my ($cmd,@params) = @_;
2721 my %param = validate_with(params => \@params,
2722 spec => {bug => {type => SCALAR,
2725 # specific options here
2726 $cmd , {type => SCALAR|UNDEF,
2730 %append_action_options,
2734 __begin_control(%param,
2737 my ($debug,$transcript) =
2738 @info{qw(debug transcript)};
2739 my @data = @{$info{data}};
2740 my @bugs = @{$info{bugs}};
2741 # figure out the log that we're going to use
2743 my $summary_msg = '';
2745 if (not defined $param{$cmd}) {
2747 print {$debug} "Removing $cmd fields\n";
2748 $action = "Removed $cmd";
2750 elsif ($param{$cmd} =~ /^\d+$/) {
2752 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
2753 if ($param{$cmd} == 0) {
2754 $log = $param{message};
2755 $summary_msg = @records + 1;
2758 if (($param{$cmd} - 1 ) > $#records) {
2759 die "Message number '$param{$cmd}' exceeds the maximum message '$#records'";
2761 my $record = $records[($param{$cmd} - 1 )];
2762 if ($record->{type} !~ /incoming-recv|recips/) {
2763 die "Message number '$param{$cmd}' is a invalid message type '$record->{type}'";
2765 $summary_msg = $param{$cmd};
2766 $log = [$record->{text}];
2768 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
2769 my $body = $p_o->{body};
2770 my $in_pseudoheaders = 0;
2772 # walk through body until we get non-blank lines
2773 for my $line (@{$body}) {
2774 if ($line =~ /^\s*$/) {
2775 if (length $paragraph) {
2776 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
2782 $in_pseudoheaders = 0;
2785 # skip a paragraph if it looks like it's control or
2787 if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity)\:\s+\S}xi or #pseudo headers
2788 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
2789 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
2790 debug|(?:not|)forwarded|priority|
2791 (?:un|)block|limit|(?:un|)archive|
2792 reassign|retitle|affects|wrongpackage
2793 (?:un|force|)merge|user(?:category|tags?|)
2795 if (not length $paragraph) {
2796 print {$debug} "Found control/pseudo-headers and skiping them\n";
2797 $in_pseudoheaders = 1;
2801 next if $in_pseudoheaders;
2802 $paragraph .= $line ." \n";
2804 print {$debug} ucfirst($cmd)." is going to be '$paragraph'\n";
2805 $summary = $paragraph;
2806 $summary =~ s/[\n\r]/ /g;
2807 if (not length $summary) {
2808 die "Unable to find $cmd message to use";
2810 # trim off a trailing spaces
2811 $summary =~ s/\ *$//;
2814 $summary = $param{$cmd};
2816 for my $data (@data) {
2817 print {$debug} "Going to change $cmd\n";
2818 if (((not defined $summary or not length $summary) and
2819 (not defined $data->{$cmd} or not length $data->{$cmd})) or
2820 $summary eq $data->{$cmd}) {
2821 print {$transcript} "Ignoring request to change the $cmd of bug $param{bug} to the same value\n";
2824 if (length $summary) {
2825 if (length $data->{$cmd}) {
2826 $action = ucfirst($cmd)." replaced with message bug $param{bug} message $summary_msg";
2829 $action = ucfirst($cmd)." recorded from message bug $param{bug} message $summary_msg";
2832 my $old_data = dclone($data);
2833 $data->{$cmd} = $summary;
2834 append_action_to_log(bug => $data->{bug_num},
2836 old_data => $old_data,
2839 __return_append_to_log_options(
2844 if not exists $param{append_log} or $param{append_log};
2845 writebug($data->{bug_num},$data);
2846 print {$transcript} "$action\n";
2848 __end_control(%info);
2856 clone_bug(bug => $ref,
2857 transcript => $transcript,
2858 ($dl > 0 ? (debug => $transcript):()),
2859 requester => $header{from},
2860 request_addr => $controlrequestaddr,
2862 affected_packages => \%affected_packages,
2863 recipients => \%recipients,
2868 print {$transcript} "Failed to clone bug $ref bar: $@";
2871 Clones the given bug.
2873 We currently don't support cloning merged bugs, but this could be
2874 handled by internally unmerging, cloning, then remerging the bugs.
2879 my %param = validate_with(params => \@_,
2880 spec => {bug => {type => SCALAR,
2883 new_bugs => {type => ARRAYREF,
2885 new_clones => {type => HASHREF,
2889 %append_action_options,
2893 __begin_control(%param,
2896 my ($debug,$transcript) =
2897 @info{qw(debug transcript)};
2898 my @data = @{$info{data}};
2899 my @bugs = @{$info{bugs}};
2902 for my $data (@data) {
2903 if (length($data->{mergedwith})) {
2904 die "Bug is marked as being merged with others. Use an existing clone.\n";
2908 die "Not exactly one bug‽ This shouldn't happen.";
2910 my $data = $data[0];
2912 for my $newclone_id (@{$param{new_bugs}}) {
2913 my $new_bug_num = new_bug(copy => $data->{bug_num});
2914 $param{new_clones}{$newclone_id} = $new_bug_num;
2915 $clones{$newclone_id} = $new_bug_num;
2917 my @new_bugs = sort values %clones;
2919 for my $new_bug (@new_bugs) {
2920 # no collapsed ids or the higher collapsed id is not one less
2921 # than the next highest new bug
2922 if (not @collapsed_ids or
2923 $collapsed_ids[-1][1]+1 != $new_bug) {
2924 push @collapsed_ids,[$new_bug,$new_bug];
2927 $collapsed_ids[-1][1] = $new_bug;
2931 for my $ci (@collapsed_ids) {
2932 if ($ci->[0] == $ci->[1]) {
2933 push @collapsed,$ci->[0];
2936 push @collapsed,$ci->[0].'-'.$ci->[1]
2939 my $collapsed_str = english_join(\@collapsed);
2940 $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
2941 for my $new_bug (@new_bugs) {
2942 append_action_to_log(bug => $new_bug,
2944 __return_append_to_log_options(
2949 if not exists $param{append_log} or $param{append_log};
2951 append_action_to_log(bug => $data->{bug_num},
2953 __return_append_to_log_options(
2958 if not exists $param{append_log} or $param{append_log};
2959 writebug($data->{bug_num},$data);
2960 print {$transcript} "$action\n";
2961 __end_control(%info);
2962 # bugs that this bug is blocking are also blocked by the new clone(s)
2963 for my $bug (split ' ', $data->{blocks}) {
2964 for my $new_bug (@new_bugs) {
2965 set_blocks(bug => $new_bug,
2968 keys %common_options,
2969 keys %append_action_options),
2973 # bugs that this bug is blocked by are also blocking the new clone(s)
2974 for my $bug (split ' ', $data->{blockedby}) {
2975 for my $new_bug (@new_bugs) {
2976 set_blocks(bug => $bug,
2979 keys %common_options,
2980 keys %append_action_options),
2988 =head1 OWNER FUNCTIONS
2994 transcript => $transcript,
2995 ($dl > 0 ? (debug => $transcript):()),
2996 requester => $header{from},
2997 request_addr => $controlrequestaddr,
2999 recipients => \%recipients,
3005 print {$transcript} "Failed to mark $ref as having an owner: $@";
3008 Handles all setting of the owner field; given an owner of undef or of
3009 no length, indicates that a bug is not owned by anyone.
3014 my %param = validate_with(params => \@_,
3015 spec => {bug => {type => SCALAR,
3018 owner => {type => SCALAR|UNDEF,
3021 %append_action_options,
3025 __begin_control(%param,
3028 my ($debug,$transcript) =
3029 @info{qw(debug transcript)};
3030 my @data = @{$info{data}};
3031 my @bugs = @{$info{bugs}};
3033 for my $data (@data) {
3034 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
3035 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
3036 if (not defined $param{owner} or not length $param{owner}) {
3037 if (not defined $data->{owner} or not length $data->{owner}) {
3038 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
3042 $action = "Removed annotation that $config{bug} was owned by " .
3046 if ($data->{owner} eq $param{owner}) {
3047 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
3050 if (length $data->{owner}) {
3051 $action = "Owner changed from $data->{owner} to $param{owner}.";
3054 $action = "Owner recorded as $param{owner}."
3057 my $old_data = dclone($data);
3058 $data->{owner} = $param{owner};
3059 append_action_to_log(bug => $data->{bug_num},
3062 old_data => $old_data,
3064 __return_append_to_log_options(
3069 if not exists $param{append_log} or $param{append_log};
3070 writebug($data->{bug_num},$data);
3071 print {$transcript} "$action\n";
3073 __end_control(%info);
3077 =head1 ARCHIVE FUNCTIONS
3084 bug_archive(bug => $bug_num,
3086 transcript => \$transcript,
3091 transcript("Unable to archive $bug_num\n");
3094 transcript($transcript);
3097 This routine archives a bug
3101 =item bug -- bug number
3103 =item check_archiveable -- check wether a bug is archiveable before
3104 archiving; defaults to 1
3106 =item archive_unarchived -- whether to archive bugs which have not
3107 previously been archived; defaults to 1. [Set to 0 when used from
3110 =item ignore_time -- whether to ignore time constraints when archiving
3111 a bug; defaults to 0.
3118 my %param = validate_with(params => \@_,
3119 spec => {bug => {type => SCALAR,
3122 check_archiveable => {type => BOOLEAN,
3125 archive_unarchived => {type => BOOLEAN,
3128 ignore_time => {type => BOOLEAN,
3132 %append_action_options,
3135 my %info = __begin_control(%param,
3136 command => 'archive',
3138 my ($debug,$transcript) = @info{qw(debug transcript)};
3139 my @data = @{$info{data}};
3140 my @bugs = @{$info{bugs}};
3141 my $action = "$config{bug} archived.";
3142 if ($param{check_archiveable} and
3143 not bug_archiveable(bug=>$param{bug},
3144 ignore_time => $param{ignore_time},
3146 print {$transcript} "Bug $param{bug} cannot be archived\n";
3147 die "Bug $param{bug} cannot be archived";
3149 if (not $param{archive_unarchived} and
3150 not exists $data[0]{unarchived}
3152 print {$transcript} "$param{bug} has not been archived previously\n";
3153 die "$param{bug} has not been archived previously";
3155 add_recipients(recipients => $param{recipients},
3158 transcript => $transcript,
3160 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3161 for my $bug (@bugs) {
3162 if ($param{check_archiveable}) {
3163 die "Bug $bug cannot be archived (but $param{bug} can?)"
3164 unless bug_archiveable(bug=>$bug,
3165 ignore_time => $param{ignore_time},
3169 # If we get here, we can archive/remove this bug
3170 print {$debug} "$param{bug} removing\n";
3171 for my $bug (@bugs) {
3172 #print "$param{bug} removing $bug\n" if $debug;
3173 my $dir = get_hashname($bug);
3174 # First indicate that this bug is being archived
3175 append_action_to_log(bug => $bug,
3177 command => 'archive',
3178 # we didn't actually change the data
3179 # when we archived, so we don't pass
3180 # a real new_data or old_data
3183 __return_append_to_log_options(
3188 if not exists $param{append_log} or $param{append_log};
3189 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
3190 if ($config{save_old_bugs}) {
3191 mkpath("$config{spool_dir}/archive/$dir");
3192 foreach my $file (@files_to_remove) {
3193 link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3194 copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
3195 # we need to bail out here if things have
3196 # gone horribly wrong to avoid removing a
3198 die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
3201 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
3203 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
3204 print {$debug} "deleted $bug (from $param{bug})\n";
3206 bughook_archive(@bugs);
3207 __end_control(%info);
3210 =head2 bug_unarchive
3214 bug_unarchive(bug => $bug_num,
3216 transcript => \$transcript,
3221 transcript("Unable to archive bug: $bug_num");
3223 transcript($transcript);
3225 This routine unarchives a bug
3230 my %param = validate_with(params => \@_,
3231 spec => {bug => {type => SCALAR,
3235 %append_action_options,
3239 my %info = __begin_control(%param,
3241 command=>'unarchive');
3242 my ($debug,$transcript) =
3243 @info{qw(debug transcript)};
3244 my @data = @{$info{data}};
3245 my @bugs = @{$info{bugs}};
3246 my $action = "$config{bug} unarchived.";
3247 my @files_to_remove;
3248 for my $bug (@bugs) {
3249 print {$debug} "$param{bug} removing $bug\n";
3250 my $dir = get_hashname($bug);
3251 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
3252 mkpath("archive/$dir");
3253 foreach my $file (@files_to_copy) {
3254 # die'ing here sucks
3255 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3256 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3257 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
3259 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
3260 print {$transcript} "Unarchived $config{bug} $bug\n";
3262 unlink(@files_to_remove) or die "Unable to unlink bugs";
3263 # Indicate that this bug has been archived previously
3264 for my $bug (@bugs) {
3265 my $newdata = readbug($bug);
3266 my $old_data = dclone($newdata);
3267 if (not defined $newdata) {
3268 print {$transcript} "$config{bug} $bug disappeared!\n";
3269 die "Bug $bug disappeared!";
3271 $newdata->{unarchived} = time;
3272 append_action_to_log(bug => $bug,
3274 command => 'unarchive',
3275 new_data => $newdata,
3276 old_data => $old_data,
3277 __return_append_to_log_options(
3282 if not exists $param{append_log} or $param{append_log};
3283 writebug($bug,$newdata);
3285 __end_control(%info);
3288 =head2 append_action_to_log
3290 append_action_to_log
3292 This should probably be moved to Debbugs::Log; have to think that out
3297 sub append_action_to_log{
3298 my %param = validate_with(params => \@_,
3299 spec => {bug => {type => SCALAR,
3302 new_data => {type => HASHREF,
3305 old_data => {type => HASHREF,
3308 command => {type => SCALAR,
3311 action => {type => SCALAR,
3313 requester => {type => SCALAR,
3316 request_addr => {type => SCALAR,
3319 location => {type => SCALAR,
3322 message => {type => SCALAR|ARRAYREF,
3325 recips => {type => SCALAR|ARRAYREF,
3328 desc => {type => SCALAR,
3331 get_lock => {type => BOOLEAN,
3334 locks => {type => HASHREF,
3338 # append_action_options here
3339 # because some of these
3340 # options aren't actually
3341 # optional, even though the
3342 # original function doesn't
3346 # Fix this to use $param{location}
3347 my $log_location = buglog($param{bug});
3348 die "Unable to find .log for $param{bug}"
3349 if not defined $log_location;
3350 if ($param{get_lock}) {
3351 filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
3355 my $logfh = IO::File->new(">>$log_location") or
3356 die "Unable to open $log_location for appending: $!";
3357 # determine difference between old and new
3359 if (exists $param{old_data} and exists $param{new_data}) {
3360 my $old_data = dclone($param{old_data});
3361 my $new_data = dclone($param{new_data});
3362 for my $key (keys %{$old_data}) {
3363 if (not exists $Debbugs::Status::fields{$key}) {
3364 delete $old_data->{$key};
3367 next unless exists $new_data->{$key};
3368 next unless defined $new_data->{$key};
3369 if (not defined $old_data->{$key}) {
3370 delete $old_data->{$key};
3373 if (ref($new_data->{$key}) and
3374 ref($old_data->{$key}) and
3375 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3376 local $Storable::canonical = 1;
3377 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3378 delete $new_data->{$key};
3379 delete $old_data->{$key};
3382 elsif ($new_data->{$key} eq $old_data->{$key}) {
3383 delete $new_data->{$key};
3384 delete $old_data->{$key};
3387 for my $key (keys %{$new_data}) {
3388 if (not exists $Debbugs::Status::fields{$key}) {
3389 delete $new_data->{$key};
3392 next unless exists $old_data->{$key};
3393 next unless defined $old_data->{$key};
3394 if (not defined $new_data->{$key} or
3395 not exists $Debbugs::Status::fields{$key}) {
3396 delete $new_data->{$key};
3399 if (ref($new_data->{$key}) and
3400 ref($old_data->{$key}) and
3401 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3402 local $Storable::canonical = 1;
3403 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3404 delete $new_data->{$key};
3405 delete $old_data->{$key};
3408 elsif ($new_data->{$key} eq $old_data->{$key}) {
3409 delete $new_data->{$key};
3410 delete $old_data->{$key};
3413 $data_diff .= "<!-- new_data:\n";
3415 for my $key (keys %{$new_data}) {
3416 if (not exists $Debbugs::Status::fields{$key}) {
3417 warn "No such field $key";
3420 $nd{$key} = $new_data->{$key};
3421 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
3423 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%nd)],[qw(new_data)]));
3424 $data_diff .= "-->\n";
3425 $data_diff .= "<!-- old_data:\n";
3427 for my $key (keys %{$old_data}) {
3428 if (not exists $Debbugs::Status::fields{$key}) {
3429 warn "No such field $key";
3432 $od{$key} = $old_data->{$key};
3433 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
3435 $data_diff .= html_escape(Data::Dumper->Dump([encode_utf8_structure(\%od)],[qw(old_data)]));
3436 $data_diff .= "-->\n";
3439 (exists $param{command} ?
3440 "<!-- command:".html_escape(encode_utf8($param{command}))." -->\n":""
3442 (length $param{requester} ?
3443 "<!-- requester: ".html_escape(encode_utf8($param{requester}))." -->\n":""
3445 (length $param{request_addr} ?
3446 "<!-- request_addr: ".html_escape(encode_utf8($param{request_addr}))." -->\n":""
3448 "<!-- time:".time()." -->\n",
3450 "<strong>".html_escape(encode_utf8($param{action}))."</strong>\n");
3451 if (length $param{requester}) {
3452 $msg .= "Request was from <code>".html_escape(encode_utf8($param{requester}))."</code>\n";
3454 if (length $param{request_addr}) {
3455 $msg .= "to <code>".html_escape(encode_utf8($param{request_addr}))."</code>";
3457 if (length $param{desc}) {
3458 $msg .= ":<br>\n".encode_utf8($param{desc})."\n";
3463 push @records, {type => 'html',
3467 if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
3468 push @records, {type => exists $param{recips}?'recips':'incoming-recv',
3469 exists $param{recips}?(recips => [make_list($param{recips})]):(),
3470 text => join('',make_list($param{message})),
3473 write_log_records(logfh=>$logfh,
3474 records => \@records,
3476 close $logfh or die "Unable to close $log_location: $!";
3477 if ($param{get_lock}) {
3478 unfilelock(exists $param{locks}?$param{locks}:());
3486 =head1 PRIVATE FUNCTIONS
3488 =head2 __handle_affected_packages
3490 __handle_affected_packages(affected_packages => {},
3498 sub __handle_affected_packages{
3499 my %param = validate_with(params => \@_,
3500 spec => {%common_options,
3501 data => {type => ARRAYREF|HASHREF
3506 for my $data (make_list($param{data})) {
3507 next unless exists $data->{package} and defined $data->{package};
3508 my @packages = split /\s*,\s*/,$data->{package};
3509 @{$param{affected_packages}}{@packages} = (1) x @packages;
3513 =head2 __handle_debug_transcript
3515 my ($debug,$transcript) = __handle_debug_transcript(%param);
3517 Returns a debug and transcript filehandle
3522 sub __handle_debug_transcript{
3523 my %param = validate_with(params => \@_,
3524 spec => {%common_options},
3527 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
3528 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3529 return ($debug,$transcript);
3536 Produces a small bit of bug information to kick out to the transcript
3543 next unless defined $data and exists $data->{bug_num};
3544 $return .= "Bug #".($data->{bug_num}||'').
3545 ((defined $data->{done} and length $data->{done})?
3546 " {Done: $data->{done}}":''
3548 " [".($data->{package}||'(no package)'). "] ".
3549 ($data->{subject}||'(no subject)')."\n";
3555 =head2 __internal_request
3557 __internal_request()
3558 __internal_request($level)
3560 Returns true if the caller of the function calling __internal_request
3561 belongs to __PACKAGE__
3563 This allows us to be magical, and don't bother to print bug info if
3564 the second caller is from this package, amongst other things.
3566 An optional level is allowed, which increments the number of levels to
3567 check by the given value. [This is basically for use by internal
3568 functions like __begin_control which are always called by
3573 sub __internal_request{
3575 $l = 0 if not defined $l;
3576 if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
3582 sub __return_append_to_log_options{
3584 my $action = $param{action} if exists $param{action};
3585 if (not exists $param{requester}) {
3586 $param{requester} = $config{control_internal_requester};
3588 if (not exists $param{request_addr}) {
3589 $param{request_addr} = $config{control_internal_request_addr};
3591 if (not exists $param{message}) {
3592 my $date = rfc822_date();
3593 $param{message} = fill_in_template(template => 'mail/fake_control_message',
3594 variables => {request_addr => $param{request_addr},
3595 requester => $param{requester},
3601 if (not defined $action) {
3602 carp "Undefined action!";
3603 $action = "unknown action";
3605 return (action => $action,
3606 hash_slice(%param,keys %append_action_options),
3610 =head2 __begin_control
3612 my %info = __begin_control(%param,
3614 command=>'unarchive');
3615 my ($debug,$transcript) = @info{qw(debug transcript)};
3616 my @data = @{$info{data}};
3617 my @bugs = @{$info{bugs}};
3620 Starts the process of modifying a bug; handles all of the generic
3621 things that almost every control request needs
3623 Returns a hash containing
3627 =item new_locks -- number of new locks taken out by this call
3629 =item debug -- the debug file handle
3631 =item transcript -- the transcript file handle
3633 =item data -- an arrayref containing the data of the bugs
3634 corresponding to this request
3636 =item bugs -- an arrayref containing the bug numbers of the bugs
3637 corresponding to this request
3645 sub __begin_control {
3646 my %param = validate_with(params => \@_,
3647 spec => {bug => {type => SCALAR,
3650 archived => {type => BOOLEAN,
3653 command => {type => SCALAR,
3661 my ($debug,$transcript) = __handle_debug_transcript(@_);
3662 print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
3663 # print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
3664 $lockhash = $param{locks} if exists $param{locks};
3666 my $old_die = $SIG{__DIE__};
3667 $SIG{__DIE__} = *sig_die{CODE};
3669 ($new_locks, @data) =
3670 lock_read_all_merged_bugs(bug => $param{bug},
3671 $param{archived}?(location => 'archive'):(),
3672 exists $param{locks} ? (locks => $param{locks}):(),
3674 $locks += $new_locks;
3676 die "Unable to read any bugs successfully.";
3678 if (not $param{archived}) {
3679 for my $data (@data) {
3680 if ($data->{archived}) {
3681 die "Not altering archived bugs; see unarchive.";
3685 if (not check_limit(data => \@data,
3686 exists $param{limit}?(limit => $param{limit}):(),
3687 transcript => $transcript,
3689 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
3692 __handle_affected_packages(%param,data => \@data);
3693 print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
3694 print {$debug} "$param{bug} read $locks locks\n";
3695 if (not @data or not defined $data[0]) {
3696 print {$transcript} "No bug found for $param{bug}\n";
3697 die "No bug found for $param{bug}";
3700 add_recipients(data => \@data,
3701 recipients => $param{recipients},
3702 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
3704 (__internal_request()?(transcript => $transcript):()),
3707 print {$debug} "$param{bug} read done\n";
3708 my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
3709 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3710 return (data => \@data,
3712 old_die => $old_die,
3713 new_locks => $new_locks,
3715 transcript => $transcript,
3717 exists $param{locks}?(locks => $param{locks}):(),
3721 =head2 __end_control
3723 __end_control(%info);
3725 Handles tearing down from a control request
3731 if (exists $info{new_locks} and $info{new_locks} > 0) {
3732 print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
3733 for (1..$info{new_locks}) {
3734 unfilelock(exists $info{locks}?$info{locks}:());
3738 $SIG{__DIE__} = $info{old_die};
3739 if (exists $info{param}{affected_bugs}) {
3740 @{$info{param}{affected_bugs}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
3742 add_recipients(recipients => $info{param}{recipients},
3743 (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
3744 data => $info{data},
3745 debug => $info{debug},
3746 transcript => $info{transcript},
3748 __handle_affected_packages(%{$info{param}},data=>$info{data});
3754 check_limit(data => \@data, limit => $param{limit});
3757 Checks to make sure that bugs match any limits; each entry of @data
3758 much satisfy the limit.
3760 Returns true if there are no entries in data, or there are no keys in
3761 limit; returns false (0) if there are any entries which do not match.
3763 The limit hashref elements can contain an arrayref of scalars to
3764 match; regexes are also acccepted. At least one of the entries in each
3765 element needs to match the corresponding field in all data for the
3772 my %param = validate_with(params => \@_,
3773 spec => {data => {type => ARRAYREF|HASHREF,
3775 limit => {type => HASHREF|UNDEF,
3777 transcript => {type => SCALARREF|HANDLE,
3782 my @data = make_list($param{data});
3784 not defined $param{limit} or
3785 not keys %{$param{limit}}) {
3788 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3789 my $going_to_fail = 0;
3790 for my $data (@data) {
3791 $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
3792 status => dclone($data),
3794 for my $field (keys %{$param{limit}}) {
3795 next unless exists $param{limit}{$field};
3797 my @data_fields = make_list($data->{$field});
3798 LIMIT: for my $limit (make_list($param{limit}{$field})) {
3799 if (not ref $limit) {
3800 for my $data_field (@data_fields) {
3801 if ($data_field eq $limit) {
3807 elsif (ref($limit) eq 'Regexp') {
3808 for my $data_field (@data_fields) {
3809 if ($data_field =~ $limit) {
3816 warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
3821 print {$transcript} qq($field: ).join(', ',map{qq("$_")} make_list($data->{$field})).
3822 "' does not match at least one of ".
3823 join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
3827 return $going_to_fail?0:1;
3835 We override die to specially handle unlocking files in the cases where
3836 we are called via eval. [If we're not called via eval, it doesn't
3842 if ($^S) { # in eval
3844 for (1..$locks) { unfilelock(defined $lockhash?$lockhash:()); }
3851 # =head2 __message_body_template
3853 # message_body_template('mail/ack',{ref=>'foo'});
3855 # Creates a message body using a template
3859 sub __message_body_template{
3860 my ($template,$extra_var) = @_;
3862 my $hole_var = {'&bugurl' =>
3864 'http://'.$config{cgi_domain}.'/'.
3865 Debbugs::CGI::bug_links(bug => $_[0],
3871 my $body = fill_in_template(template => $template,
3872 variables => {config => \%config,
3875 hole_var => $hole_var,
3877 return fill_in_template(template => 'mail/message_body',
3878 variables => {config => \%config,
3882 hole_var => $hole_var,
3886 sub __all_undef_or_equal {
3888 return 1 if @values == 1 or @values == 0;
3889 my $not_def = grep {not defined $_} @values;
3890 if ($not_def == @values) {
3893 if ($not_def > 0 and $not_def != @values) {
3896 my $first_val = shift @values;
3897 for my $val (@values) {
3898 if ($first_val ne $val) {