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 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 location -- Optional location; currently ignored but may be
53 supported in the future for updating archived bugs upon archival
55 =item message -- The original message which caused the action to be taken
57 =item append_log -- Whether or not to append information to the log.
61 B<append_log> (for most functions) is a special option. When set to
62 false, no appending to the log is done at all. When it is not present,
63 the above information is faked, and appended to the log file. When it
64 is true, the above options must be present, and their values are used.
73 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
74 use base qw(Exporter);
78 $DEBUG = 0 unless defined $DEBUG;
81 %EXPORT_TAGS = (archive => [qw(bug_archive bug_unarchive),
83 log => [qw(append_action_to_log),
87 Exporter::export_ok_tags(qw(archive log));
88 $EXPORT_TAGS{all} = [@EXPORT_OK];
91 use Debbugs::Config qw(:config);
92 use Debbugs::Common qw(:lock buglog :misc get_hashname);
93 use Debbugs::Status qw(bug_archiveable :read :hook writebug);
94 use Debbugs::CGI qw(html_escape);
95 use Debbugs::Log qw(:misc);
97 use Params::Validate qw(validate_with :types);
98 use File::Path qw(mkpath);
101 use Debbugs::Text qw(:templates);
103 use Debbugs::Mail qw(rfc822_date);
105 use POSIX qw(strftime);
107 # These are a set of options which are common to all of these functions
109 my %common_options = (debug => {type => SCALARREF,
112 transcript => {type => SCALARREF|HANDLE,
115 affected_bugs => {type => HASHREF,
118 recipients => {type => HASHREF,
124 my %append_action_options =
125 (action => {type => SCALAR,
128 requester => {type => SCALAR,
131 request_addr => {type => SCALAR,
134 location => {type => SCALAR,
137 message => {type => SCALAR|ARRAYREF,
140 append_log => {type => BOOLEAN,
142 depends => [qw(requester request_addr),
153 bug_archive(bug => $bug_num,
155 transcript => \$transcript,
160 transcript("Unable to archive $bug_num\n");
163 transcript($transcript);
166 This routine archives a bug
170 =item bug -- bug number
172 =item check_archiveable -- check wether a bug is archiveable before
173 archiving; defaults to 1
175 =item archive_unarchived -- whether to archive bugs which have not
176 previously been archived; defaults to 1. [Set to 0 when used from
179 =item ignore_time -- whether to ignore time constraints when archiving
180 a bug; defaults to 0.
187 my %param = validate_with(params => \@_,
188 spec => {bug => {type => SCALAR,
191 check_archiveable => {type => BOOLEAN,
194 archive_unarchived => {type => BOOLEAN,
197 ignore_time => {type => BOOLEAN,
201 %append_action_options,
205 local $SIG{__DIE__} = sub {
207 for (1..$locks) { unfilelock(); }
211 my $action = "$config{bug} archived.";
212 my ($debug,$transcript) = __handle_debug_transcript(%param);
213 if ($param{check_archiveable} and
214 not bug_archiveable(bug=>$param{bug},
215 ignore_time => $param{ignore_time},
217 print {$transcript} "Bug $param{bug} cannot be archived\n";
218 die "Bug $param{bug} cannot be archived";
220 print {$debug} "$param{bug} considering\n";
222 ($locks, $data) = lockreadbugmerge($param{bug});
223 print {$debug} "$param{bug} read $locks\n";
224 defined $data or die "No bug found for $param{bug}";
225 print {$debug} "$param{bug} read ok (done $data->{done})\n";
226 print {$debug} "$param{bug} read done\n";
228 if (not $param{archive_unarchived} and
229 not exists $data->{unarchived}
231 print {$transcript} "$param{bug} has not been archived previously\n";
232 die "$param{bug} has not been archived previously";
235 my @bugs = ($param{bug});
237 # @bugs{@bugs} = (1) x @bugs;
238 if (length($data->{mergedwith})) {
239 push(@bugs,split / /,$data->{mergedwith});
241 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
242 for my $bug (@bugs) {
244 print {$debug} "$param{bug} $bug check\n";
245 if ($bug != $param{bug}) {
246 print {$debug} "$param{bug} $bug reading\n";
247 $newdata = lockreadbug($bug) || die "huh $bug ?";
248 print {$debug} "$param{bug} $bug read ok\n";
253 print {$debug} "$param{bug} $bug read/not\n";
254 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
255 $newdata->{mergedwith} eq $expectmerge ||
256 die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
257 print {$debug} "$param{bug} $bug merge-ok\n";
258 if ($param{check_archiveable}) {
259 die "Bug $bug cannot be archived (but $param{bug} can?)"
260 unless bug_archiveable(bug=>$bug,
261 ignore_time => $param{ignore_time},
265 # If we get here, we can archive/remove this bug
266 print {$debug} "$param{bug} removing\n";
267 for my $bug (@bugs) {
268 #print "$param{bug} removing $bug\n" if $debug;
269 my $dir = get_hashname($bug);
270 # First indicate that this bug is being archived
271 append_action_to_log(bug => $bug,
273 __return_append_to_log_options(
278 if not exists $param{append_log} or $param{append_log};
279 my @files_to_remove = map {s#db-h/$dir/##; $_} glob("db-h/$dir/$bug.*");
280 if ($config{save_old_bugs}) {
281 mkpath("archive/$dir");
282 foreach my $file (@files_to_remove) {
283 link( "db-h/$dir/$file", "archive/$dir/$file" ) || copy( "db-h/$dir/$file", "archive/$dir/$file" );
286 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
288 unlink(map {"db-h/$dir/$_"} @files_to_remove);
289 print {$transcript} "deleted $bug (from $param{bug})\n";
291 bughook_archive(@bugs);
292 if (exists $param{bugs_affected}) {
293 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
295 print {$debug} "$param{bug} unlocking $locks\n";
297 for (1..$locks) { unfilelock(); }
299 print {$debug} "$param{bug} unlocking done\n";
306 bug_unarchive(bug => $bug_num,
308 transcript => \$transcript,
313 transcript("Unable to archive bug: $bug_num");
315 transcript($transcript);
317 This routine unarchives a bug
322 my %param = validate_with(params => \@_,
323 spec => {bug => {type => SCALAR,
327 %append_action_options,
330 my $action = "$config{bug} unarchived.";
331 my ($debug,$transcript) = __handle_debug_transcript(%param);
332 print {$debug} "$param{bug} considering\n";
333 my ($locks, $data) = lockreadbugmerge($param{bug},'archive');
334 print {$debug} "$param{bug} read $locks\n";
335 if (not defined $data) {
336 print {$transcript} "No bug found for $param{bug}\n";
337 die "No bug found for $param{bug}";
339 print {$debug} "$param{bug} read ok (done $data->{done})\n";
340 print {$debug} "$param{bug} read done\n";
341 my @bugs = ($param{bug});
343 # @bugs{@bugs} = (1) x @bugs;
344 if (length($data->{mergedwith})) {
345 push(@bugs,split / /,$data->{mergedwith});
347 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
348 for my $bug (@bugs) {
350 print {$debug} "$param{bug} $bug check\n";
351 if ($bug != $param{bug}) {
352 print {$debug} "$param{bug} $bug reading\n";
353 $newdata = lockreadbug($bug,'archive') or die "huh $bug ?";
354 print {$debug} "$param{bug} $bug read ok\n";
359 print {$debug} "$param{bug} $bug read/not\n";
360 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
361 if ($newdata->{mergedwith} ne $expectmerge ) {
362 print {$transcript} "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
363 die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
365 print {$debug} "$param{bug} $bug merge-ok\n";
367 # If we get here, we can archive/remove this bug
368 print {$debug} "$param{bug} removing\n";
370 for my $bug (@bugs) {
371 print {$debug} "$param{bug} removing $bug\n";
372 my $dir = get_hashname($bug);
373 my @files_to_copy = map {s#archive/$dir/##; $_} glob("archive/$dir/$bug.*");
374 mkpath("archive/$dir");
375 foreach my $file (@files_to_copy) {
377 link( "archive/$dir/$file", "db-h/$dir/$file" ) or
378 copy( "archive/$dir/$file", "db-h/$dir/$file" ) or
379 die "Unable to copy archive/$dir/$file to db-h/$dir/$file";
381 push @files_to_remove, map {"archive/$dir/$_"} @files_to_copy;
382 print {$transcript} "Unarchived $config{bug} $bug\n";
384 unlink(@files_to_remove) or die "Unable to unlink bugs";
385 # Indicate that this bug has been archived previously
386 for my $bug (@bugs) {
387 my $newdata = readbug($bug);
388 if (not defined $newdata) {
389 print {$transcript} "$config{bug} $bug disappeared!\n";
390 die "Bug $bug disappeared!";
392 $newdata->{unarchived} = time;
393 append_action_to_log(bug => $bug,
395 __return_append_to_log_options(
400 if not exists $param{append_log} or $param{append_log};
401 writebug($bug,$newdata);
403 print {$debug} "$param{bug} unlocking $locks\n";
405 for (1..$locks) { unfilelock(); };
407 if (exists $param{bugs_affected}) {
408 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
410 print {$debug} "$param{bug} unlocking done\n";
413 =head2 append_action_to_log
417 This should probably be moved to Debbugs::Log; have to think that out
422 sub append_action_to_log{
423 my %param = validate_with(params => \@_,
424 spec => {bug => {type => SCALAR,
427 action => {type => SCALAR,
429 requester => {type => SCALAR,
431 request_addr => {type => SCALAR,
433 location => {type => SCALAR,
436 message => {type => SCALAR|ARRAYREF,
438 get_lock => {type => BOOLEAN,
443 # Fix this to use $param{location}
444 my $log_location = buglog($param{bug});
445 die "Unable to find .log for $param{bug}"
446 if not defined $log_location;
447 if ($param{get_lock}) {
448 filelock("lock/$param{bug}");
450 my $log = IO::File->new(">>$log_location") or
451 die "Unable to open $log_location for appending: $!";
453 "<!-- time:".time." -->\n".
454 "<strong>".html_escape($param{action})."</strong>\n".
455 "Request was from <code>".html_escape($param{requester})."</code>\n".
456 "to <code>".html_escape($param{request_addr})."</code>. \n".
458 "\7\n",escape_log(make_list($param{message})),"\n\3\n"
459 or die "Unable to append to $log_location: $!";
460 close $log or die "Unable to close $log_location: $!";
461 if ($param{get_lock}) {
469 =head1 PRIVATE FUNCTIONS
471 =head2 __handle_debug_transcript
473 my ($debug,$transcript) = __handle_debug_transcript(%param);
475 Returns a debug and transcript filehandle
480 sub __handle_debug_transcript{
481 my %param = validate_with(params => \@_,
482 spec => {%common_options},
485 my $fake_scalar = '';
486 my $debug = globify_scalar(exists $param{debug}?$param{debug}:\$fake_scalar);
487 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:\$fake_scalar);
488 return ($debug,$transcript);
492 sub __return_append_to_log_options{
494 if (not exists $param{requester}) {
495 $param{requester} = $config{control_internal_requester};
497 if (not exists $param{request_addr}) {
498 $param{request_addr} = $config{control_internal_request_addr};
500 if (not exists $param{message}) {
501 $action = $param{action} if exists $param{action};
502 my $date = rfc822_date();
503 $param{message} = fill_in_template(template => 'mail/fake_control_message',
504 variables => {request_addr => $param{request_addr},
505 requester => $param{requester},
511 return (action => $action,