block => [qw(set_blocks)],
merge => [qw(set_merged)],
tag => [qw(set_tag)],
+ clone => [qw(clone_bug)],
archive => [qw(bug_archive bug_unarchive),
],
log => [qw(append_action_to_log),
use Debbugs::Config qw(:config);
use Debbugs::Common qw(:lock buglog :misc get_hashname sort_versions);
-use Debbugs::Status qw(bug_archiveable :read :hook writebug splitpackages split_status_fields get_bug_status);
+use Debbugs::Status qw(bug_archiveable :read :hook writebug new_bug splitpackages split_status_fields get_bug_status);
use Debbugs::CGI qw(html_escape);
use Debbugs::Log qw(:misc :write);
use Debbugs::Recipients qw(:add);
use Data::Dumper qw();
use Params::Validate qw(validate_with :types);
use File::Path qw(mkpath);
+use File::Copy qw(copy);
use IO::File;
use Debbugs::Text qw(:templates);
push @changed, 'removed blocking bug(s) of '.$data->{bug_num}.': '.english_join([keys %removed_blockers]) if keys %removed_blockers;
$action = ucfirst(join ('; ',@changed)) if @changed;
if (not @changed) {
- print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to alter blocking bugs of bug #$data->{bug_num} to the same blocks previously set\n";
next;
}
$data->{blockedby} = join(' ',keys %blockers);
push @changed, 'removed tag(s) '.english_join([keys %tag_removed]) if keys %tag_removed;
$action = ucfirst(join ('; ',@changed)) if @changed;
if (not @changed) {
- print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to alter tags of bug #$data->{bug_num} to the same tags previously set\n";
next;
}
$action .= '.';
$warn_fixed = 0;
}
}
+ $action = "Bug reopened";
+ for my $data (@data) {
+ my $old_data = dclone($data);
+ $data->{done} = '';
+ append_action_to_log(bug => $data->{bug_num},
+ command => 'done',
+ new_data => $data,
+ old_data => $old_data,
+ get_lock => 0,
+ __return_append_to_log_options(
+ %param,
+ action => $action,
+ ),
+ )
+ if not exists $param{append_log} or $param{append_log};
+ writebug($data->{bug_num},$data);
+ }
+ print {$transcript} "$action\n";
__end_control(%info);
if (exists $param{submitter}) {
set_submitter(bug => $param{bug},
my %submitter_notified;
my $requester_notified = 0;
my $orig_report_set = 0;
+ for my $data (@data) {
+ if (exists $data->{done} and
+ defined $data->{done} and
+ length $data->{done}) {
+ print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
+ __end_control(%info);
+ return;
+ }
+ }
for my $data (@data) {
my $old_data = dclone($data);
my $hash = get_hashname($data->{bug_num});
- my $report_fh = IO::File->new("db-h/$hash/$data->{bug_num}.report",'r') or
- die "Unable to open original report db-h/$hash/$data->{bug_num}.report for reading: $!";
+ my $report_fh = IO::File->new("$config{spool_dir}/db-h/$hash/$data->{bug_num}.report",'r') or
+ die "Unable to open original report $config{spool_dir}/db-h/$hash/$data->{bug_num}.report for reading: $!";
my $orig_report;
{
local $/;
$orig_report_set = 1;
}
- if (exists $data->{done} and
- defined $data->{done} and
- length $data->{done}) {
- print {$transcript} "Bug $data->{bug_num} is already marked as done; not doing anything.\n";
- __end_control(%info);
- return;
- }
$action = "Marked $config{bug} as done";
# set done to the requester
)
if not exists $param{append_log} or $param{append_log};
writebug($data->{bug_num},$data);
+ print {$transcript} "$action\n";
# get the original report
if ($param{notify_submitter}) {
my $submitter_message;
);
}
}
+ __end_control(%info);
if (exists $param{fixed}) {
set_fixed(fixed => $param{fixed},
bug => $param{bug},
(not defined $data->{originator} or not length $data->{originator})) or
(defined $param{submitter} and defined $data->{originator} and
$param{submitter} eq $data->{originator})) {
- print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to change the submitter of bug#$data->{bug_num} to the same value\n";
next;
}
else {
if (__all_undef_or_equal($param{forwarded},$data->{forwarded}) or
(not defined $param{forwarded} and
defined $data->{forwarded} and not length $data->{forwarded})) {
- print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to change the forwarded-to-address of bug#$data->{bug_num} to the same value\n";
next;
}
else {
print {$debug} "Going to change bug title\n";
if (defined $data->{subject} and length($data->{subject}) and
$data->{subject} eq $param{title}) {
- print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to change the title of bug#$data->{bug_num} to the same title\n";
next;
}
else {
print {$debug} "Going to change assigned package\n";
if (defined $data->{package} and length($data->{package}) and
$data->{package} eq $new_package) {
- print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to reassign bug #$data->{bug_num} to the same package\n";
next;
}
else {
if (not @svers) {
@svers = $version;
}
+ else {
+ if (exists $found_versions{$version}) {
+ delete $found_versions{$version};
+ $found_removed{$version} = 1;
+ }
+ }
for my $sver (@svers) {
if (not exists $found_versions{$sver}) {
$found_versions{$sver} = 1;
$action .= " and reopened"
}
if (not $reopened and not @changed) {
- print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to alter found versions of bug #$data->{bug_num} to the same values previously set\n";
next;
}
$action .= '.';
if (not @svers) {
@svers = $version;
}
+ else {
+ if (exists $fixed_versions{$version}) {
+ $fixed_removed{$version} = 1;
+ delete $fixed_versions{$version};
+ }
+ }
for my $sver (@svers) {
if (not exists $fixed_versions{$sver}) {
$fixed_versions{$sver} = 1;
$action .= " and reopened"
}
if (not $reopened and not @changed) {
- print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to alter fixed versions of bug #$data->{bug_num} to the same values previously set\n";
next;
}
$action .= '.';
# figure out the problems
print {$transcript} "Unable to merge bugs because:\n";
for my $change (@{$disallowed_changes}) {
- print {$transcript} "$change->{field} of #$change->{bug} is '$change->{orig_value}' not '$change->{value}'\n";
+ print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
}
if ($attempts > 0) {
croak "Some bugs were altered while attempting to merge";
croak "Did not alter merged bugs";
}
}
- my ($change_bug) = keys %{$changes};
- $bug_changed{$change_bug}++;
- print {$transcript} __bug_info($data{$change_bug}) if
- $param{show_bug_info} and not __internal_request(1);
- $bug_info_shown{$change_bug} = 1;
- __allow_relocking($param{locks},[keys %data]);
- for my $change (@{$changes->{$change_bug}}) {
- if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
- my %target_blockedby;
- @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
- my %unhandled_targets = %target_blockedby;
- my @blocks_to_remove;
- for my $key (split / /,$change->{orig_value}) {
- delete $unhandled_targets{$key};
- next if exists $target_blockedby{$key};
- set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
- block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
- remove => 1,
- hash_slice(%param,
- keys %common_options,
- keys %append_action_options),
- );
+ my @bugs_to_change = keys %{$changes};
+ for my $change_bug (@bugs_to_change) {
+ next unless exists $changes->{$change_bug};
+ $bug_changed{$change_bug}++;
+ print {$transcript} __bug_info($data{$change_bug}) if
+ $param{show_bug_info} and not __internal_request(1);
+ $bug_info_shown{$change_bug} = 1;
+ __allow_relocking($param{locks},[keys %data]);
+ for my $change (@{$changes->{$change_bug}}) {
+ if ($change->{field} eq 'blockedby' or $change->{field} eq 'blocks') {
+ my %target_blockedby;
+ @target_blockedby{@{$change->{func_value}}} = (1) x @{$change->{func_value}};
+ my %unhandled_targets = %target_blockedby;
+ my @blocks_to_remove;
+ for my $key (split / /,$change->{orig_value}) {
+ delete $unhandled_targets{$key};
+ next if exists $target_blockedby{$key};
+ set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
+ block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
+ remove => 1,
+ hash_slice(%param,
+ keys %common_options,
+ keys %append_action_options),
+ );
+ }
+ for my $key (keys %unhandled_targets) {
+ set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
+ block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
+ add => 1,
+ hash_slice(%param,
+ keys %common_options,
+ keys %append_action_options),
+ );
+ }
}
- for my $key (keys %unhandled_targets) {
- set_blocks(bug => $change->{field} eq 'blocks' ? $key : $change->{bug},
- block => $change->{field} eq 'blocks' ? $change->{bug} : $key,
- add => 1,
- hash_slice(%param,
- keys %common_options,
- keys %append_action_options),
- );
+ else {
+ $change->{function}->(bug => $change->{bug},
+ $change->{key}, $change->{func_value},
+ exists $change->{options}?@{$change->{options}}:(),
+ hash_slice(%param,
+ keys %common_options,
+ keys %append_action_options),
+ );
}
}
- else {
- $change->{function}->(bug => $change->{bug},
- $change->{key}, $change->{func_value},
- exists $change->{options}?@{$change->{options}}:(),
- hash_slice(%param,
- keys %common_options,
- keys %append_action_options),
- );
- }
+ __disallow_relocking($param{locks});
+ my ($data,$n_locks) =
+ __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
+ data => \@data,
+ locks => $param{locks},
+ debug => $debug,
+ reload_all => 1,
+ );
+ $new_locks += $n_locks;
+ $locks += $n_locks;
+ %data = %{$data};
+ @data = values %data;
+ ($merge_status,$bugs_to_merge) =
+ __calculate_merge_status(\@data,\%data,$param{bug},$merge_status);
+ ($disallowed_changes,$changes) =
+ __calculate_merge_changes(\@data,$merge_status,\%param);
+ $attempts = max(values %bug_changed);
}
- __disallow_relocking($param{locks});
- my ($data,$n_locks) =
- __lock_and_load_merged_bugs(bugs_to_load => [keys %merging],
- data => \@data,
- locks => $param{locks},
- debug => $debug,
- reload_all => 1,
- );
- $new_locks += $n_locks;
- $locks += $n_locks;
- %data = %{$data};
- @data = values %data;
- ($merge_status,$bugs_to_merge) =
- __calculate_merge_status(\@data,\%data,$param{bug});
- ($disallowed_changes,$changes) =
- __calculate_merge_changes(\@data,$merge_status,\%param);
- $attempts = max(values %bug_changed);
}
if ($param{show_bug_info} and not __internal_request(1)) {
for my $data (sort {$a->{bug_num} <=> $b->{bug_num}} @data) {
}
}
if (keys %{$changes} or @{$disallowed_changes}) {
- print {$transcript} "Unable to modify bugs so that they could be merged\n";
+ print {$transcript} "After four attempts, the following changes were unable to be made:\n";
for (1..$new_locks) {
unfilelock($param{locks});
$locks--;
}
__end_control(%info);
+ for my $change (values %{$changes}, @{$disallowed_changes}) {
+ print {$transcript} "$change->{field} of #$change->{bug} is '$change->{text_orig_value}' not '$change->{text_value}'\n";
+ }
+ die "Unable to modify bugs so they could be merged";
return;
}
sub __allow_relocking{
my ($locks,$bugs) = @_;
- for my $bug (@{$bugs}) {
- my @lockfiles = grep {m{/\Q$bug\E$}} keys %{$locks->{locks}};
+ my @locks = (@{$bugs},'merge');
+ for my $lock (@locks) {
+ my @lockfiles = grep {m{/\Q$lock\E$}} keys %{$locks->{locks}};
next unless @lockfiles;
$locks->{relockable}{$lockfiles[0]} = 0;
}
sub __calculate_merge_status{
- my ($data_a,$data_h,$master_bug,$merge) = @_;
- my %merge_status;
+ my ($data_a,$data_h,$master_bug,$merge_status) = @_;
+ my %merge_status = %{$merge_status // {}};
my %merged_bugs;
my $bugs_to_merge = 0;
for my $data (@{$data_a}) {
$merge_status{$_} = $data->{$_}
}
}
- if (not $merge) {
- next unless $data->{bug_num} == $master_bug;
- }
+ if (defined $merge_status) {
+ next unless $data->{bug_num} == $master_bug;
+ }
$merge_status{tag} = {} if not exists $merge_status{tag};
for my $tag (split /\s+/, $data->{keywords}) {
$merge_status{tag}{$tag} = 1;
},
fixed_versions => {func => \&set_fixed,
key => 'fixed',
+ modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
allowed => 1,
},
found_versions => {func => \&set_found,
key => 'found',
+ modify_value => sub {(defined $_[0] and ref($_[0]) eq 'HASH')?[sort keys %{$_[0]}]:$_[0]},
allowed => 1,
},
);
next if join(' ', sort @{$data->{$field}}) eq
join(' ',sort keys %{$merge_status->{$field}});
}
+ elsif ($field eq 'done') {
+ # for done, we only care if the bug is done or not
+ # done, not the value it's set to.
+ if (defined $merge_status->{$field} and length $merge_status->{$field} and
+ defined $data->{$field} and length $data->{$field}) {
+ next;
+ }
+ elsif ((not defined $merge_status->{$field} or not length $merge_status->{$field}) and
+ (not defined $data->{$field} or not length $data->{$field})
+ ) {
+ next;
+ }
+ }
elsif ($merge_status->{$field} eq $data->{$field}) {
next;
}
function => $force_functions{$field}{func},
key => $force_functions{$field}{key},
options => $force_functions{$field}{options},
- allowed => exists $force_functions{$field}{allowed} ? 0 : $force_functions{$field}{allowed},
+ allowed => exists $force_functions{$field}{allowed} ? $force_functions{$field}{allowed} : 0,
};
- if ($param->{force}) {
- if ($field ne 'package') {
+ $change->{text_value} = ref($change->{func_value}) eq 'ARRAY'?join(' ',@{$change->{func_value}}):$change->{func_value};
+ $change->{text_orig_value} = ref($change->{orig_value}) eq 'ARRAY'?join(' ',@{$change->{orig_value}}):$change->{orig_value};
+ if ($param->{force} or $change->{allowed}) {
+ if ($field ne 'package' or $change->{allowed}) {
push @{$changes{$data->{bug_num}}},$change;
next;
}
}
}
if (not length $action) {
- print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to set affects of bug $data->{bug_num} to the same value previously set\n";
next;
}
my $old_data = dclone($data);
if (((not defined $summary or not length $summary) and
(not defined $data->{summary} or not length $data->{summary})) or
$summary eq $data->{summary}) {
- print {$transcript} "Ignoring request to change the summary of bug $param{bug} to the same value\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to change the summary of bug $param{bug} to the same value\n";
next;
}
if (length $summary) {
+=head2 clone_bug
+
+ eval {
+ clone_bug(bug => $ref,
+ transcript => $transcript,
+ ($dl > 0 ? (debug => $transcript):()),
+ requester => $header{from},
+ request_addr => $controlrequestaddr,
+ message => \@log,
+ affected_packages => \%affected_packages,
+ recipients => \%recipients,
+ );
+ };
+ if ($@) {
+ $errors++;
+ print {$transcript} "Failed to clone bug $ref bar: $@";
+ }
+
+Clones the given bug.
+
+We currently don't support cloning merged bugs, but this could be
+handled by internally unmerging, cloning, then remerging the bugs.
+
+=cut
+
+sub clone_bug {
+ my %param = validate_with(params => \@_,
+ spec => {bug => {type => SCALAR,
+ regex => qr/^\d+$/,
+ },
+ new_bugs => {type => ARRAYREF,
+ },
+ new_clones => {type => HASHREF,
+ default => {},
+ },
+ %common_options,
+ %append_action_options,
+ },
+ );
+ my %info =
+ __begin_control(%param,
+ command => 'clone'
+ );
+ my ($debug,$transcript) =
+ @info{qw(debug transcript)};
+ my @data = @{$info{data}};
+ my @bugs = @{$info{bugs}};
+
+ my $action = '';
+ for my $data (@data) {
+ if (length($data->{mergedwith})) {
+ die "Bug is marked as being merged with others. Use an existing clone.\n";
+ }
+ }
+ if (@data != 1) {
+ die "Not exactly one bug‽ This shouldn't happen.";
+ }
+ my $data = $data[0];
+ my %clones;
+ for my $newclone_id (@{$param{new_bugs}}) {
+ my $new_bug_num = new_bug(copy => $data->{bug_num});
+ $param{new_clones}{$newclone_id} = $new_bug_num;
+ $clones{$newclone_id} = $new_bug_num;
+ }
+ my @new_bugs = sort values %clones;
+ my @collapsed_ids;
+ for my $new_bug (@new_bugs) {
+ # no collapsed ids or the higher collapsed id is not one less
+ # than the next highest new bug
+ if (not @collapsed_ids or
+ $collapsed_ids[-1][1]+1 != $new_bug) {
+ push @collapsed_ids,[$new_bug,$new_bug];
+ }
+ else {
+ $collapsed_ids[-1][1] = $new_bug;
+ }
+ }
+ my @collapsed;
+ for my $ci (@collapsed_ids) {
+ if ($ci->[0] == $ci->[1]) {
+ push @collapsed,$ci->[0];
+ }
+ else {
+ push @collapsed,$ci->[0].'-'.$ci->[1]
+ }
+ }
+ my $collapsed_str = english_join(\@collapsed);
+ $action = "Bug $data->{bug_num} cloned as bug".(@new_bugs > 1?'s':'').' '.$collapsed_str;
+ for my $new_bug (@new_bugs) {
+ append_action_to_log(bug => $new_bug,
+ get_lock => 1,
+ __return_append_to_log_options(
+ %param,
+ action => $action,
+ ),
+ )
+ if not exists $param{append_log} or $param{append_log};
+ }
+ append_action_to_log(bug => $data->{bug_num},
+ get_lock => 0,
+ __return_append_to_log_options(
+ %param,
+ action => $action,
+ ),
+ )
+ if not exists $param{append_log} or $param{append_log};
+ writebug($data->{bug_num},$data);
+ print {$transcript} "$action\n";
+ __end_control(%info);
+ # bugs that this bug is blocking are also blocked by the new clone(s)
+ for my $bug (split ' ', $data->{blocks}) {
+ for my $new_bug (@new_bugs) {
+ set_blocks(bug => $new_bug,
+ block => $bug,
+ hash_slice(%param,
+ keys %common_options,
+ keys %append_action_options),
+ );
+ }
+ }
+ # bugs that this bug is blocked by are also blocking the new clone(s)
+ for my $bug (split ' ', $data->{blockedby}) {
+ for my $new_bug (@new_bugs) {
+ set_blocks(bug => $bug,
+ block => $new_bug,
+ hash_slice(%param,
+ keys %common_options,
+ keys %append_action_options),
+ );
+ }
+ }
+}
print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
if (not defined $param{owner} or not length $param{owner}) {
if (not defined $data->{owner} or not length $data->{owner}) {
- print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n"
- unless __internal_request();
+ print {$transcript} "Ignoring request to unset the owner of bug #$data->{bug_num} which was not set\n";
next;
}
$param{owner} = '';
print {$transcript} "Bug $param{bug} cannot be archived\n";
die "Bug $param{bug} cannot be archived";
}
- print {$debug} "$param{bug} considering\n";
if (not $param{archive_unarchived} and
not exists $data[0]{unarchived}
) {
print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
}
unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
- print {$transcript} "deleted $bug (from $param{bug})\n";
+ print {$debug} "deleted $bug (from $param{bug})\n";
}
bughook_archive(@bugs);
__end_control(%info);
);
my $new_locks;
my ($debug,$transcript) = __handle_debug_transcript(@_);
- print {$debug} "$param{bug} considering\n";
+ print {$debug} "considering bug $param{bug} for ".(exists $param{command}?$param{command}:scalar caller())."\n";
+# print {$debug} Data::Dumper->Dump([[caller(1)],\%param],[qw(caller param)])."\n";
$lockhash = $param{locks} if exists $param{locks};
my @data = ();
my $old_die = $SIG{__DIE__};