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 make_list 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);
102 use POSIX qw(strftime);
104 # These are a set of options which are common to all of these functions
106 my %common_options = (debug => {type => SCALARREF,
109 transcript => {type => SCALARREF,
112 affected_bugs => {type => HASHREF,
118 my %append_action_options =
119 (action => {type => SCALAR,
122 requester => {type => SCALAR,
125 request_addr => {type => SCALAR,
128 location => {type => SCALAR,
131 message => {type => SCALAR|ARRAYREF,
134 append_log => {type => BOOLEAN,
136 depends => [qw(requester request_addr),
147 bug_archive(bug => $bug_num,
149 transcript => \$transcript,
154 transcript("Unable to archive $bug_num\n");
157 transcript($transcript);
160 This routine archives a bug
165 my %param = validate_with(params => \@_,
166 spec => {bug => {type => SCALAR,
169 check_archiveable => {type => BOOLEAN,
172 ignore_time => {type => BOOLEAN,
176 %append_action_options,
180 local $SIG{__DIE__} = sub {
182 for (1..$locks) { unfilelock(); }
186 my $action = "$config{bug} archived.";
187 my ($debug,$transcript) = __handle_debug_transcript(%param);
188 if ($param{check_archiveable} and
189 not bug_archiveable(bug=>$param{bug},
190 ignore_time => $param{ignore_time},
192 print {$transcript} "Bug $param{bug} cannot be archived\n";
193 die "Bug $param{bug} cannot be archived";
195 print {$debug} "$param{bug} considering\n";
197 ($locks, $data) = lockreadbugmerge($param{bug});
198 print {$debug} "$param{bug} read $locks\n";
199 defined $data or die "No bug found for $param{bug}";
200 print {$debug} "$param{bug} read ok (done $data->{done})\n";
201 print {$debug} "$param{bug} read done\n";
202 my @bugs = ($param{bug});
204 # @bugs{@bugs} = (1) x @bugs;
205 if (length($data->{mergedwith})) {
206 push(@bugs,split / /,$data->{mergedwith});
208 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
209 for my $bug (@bugs) {
211 print {$debug} "$param{bug} $bug check\n";
212 if ($bug != $param{bug}) {
213 print {$debug} "$param{bug} $bug reading\n";
214 $newdata = lockreadbug($bug) || die "huh $bug ?";
215 print {$debug} "$param{bug} $bug read ok\n";
220 print {$debug} "$param{bug} $bug read/not\n";
221 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
222 $newdata->{mergedwith} eq $expectmerge ||
223 die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")";
224 print {$debug} "$param{bug} $bug merge-ok\n";
225 if ($param{check_archiveable}) {
226 die "Bug $bug cannot be archived (but $param{bug} can?)"
227 unless bug_archiveable(bug=>$bug,
228 ignore_time => $param{ignore_time},
232 # If we get here, we can archive/remove this bug
233 print {$debug} "$param{bug} removing\n";
234 for my $bug (@bugs) {
235 #print "$param{bug} removing $bug\n" if $debug;
236 my $dir = get_hashname($bug);
237 # First indicate that this bug is being archived
238 append_action_to_log(bug => $bug,
240 __return_append_to_log_options(
241 (map {exists $param{$_}?($_,$param{$_}):()}
242 keys %append_action_options,
247 if not exists $param{append_log} or $param{append_log};
248 my @files_to_remove = map {s#db-h/$dir/##; $_} glob("db-h/$dir/$bug.*");
249 if ($config{save_old_bugs}) {
250 mkpath("archive/$dir");
251 foreach my $file (@files_to_remove) {
252 link( "db-h/$dir/$file", "archive/$dir/$file" ) || copy( "db-h/$dir/$file", "archive/$dir/$file" );
255 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
257 unlink(map {"db-h/$dir/$_"} @files_to_remove);
258 print {$transcript} "deleted $bug (from $param{bug})\n";
260 bughook_archive(@bugs);
261 if (exists $param{bugs_affected}) {
262 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
264 print {$debug} "$param{bug} unlocking $locks\n";
266 for (1..$locks) { unfilelock(); }
268 print {$debug} "$param{bug} unlocking done\n";
275 bug_unarchive(bug => $bug_num,
277 transcript => \$transcript,
282 transcript("Unable to archive bug: $bug_num");
284 transcript($transcript);
286 This routine unarchives a bug
291 my %param = validate_with(params => \@_,
292 spec => {bug => {type => SCALAR,
296 %append_action_options,
299 my $action = "$config{bug} unarchived.";
300 my ($debug,$transcript) = __handle_debug_transcript(%param);
301 print {$debug} "$param{bug} considering\n";
302 my ($locks, $data) = lockreadbugmerge($param{bug},'archive');
303 print {$debug} "$param{bug} read $locks\n";
304 if (not defined $data) {
305 print {$transcript} "No bug found for $param{bug}\n";
306 die "No bug found for $param{bug}";
308 print {$debug} "$param{bug} read ok (done $data->{done})\n";
309 print {$debug} "$param{bug} read done\n";
310 my @bugs = ($param{bug});
312 # @bugs{@bugs} = (1) x @bugs;
313 if (length($data->{mergedwith})) {
314 push(@bugs,split / /,$data->{mergedwith});
316 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
317 for my $bug (@bugs) {
319 print {$debug} "$param{bug} $bug check\n";
320 if ($bug != $param{bug}) {
321 print {$debug} "$param{bug} $bug reading\n";
322 $newdata = lockreadbug($bug,'archive') or die "huh $bug ?";
323 print {$debug} "$param{bug} $bug read ok\n";
328 print {$debug} "$param{bug} $bug read/not\n";
329 my $expectmerge= join(' ',grep($_ != $bug, sort { $a <=> $b } @bugs));
330 if ($newdata->{mergedwith} ne $expectmerge ) {
331 print {$transcript} "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
332 die "$param{bug} differs from $bug: ($newdata->{mergedwith}) vs. ($expectmerge) (@bugs)";
334 print {$debug} "$param{bug} $bug merge-ok\n";
336 # If we get here, we can archive/remove this bug
337 print {$debug} "$param{bug} removing\n";
339 for my $bug (@bugs) {
340 print {$debug} "$param{bug} removing $bug\n";
341 my $dir = get_hashname($bug);
342 my @files_to_copy = map {s#archive/$dir/##; $_} glob("archive/$dir/$bug.*");
343 mkpath("archive/$dir");
344 foreach my $file (@files_to_copy) {
346 link( "archive/$dir/$file", "db-h/$dir/$file" ) or
347 copy( "archive/$dir/$file", "db-h/$dir/$file" ) or
348 die "Unable to copy archive/$dir/$file to db-h/$dir/$file";
350 push @files_to_remove, map {"archive/$dir/$_"} @files_to_copy;
351 print {$transcript} "Unarchived $config{bug} $bug\n";
353 unlink(@files_to_remove) or die "Unable to unlink bugs";
354 # Indicate that this bug has been archived previously
355 for my $bug (@bugs) {
356 my $newdata = readbug($bug);
357 if (not defined $newdata) {
358 print {$transcript} "$config{bug} $bug disappeared!\n";
359 die "Bug $bug disappeared!";
361 $newdata->{unarchived} = time;
362 append_action_to_log(bug => $bug,
364 __return_append_to_log_options(
365 (map {exists $param{$_}?($_,$param{$_}):()}
366 keys %append_action_options,
371 if not exists $param{append_log} or $param{append_log};
372 writebug($bug,$newdata);
374 print {$debug} "$param{bug} unlocking $locks\n";
376 for (1..$locks) { unfilelock(); };
378 if (exists $param{bugs_affected}) {
379 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
381 print {$debug} "$param{bug} unlocking done\n";
384 =head2 append_action_to_log
388 This should probably be moved to Debbugs::Log; have to think that out
393 sub append_action_to_log{
394 my %param = validate_with(params => \@_,
395 spec => {bug => {type => SCALAR,
398 action => {type => SCALAR,
400 requester => {type => SCALAR,
402 request_addr => {type => SCALAR,
404 location => {type => SCALAR,
407 message => {type => SCALAR|ARRAYREF,
409 get_lock => {type => BOOLEAN,
414 # Fix this to use $param{location}
415 my $log_location = buglog($param{bug});
416 die "Unable to find .log for $param{bug}"
417 if not defined $log_location;
418 if ($param{get_lock}) {
419 filelock("lock/$param{bug}");
421 my $log = IO::File->new(">>$log_location") or
422 die "Unable to open $log_location for appending: $!";
424 "<!-- time:".time." -->\n".
425 "<strong>".html_escape($param{action})."</strong>\n".
426 "Request was from <code>".html_escape($param{requester})."</code>\n".
427 "to <code>".html_escape($param{request_addr})."</code>. \n".
429 "\7\n",escape_log(make_list($param{message})),"\n\3\n"
430 or die "Unable to append to $log_location: $!";
431 close $log or die "Unable to close $log_location: $!";
432 if ($param{get_lock}) {
440 =head1 PRIVATE FUNCTIONS
442 =head2 __handle_debug_transcript
444 my ($debug,$transcript) = __handle_debug_transcript(%param);
446 Returns a debug and transcript IO::Scalar filehandle
451 sub __handle_debug_transcript{
452 my %param = validate_with(params => \@_,
453 spec => {%common_options},
457 my $debug = IO::Scalar->new(exists $param{debug}?$param{debug}:\$fake_scalar);
458 my $transcript = IO::Scalar->new(exists $param{transcript}?$param{transcript}:\$fake_scalar);
459 return ($debug,$transcript);
463 sub __return_append_to_log_options{
465 my $action = 'Unknown action';
466 if (not exists $param{requester}) {
467 $param{requester} = $config{control_internal_requester};
469 if (not exists $param{request_addr}) {
470 $param{request_addr} = $config{control_internal_request_addr};
472 if (not exists $param{message}) {
473 $action = $param{action} if exists $param{action};
474 my $date = strftime "%a, %d %h %Y %T +0000", gmtime;
475 $param{message} = <<END;
476 Received: (at fakecontrol) by fakecontrolmessage;
477 To: $param{request_addr}
478 From: $param{requester}
479 Subject: Internal Control
482 User-Agent: Fakemail v42.6.9
485 # A log time ago, in a galaxy far, far away
486 # something happened.
488 # Magically this resulted in the following
489 # action being taken, but this fake control
490 # message doesn't tell you why it happened
495 # This fakemail brought to you by your local debbugs
499 return (action => $action,