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)],
91 title => [qw(set_title)],
92 forward => [qw(set_forwarded)],
93 found => [qw(set_found set_fixed)],
94 fixed => [qw(set_found set_fixed)],
95 package => [qw(set_package)],
96 block => [qw(set_blocks)],
97 merge => [qw(set_merged)],
99 archive => [qw(bug_archive bug_unarchive),
101 log => [qw(append_action_to_log),
105 Exporter::export_ok_tags(keys %EXPORT_TAGS);
106 $EXPORT_TAGS{all} = [@EXPORT_OK];
109 use Debbugs::Config qw(:config);
110 use Debbugs::Common qw(:lock buglog :misc get_hashname sort_versions);
111 use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages split_status_fields get_bug_status);
112 use Debbugs::CGI qw(html_escape);
113 use Debbugs::Log qw(:misc :write);
114 use Debbugs::Recipients qw(:add);
115 use Debbugs::Packages qw(:versions :mapping);
117 use Data::Dumper qw();
118 use Params::Validate qw(validate_with :types);
119 use File::Path qw(mkpath);
122 use Debbugs::Text qw(:templates);
124 use Debbugs::Mail qw(rfc822_date send_mail_message default_headers);
125 use Debbugs::MIME qw(create_mime_message);
127 use Mail::RFC822::Address qw();
129 use POSIX qw(strftime);
131 use Storable qw(dclone nfreeze);
132 use List::Util qw(first max);
136 # These are a set of options which are common to all of these functions
138 my %common_options = (debug => {type => SCALARREF|HANDLE,
141 transcript => {type => SCALARREF|HANDLE,
144 affected_bugs => {type => HASHREF,
147 affected_packages => {type => HASHREF,
150 recipients => {type => HASHREF,
153 limit => {type => HASHREF,
156 show_bug_info => {type => BOOLEAN,
159 request_subject => {type => SCALAR,
160 default => 'Unknown Subject',
162 request_msgid => {type => SCALAR,
165 request_nn => {type => SCALAR,
168 request_replyto => {type => SCALAR,
171 locks => {type => HASHREF,
177 my %append_action_options =
178 (action => {type => SCALAR,
181 requester => {type => SCALAR,
184 request_addr => {type => SCALAR,
187 location => {type => SCALAR,
190 message => {type => SCALAR|ARRAYREF,
193 append_log => {type => BOOLEAN,
195 depends => [qw(requester request_addr),
199 # locks is both an append_action option, and a common option;
200 # it's ok for it to be in both places.
201 locks => {type => HASHREF,
209 # this is just a generic stub for Debbugs::Control functions.
214 # set_foo(bug => $ref,
215 # transcript => $transcript,
216 # ($dl > 0 ? (debug => $transcript):()),
217 # requester => $header{from},
218 # request_addr => $controlrequestaddr,
220 # affected_packages => \%affected_packages,
221 # recipients => \%recipients,
227 # print {$transcript} "Failed to set foo $ref bar: $@";
235 # my %param = validate_with(params => \@_,
236 # spec => {bug => {type => SCALAR,
237 # regex => qr/^\d+$/,
239 # # specific options here
241 # %append_action_options,
245 # __begin_control(%param,
248 # my ($debug,$transcript) =
249 # @info{qw(debug transcript)};
250 # my @data = @{$info{data}};
251 # my @bugs = @{$info{bugs}};
254 # for my $data (@data) {
255 # append_action_to_log(bug => $data->{bug_num},
257 # __return_append_to_log_options(
262 # if not exists $param{append_log} or $param{append_log};
263 # writebug($data->{bug_num},$data);
264 # print {$transcript} "$action\n";
266 # __end_control(%info);
273 set_block(bug => $ref,
274 transcript => $transcript,
275 ($dl > 0 ? (debug => $transcript):()),
276 requester => $header{from},
277 request_addr => $controlrequestaddr,
279 affected_packages => \%affected_packages,
280 recipients => \%recipients,
286 print {$transcript} "Failed to set blockers of $ref: $@";
289 Alters the set of bugs that block this bug from being fixed
291 This requires altering both this bug (and those it's merged with) as
292 well as the bugs that block this bug from being fixed (and those that
297 =item block -- scalar or arrayref of blocking bugs to set, add or remove
299 =item add -- if true, add blocking bugs
301 =item remove -- if true, remove blocking bugs
308 my %param = validate_with(params => \@_,
309 spec => {bug => {type => SCALAR,
312 # specific options here
313 block => {type => SCALAR|ARRAYREF,
316 add => {type => BOOLEAN,
319 remove => {type => BOOLEAN,
323 %append_action_options,
326 if ($param{add} and $param{remove}) {
327 croak "It's nonsensical to add and remove the same blocking bugs";
329 if (grep {$_ !~ /^\d+$/} make_list($param{block})) {
330 croak "Invalid blocking bug(s):".
331 join(', ',grep {$_ !~ /^\d+$/} make_list($param{block}));
337 elsif ($param{remove}) {
342 __begin_control(%param,
345 my ($debug,$transcript) =
346 @info{qw(debug transcript)};
347 my @data = @{$info{data}};
348 my @bugs = @{$info{bugs}};
351 # The first bit of this code is ugly, and should be cleaned up.
352 # Its purpose is to populate %removed_blockers and %add_blockers
353 # with all of the bugs that should be added or removed as blockers
354 # of all of the bugs which are merged with $param{bug}
357 for my $blocker (make_list($param{block})) {
358 next if $ok_blockers{$blocker} or $bad_blockers{$blocker};
359 my $data = read_bug(bug=>$blocker,
361 if (defined $data and not $data->{archive}) {
362 $data = split_status_fields($data);
363 $ok_blockers{$blocker} = 1;
365 push @merged_bugs, make_list($data->{mergedwith});
366 @ok_blockers{@merged_bugs} = (1) x @merged_bugs if @merged_bugs;
369 $bad_blockers{$blocker} = 1;
373 # throw an error if we are setting the blockers and there is a bad
375 if (keys %bad_blockers and $mode eq 'set') {
376 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers).
377 keys %ok_blockers?'':" and no known blocking bug(s)";
379 # if there are no ok blockers and we are not setting the blockers,
381 if (not keys %ok_blockers and $mode ne 'set') {
382 print {$transcript} "No valid blocking bug(s) given; not doing anything\n";
383 if (keys %bad_blockers) {
384 croak "Unknown blocking bug(s):".join(', ',keys %bad_blockers);
386 __end_control(%info);
390 my @change_blockers = keys %ok_blockers;
392 my %removed_blockers;
395 my @blockers = map {split ' ', $_->{blockedby}} @data;
397 @blockers{@blockers} = (1) x @blockers;
399 # it is nonsensical for a bug to block itself (or a merged
400 # partner); We currently don't allow removal because we'd possibly
404 @bugs{@bugs} = (1) x @bugs;
405 for my $blocker (@change_blockers) {
406 if ($bugs{$blocker}) {
407 croak "It is nonsensical for a bug to block itself (or a merged partner): $blocker";
410 @blockers = keys %blockers;
412 %removed_blockers = ();
413 for my $blocker (@change_blockers) {
414 next if exists $blockers{$blocker};
415 $blockers{$blocker} = 1;
416 $added_blockers{$blocker} = 1;
419 elsif ($param{remove}) {
420 %added_blockers = ();
421 for my $blocker (@change_blockers) {
422 next if exists $removed_blockers{$blocker};
423 delete $blockers{$blocker};
424 $removed_blockers{$blocker} = 1;
428 @removed_blockers{@blockers} = (1) x @blockers;
430 for my $blocker (@change_blockers) {
431 next if exists $blockers{$blocker};
432 $blockers{$blocker} = 1;
433 if (exists $removed_blockers{$blocker}) {
434 delete $removed_blockers{$blocker};
437 $added_blockers{$blocker} = 1;
441 my @new_blockers = keys %blockers;
442 for my $data (@data) {
443 my $old_data = dclone($data);
444 # remove blockers and/or add new ones as appropriate
445 if ($data->{blockedby} eq '') {
446 print {$transcript} "$data->{bug_num} was not blocked by any bugs.\n";
448 print {$transcript} "$data->{bug_num} was blocked by: $data->{blockedby}\n";
450 if ($data->{blocks} eq '') {
451 print {$transcript} "$data->{bug_num} was not blocking any bugs.\n";
453 print {$transcript} "$data->{bug_num} was blocking: $data->{blocks}\n";
456 push @changed, 'added blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %added_blockers]) if keys %added_blockers;
457 push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
458 $action = ucfirst(join ('; ',@changed)) if @changed;
460 print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n"
461 unless __internal_request();
464 $data->{blockedby} = join(' ',keys %blockers);
465 append_action_to_log(bug => $data->{bug_num},
467 old_data => $old_data,
470 __return_append_to_log_options(
475 if not exists $param{append_log} or $param{append_log};
476 writebug($data->{bug_num},$data);
477 print {$transcript} "$action\n";
479 # we do this bit below to avoid code duplication
481 $mungable_blocks{remove} = \%removed_blockers if keys %removed_blockers;
482 $mungable_blocks{add} = \%added_blockers if keys %added_blockers;
484 for my $add_remove (keys %mungable_blocks) {
488 for my $blocker (keys %{$mungable_blocks{$add_remove}}) {
489 next if $munge_blockers{$blocker};
490 my ($temp_locks, @blocking_data) =
491 lock_read_all_merged_bugs(bug => $blocker,
492 ($param{archived}?(location => 'archive'):()),
493 exists $param{locks}?(locks => $param{locks}):(),
495 $locks+= $temp_locks;
496 $new_locks+=$temp_locks;
497 if (not @blocking_data) {
498 for (1..$new_locks) {
499 unfilelock(exists $param{locks}?$param{locks}:());
502 die "Unable to get file lock while trying to $add_remove blocker '$blocker'";
504 for (map {$_->{bug_num}} @blocking_data) {
505 $munge_blockers{$_} = 1;
507 for my $data (@blocking_data) {
508 my $old_data = dclone($data);
510 my @blocks = split ' ', $data->{blocks};
511 @blocks{@blocks} = (1) x @blocks;
513 for my $bug (@bugs) {
514 if ($add_remove eq 'remove') {
515 next unless exists $blocks{$bug};
516 delete $blocks{$bug};
519 next if exists $blocks{$bug};
524 $data->{blocks} = join(' ',sort keys %blocks);
525 my $action = ($add_remove eq 'add'?'Added':'Removed').
526 " indication that bug $data->{bug_num} blocks ".
528 append_action_to_log(bug => $data->{bug_num},
530 old_data => $old_data,
533 __return_append_to_log_options(%param,
537 writebug($data->{bug_num},$data);
539 __handle_affected_packages(%param,data=>\@blocking_data);
540 add_recipients(recipients => $param{recipients},
541 actions_taken => {blocks => 1},
542 data => \@blocking_data,
544 transcript => $transcript,
547 for (1..$new_locks) {
548 unfilelock(exists $param{locks}?$param{locks}:());
553 __end_control(%info);
562 transcript => $transcript,
563 ($dl > 0 ? (debug => $transcript):()),
564 requester => $header{from},
565 request_addr => $controlrequestaddr,
567 affected_packages => \%affected_packages,
568 recipients => \%recipients,
575 print {$transcript} "Failed to set tag on $ref: $@";
579 Sets, adds, or removes the specified tags on a bug
583 =item tag -- scalar or arrayref of tags to set, add or remove
585 =item add -- if true, add tags
587 =item remove -- if true, remove tags
589 =item warn_on_bad_tags -- if true (the default) warn if bad tags are
597 my %param = validate_with(params => \@_,
598 spec => {bug => {type => SCALAR,
601 # specific options here
602 tag => {type => SCALAR|ARRAYREF,
605 add => {type => BOOLEAN,
608 remove => {type => BOOLEAN,
611 warn_on_bad_tags => {type => BOOLEAN,
615 %append_action_options,
618 if ($param{add} and $param{remove}) {
619 croak "It's nonsensical to add and remove the same tags";
623 __begin_control(%param,
626 my ($debug,$transcript) =
627 @info{qw(debug transcript)};
628 my @data = @{$info{data}};
629 my @bugs = @{$info{bugs}};
630 my @tags = make_list($param{tag});
631 if (not @tags and ($param{remove} or $param{add})) {
632 if ($param{remove}) {
633 print {$transcript} "Requested to remove no tags; doing nothing.\n";
636 print {$transcript} "Requested to add no tags; doing nothing.\n";
638 __end_control(%info);
641 # first things first, make the versions fully qualified source
643 for my $data (@data) {
644 my $action = 'Did not alter tags';
646 my %tag_removed = ();
647 my %fixed_removed = ();
648 my @old_tags = split /\,?\s+/, $data->{keywords};
650 @tags{@old_tags} = (1) x @old_tags;
652 my $old_data = dclone($data);
653 if (not $param{add} and not $param{remove}) {
654 $tag_removed{$_} = 1 for @old_tags;
658 for my $tag (@tags) {
659 if (not $param{remove} and
660 not defined first {$_ eq $tag} @{$config{tags}}) {
661 push @bad_tags, $tag;
665 if (not exists $tags{$tag}) {
667 $tag_added{$tag} = 1;
670 elsif ($param{remove}) {
671 if (exists $tags{$tag}) {
673 $tag_removed{$tag} = 1;
677 if (exists $tag_removed{$tag}) {
678 delete $tag_removed{$tag};
681 $tag_added{$tag} = 1;
686 if (@bad_tags and $param{warn_on_bad_tags}) {
687 print {$transcript} "Unknown tag(s): ".join(', ',@bad_tags).".\n";
688 print {$transcript} "These tags are recognized: ".join(', ',@{$config{tags}}).".\n";
690 $data->{keywords} = join(' ',keys %tags);
693 push @changed, 'added tag(s) '.english_join([keys %tag_added]) if keys %tag_added;
694 push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
695 $action = ucfirst(join ('; ',@changed)) if @changed;
697 print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n"
698 unless __internal_request();
702 append_action_to_log(bug => $data->{bug_num},
705 old_data => $old_data,
707 __return_append_to_log_options(
712 if not exists $param{append_log} or $param{append_log};
713 writebug($data->{bug_num},$data);
714 print {$transcript} "$action\n";
716 __end_control(%info);
724 set_severity(bug => $ref,
725 transcript => $transcript,
726 ($dl > 0 ? (debug => $transcript):()),
727 requester => $header{from},
728 request_addr => $controlrequestaddr,
730 affected_packages => \%affected_packages,
731 recipients => \%recipients,
732 severity => 'normal',
737 print {$transcript} "Failed to set the severity of bug $ref: $@";
740 Sets the severity of a bug. If severity is not passed, is undefined,
741 or has zero length, sets the severity to the default severity.
746 my %param = validate_with(params => \@_,
747 spec => {bug => {type => SCALAR,
750 # specific options here
751 severity => {type => SCALAR|UNDEF,
752 default => $config{default_severity},
755 %append_action_options,
758 if (not defined $param{severity} or
759 not length $param{severity}
761 $param{severity} = $config{default_severity};
764 # check validity of new severity
765 if (not defined first {$_ eq $param{severity}} (@{$config{severity_list}},$config{default_severity})) {
766 die "Severity '$param{severity}' is not a valid severity level";
769 __begin_control(%param,
770 command => 'severity'
772 my ($debug,$transcript) =
773 @info{qw(debug transcript)};
774 my @data = @{$info{data}};
775 my @bugs = @{$info{bugs}};
778 for my $data (@data) {
779 if (not defined $data->{severity}) {
780 $data->{severity} = $param{severity};
781 $action = "Severity set to '$param{severity}'";
784 if ($data->{severity} eq '') {
785 $data->{severity} = $config{default_severity};
787 if ($data->{severity} eq $param{severity}) {
788 print {$transcript} "Ignoring request to change severity of $config{bug} $data->{bug_num} to the same value.\n";
791 $action = "Severity set to '$param{severity}' from '$data->{severity}'";
792 $data->{severity} = $param{severity};
794 append_action_to_log(bug => $data->{bug_num},
796 __return_append_to_log_options(
801 if not exists $param{append_log} or $param{append_log};
802 writebug($data->{bug_num},$data);
803 print {$transcript} "$action\n";
805 __end_control(%info);
812 set_done(bug => $ref,
813 transcript => $transcript,
814 ($dl > 0 ? (debug => $transcript):()),
815 requester => $header{from},
816 request_addr => $controlrequestaddr,
818 affected_packages => \%affected_packages,
819 recipients => \%recipients,
824 print {$transcript} "Failed to set foo $ref bar: $@";
832 my %param = validate_with(params => \@_,
833 spec => {bug => {type => SCALAR,
836 reopen => {type => BOOLEAN,
839 submitter => {type => SCALAR,
842 clear_fixed => {type => BOOLEAN,
845 notify_submitter => {type => BOOLEAN,
848 original_report => {type => SCALARREF,
851 done => {type => SCALAR|UNDEF,
855 %append_action_options,
859 if (exists $param{submitter} and
860 not Mail::RFC822::Address::valid($param{submitter})) {
861 die "New submitter address '$param{submitter}' is not a valid e-mail address";
863 if (exists $param{done} and defined $param{done} and $param{done} eq 1) { #special case this as using the requester address
864 $param{done} = $param{requester};
866 if (exists $param{done} and
867 (not defined $param{done} or
868 not length $param{done})) {
874 __begin_control(%param,
875 command => $param{reopen}?'reopen':'done',
877 my ($debug,$transcript) =
878 @info{qw(debug transcript)};
879 my @data = @{$info{data}};
880 my @bugs = @{$info{bugs}};
883 if ($param{reopen}) {
884 # avoid warning multiple times if there are fixed versions
886 for my $data (@data) {
887 if (not exists $data->{done} or
888 not defined $data->{done} or
889 not length $data->{done}) {
890 print {$transcript} "Bug $data->{bug_num} is not marked as done; doing nothing.\n";
891 __end_control(%info);
894 if (@{$data->{fixed_versions}} and $warn_fixed) {
895 print {$transcript} "'reopen' may be inappropriate when a bug has been closed with a version;\n";
896 print {$transcript} "all fixed versions will be cleared, and you may need to re-add them.\n";
900 __end_control(%info);
901 if (exists $param{submitter}) {
902 set_submitter(bug => $param{bug},
903 submitter => $param{submitter},
905 keys %common_options,
906 keys %append_action_options)
909 # clear the fixed revisions
910 if ($param{clear_fixed}) {
911 set_fixed(fixed => [],
915 keys %common_options,
916 keys %append_action_options),
921 my %submitter_notified;
922 my $requester_notified = 0;
923 my $orig_report_set = 0;
924 for my $data (@data) {
925 if (exists $data->{done} and
926 defined $data->{done} and
927 length $data->{done}) {
928 print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
929 __end_control(%info);
933 for my $data (@data) {
934 my $old_data = dclone($data);
935 my $hash = get_hashname($data->{bug_num});
936 my $report_fh = IO::File->new("db-h/$hash/$data->{bug_num}.report",'r') or
937 die "Unable to open original report db-h/$hash/$data->{bug_num}.report for reading: $!";
941 $orig_report= <$report_fh>;
944 if (not $orig_report_set and defined $orig_report and
945 length $orig_report and
946 exists $param{original_report}){
947 ${$param{original_report}} = $orig_report;
948 $orig_report_set = 1;
951 $action = "Marked $config{bug} as done";
953 # set done to the requester
954 $data->{done} = exists $param{done}?$param{done}:$param{requester};
955 append_action_to_log(bug => $data->{bug_num},
958 old_data => $old_data,
960 __return_append_to_log_options(
965 if not exists $param{append_log} or $param{append_log};
966 writebug($data->{bug_num},$data);
967 print {$transcript} "$action\n";
968 # get the original report
969 if ($param{notify_submitter}) {
970 my $submitter_message;
971 if(not exists $submitter_notified{$data->{originator}}) {
973 create_mime_message([default_headers(queue_file => $param{request_nn},
975 msgid => $param{request_msgid},
976 msgtype => 'notifdone',
977 pr_msg => 'they-closed',
979 [To => $data->{submitter},
980 Subject => "$config{ubug}#$data->{bug_num} ".
981 "closed by $param{requester} ($param{request_subject})",
985 __message_body_template('mail/process_your_bug_done',
987 replyto => (exists $param{request_replyto} ?
988 $param{request_replyto} :
989 $param{requester} || 'Unknown'),
990 markedby => $param{requester},
991 subject => $param{request_subject},
992 messageid => $param{request_msgid},
995 [join('',make_list($param{message})),$orig_report]
997 send_mail_message(message => $submitter_message,
998 recipients => $old_data->{submitter},
1000 $submitter_notified{$data->{originator}} = $submitter_message;
1003 $submitter_message = $submitter_notified{$data->{originator}};
1005 append_action_to_log(bug => $data->{bug_num},
1006 action => "Notification sent",
1008 request_addr => $data->{originator},
1009 desc => "$config{bug} acknowledged by developer.",
1010 recips => [$data->{originator}],
1011 message => $submitter_message,
1016 if (exists $param{fixed}) {
1017 set_fixed(fixed => $param{fixed},
1021 keys %common_options,
1022 keys %append_action_options
1030 =head2 set_submitter
1033 set_submitter(bug => $ref,
1034 transcript => $transcript,
1035 ($dl > 0 ? (debug => $transcript):()),
1036 requester => $header{from},
1037 request_addr => $controlrequestaddr,
1039 affected_packages => \%affected_packages,
1040 recipients => \%recipients,
1041 submitter => $new_submitter,
1042 notify_submitter => 1,
1047 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1050 Sets the submitter of a bug. If notify_submitter is true (the
1051 default), notifies the old submitter of a bug on changes
1056 my %param = validate_with(params => \@_,
1057 spec => {bug => {type => SCALAR,
1060 # specific options here
1061 submitter => {type => SCALAR,
1063 notify_submitter => {type => BOOLEAN,
1067 %append_action_options,
1070 if (not Mail::RFC822::Address::valid($param{submitter})) {
1071 die "New submitter address $param{submitter} is not a valid e-mail address";
1074 __begin_control(%param,
1075 command => 'submitter'
1077 my ($debug,$transcript) =
1078 @info{qw(debug transcript)};
1079 my @data = @{$info{data}};
1080 my @bugs = @{$info{bugs}};
1082 # here we only concern ourselves with the first of the merged bugs
1083 for my $data ($data[0]) {
1084 my $notify_old_submitter = 0;
1085 my $old_data = dclone($data);
1086 print {$debug} "Going to change bug submitter\n";
1087 if (((not defined $param{submitter} or not length $param{submitter}) and
1088 (not defined $data->{originator} or not length $data->{originator})) or
1089 (defined $param{submitter} and defined $data->{originator} and
1090 $param{submitter} eq $data->{originator})) {
1091 print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n"
1092 unless __internal_request();
1096 if (defined $data->{originator} and length($data->{originator})) {
1097 $action= "Changed $config{bug} submitter to '$param{submitter}' from '$data->{originator}'";
1098 $notify_old_submitter = 1;
1101 $action= "Set $config{bug} submitter to '$param{submitter}'.";
1103 $data->{originator} = $param{submitter};
1105 append_action_to_log(bug => $data->{bug_num},
1106 command => 'submitter',
1108 old_data => $old_data,
1110 __return_append_to_log_options(
1115 if not exists $param{append_log} or $param{append_log};
1116 writebug($data->{bug_num},$data);
1117 print {$transcript} "$action\n";
1118 # notify old submitter
1119 if ($notify_old_submitter and $param{notify_submitter}) {
1120 send_mail_message(message =>
1121 create_mime_message([default_headers(queue_file => $param{request_nn},
1123 msgid => $param{request_msgid},
1125 pr_msg => 'submitter-changed',
1127 [To => $old_data->{submitter},
1128 Subject => "$config{ubug}#$data->{bug_num} submitter addressed changed ($param{request_subject})",
1132 __message_body_template('mail/submitter_changed',
1133 {old_data => $old_data,
1135 replyto => exists $param{header}{'reply-to'} ? $param{request_replyto} : $param{requester} || 'Unknown',
1139 recipients => $old_data->{submitter},
1143 __end_control(%info);
1148 =head2 set_forwarded
1151 set_forwarded(bug => $ref,
1152 transcript => $transcript,
1153 ($dl > 0 ? (debug => $transcript):()),
1154 requester => $header{from},
1155 request_addr => $controlrequestaddr,
1157 affected_packages => \%affected_packages,
1158 recipients => \%recipients,
1159 forwarded => $forward_to,
1164 print {$transcript} "Failed to set the forwarded-to-address of $ref: $@";
1167 Sets the location to which a bug is forwarded. Given an undef
1168 forwarded, unsets forwarded.
1174 my %param = validate_with(params => \@_,
1175 spec => {bug => {type => SCALAR,
1178 # specific options here
1179 forwarded => {type => SCALAR|UNDEF,
1182 %append_action_options,
1185 if (defined $param{forwarded} and $param{forwarded} =~ /[^[:print:]]/) {
1186 die "Non-printable characters are not allowed in the forwarded field";
1188 $param{forwarded} = undef if defined $param{forwarded} and not length $param{forwarded};
1190 __begin_control(%param,
1191 command => 'forwarded'
1193 my ($debug,$transcript) =
1194 @info{qw(debug transcript)};
1195 my @data = @{$info{data}};
1196 my @bugs = @{$info{bugs}};
1198 for my $data (@data) {
1199 my $old_data = dclone($data);
1200 print {$debug} "Going to change bug forwarded\n";
1201 if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
1202 (not defined $param{forwarded} and
1203 defined $data->{forwarded} and not length $data->{forwarded})) {
1204 print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n"
1205 unless __internal_request();
1209 if (not defined $param{forwarded}) {
1210 $action= "Unset $config{bug} forwarded-to-address";
1212 elsif (defined $data->{forwarded} and length($data->{forwarded})) {
1213 $action= "Changed $config{bug} forwarded-to-address to '$param{forwarded}' from '$data->{forwarded}'";
1216 $action= "Set $config{bug} forwarded-to-address to '$param{forwarded}'.";
1218 $data->{forwarded} = $param{forwarded};
1220 append_action_to_log(bug => $data->{bug_num},
1221 command => 'forwarded',
1223 old_data => $old_data,
1225 __return_append_to_log_options(
1230 if not exists $param{append_log} or $param{append_log};
1231 writebug($data->{bug_num},$data);
1232 print {$transcript} "$action\n";
1234 __end_control(%info);
1243 set_title(bug => $ref,
1244 transcript => $transcript,
1245 ($dl > 0 ? (debug => $transcript):()),
1246 requester => $header{from},
1247 request_addr => $controlrequestaddr,
1249 affected_packages => \%affected_packages,
1250 recipients => \%recipients,
1251 title => $new_title,
1256 print {$transcript} "Failed to set the title of $ref: $@";
1259 Sets the title of a specific bug
1265 my %param = validate_with(params => \@_,
1266 spec => {bug => {type => SCALAR,
1269 # specific options here
1270 title => {type => SCALAR,
1273 %append_action_options,
1276 if ($param{title} =~ /[^[:print:]]/) {
1277 die "Non-printable characters are not allowed in bug titles";
1280 my %info = __begin_control(%param,
1283 my ($debug,$transcript) =
1284 @info{qw(debug transcript)};
1285 my @data = @{$info{data}};
1286 my @bugs = @{$info{bugs}};
1288 for my $data (@data) {
1289 my $old_data = dclone($data);
1290 print {$debug} "Going to change bug title\n";
1291 if (defined $data->{subject} and length($data->{subject}) and
1292 $data->{subject} eq $param{title}) {
1293 print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n"
1294 unless __internal_request();
1298 if (defined $data->{subject} and length($data->{subject})) {
1299 $action= "Changed $config{bug} title to '$param{title}' from '$data->{subject}'";
1301 $action= "Set $config{bug} title to '$param{title}'.";
1303 $data->{subject} = $param{title};
1305 append_action_to_log(bug => $data->{bug_num},
1308 old_data => $old_data,
1310 __return_append_to_log_options(
1315 if not exists $param{append_log} or $param{append_log};
1316 writebug($data->{bug_num},$data);
1317 print {$transcript} "$action\n";
1319 __end_control(%info);
1326 set_package(bug => $ref,
1327 transcript => $transcript,
1328 ($dl > 0 ? (debug => $transcript):()),
1329 requester => $header{from},
1330 request_addr => $controlrequestaddr,
1332 affected_packages => \%affected_packages,
1333 recipients => \%recipients,
1334 package => $new_package,
1340 print {$transcript} "Failed to assign or reassign $ref to a package: $@";
1343 Indicates that a bug is in a particular package. If is_source is true,
1344 indicates that the package is a source package. [Internally, this
1345 causes src: to be prepended to the package name.]
1347 The default for is_source is 0. As a special case, if the package
1348 starts with 'src:', it is assumed to be a source package and is_source
1351 The package option must match the package_name_re regex.
1356 my %param = validate_with(params => \@_,
1357 spec => {bug => {type => SCALAR,
1360 # specific options here
1361 package => {type => SCALAR|ARRAYREF,
1363 is_source => {type => BOOLEAN,
1367 %append_action_options,
1370 my @new_packages = map {splitpackages($_)} make_list($param{package});
1371 if (grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages) {
1372 croak "Invalid package name '".
1373 join(',',grep {$_ !~ /^(?:src:|)$config{package_name_re}$/} @new_packages).
1376 my %info = __begin_control(%param,
1377 command => 'package',
1379 my ($debug,$transcript) =
1380 @info{qw(debug transcript)};
1381 my @data = @{$info{data}};
1382 my @bugs = @{$info{bugs}};
1383 # clean up the new package
1387 ($temp =~ s/^src:// or
1388 $param{is_source}) ? 'src:'.$temp:$temp;
1392 my $package_reassigned = 0;
1393 for my $data (@data) {
1394 my $old_data = dclone($data);
1395 print {$debug} "Going to change assigned package\n";
1396 if (defined $data->{package} and length($data->{package}) and
1397 $data->{package} eq $new_package) {
1398 print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n"
1399 unless __internal_request();
1403 if (defined $data->{package} and length($data->{package})) {
1404 $package_reassigned = 1;
1405 $action= "$config{bug} reassigned from package '$data->{package}'".
1406 " to '$new_package'.";
1408 $action= "$config{bug} assigned to package '$new_package'.";
1410 $data->{package} = $new_package;
1412 append_action_to_log(bug => $data->{bug_num},
1413 command => 'package',
1415 old_data => $old_data,
1417 __return_append_to_log_options(
1422 if not exists $param{append_log} or $param{append_log};
1423 writebug($data->{bug_num},$data);
1424 print {$transcript} "$action\n";
1426 __end_control(%info);
1427 # Only clear the fixed/found versions if the package has been
1429 if ($package_reassigned) {
1430 my @params_for_found_fixed =
1431 map {exists $param{$_}?($_,$param{$_}):()}
1433 keys %common_options,
1434 keys %append_action_options,
1436 set_found(found => [],
1437 @params_for_found_fixed,
1439 set_fixed(fixed => [],
1440 @params_for_found_fixed,
1448 set_found(bug => $ref,
1449 transcript => $transcript,
1450 ($dl > 0 ? (debug => $transcript):()),
1451 requester => $header{from},
1452 request_addr => $controlrequestaddr,
1454 affected_packages => \%affected_packages,
1455 recipients => \%recipients,
1462 print {$transcript} "Failed to set found on $ref: $@";
1466 Sets, adds, or removes the specified found versions of a package
1468 If the version list is empty, and the bug is currently not "done",
1469 causes the done field to be cleared.
1471 If any of the versions added to found are greater than any version in
1472 which the bug is fixed (or when the bug is found and there are no
1473 fixed versions) the done field is cleared.
1478 my %param = validate_with(params => \@_,
1479 spec => {bug => {type => SCALAR,
1482 # specific options here
1483 found => {type => SCALAR|ARRAYREF,
1486 add => {type => BOOLEAN,
1489 remove => {type => BOOLEAN,
1493 %append_action_options,
1496 if ($param{add} and $param{remove}) {
1497 croak "It's nonsensical to add and remove the same versions";
1501 __begin_control(%param,
1504 my ($debug,$transcript) =
1505 @info{qw(debug transcript)};
1506 my @data = @{$info{data}};
1507 my @bugs = @{$info{bugs}};
1509 for my $version (make_list($param{found})) {
1510 next unless defined $version;
1511 $versions{$version} =
1512 [make_source_versions(package => [splitpackages($data[0]{package})],
1513 warnings => $transcript,
1516 versions => $version,
1519 # This is really ugly, but it's what we have to do
1520 if (not @{$versions{$version}}) {
1521 print {$transcript} "Unable to make a source version for version '$version'\n";
1524 if (not keys %versions and ($param{remove} or $param{add})) {
1525 if ($param{remove}) {
1526 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1529 print {$transcript} "Requested to add no versions; doing nothing.\n";
1531 __end_control(%info);
1534 # first things first, make the versions fully qualified source
1536 for my $data (@data) {
1537 # The 'done' field gets a bit weird with version tracking,
1538 # because a bug may be closed by multiple people in different
1539 # branches. Until we have something more flexible, we set it
1540 # every time a bug is fixed, and clear it when a bug is found
1541 # in a version greater than any version in which the bug is
1542 # fixed or when a bug is found and there is no fixed version
1543 my $action = 'Did not alter found versions';
1544 my %found_added = ();
1545 my %found_removed = ();
1546 my %fixed_removed = ();
1548 my $old_data = dclone($data);
1549 if (not $param{add} and not $param{remove}) {
1550 $found_removed{$_} = 1 for @{$data->{found_versions}};
1551 $data->{found_versions} = [];
1554 @found_versions{@{$data->{found_versions}}} = (1) x @{$data->{found_versions}};
1556 @fixed_versions{@{$data->{fixed_versions}}} = (1) x @{$data->{fixed_versions}};
1557 for my $version (keys %versions) {
1559 my @svers = @{$versions{$version}};
1563 for my $sver (@svers) {
1564 if (not exists $found_versions{$sver}) {
1565 $found_versions{$sver} = 1;
1566 $found_added{$sver} = 1;
1568 # if the found we are adding matches any fixed
1569 # versions, remove them
1570 my @temp = grep m{(^|/)\Q$sver\E}, keys %fixed_versions;
1571 delete $fixed_versions{$_} for @temp;
1572 $fixed_removed{$_} = 1 for @temp;
1575 # We only care about reopening the bug if the bug is
1577 if (defined $data->{done} and length $data->{done}) {
1578 my @svers_order = sort_versions(map {m{([^/]+)$}; $1;}
1580 # determine if we need to reopen
1581 my @fixed_order = sort_versions(map {m{([^/]+)$}; $1;}
1582 keys %fixed_versions);
1583 if (not @fixed_order or
1584 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1590 elsif ($param{remove}) {
1591 # in the case of removal, we only concern ourself with
1592 # the version passed, not the source version it maps
1594 my @temp = grep m{(^|/)\Q$version\E}, keys %found_versions;
1595 delete $found_versions{$_} for @temp;
1596 $found_removed{$_} = 1 for @temp;
1599 # set the keys to exactly these values
1600 my @svers = @{$versions{$version}};
1604 for my $sver (@svers) {
1605 if (not exists $found_versions{$sver}) {
1606 $found_versions{$sver} = 1;
1607 if (exists $found_removed{$sver}) {
1608 delete $found_removed{$sver};
1611 $found_added{$sver} = 1;
1618 $data->{found_versions} = [keys %found_versions];
1619 $data->{fixed_versions} = [keys %fixed_versions];
1622 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1623 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1624 # push @changed, 'marked as fixed in versions '.english_join([keys %fixed_addded]) if keys %fixed_added;
1625 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1626 $action = ucfirst(join ('; ',@changed)) if @changed;
1628 $action .= " and reopened"
1630 if (not $reopened and not @changed) {
1631 print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n"
1632 unless __internal_request();
1636 append_action_to_log(bug => $data->{bug_num},
1639 old_data => $old_data,
1641 __return_append_to_log_options(
1646 if not exists $param{append_log} or $param{append_log};
1647 writebug($data->{bug_num},$data);
1648 print {$transcript} "$action\n";
1650 __end_control(%info);
1656 set_fixed(bug => $ref,
1657 transcript => $transcript,
1658 ($dl > 0 ? (debug => $transcript):()),
1659 requester => $header{from},
1660 request_addr => $controlrequestaddr,
1662 affected_packages => \%affected_packages,
1663 recipients => \%recipients,
1671 print {$transcript} "Failed to set fixed on $ref: $@";
1675 Sets, adds, or removes the specified fixed versions of a package
1677 If the fixed versions are empty (or end up being empty after this
1678 call) or the greatest fixed version is less than the greatest found
1679 version and the reopen option is true, the bug is reopened.
1681 This function is also called by the reopen function, which causes all
1682 of the fixed versions to be cleared.
1687 my %param = validate_with(params => \@_,
1688 spec => {bug => {type => SCALAR,
1691 # specific options here
1692 fixed => {type => SCALAR|ARRAYREF,
1695 add => {type => BOOLEAN,
1698 remove => {type => BOOLEAN,
1701 reopen => {type => BOOLEAN,
1705 %append_action_options,
1708 if ($param{add} and $param{remove}) {
1709 croak "It's nonsensical to add and remove the same versions";
1712 __begin_control(%param,
1715 my ($debug,$transcript) =
1716 @info{qw(debug transcript)};
1717 my @data = @{$info{data}};
1718 my @bugs = @{$info{bugs}};
1720 for my $version (make_list($param{fixed})) {
1721 next unless defined $version;
1722 $versions{$version} =
1723 [make_source_versions(package => [splitpackages($data[0]{package})],
1724 warnings => $transcript,
1727 versions => $version,
1730 # This is really ugly, but it's what we have to do
1731 if (not @{$versions{$version}}) {
1732 print {$transcript} "Unable to make a source version for version '$version'\n";
1735 if (not keys %versions and ($param{remove} or $param{add})) {
1736 if ($param{remove}) {
1737 print {$transcript} "Requested to remove no versions; doing nothing.\n";
1740 print {$transcript} "Requested to add no versions; doing nothing.\n";
1742 __end_control(%info);
1745 # first things first, make the versions fully qualified source
1747 for my $data (@data) {
1748 my $old_data = dclone($data);
1749 # The 'done' field gets a bit weird with version tracking,
1750 # because a bug may be closed by multiple people in different
1751 # branches. Until we have something more flexible, we set it
1752 # every time a bug is fixed, and clear it when a bug is found
1753 # in a version greater than any version in which the bug is
1754 # fixed or when a bug is found and there is no fixed version
1755 my $action = 'Did not alter fixed versions';
1756 my %found_added = ();
1757 my %found_removed = ();
1758 my %fixed_added = ();
1759 my %fixed_removed = ();
1761 if (not $param{add} and not $param{remove}) {
1762 $fixed_removed{$_} = 1 for @{$data->{fixed_versions}};
1763 $data->{fixed_versions} = [];
1766 @found_versions{@{$data->{found_versions}||[]}} = (1) x @{$data->{found_versions}||[]};
1768 @fixed_versions{@{$data->{fixed_versions}||[]}} = (1) x @{$data->{fixed_versions}||[]};
1769 for my $version (keys %versions) {
1771 my @svers = @{$versions{$version}};
1775 for my $sver (@svers) {
1776 if (not exists $fixed_versions{$sver}) {
1777 $fixed_versions{$sver} = 1;
1778 $fixed_added{$sver} = 1;
1782 elsif ($param{remove}) {
1783 # in the case of removal, we only concern ourself with
1784 # the version passed, not the source version it maps
1786 my @temp = grep m{(?:^|\/)\Q$version\E$}, keys %fixed_versions;
1787 delete $fixed_versions{$_} for @temp;
1788 $fixed_removed{$_} = 1 for @temp;
1791 # set the keys to exactly these values
1792 my @svers = @{$versions{$version}};
1796 for my $sver (@svers) {
1797 if (not exists $fixed_versions{$sver}) {
1798 $fixed_versions{$sver} = 1;
1799 if (exists $fixed_removed{$sver}) {
1800 delete $fixed_removed{$sver};
1803 $fixed_added{$sver} = 1;
1810 $data->{found_versions} = [keys %found_versions];
1811 $data->{fixed_versions} = [keys %fixed_versions];
1813 # If we're supposed to consider reopening, reopen if the
1814 # fixed versions are empty or the greatest found version
1815 # is greater than the greatest fixed version
1816 if ($param{reopen} and defined $data->{done}
1817 and length $data->{done}) {
1818 my @svers_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1819 map {m{([^/]+)$}; $1;} @{$data->{found_versions}};
1820 # determine if we need to reopen
1821 my @fixed_order = sort {Debbugs::Versions::Dpkg::vercmp($a,$b);}
1822 map {m{([^/]+)$}; $1;} @{$data->{fixed_versions}};
1823 if (not @fixed_order or
1824 (Debbugs::Versions::Dpkg::vercmp($svers_order[-1],$fixed_order[-1]) >= 0)) {
1831 push @changed, 'marked as found in versions '.english_join([keys %found_added]) if keys %found_added;
1832 push @changed, 'no longer marked as found in versions '.english_join([keys %found_removed]) if keys %found_removed;
1833 push @changed, 'marked as fixed in versions '.english_join([keys %fixed_added]) if keys %fixed_added;
1834 push @changed, 'no longer marked as fixed in versions '.english_join([keys %fixed_removed]) if keys %fixed_removed;
1835 $action = ucfirst(join ('; ',@changed)) if @changed;
1837 $action .= " and reopened"
1839 if (not $reopened and not @changed) {
1840 print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n"
1841 unless __internal_request();
1845 append_action_to_log(bug => $data->{bug_num},
1848 old_data => $old_data,
1850 __return_append_to_log_options(
1855 if not exists $param{append_log} or $param{append_log};
1856 writebug($data->{bug_num},$data);
1857 print {$transcript} "$action\n";
1859 __end_control(%info);
1866 set_merged(bug => $ref,
1867 transcript => $transcript,
1868 ($dl > 0 ? (debug => $transcript):()),
1869 requester => $header{from},
1870 request_addr => $controlrequestaddr,
1872 affected_packages => \%affected_packages,
1873 recipients => \%recipients,
1874 merge_with => 12345,
1877 allow_reassign => 1,
1878 reassign_same_source_only => 1,
1883 print {$transcript} "Failed to set merged on $ref: $@";
1887 Sets, adds, or removes the specified merged bugs of a bug
1889 By default, requires
1894 my %param = validate_with(params => \@_,
1895 spec => {bug => {type => SCALAR,
1898 # specific options here
1899 merge_with => {type => ARRAYREF|SCALAR,
1902 remove => {type => BOOLEAN,
1905 force => {type => BOOLEAN,
1908 masterbug => {type => BOOLEAN,
1911 allow_reassign => {type => BOOLEAN,
1914 reassign_different_sources => {type => BOOLEAN,
1918 %append_action_options,
1921 my @merging = exists $param{merge_with} ? make_list($param{merge_with}):();
1923 @merging{@merging} = (1) x @merging;
1924 if (grep {$_ !~ /^\d+$/} @merging) {
1925 croak "Requested to merge with a non-numeric bug: ".join(',',map {qq('$_');} @merging);
1927 $param{locks} = {} if not exists $param{locks};
1929 __begin_control(%param,
1932 my ($debug,$transcript) =
1933 @info{qw(debug transcript)};
1934 if (not @merging and exists $param{merge_with}) {
1935 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
1936 __end_control(%info);
1939 my @data = @{$info{data}};
1940 my @bugs = @{$info{bugs}};
1943 for my $data (@data) {
1944 $data{$data->{bug_num}} = $data;
1945 my @merged_bugs = split / /, $data->{mergedwith};
1946 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
1950 if (not exists $param{merge_with}) {
1951 my $ok_to_unmerge = 1;
1952 delete $merged_bugs{$param{bug}};
1953 if (not keys %merged_bugs) {
1954 print {$transcript} "Ignoring request to unmerge a bug which is not merged with any others.\n";
1955 __end_control(%info);
1958 my $action = "Disconnected #$param{bug} from all other report(s).";
1959 for my $data (@data) {
1960 my $old_data = dclone($data);
1961 if ($data->{bug_num} == $param{bug}) {
1962 $data->{mergedwith} = '';
1965 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
1968 append_action_to_log(bug => $data->{bug_num},
1971 old_data => $old_data,
1973 __return_append_to_log_options(%param,
1977 if not exists $param{append_log} or $param{append_log};
1978 writebug($data->{bug_num},$data);
1980 print {$transcript} "$action\n";
1981 __end_control(%info);
1984 # lock and load all of the bugs we need
1985 my @bugs_to_load = keys %merging;
1988 my ($data,$n_locks) =
1989 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
1991 locks => $param{locks},
1994 $new_locks += $n_locks;
1996 @data = values %data;
1997 if (not __check_limit(data => [@data],
1998 exists $param{limit}?(limit => $param{limit}):(),
1999 transcript => $transcript,
2001 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
2003 for my $data (@data) {
2004 $data{$data->{bug_num}} = $data;
2005 $merged_bugs{$data->{bug_num}} = 1;
2006 my @merged_bugs = split / /, $data->{mergedwith};
2007 @merged_bugs{@merged_bugs} = (1) x @merged_bugs;
2008 if (exists $param{affected_bugs}) {
2009 $param{affected_bugs}{$data->{bug_num}} = 1;
2012 __handle_affected_packages(%param,data => [@data]);
2013 my %bug_info_shown; # which bugs have had information shown
2014 $bug_info_shown{$param{bug}} = 1;
2015 add_recipients(data => [@data],
2016 recipients => $param{recipients},
2017 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
2019 (__internal_request()?(transcript => $transcript):()),
2022 # Figure out what the ideal state is for the bug,
2023 my ($merge_status,$bugs_to_merge) =
2024 __calculate_merge_status(\@data,\%data,$param{bug});
2025 # find out if we actually have any bugs to merge
2026 if (not $bugs_to_merge) {
2027 print {$transcript} "Requested to merge with no additional bugs; not doing anything\n";
2028 for (1..$new_locks) {
2029 unfilelock($param{locks});
2032 __end_control(%info);
2035 # see what changes need to be made to merge the bugs
2036 # check to make sure that the set of changes we need to make is allowed
2037 my ($disallowed_changes,$changes) =
2038 __calculate_merge_changes(\@data,$merge_status,\%param);
2039 # at this point, stop if there are disallowed changes, otherwise
2040 # make the allowed changes, and then reread the bugs in question
2041 # to get the new data, then recaculate the merges; repeat
2042 # reloading and recalculating until we try too many times or there
2043 # are no changes to make.
2046 # we will allow at most 4 times through this; more than 1
2047 # shouldn't really happen.
2049 while ($attempts < 4 and (@{$disallowed_changes} or keys %{$changes})) {
2050 if ($attempts > 1) {
2051 print {$transcript} "Unable to complete merge on previous attempt; trying again (retry: $attempts)\n";
2053 if (@{$disallowed_changes}) {
2054 # figure out the problems
2055 print {$transcript} "Unable to merge bugs because:\n";
2056 for my $change (@{$disallowed_changes}) {
2057 print {$transcript} "$change->{field} of #$change->{bug} is '$change->{orig_value}' not '$change->{value}'\n";
2059 if ($attempts > 0) {
2060 croak "Some bugs were altered while attempting to merge";
2063 croak "Did not alter merged bugs";
2066 my ($change_bug) = keys %{$changes};
2067 $bug_changed{$change_bug}++;
2068 print {$transcript} __bug_info($data{$change_bug}) if
2069 $param{show_bug_info} and not __internal_request(1);
2070 $bug_info_shown{$change_bug} = 1;
2071 __allow_relocking($param{locks},[keys %data]);
2072 for my $change (@{$changes->{$change_bug}}) {
2073 if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
2074 my %target_blockedby;
2075 @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
2076 my %unhandled_targets = %target_blockedby;
2077 my @blocks_to_remove;
2078 for my $key (split / /,$change->{orig_value}) {
2079 delete $unhandled_targets{$key};
2080 next if exists $target_blockedby{$key};
2081 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2082 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2085 keys %common_options,
2086 keys %append_action_options),
2089 for my $key (keys %unhandled_targets) {
2090 set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
2091 block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
2094 keys %common_options,
2095 keys %append_action_options),
2100 $change->{function}->(bug => $change->{bug},
2101 $change->{key}, $change->{func_value},
2102 exists $change->{options}?@{$change->{options}}:(),
2104 keys %common_options,
2105 keys %append_action_options),
2109 __disallow_relocking($param{locks});
2110 my ($data,$n_locks) =
2111 __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
2113 locks => $param{locks},
2117 $new_locks += $n_locks;
2120 @data = values %data;
2121 ($merge_status,$bugs_to_merge) =
2122 __calculate_merge_status(\@data,\%data,$param{bug});
2123 ($disallowed_changes,$changes) =
2124 __calculate_merge_changes(\@data,$merge_status,\%param);
2125 $attempts = max(values %bug_changed);
2127 if ($param{show_bug_info} and not __internal_request(1)) {
2128 for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
2129 next if $bug_info_shown{$data->{bug_num}};
2130 print {$transcript} __bug_info($data);
2133 if (keys %{$changes} or @{$disallowed_changes}) {
2134 print {$transcript} "Unable to modify bugs so that they could be merged\n";
2135 for (1..$new_locks) {
2136 unfilelock($param{locks});
2139 __end_control(%info);
2143 # finally, we can merge the bugs
2144 my $action = "Merged ".join(' ',sort keys %merged_bugs);
2145 for my $data (@data) {
2146 my $old_data = dclone($data);
2147 $data->{mergedwith} = join(' ',sort grep {$_ != $data->{bug_num}}
2149 append_action_to_log(bug => $data->{bug_num},
2152 old_data => $old_data,
2154 __return_append_to_log_options(%param,
2158 if not exists $param{append_log} or $param{append_log};
2159 writebug($data->{bug_num},$data);
2161 print {$transcript} "$action\n";
2162 # unlock the extra locks that we got earlier
2163 for (1..$new_locks) {
2164 unfilelock($param{locks});
2167 __end_control(%info);
2170 sub __allow_relocking{
2171 my ($locks,$bugs) = @_;
2173 for my $bug (@{$bugs}) {
2174 my @lockfiles = grep {m{/\Q$bug\E$}} keys %{$locks->{locks}};
2175 next unless @lockfiles;
2176 $locks->{relockable}{$lockfiles[0]} = 0;
2180 sub __disallow_relocking{
2182 delete $locks->{relockable};
2185 sub __lock_and_load_merged_bugs{
2187 validate_with(params => \@_,
2189 {bugs_to_load => {type => ARRAYREF,
2190 default => sub {[]},
2192 data => {type => HASHREF|ARRAYREF,
2194 locks => {type => HASHREF,
2195 default => sub {{};},
2197 reload_all => {type => BOOLEAN,
2200 debug => {type => HANDLE,
2206 if (ref($param{data}) eq 'ARRAY') {
2207 for my $data (@{$param{data}}) {
2208 $data{$data->{bug_num}} = dclone($data);
2212 %data = %{dclone($param{data})};
2214 my @bugs_to_load = @{$param{bugs_to_load}};
2215 if ($param{reload_all}) {
2216 push @bugs_to_load, keys %data;
2219 @temp{@bugs_to_load} = (1) x @bugs_to_load;
2220 @bugs_to_load = keys %temp;
2221 my %loaded_this_time;
2223 while ($bug_to_load = shift @bugs_to_load) {
2224 if (not $param{reload_all}) {
2225 next if exists $data{$bug_to_load};
2228 next if $loaded_this_time{$bug_to_load};
2231 if ($param{reload_all}) {
2232 if (exists $data{$bug_to_load}) {
2237 read_bug(bug => $bug_to_load,
2239 locks => $param{locks},
2241 die "Unable to load bug $bug_to_load";
2242 print {$param{debug}} "read bug $bug_to_load\n";
2243 $data{$data->{bug_num}} = $data;
2244 $new_locks += $lock_bug;
2245 $loaded_this_time{$data->{bug_num}} = 1;
2247 grep {not exists $data{$_}}
2248 split / /,$data->{mergedwith};
2250 return (\%data,$new_locks);
2254 sub __calculate_merge_status{
2255 my ($data_a,$data_h,$master_bug,$merge) = @_;
2258 my $bugs_to_merge = 0;
2259 for my $data (@{$data_a}) {
2260 # check to see if this bug is unmerged in the set
2261 if (not length $data->{mergedwith} or
2262 grep {not exists $merged_bugs{$_}} split / /,$data->{mergedwith}) {
2263 $merged_bugs{$data->{bug_num}} = 1;
2266 # the master_bug is the bug that every other bug is made to
2267 # look like. However, if merge is set, tags, fixed and found
2269 if ($data->{bug_num} == $master_bug) {
2270 for (qw(package forwarded severity blocks blockedby done owner summary affects)) {
2271 $merge_status{$_} = $data->{$_}
2275 next unless $data->{bug_num} == $master_bug;
2277 $merge_status{tag} = {} if not exists $merge_status{tag};
2278 for my $tag (split /\s+/, $data->{keywords}) {
2279 $merge_status{tag}{$tag} = 1;
2281 $merge_status{keywords} = join(' ',sort keys %{$merge_status{tag}});
2282 for (qw(fixed found)) {
2283 @{$merge_status{"${_}_versions"}}{@{$data->{"${_}_versions"}}} = (1) x @{$data->{"${_}_versions"}};
2286 return (\%merge_status,$bugs_to_merge);
2291 sub __calculate_merge_changes{
2292 my ($datas,$merge_status,$param) = @_;
2294 my @disallowed_changes;
2295 for my $data (@{$datas}) {
2296 # things that can be forced
2298 # * func is the function to set the new value
2300 # * key is the key of the function to set the value,
2302 # * modify_value is a function which is called to modify the new
2303 # value so that the function will accept it
2305 # * options is an ARRAYREF of options to pass to the function
2307 # * allowed is a BOOLEAN which controls whether this setting
2308 # is allowed to be different by default.
2309 my %force_functions =
2310 (forwarded => {func => \&set_forwarded,
2314 severity => {func => \&set_severity,
2318 blocks => {func => \&set_blocks,
2319 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2323 blockedby => {func => \&set_blocks,
2324 modify_value => sub {defined $_[0]?[split ' ',$_[0]]:[]},
2328 done => {func => \&set_done,
2332 owner => {func => \&owner,
2336 summary => {func => \&summary,
2340 affects => {func => \&affects,
2344 package => {func => \&set_package,
2348 keywords => {func => \&set_tag,
2350 modify_value => sub {defined $_[0]?[sort split /\s+/,$_[0]]:[]},
2353 fixed_versions => {func => \&set_fixed,
2357 found_versions => {func => \&set_found,
2362 for my $field (qw(forwarded severity blocks blockedby done owner summary affects package fixed_versions found_versions keywords)) {
2363 # if the ideal bug already has the field set properly, we
2365 if ($field eq 'keywords'){
2366 next if join(' ',sort split /\s+/,$data->{keywords}) eq
2367 join(' ',sort keys %{$merge_status->{tag}});
2369 elsif ($field =~ /^(?:fixed|found)_versions$/) {
2370 next if join(' ', sort @{$data->{$field}}) eq
2371 join(' ',sort keys %{$merge_status->{$field}});
2373 elsif ($merge_status->{$field} eq $data->{$field}) {
2378 bug => $data->{bug_num},
2379 orig_value => $data->{$field},
2381 (exists $force_functions{$field}{modify_value} ?
2382 $force_functions{$field}{modify_value}->($merge_status->{$field}):
2383 $merge_status->{$field}),
2384 value => $merge_status->{$field},
2385 function => $force_functions{$field}{func},
2386 key => $force_functions{$field}{key},
2387 options => $force_functions{$field}{options},
2388 allowed => exists $force_functions{$field}{allowed} ? 0 : $force_functions{$field}{allowed},
2390 if ($param->{force}) {
2391 if ($field ne 'package') {
2392 push @{$changes{$data->{bug_num}}},$change;
2395 if ($param->{allow_reassign}) {
2396 if ($param->{reassign_different_sources}) {
2397 push @{$changes{$data->{bug_num}}},$change;
2400 # allow reassigning if binary_to_source returns at
2401 # least one of the same source packages
2402 my @merge_status_source =
2403 binary_to_source(package => $merge_status->{package},
2406 my @other_bug_source =
2407 binary_to_source(package => $data->{package},
2410 my %merge_status_sources;
2411 @merge_status_sources{@merge_status_source} =
2412 (1) x @merge_status_source;
2413 if (grep {$merge_status_sources{$_}} @other_bug_source) {
2414 push @{$changes{$data->{bug_num}}},$change;
2419 push @disallowed_changes,$change;
2421 # blocks and blocked by are weird; we have to go through and
2422 # set blocks to the other half of the merged bugs
2424 return (\@disallowed_changes,\%changes);
2430 affects(bug => $ref,
2431 transcript => $transcript,
2432 ($dl > 0 ? (debug => $transcript):()),
2433 requester => $header{from},
2434 request_addr => $controlrequestaddr,
2436 affected_packages => \%affected_packages,
2437 recipients => \%recipients,
2445 print {$transcript} "Failed to mark $ref as affecting $packages: $@";
2448 This marks a bug as affecting packages which the bug is not actually
2449 in. This should only be used in cases where fixing the bug instantly
2450 resolves the problem in the other packages.
2452 By default, the packages are set to the list of packages passed.
2453 However, if you pass add => 1 or remove => 1, the list of packages
2454 passed are added or removed from the affects list, respectively.
2459 my %param = validate_with(params => \@_,
2460 spec => {bug => {type => SCALAR,
2463 # specific options here
2464 package => {type => SCALAR|ARRAYREF|UNDEF,
2467 add => {type => BOOLEAN,
2470 remove => {type => BOOLEAN,
2474 %append_action_options,
2477 if ($param{add} and $param{remove}) {
2478 croak "Asking to both add and remove affects is nonsensical";
2480 if (not defined $param{package}) {
2481 $param{package} = [];
2484 __begin_control(%param,
2485 command => 'affects'
2487 my ($debug,$transcript) =
2488 @info{qw(debug transcript)};
2489 my @data = @{$info{data}};
2490 my @bugs = @{$info{bugs}};
2492 for my $data (@data) {
2494 print {$debug} "Going to change affects\n";
2495 my @packages = splitpackages($data->{affects});
2497 @packages{@packages} = (1) x @packages;
2500 for my $package (make_list($param{package})) {
2501 next unless defined $package and length $package;
2502 if (not $packages{$package}) {
2503 $packages{$package} = 1;
2504 push @added,$package;
2508 $action = "Added indication that $data->{bug_num} affects ".
2509 english_join(\@added);
2512 elsif ($param{remove}) {
2514 for my $package (make_list($param{package})) {
2515 if ($packages{$package}) {
2516 next unless defined $package and length $package;
2517 delete $packages{$package};
2518 push @removed,$package;
2521 $action = "Removed indication that $data->{bug_num} affects " .
2522 english_join(\@removed);
2525 my %added_packages = ();
2526 my %removed_packages = %packages;
2528 for my $package (make_list($param{package})) {
2529 next unless defined $package and length $package;
2530 $packages{$package} = 1;
2531 delete $removed_packages{$package};
2532 $added_packages{$package} = 1;
2534 if (keys %removed_packages) {
2535 $action = "Removed indication that $data->{bug_num} affects ".
2536 english_join([keys %removed_packages]);
2537 $action .= "\n" if keys %added_packages;
2539 if (keys %added_packages) {
2540 $action .= "Added indication that $data->{bug_num} affects " .
2541 english_join([keys %added_packages]);
2544 if (not length $action) {
2545 print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n"
2546 unless __internal_request();
2549 my $old_data = dclone($data);
2550 $data->{affects} = join(',',keys %packages);
2551 append_action_to_log(bug => $data->{bug_num},
2553 command => 'affects',
2555 old_data => $old_data,
2556 __return_append_to_log_options(
2561 if not exists $param{append_log} or $param{append_log};
2562 writebug($data->{bug_num},$data);
2563 print {$transcript} "$action\n";
2565 __end_control(%info);
2569 =head1 SUMMARY FUNCTIONS
2574 summary(bug => $ref,
2575 transcript => $transcript,
2576 ($dl > 0 ? (debug => $transcript):()),
2577 requester => $header{from},
2578 request_addr => $controlrequestaddr,
2580 affected_packages => \%affected_packages,
2581 recipients => \%recipients,
2587 print {$transcript} "Failed to mark $ref with summary foo: $@";
2590 Handles all setting of summary fields
2592 If summary is undef, unsets the summary
2594 If summary is 0, sets the summary to the first paragraph contained in
2597 If summary is a positive integer, sets the summary to the message specified.
2599 Otherwise, sets summary to the value passed.
2605 my %param = validate_with(params => \@_,
2606 spec => {bug => {type => SCALAR,
2609 # specific options here
2610 summary => {type => SCALAR|UNDEF,
2614 %append_action_options,
2617 # croak "summary must be numeric or undef" if
2618 # defined $param{summary} and not $param{summary} =~ /^\d+/;
2620 __begin_control(%param,
2621 command => 'summary'
2623 my ($debug,$transcript) =
2624 @info{qw(debug transcript)};
2625 my @data = @{$info{data}};
2626 my @bugs = @{$info{bugs}};
2627 # figure out the log that we're going to use
2629 my $summary_msg = '';
2631 if (not defined $param{summary}) {
2633 print {$debug} "Removing summary fields\n";
2634 $action = 'Removed summary';
2636 elsif ($param{summary} =~ /^\d+$/) {
2638 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
2639 if ($param{summary} == 0) {
2640 $log = $param{message};
2641 $summary_msg = @records + 1;
2644 if (($param{summary} - 1 ) > $#records) {
2645 die "Message number '$param{summary}' exceeds the maximum message '$#records'";
2647 my $record = $records[($param{summary} - 1 )];
2648 if ($record->{type} !~ /incoming-recv|recips/) {
2649 die "Message number '$param{summary}' is a invalid message type '$record->{type}'";
2651 $summary_msg = $param{summary};
2652 $log = [$record->{text}];
2654 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
2655 my $body = $p_o->{body};
2656 my $in_pseudoheaders = 0;
2658 # walk through body until we get non-blank lines
2659 for my $line (@{$body}) {
2660 if ($line =~ /^\s*$/) {
2661 if (length $paragraph) {
2662 if ($paragraph =~ m/^(?:.+\n\>)+.+\n/x) {
2668 $in_pseudoheaders = 0;
2671 # skip a paragraph if it looks like it's control or
2673 if ($line =~ m{^\s*(?:Package|Source|Version|User|Tag|Severity)\:\s+\S}xi or #pseudo headers
2674 $line =~ m{^(?:package:?|(?:no|)owner|severity|tags?|summary| #control
2675 \#|reopen|close|(?:not|)(?:fixed|found)|clone|
2676 debug|(?:not|)forwarded|priority|
2677 (?:un|)block|limit|(?:un|)archive|
2678 reassign|retitle|affects|wrongpackage
2679 (?:un|force|)merge|user(?:category|tags?|)
2681 if (not length $paragraph) {
2682 print {$debug} "Found control/pseudo-headers and skiping them\n";
2683 $in_pseudoheaders = 1;
2687 next if $in_pseudoheaders;
2688 $paragraph .= $line ." \n";
2690 print {$debug} "Summary is going to be '$paragraph'\n";
2691 $summary = $paragraph;
2692 $summary =~ s/[\n\r]/ /g;
2693 if (not length $summary) {
2694 die "Unable to find summary message to use";
2696 # trim off a trailing spaces
2697 $summary =~ s/\ *$//;
2700 $summary = $param{summary};
2702 for my $data (@data) {
2703 print {$debug} "Going to change summary\n";
2704 if (((not defined $summary or not length $summary) and
2705 (not defined $data->{summary} or not length $data->{summary})) or
2706 $summary eq $data->{summary}) {
2707 print {$transcript} "Ignoring request to change the summary of bug $param{bug} to the same value\n"
2708 unless __internal_request();
2711 if (length $summary) {
2712 if (length $data->{summary}) {
2713 $action = "Summary replaced with message bug $param{bug} message $summary_msg";
2716 $action = "Summary recorded from message bug $param{bug} message $summary_msg";
2719 my $old_data = dclone($data);
2720 $data->{summary} = $summary;
2721 append_action_to_log(bug => $data->{bug_num},
2722 command => 'summary',
2723 old_data => $old_data,
2726 __return_append_to_log_options(
2731 if not exists $param{append_log} or $param{append_log};
2732 writebug($data->{bug_num},$data);
2733 print {$transcript} "$action\n";
2735 __end_control(%info);
2743 =head1 OWNER FUNCTIONS
2749 transcript => $transcript,
2750 ($dl > 0 ? (debug => $transcript):()),
2751 requester => $header{from},
2752 request_addr => $controlrequestaddr,
2754 recipients => \%recipients,
2760 print {$transcript} "Failed to mark $ref as having an owner: $@";
2763 Handles all setting of the owner field; given an owner of undef or of
2764 no length, indicates that a bug is not owned by anyone.
2769 my %param = validate_with(params => \@_,
2770 spec => {bug => {type => SCALAR,
2773 owner => {type => SCALAR|UNDEF,
2776 %append_action_options,
2780 __begin_control(%param,
2783 my ($debug,$transcript) =
2784 @info{qw(debug transcript)};
2785 my @data = @{$info{data}};
2786 my @bugs = @{$info{bugs}};
2788 for my $data (@data) {
2789 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
2790 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
2791 if (not defined $param{owner} or not length $param{owner}) {
2792 if (not defined $data->{owner} or not length $data->{owner}) {
2793 print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n"
2794 unless __internal_request();
2798 $action = "Removed annotation that $config{bug} was owned by " .
2802 if ($data->{owner} eq $param{owner}) {
2803 print {$transcript} "Ignoring request to set the owner of bug #$data->{bug_num} to the same value\n";
2806 if (length $data->{owner}) {
2807 $action = "Owner changed from $data->{owner} to $param{owner}.";
2810 $action = "Owner recorded as $param{owner}."
2813 my $old_data = dclone($data);
2814 $data->{owner} = $param{owner};
2815 append_action_to_log(bug => $data->{bug_num},
2818 old_data => $old_data,
2820 __return_append_to_log_options(
2825 if not exists $param{append_log} or $param{append_log};
2826 writebug($data->{bug_num},$data);
2827 print {$transcript} "$action\n";
2829 __end_control(%info);
2833 =head1 ARCHIVE FUNCTIONS
2840 bug_archive(bug => $bug_num,
2842 transcript => \$transcript,
2847 transcript("Unable to archive $bug_num\n");
2850 transcript($transcript);
2853 This routine archives a bug
2857 =item bug -- bug number
2859 =item check_archiveable -- check wether a bug is archiveable before
2860 archiving; defaults to 1
2862 =item archive_unarchived -- whether to archive bugs which have not
2863 previously been archived; defaults to 1. [Set to 0 when used from
2866 =item ignore_time -- whether to ignore time constraints when archiving
2867 a bug; defaults to 0.
2874 my %param = validate_with(params => \@_,
2875 spec => {bug => {type => SCALAR,
2878 check_archiveable => {type => BOOLEAN,
2881 archive_unarchived => {type => BOOLEAN,
2884 ignore_time => {type => BOOLEAN,
2888 %append_action_options,
2891 my %info = __begin_control(%param,
2892 command => 'archive',
2894 my ($debug,$transcript) = @info{qw(debug transcript)};
2895 my @data = @{$info{data}};
2896 my @bugs = @{$info{bugs}};
2897 my $action = "$config{bug} archived.";
2898 if ($param{check_archiveable} and
2899 not bug_archiveable(bug=>$param{bug},
2900 ignore_time => $param{ignore_time},
2902 print {$transcript} "Bug $param{bug} cannot be archived\n";
2903 die "Bug $param{bug} cannot be archived";
2905 print {$debug} "$param{bug} considering\n";
2906 if (not $param{archive_unarchived} and
2907 not exists $data[0]{unarchived}
2909 print {$transcript} "$param{bug} has not been archived previously\n";
2910 die "$param{bug} has not been archived previously";
2912 add_recipients(recipients => $param{recipients},
2915 transcript => $transcript,
2917 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
2918 for my $bug (@bugs) {
2919 if ($param{check_archiveable}) {
2920 die "Bug $bug cannot be archived (but $param{bug} can?)"
2921 unless bug_archiveable(bug=>$bug,
2922 ignore_time => $param{ignore_time},
2926 # If we get here, we can archive/remove this bug
2927 print {$debug} "$param{bug} removing\n";
2928 for my $bug (@bugs) {
2929 #print "$param{bug} removing $bug\n" if $debug;
2930 my $dir = get_hashname($bug);
2931 # First indicate that this bug is being archived
2932 append_action_to_log(bug => $bug,
2934 command => 'archive',
2935 # we didn't actually change the data
2936 # when we archived, so we don't pass
2937 # a real new_data or old_data
2940 __return_append_to_log_options(
2945 if not exists $param{append_log} or $param{append_log};
2946 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
2947 if ($config{save_old_bugs}) {
2948 mkpath("$config{spool_dir}/archive/$dir");
2949 foreach my $file (@files_to_remove) {
2950 link("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
2951 copy("$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file") or
2952 # we need to bail out here if things have
2953 # gone horribly wrong to avoid removing a
2955 die "Unable to link or copy $config{spool_dir}/db-h/$dir/$file to $config{spool_dir}/archive/$dir/$file; $!";
2958 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
2960 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
2961 print {$transcript} "deleted $bug (from $param{bug})\n";
2963 bughook_archive(@bugs);
2964 __end_control(%info);
2967 =head2 bug_unarchive
2971 bug_unarchive(bug => $bug_num,
2973 transcript => \$transcript,
2978 transcript("Unable to archive bug: $bug_num");
2980 transcript($transcript);
2982 This routine unarchives a bug
2987 my %param = validate_with(params => \@_,
2988 spec => {bug => {type => SCALAR,
2992 %append_action_options,
2996 my %info = __begin_control(%param,
2998 command=>'unarchive');
2999 my ($debug,$transcript) =
3000 @info{qw(debug transcript)};
3001 my @data = @{$info{data}};
3002 my @bugs = @{$info{bugs}};
3003 my $action = "$config{bug} unarchived.";
3004 my @files_to_remove;
3005 for my $bug (@bugs) {
3006 print {$debug} "$param{bug} removing $bug\n";
3007 my $dir = get_hashname($bug);
3008 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
3009 mkpath("archive/$dir");
3010 foreach my $file (@files_to_copy) {
3011 # die'ing here sucks
3012 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3013 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
3014 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
3016 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
3017 print {$transcript} "Unarchived $config{bug} $bug\n";
3019 unlink(@files_to_remove) or die "Unable to unlink bugs";
3020 # Indicate that this bug has been archived previously
3021 for my $bug (@bugs) {
3022 my $newdata = readbug($bug);
3023 my $old_data = dclone($newdata);
3024 if (not defined $newdata) {
3025 print {$transcript} "$config{bug} $bug disappeared!\n";
3026 die "Bug $bug disappeared!";
3028 $newdata->{unarchived} = time;
3029 append_action_to_log(bug => $bug,
3031 command => 'unarchive',
3032 new_data => $newdata,
3033 old_data => $old_data,
3034 __return_append_to_log_options(
3039 if not exists $param{append_log} or $param{append_log};
3040 writebug($bug,$newdata);
3042 __end_control(%info);
3045 =head2 append_action_to_log
3047 append_action_to_log
3049 This should probably be moved to Debbugs::Log; have to think that out
3054 sub append_action_to_log{
3055 my %param = validate_with(params => \@_,
3056 spec => {bug => {type => SCALAR,
3059 new_data => {type => HASHREF,
3062 old_data => {type => HASHREF,
3065 command => {type => SCALAR,
3068 action => {type => SCALAR,
3070 requester => {type => SCALAR,
3073 request_addr => {type => SCALAR,
3076 location => {type => SCALAR,
3079 message => {type => SCALAR|ARRAYREF,
3082 recips => {type => SCALAR|ARRAYREF,
3085 desc => {type => SCALAR,
3088 get_lock => {type => BOOLEAN,
3091 locks => {type => HASHREF,
3095 # append_action_options here
3096 # because some of these
3097 # options aren't actually
3098 # optional, even though the
3099 # original function doesn't
3103 # Fix this to use $param{location}
3104 my $log_location = buglog($param{bug});
3105 die "Unable to find .log for $param{bug}"
3106 if not defined $log_location;
3107 if ($param{get_lock}) {
3108 filelock("lock/$param{bug}",exists $param{locks}?$param{locks}:());
3112 my $logfh = IO::File->new(">>$log_location") or
3113 die "Unable to open $log_location for appending: $!";
3114 # determine difference between old and new
3116 if (exists $param{old_data} and exists $param{new_data}) {
3117 my $old_data = dclone($param{old_data});
3118 my $new_data = dclone($param{new_data});
3119 for my $key (keys %{$old_data}) {
3120 if (not exists $Debbugs::Status::fields{$key}) {
3121 delete $old_data->{$key};
3124 next unless exists $new_data->{$key};
3125 next unless defined $new_data->{$key};
3126 if (not defined $old_data->{$key}) {
3127 delete $old_data->{$key};
3130 if (ref($new_data->{$key}) and
3131 ref($old_data->{$key}) and
3132 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3133 local $Storable::canonical = 1;
3134 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3135 delete $new_data->{$key};
3136 delete $old_data->{$key};
3139 elsif ($new_data->{$key} eq $old_data->{$key}) {
3140 delete $new_data->{$key};
3141 delete $old_data->{$key};
3144 for my $key (keys %{$new_data}) {
3145 if (not exists $Debbugs::Status::fields{$key}) {
3146 delete $new_data->{$key};
3149 next unless exists $old_data->{$key};
3150 next unless defined $old_data->{$key};
3151 if (not defined $new_data->{$key} or
3152 not exists $Debbugs::Status::fields{$key}) {
3153 delete $new_data->{$key};
3156 if (ref($new_data->{$key}) and
3157 ref($old_data->{$key}) and
3158 ref($new_data->{$key}) eq ref($old_data->{$key})) {
3159 local $Storable::canonical = 1;
3160 if (nfreeze($new_data->{$key}) eq nfreeze($old_data->{$key})) {
3161 delete $new_data->{$key};
3162 delete $old_data->{$key};
3165 elsif ($new_data->{$key} eq $old_data->{$key}) {
3166 delete $new_data->{$key};
3167 delete $old_data->{$key};
3170 $data_diff .= "<!-- new_data:\n";
3172 for my $key (keys %{$new_data}) {
3173 if (not exists $Debbugs::Status::fields{$key}) {
3174 warn "No such field $key";
3177 $nd{$key} = $new_data->{$key};
3178 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $new_data->{$key}")."\n";
3180 $data_diff .= html_escape(Data::Dumper->Dump([\%nd],[qw(new_data)]));
3181 $data_diff .= "-->\n";
3182 $data_diff .= "<!-- old_data:\n";
3184 for my $key (keys %{$old_data}) {
3185 if (not exists $Debbugs::Status::fields{$key}) {
3186 warn "No such field $key";
3189 $od{$key} = $old_data->{$key};
3190 # $data_diff .= html_escape("$Debbugs::Status::fields{$key}: $old_data->{$key}")."\n";
3192 $data_diff .= html_escape(Data::Dumper->Dump([\%od],[qw(old_data)]));
3193 $data_diff .= "-->\n";
3196 (exists $param{command} ?
3197 "<!-- command:".html_escape($param{command})." -->\n":""
3199 (length $param{requester} ?
3200 "<!-- requester: ".html_escape($param{requester})." -->\n":""
3202 (length $param{request_addr} ?
3203 "<!-- request_addr: ".html_escape($param{request_addr})." -->\n":""
3205 "<!-- time:".time()." -->\n",
3207 "<strong>".html_escape($param{action})."</strong>\n");
3208 if (length $param{requester}) {
3209 $msg .= "Request was from <code>".html_escape($param{requester})."</code>\n";
3211 if (length $param{request_addr}) {
3212 $msg .= "to <code>".html_escape($param{request_addr})."</code>";
3214 if (length $param{desc}) {
3215 $msg .= ":<br>\n$param{desc}\n";
3220 push @records, {type => 'html',
3224 if ((ref($param{message}) and @{$param{message}}) or length($param{message})) {
3225 push @records, {type => exists $param{recips}?'recips':'incoming-recv',
3226 exists $param{recips}?(recips => [make_list($param{recips})]):(),
3227 text => join('',make_list($param{message})),
3230 write_log_records(logfh=>$logfh,
3231 records => \@records,
3233 close $logfh or die "Unable to close $log_location: $!";
3234 if ($param{get_lock}) {
3235 unfilelock(exists $param{locks}?$param{locks}:());
3243 =head1 PRIVATE FUNCTIONS
3245 =head2 __handle_affected_packages
3247 __handle_affected_packages(affected_packages => {},
3255 sub __handle_affected_packages{
3256 my %param = validate_with(params => \@_,
3257 spec => {%common_options,
3258 data => {type => ARRAYREF|HASHREF
3263 for my $data (make_list($param{data})) {
3264 next unless exists $data->{package} and defined $data->{package};
3265 my @packages = split /\s*,\s*/,$data->{package};
3266 @{$param{affected_packages}}{@packages} = (1) x @packages;
3270 =head2 __handle_debug_transcript
3272 my ($debug,$transcript) = __handle_debug_transcript(%param);
3274 Returns a debug and transcript filehandle
3279 sub __handle_debug_transcript{
3280 my %param = validate_with(params => \@_,
3281 spec => {%common_options},
3284 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
3285 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3286 return ($debug,$transcript);
3293 Produces a small bit of bug information to kick out to the transcript
3300 next unless defined $data and exists $data->{bug_num};
3301 $return .= "Bug #".($data->{bug_num}||'').
3302 ((defined $data->{done} and length $data->{done})?
3303 " {Done: $data->{done}}":''
3305 " [".($data->{package}||'(no package)'). "] ".
3306 ($data->{subject}||'(no subject)')."\n";
3312 =head2 __internal_request
3314 __internal_request()
3315 __internal_request($level)
3317 Returns true if the caller of the function calling __internal_request
3318 belongs to __PACKAGE__
3320 This allows us to be magical, and don't bother to print bug info if
3321 the second caller is from this package, amongst other things.
3323 An optional level is allowed, which increments the number of levels to
3324 check by the given value. [This is basically for use by internal
3325 functions like __begin_control which are always called by
3330 sub __internal_request{
3332 $l = 0 if not defined $l;
3333 if (defined((caller(1+$l))[0]) and (caller(1+$l))[0] eq __PACKAGE__) {
3339 sub __return_append_to_log_options{
3341 my $action = $param{action} if exists $param{action};
3342 if (not exists $param{requester}) {
3343 $param{requester} = $config{control_internal_requester};
3345 if (not exists $param{request_addr}) {
3346 $param{request_addr} = $config{control_internal_request_addr};
3348 if (not exists $param{message}) {
3349 my $date = rfc822_date();
3350 $param{message} = fill_in_template(template => 'mail/fake_control_message',
3351 variables => {request_addr => $param{request_addr},
3352 requester => $param{requester},
3358 if (not defined $action) {
3359 carp "Undefined action!";
3360 $action = "unknown action";
3362 return (action => $action,
3363 hash_slice(%param,keys %append_action_options),
3367 =head2 __begin_control
3369 my %info = __begin_control(%param,
3371 command=>'unarchive');
3372 my ($debug,$transcript) = @info{qw(debug transcript)};
3373 my @data = @{$info{data}};
3374 my @bugs = @{$info{bugs}};
3377 Starts the process of modifying a bug; handles all of the generic
3378 things that almost every control request needs
3380 Returns a hash containing
3384 =item new_locks -- number of new locks taken out by this call
3386 =item debug -- the debug file handle
3388 =item transcript -- the transcript file handle
3390 =item data -- an arrayref containing the data of the bugs
3391 corresponding to this request
3393 =item bugs -- an arrayref containing the bug numbers of the bugs
3394 corresponding to this request
3402 sub __begin_control {
3403 my %param = validate_with(params => \@_,
3404 spec => {bug => {type => SCALAR,
3407 archived => {type => BOOLEAN,
3410 command => {type => SCALAR,
3418 my ($debug,$transcript) = __handle_debug_transcript(@_);
3419 print {$debug} "$param{bug} considering\n";
3420 $lockhash = $param{locks} if exists $param{locks};
3422 my $old_die = $SIG{__DIE__};
3423 $SIG{__DIE__} = *sig_die{CODE};
3425 ($new_locks, @data) =
3426 lock_read_all_merged_bugs(bug => $param{bug},
3427 $param{archived}?(location => 'archive'):(),
3428 exists $param{locks} ? (locks => $param{locks}):(),
3430 $locks += $new_locks;
3432 die "Unable to read any bugs successfully.";
3434 if (not $param{archived}) {
3435 for my $data (@data) {
3436 if ($data->{archived}) {
3437 die "Not altering archived bugs; see unarchive.";
3441 if (not __check_limit(data => \@data,
3442 exists $param{limit}?(limit => $param{limit}):(),
3443 transcript => $transcript,
3445 die "limit failed for bugs: ".join(', ',map {$_->{bug_num}} @data);
3448 __handle_affected_packages(%param,data => \@data);
3449 print {$transcript} __bug_info(@data) if $param{show_bug_info} and not __internal_request(1);
3450 print {$debug} "$param{bug} read $locks locks\n";
3451 if (not @data or not defined $data[0]) {
3452 print {$transcript} "No bug found for $param{bug}\n";
3453 die "No bug found for $param{bug}";
3456 add_recipients(data => \@data,
3457 recipients => $param{recipients},
3458 (exists $param{command}?(actions_taken => {$param{command} => 1}):()),
3460 (__internal_request()?(transcript => $transcript):()),
3463 print {$debug} "$param{bug} read done\n";
3464 my @bugs = map {(defined $_ and exists $_->{bug_num} and defined $_->{bug_num})?$_->{bug_num}:()} @data;
3465 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
3466 return (data => \@data,
3468 old_die => $old_die,
3469 new_locks => $new_locks,
3471 transcript => $transcript,
3473 exists $param{locks}?(locks => $param{locks}):(),
3477 =head2 __end_control
3479 __end_control(%info);
3481 Handles tearing down from a control request
3487 if (exists $info{new_locks} and $info{new_locks} > 0) {
3488 print {$info{debug}} "For bug $info{param}{bug} unlocking $locks locks\n";
3489 for (1..$info{new_locks}) {
3490 unfilelock(exists $info{locks}?$info{locks}:());
3494 $SIG{__DIE__} = $info{old_die};
3495 if (exists $info{param}{affected_bugs}) {
3496 @{$info{param}{affected_bugs}}{@{$info{bugs}}} = (1) x @{$info{bugs}};
3498 add_recipients(recipients => $info{param}{recipients},
3499 (exists $info{param}{command}?(actions_taken => {$info{param}{command} , 1}):()),
3500 data => $info{data},
3501 debug => $info{debug},
3502 transcript => $info{transcript},
3504 __handle_affected_packages(%{$info{param}},data=>$info{data});
3508 =head2 __check_limit
3510 __check_limit(data => \@data, limit => $param{limit});
3513 Checks to make sure that bugs match any limits; each entry of @data
3514 much satisfy the limit.
3516 Returns true if there are no entries in data, or there are no keys in
3517 limit; returns false (0) if there are any entries which do not match.
3519 The limit hashref elements can contain an arrayref of scalars to
3520 match; regexes are also acccepted. At least one of the entries in each
3521 element needs to match the corresponding field in all data for the
3528 my %param = validate_with(params => \@_,
3529 spec => {data => {type => ARRAYREF|SCALAR,
3531 limit => {type => HASHREF|UNDEF,
3533 transcript => {type => SCALARREF|HANDLE,
3538 my @data = make_list($param{data});
3540 not defined $param{limit} or
3541 not keys %{$param{limit}}) {
3544 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
3545 my $going_to_fail = 0;
3546 for my $data (@data) {
3547 $data = split_status_fields(get_bug_status(bug => $data->{bug_num},
3548 status => dclone($data),
3550 for my $field (keys %{$param{limit}}) {
3551 next unless exists $param{limit}{$field};
3553 my @data_fields = make_list($data->{$field});
3554 LIMIT: for my $limit (make_list($param{limit}{$field})) {
3555 if (not ref $limit) {
3556 for my $data_field (@data_fields) {
3557 if ($data_field eq $limit) {
3563 elsif (ref($limit) eq 'Regexp') {
3564 for my $data_field (@data_fields) {
3565 if ($data_field =~ $limit) {
3572 warn "Unknown type of reference: '".ref($limit)."' in key '$field'";
3577 print {$transcript} qq($field: ).join(', ',map{qq("$_")} make_list($data->{$field})).
3578 "' does not match at least one of ".
3579 join(', ',map {ref($_)?'(regex)':qq("$_")} make_list($param{limit}{$field}))."\n";
3583 return $going_to_fail?0:1;
3591 We override die to specially handle unlocking files in the cases where
3592 we are called via eval. [If we're not called via eval, it doesn't
3598 if ($^S) { # in eval
3600 for (1..$locks) { unfilelock(defined $lockhash?$lockhash:()); }
3607 # =head2 __message_body_template
3609 # message_body_template('mail/ack',{ref=>'foo'});
3611 # Creates a message body using a template
3615 sub __message_body_template{
3616 my ($template,$extra_var) = @_;
3618 my $hole_var = {'&bugurl' =>
3620 'http://'.$config{cgi_domain}.'/'.
3621 Debbugs::CGI::bug_url($_[0]);
3625 my $body = fill_in_template(template => $template,
3626 variables => {config => \%config,
3629 hole_var => $hole_var,
3631 return fill_in_template(template => 'mail/message_body',
3632 variables => {config => \%config,
3636 hole_var => $hole_var,
3640 sub __all_undef_or_equal {
3642 return 1 if @values == 1 or @values == 0;
3643 my $not_def = grep {not defined $_} @values;
3644 if ($not_def == @values) {
3647 if ($not_def > 0 and $not_def != @values) {
3650 my $first_val = shift @values;
3651 for my $val (@values) {
3652 if ($first_val ne $val) {