use constant NAME_FIELD => 'alias';
use constant LIST_ORDER => ID_FIELD;
+# Set up Dependencies and Regressions
+use constant BUG_RELATIONS => (
+ [dependencies => qw(dependson blocked)],
+ [regressions => qw(regressed_by regresses)],
+);
+
# Bugs have their own auditing table, bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
last_change_time => 'delta_ts',
comment_count => 'longdescs.count',
platform => 'rep_platform',
+ regressions => 'regresses',
severity => 'bug_severity',
status => 'bug_status',
summary => 'short_desc',
next unless $bug;
delete $bug->{depends_on_obj};
delete $bug->{blocks_obj};
+ delete $bug->{regressed_by_obj};
+ delete $bug->{regresses_obj};
}
%CLEANUP = ();
}
}
}
- unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
+ state $relation_field = {
+ dependson => 1,
+ blocked => 1,
+ regressed_by => 1,
+ regresses => 1,
+ dup_id => 1
+ };
+ unless ($field && $relation_field->{$field}) {
$self->check_is_visible;
}
return $self;
# to the more complex method.
my @all_dep_ids;
foreach my $bug (@$bugs) {
- push(@all_dep_ids, @{$bug->blocked}, @{$bug->dependson});
+ push(@all_dep_ids,
+ map { @{$bug->$_ || []} } qw(dependson blocked regressed_by regresses));
}
@all_dep_ids = uniq @all_dep_ids;
# insert_create_data.
my $cc_ids = delete $params->{cc};
my $groups = delete $params->{groups};
- my $depends_on = delete $params->{dependson};
- my $blocked = delete $params->{blocked};
my $keywords = delete $params->{keywords};
my $creation_comment = delete $params->{comment};
my $see_also = delete $params->{see_also};
my $comment_tags = delete $params->{comment_tags};
+ my %relation_values = map { $_ => delete $params->{$_} }
+ qw(dependson blocked regressed_by regresses);
# We don't want the bug to appear in the system until it's correctly
# protected by groups.
$sth_keyword->execute($bug->bug_id, $keyword_id);
}
- # Set up dependencies (blocked/dependson)
- my $sth_deps = $dbh->prepare(
- 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
+ foreach my $rel (BUG_RELATIONS) {
+ my ($table, $field1, $field2) = @$rel;
+ my $sth = $dbh->prepare("INSERT INTO $table ($field1, $field2) VALUES (?, ?)");
- foreach my $depends_on_id (@$depends_on) {
- $sth_deps->execute($bug->bug_id, $depends_on_id);
+ foreach my $id (@{$relation_values{$field1}}) {
+ $sth->execute($id, $bug->bug_id);
- # Log the reverse action on the other bug.
- LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
- $bug->{reporter_id}, $timestamp);
- _update_delta_ts($depends_on_id, $timestamp);
- }
- foreach my $blocked_id (@$blocked) {
- $sth_deps->execute($blocked_id, $bug->bug_id);
+ # Log the reverse action on the other bug.
+ LogActivityEntry($id, $field2, '', $bug->bug_id, $bug->{reporter_id},
+ $timestamp);
+ _update_delta_ts($id, $timestamp);
+ }
- # Log the reverse action on the other bug.
- LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
- $bug->{reporter_id}, $timestamp);
- _update_delta_ts($blocked_id, $timestamp);
+ foreach my $id (@{$relation_values{$field2}}) {
+ $sth->execute($bug->bug_id, $id);
+
+ # Log the reverse action on the other bug.
+ LogActivityEntry($id, $field1, '', $bug->bug_id, $bug->{reporter_id},
+ $timestamp);
+ _update_delta_ts($id, $timestamp);
+ }
}
# Insert the values into the multiselect value tables
$class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
$params->{qa_contact}, $product);
- ($params->{dependson}, $params->{blocked})
- = $class->_check_dependencies($params->{dependson}, $params->{blocked},
- $product);
+ my %dep_lists = $class->_check_relationship($product, 'dependencies',
+ map { $_ => $params->{$_} } qw(dependson blocked));
+ my %reg_lists = $class->_check_relationship($product, 'regressions',
+ map { $_ => $params->{$_} } qw(regressed_by regresses));
+
+ $params->{dependson} = $dep_lists{'dependson'};
+ $params->{blocked} = $dep_lists{'blocked'};
+ $params->{regressed_by} = $reg_lists{'regressed_by'};
+ $params->{regresses} = $reg_lists{'regresses'};
# You can't set these fields on bug creation (or sometimes ever).
delete $params->{resolution};
$changes->{keywords} = [$removed_names, $added_names];
}
- # Dependencies
- foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) {
- my ($type, $other) = @$pair;
- my $old = $old_bug->$type;
- my $new = $self->$type;
+ # Dependencies and Regressions
+ foreach my $rel (BUG_RELATIONS) {
+ my ($table, $field1, $field2) = @$rel;
- my ($removed, $added) = diff_arrays($old, $new);
- foreach my $removed_id (@$removed) {
- $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?",
- undef, $removed_id, $self->id);
+ foreach my $pair ([$field1, $field2], [$field2, $field1]) {
+ my ($field, $other_field) = @$pair;
+ my ($removed, $added) = diff_arrays($old_bug->$field, $self->$field);
- # Add an activity entry for the other bug.
- LogActivityEntry($removed_id, $other, $self->id, '', $user->id, $delta_ts);
+ foreach my $id (@$removed) {
+ $dbh->do("DELETE FROM $table WHERE $field = ? AND $other_field = ?",
+ undef, $id, $self->id);
- # Update delta_ts on the other bug so that we trigger mid-airs.
- _update_delta_ts($removed_id, $delta_ts);
- }
- foreach my $added_id (@$added) {
- $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
- undef, $added_id, $self->id);
+ # Add an activity entry for the other bug.
+ LogActivityEntry($id, $other_field, $self->id, '', $user->id, $delta_ts);
+
+ # Update delta_ts on the other bug so that we trigger mid-airs.
+ _update_delta_ts($id, $delta_ts);
+ }
- # Add an activity entry for the other bug.
- LogActivityEntry($added_id, $other, '', $self->id, $user->id, $delta_ts);
+ foreach my $id (@$added) {
+ $dbh->do("INSERT INTO $table ($field, $other_field) VALUES (?, ?)",
+ undef, $id, $self->id);
- # Update delta_ts on the other bug so that we trigger mid-airs.
- _update_delta_ts($added_id, $delta_ts);
- }
+ # Add an activity entry for the other bug.
+ LogActivityEntry($id, $other_field, '', $self->id, $user->id, $delta_ts);
- if (scalar(@$removed) || scalar(@$added)) {
- $changes->{$type} = [join(', ', @$removed), join(', ', @$added)];
+ # Update delta_ts on the other bug so that we trigger mid-airs.
+ _update_delta_ts($id, $delta_ts);
+ }
+
+ if (@$removed || @$added) {
+ $changes->{$field} = [join(', ', @$removed), join(', ', @$added)];
+ }
}
}
$dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
$dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
undef, ($bug_id, $bug_id));
+ $dbh->do("DELETE FROM regressions WHERE regresses = ? OR regressed_by = ?",
+ undef, ($bug_id, $bug_id));
$dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
undef, ($bug_id, $bug_id));
$dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
}
}
- # To get a list of all changed dependencies, convert the "changes" arrays
- # into a long string, then collapse that string into unique numbers in
- # a hash.
+ # To get a list of all changed dependencies and regressoins, convert the
+ # "changes" arrays into a long string, then collapse that string into unique
+ # numbers in a hash.
my $all_changed_deps = join(', ', @{$changes->{'dependson'} || []});
$all_changed_deps
= join(', ', @{$changes->{'blocked'} || []}, $all_changed_deps);
+ $all_changed_deps
+ = join(', ', @{$changes->{'regressed_by'} || []}, $all_changed_deps);
+ $all_changed_deps
+ = join(', ', @{$changes->{'regresses'} || []}, $all_changed_deps);
my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps);
# When clearning one field (say, blocks) and filling in the other
return $date;
}
-# Takes two comma/space-separated strings and returns arrayrefs
-# of valid bug IDs.
-sub _check_dependencies {
- my ($invocant, $depends_on, $blocks, $product) = @_;
+# Take a hash containing two keys (dependson/blocked for dependencies or
+# regressed_by/regresses for regressions) and bug IDs as a comma/space-separated
+# string value for each, and return arrayrefs of valid bug IDs.
+sub _check_relationship {
+ my ($invocant, $product, $table, %deps_in) = @_;
if (!ref $invocant) {
return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id);
}
- my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
-
- foreach my $type (qw(dependson blocked)) {
+ foreach my $type (keys %deps_in) {
my @bug_ids
= ref($deps_in{$type})
? @{$deps_in{$type}}
- : split(/[\s,]+/, $deps_in{$type});
+ : split(/[\s,]+/, $deps_in{$type} || '');
# Eliminate nulls.
@bug_ids = grep {$_} @bug_ids;
# And finally, check for dependency loops.
my $bug_id = ref($invocant) ? $invocant->id : 0;
- my %deps
- = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id);
+ my %deps = validate_relationship($bug_id, $table, %deps_in);
- return ($deps{'dependson'}, $deps{'blocked'});
+ return %deps;
}
sub _check_dup_id {
bug_status resolution dup_id see_also
bug_file_loc status_whiteboard keywords
priority bug_severity target_milestone
- dependson blocked everconfirmed
- reporter assigned_to cc estimated_time
- remaining_time actual_time deadline),
+ dependson blocked regressed_by regresses
+ everconfirmed reporter assigned_to cc
+ estimated_time remaining_time actual_time
+ deadline),
# Conditional Fields
Bugzilla->params->{'useqacontact'} ? "qa_contact" : (),
# immediately after changing the product.
$self->_add_remove($params, 'groups');
- if (exists $params->{'dependson'} or exists $params->{'blocked'}) {
+ # Dependencies and Regressions
+ foreach my $rel (BUG_RELATIONS) {
+ my ($table, $field1, $field2) = @$rel;
+
+ next unless (exists $params->{$field1} || exists $params->{$field2});
+
my %set_deps;
- foreach my $name (qw(dependson blocked)) {
+ foreach my $name ($field1, $field2) {
my @dep_ids = @{$self->$name};
# If only one of the two fields was passed in, then we need to
$set_deps{$name} = \@dep_ids;
}
- $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'});
+ $self->set_relationship($table, %set_deps);
}
if (exists $params->{'keywords'}) {
}
sub set_deadline { $_[0]->set('deadline', $_[1]); }
-sub set_dependencies {
- my ($self, $dependson, $blocked) = @_;
- ($dependson, $blocked) = $self->_check_dependencies($dependson, $blocked);
+sub set_relationship {
+ my ($self, $table, %fields) = @_;
+ my ($key1, $key2) = keys %fields;
+ my %lists = $self->_check_relationship(undef, $table, %fields);
+ my ($list1, $list2) = ($lists{$key1}, $lists{$key2});
# These may already be detainted, but all setters are supposed to
# detaint their input if they've run a validator (just as though
# we had used Bugzilla::Object::set), so we do that here.
- detaint_natural($_) foreach (@$dependson, @$blocked);
- $self->{'dependson'} = $dependson;
- $self->{'blocked'} = $blocked;
- delete $self->{depends_on_obj};
- delete $self->{blocks_obj};
+ detaint_natural($_) foreach (@$list1, @$list2);
+ $self->{$key1} = $list1;
+ $self->{$key2} = $list2;
+ delete $self->{$key1 eq 'dependson' ? 'depends_on_obj' : "${key1}_obj"};
+ delete $self->{$key2 eq 'dependson' ? 'depends_on_obj' : "${key2}_obj"};
}
sub _clear_dup_id { $_[0]->{dup_id} = undef; }
my ($self) = @_;
return $self->{'blocked'} if exists $self->{'blocked'};
return [] if $self->{'error'};
- $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id);
+ $self->{'blocked'}
+ = list_relationship('dependencies', 'dependson', 'blocked', $self->bug_id);
return $self->{'blocked'};
}
my ($self) = @_;
return $self->{'dependson'} if exists $self->{'dependson'};
return [] if $self->{'error'};
- $self->{'dependson'} = EmitDependList("blocked", "dependson", $self->bug_id);
+ $self->{'dependson'}
+ = list_relationship('dependencies', 'blocked', 'dependson', $self->bug_id);
return $self->{'dependson'};
}
return $self->{'qa_contact_obj'};
}
+sub regressed_by {
+ my ($self) = @_;
+ return $self->{'regressed_by'} if exists $self->{'regressed_by'};
+ return [] if $self->{'error'};
+ $self->{'regressed_by'}
+ = list_relationship('regressions', 'regresses', 'regressed_by',
+ $self->bug_id);
+ return $self->{'regressed_by'};
+}
+
+sub regressed_by_obj {
+ my ($self) = @_;
+ $self->{'regressed_by_obj'} ||= $self->_bugs_in_order($self->regressed_by);
+ return $self->{'regressed_by_obj'};
+}
+
+sub regresses {
+ my ($self) = @_;
+ return $self->{'regresses'} if exists $self->{'regresses'};
+ return [] if $self->{'error'};
+ $self->{'regresses'}
+ = list_relationship('regressions', 'regressed_by', 'regresses',
+ $self->bug_id);
+ return $self->{'regresses'};
+}
+
+sub regresses_obj {
+ my ($self) = @_;
+ $self->{'regresses_obj'} ||= $self->_bugs_in_order($self->regresses);
+ return $self->{'regresses_obj'};
+}
+
sub reporter {
my ($self) = @_;
return $self->{'reporter'} if exists $self->{'reporter'};
return sort(@fields);
}
-# XXX - When Bug::update() will be implemented, we should make this routine
-# a private method.
-# Join with bug_status and bugs tables to show bugs with open statuses first,
-# and then the others
-sub EmitDependList {
- my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_;
- my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {};
+# Emit a list of dependencies or regressions. Join with bug_status and bugs
+# tables to show bugs with open statuses first, and then the others
+sub list_relationship {
+ my ($table, $my_field, $target_field, $bug_id, $exclude_resolved) = @_;
+ my $cache = Bugzilla->request_cache->{"bug_$table"} ||= {};
my $dbh = Bugzilla->dbh;
$exclude_resolved = $exclude_resolved ? 1 : 0;
$cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare(
"SELECT $target_field
- FROM dependencies
- INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id
+ FROM $table
+ INNER JOIN bugs ON $table.$target_field = bugs.bug_id
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
WHERE $my_field = ? $is_open_clause
ORDER BY is_open DESC, $target_field"
return $new_change if $current_change eq '';
# Buglists and see_also need the comma restored
- if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
+ if ($field =~ /^(?:dependson|blocked|regress(?:ed_by|es)|see_also)$/) {
if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
return $current_change . $new_change;
}
# Field Validation
#
-# Validate and return a hash of dependencies
-sub ValidateDependencies {
- my $fields = {};
+# Validate and return a hash of dependencies or regressions
+sub validate_relationship {
+ my ($bug_id, $table, %fields) = @_;
+ my ($key1, $key2) = keys %fields;
- # These can be arrayrefs or they can be strings.
- $fields->{'dependson'} = shift;
- $fields->{'blocked'} = shift;
- my $id = shift || 0;
-
- unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) {
- return;
- }
+ return unless (defined($fields{$key1}) || defined($fields{$key2}));
my $dbh = Bugzilla->dbh;
my %deps;
my %deptree;
- foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
+ foreach my $pair ([$key1, $key2], [$key2, $key1]) {
my ($me, $target) = @{$pair};
$deptree{$target} = [];
$deps{$target} = [];
- next unless $fields->{$target};
+ next unless $fields{$target};
my %seen;
my $target_array
- = ref($fields->{$target})
- ? $fields->{$target}
- : [split(/[\s,]+/, $fields->{$target})];
+ = ref($fields{$target})
+ ? $fields{$target}
+ : [split(/[\s,]+/, $fields{$target})];
foreach my $i (@$target_array) {
- if ($id == $i) {
+ if ($bug_id == $i) {
ThrowUserError("dependency_loop_single");
}
if (!exists $seen{$i}) {
@{$deps{$target}} = @{$deptree{$target}};
my @stack = @{$deps{$target}};
while (@stack) {
- my $i = shift @stack;
- my $dep_list = $dbh->selectcol_arrayref(
- "SELECT $target
- FROM dependencies
- WHERE $me = ?", undef, $i
- );
+ my $i = shift @stack;
+ my $dep_list
+ = $dbh->selectcol_arrayref("SELECT $target FROM $table WHERE $me = ?",
+ undef, $i);
foreach my $t (@$dep_list) {
# ignore any _current_ dependencies involving this bug,
# as they will be overwritten with data from the form.
- if ($t != $id && !exists $seen{$t}) {
+ if ($t != $bug_id && !exists $seen{$t}) {
push(@{$deptree{$target}}, $t);
push @stack, $t;
$seen{$t} = 1;
}
}
- my @deps = @{$deptree{'dependson'}};
- my @blocks = @{$deptree{'blocked'}};
+ my @deps = @{$deptree{$key1}};
+ my @blocks = @{$deptree{$key2}};
my %union = ();
my %isect = ();
foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
}
}
- # Add dependencies to referenced bug list on new bugs
+ # Add dependencies and regressions to referenced bug list on new bugs
if (!$start) {
- push @referenced_bugs, @{$bug->dependson};
- push @referenced_bugs, @{$bug->blocked};
+ push(@referenced_bugs,
+ map { @{$bug->$_} } qw(dependson blocked regressed_by regresses));
push @referenced_bugs, _parse_see_also(map { $_->name } @{$bug->see_also});
}
$diff->{num} = $comment->count;
$diff->{isprivate} = $diff->{new};
}
- elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') {
+ elsif ($diff->{field_name} =~ /^(?:dependson|blocked|regress(?:ed_by|es))$/) {
push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{old});
push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{new});
}
{name => 'usebugaliases', type => 'b', default => 0},
+ {name => 'use_regression_fields', type => 'b', default => 1},
+
{name => 'use_see_also', type => 'b', default => 1},
{
is_numeric => 1,
buglist => 1
},
+ {
+ name => 'regressed_by',
+ desc => 'Regressed by',
+ in_new_bugmail => 1,
+ is_numeric => 1,
+ buglist => 1
+ },
+ {
+ name => 'regresses',
+ desc => 'Regressions',
+ in_new_bugmail => 1,
+ is_numeric => 1,
+ buglist => 1
+ },
{
name => 'assignee_last_login',
_default => \&_invalid_combination,
},
product => {_non_changed => \&_product_nonchanged,},
+ regressed_by => MULTI_SELECT_OVERRIDE,
+ regresses => MULTI_SELECT_OVERRIDE,
tag => MULTI_SELECT_OVERRIDE,
comment_tag => MULTI_SELECT_OVERRIDE,
},
blocked => {table => 'dependencies', to => 'dependson',},
dependson => {table => 'dependencies', to => 'blocked',},
+ regresses => {table => 'regressions', to => 'regressed_by',},
+ regressed_by => {table => 'regressions', to => 'regresses',},
'longdescs.count' => {table => 'longdescs', join => 'INNER',},
last_visit_ts => {
as => 'bug_user_last_visit',
blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'),
dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'),
+ regresses => $dbh->sql_group_concat('DISTINCT map_regresses.regresses'),
+ regressed_by => $dbh->sql_group_concat('DISTINCT map_regressed_by.regressed_by'),
+
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
last_visit_ts => 'bug_user_last_visit.last_visit_ts',
bug_interest_ts => 'bug_interest.modification_time',
keywords
longdescs.count
percentage_complete
+ regressed_by
+ regresses
);
###############
$args->{full_field} = $field;
return "dependencies";
}
+ elsif ($field eq 'regresses' or $field eq 'regressed_by') {
+ my $select = $field eq 'regresses' ? 'regressed_by' : 'regresses';
+ $args->{_select_field} = $select;
+ $args->{full_field} = $field;
+ return "regressions";
+ }
elsif ($field eq 'longdesc') {
$args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider;
$args->{full_field} = 'thetext';
};
return "dependencies_$chart_id.$to IS $not NULL";
}
+ elsif ($field eq 'regresses' or $field eq 'regressed_by') {
+ my $to = $field eq 'regresses' ? 'regressed_by' : 'regresses';
+ push @$joins,
+ {
+ table => 'regressions',
+ as => "regressions_$chart_id",
+ from => 'bug_id',
+ to => $to,
+ };
+ return "regressions_$chart_id.$to IS $not NULL";
+ }
elsif ($field eq 'longdesc') {
my @extra = ("longdescs_$chart_id.type != " . CMT_HAS_DUPE);
push @extra, "longdescs_$chart_id.isprivate = 0"
"os" => "op_sys",
"severity" => "bug_severity",
+ # Dependencies, Regressions
+ "regressions" => "regresses",
+
# People: AssignedTo, Reporter, QA Contact, CC, etc.
"assignee" => "assigned_to",
"owner" => "assigned_to",
{default => ""},
blocked => Str,
{default => ""},
+ regressed_by => Str,
+ {default => ""},
+ regresses => Str,
+ {default => ""},
assigned_to => Str,
bug_mentors => ArrayRef [Str],
{
'attachments.ispatch' => EVT_ATTACHMENT_DATA,
'dependson' => EVT_DEPEND_BLOCK,
'blocked' => EVT_DEPEND_BLOCK,
+ 'regressed_by' => EVT_DEPEND_BLOCK,
+ 'regresses' => EVT_DEPEND_BLOCK,
'product' => EVT_COMPONENT,
'component' => EVT_COMPONENT
);
$item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}];
}
+ # Regressions
+ if (Bugzilla->params->{use_regression_fields}) {
+ if (filter_wants $params, 'regressed_by') {
+ my @regressed_by = map { $self->type('int', $_) } @{$bug->regressed_by};
+ $item{'regressed_by'} = \@regressed_by;
+ }
+ if (filter_wants $params, 'regressions') {
+ my @regressions = map { $self->type('int', $_) } @{$bug->regresses};
+ $item{'regressions'} = \@regressions;
+ }
+ }
+
# And now custom fields
my @custom_fields = Bugzilla->active_custom_fields(
{
C<hash> A hash containing detailed user information for the qa_contact. To see the
keys included in the user detail hash, see below.
+=item C<regressed_by>
+
+C<array> of C<int>s. The ids of bugs that introduced this bug.
+
+=item C<regressions>
+
+C<array> of C<int>s. The ids of bugs bugs that are introduced by this bug.
+
=item C<remaining_time>
C<double> The number of hours of work remaining until work on this bug
=item The C<actual_time> item was added to the C<bugs> return value
in Bugzilla B<4.4>.
-=item The C<attachments>, C<comments>, C<duplicates>, C<history> and
-C<triage_owner> fields were added in Bugzilla B<6.0>.
+=item The C<attachments>, C<comments>, C<duplicates>, C<history>,
+C<regressed_by>, C<regressions> and C<triage_owner> fields were added in
+Bugzilla B<6.0>.
=back
=item 116 (Dependency Loop)
-You specified values in the C<blocks> or C<depends_on> fields
-that would cause a circular dependency between bugs.
+You specified values in the C<blocks> and C<depends_on> fields,
+or the C<regressions> and C<regressed_by> fields, that would cause a
+circular dependency between bugs.
=item 120 (Group Restriction Denied)
the bug, even if he or she isn't in a group that can normally access
the bug.
+=item C<regressed_by>
+
+C<array> of C<int>s. The ids of bugs that introduced this bug.
+
+=item C<regressions>
+
+C<array> of C<int>s. The ids of bugs bugs that are introduced by this bug.
+
=item C<remaining_time>
C<double> How much work time is remaining to fix the bug, in hours.
=item 116 (Dependency Loop)
-You specified a value in the C<blocks> or C<depends_on> fields that causes
-a dependency loop.
+You specified values in the C<blocks> and C<depends_on> fields,
+or the C<regressions> and C<regressed_by> fields, that would cause a
+circular dependency between bugs.
=item 117 (Invalid Comment ID)
cclist_accessible, classification_id, classification, product, component,
version, rep_platform, op_sys, bug_status, resolution?, dup_id?, see_also*,
bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity,
- target_milestone?, dependson*, blocked*, everconfirmed, reporter, assigned_to,
- cc*, (estimated_time, remaining_time, actual_time, deadline?)?, qa_contact?,
- votes?, token?, group*, flag*, long_desc*, attachment*)?)>
+ target_milestone?, dependson*, blocked*, regressed_by*, regresses*, everconfirmed,
+ reporter, assigned_to, cc*, (estimated_time, remaining_time, actual_time, deadline?)?,
+ qa_contact?, votes?, token?, group*, flag*, long_desc*, attachment*)?)>
<!ATTLIST bug
error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
>
<!ELEMENT keywords (#PCDATA)>
<!ELEMENT dependson (#PCDATA)>
<!ELEMENT blocked (#PCDATA)>
+<!ELEMENT regressed_by (#PCDATA)>
+<!ELEMENT regresses (#PCDATA)>
<!ELEMENT everconfirmed (#PCDATA)>
<!ELEMENT cc (#PCDATA)>
<!ELEMENT see_also (#PCDATA)>
easily searchable field for indexing bugs that have some trait in
common.
+use_regression_fields
+ Do you wish to use the :field:`Regressions` and :field:`Regressed by`
+ fields? These allow you to efficiently track software regressions,
+ which might previously be managed using the :field:`Depends on` and
+ :field:`Blocks` fields along with the “regression” keyword.
+
use_see_also
Do you wish to use the :field:`See Also` field? It allows you mark bugs
in other bug tracker installations as being related. Disabling this field
},
"cf_free_text": "",
"blocks": [],
+ "regressed_by": [],
+ "regressions": [],
"comment_count": 12
}
]
qa_contact_detail object An object containing detailed user information
for the qa_contact. To see the keys included in
the user detail object, see below.
+regressed_by array The IDs of bugs that introduced this bug.
+regressions array The IDs of bugs that are introduced by this bug.
remaining_time double The number of hours of work remaining until work
on this bug is complete. If you are not in the
time-tracking group, this field will not be
keywords array One or more valid keywords to add to this bug.
dependson array One or more valid bug ids that this bug depends on.
blocked array One or more valid bug ids that this bug blocks.
+regressed_by array One or more valid bug ids that introduced this bug.
================== ======= ====================================================
Flag object:
* 107 (Invalid Summary)
You didn't specify a summary for the bug.
* 116 (Dependency Loop)
- You specified values in the "blocks" or "depends_on" fields
- that would cause a circular dependency between bugs.
+ You specified values in the "blocks" and "depends_on" fields,
+ or the "regressions" and "regressed_by" fields, that would cause a
+ circular dependency between bugs.
* 120 (Group Restriction Denied)
You tried to restrict the bug to a group which does not exist, or which
you cannot use with this product.
if you specified the set key above.
assigned_to string The full login name of the user this bug is
assigned to.
-blocks object (Same as ``depends_on`` below)
-depends_on object These specify the bugs that this bug blocks or
- depends on, respectively. To set these, you
- should pass an object as the value. The object
- may contain the following items:
+blocks object (Same as ``regressed_by`` below)
+depends_on object (Same as ``regressed_by`` below)
+regressions object (Same as ``regressed_by`` below)
+regressed_by object These specify the bugs that this bug blocks,
+ depends on, regresses, or is regressed by,
+ respectively. To set these, you should pass an
+ object as the value. The object may contain the
+ following items:
* ``add`` (array) Bug IDs to add to this field.
* ``remove`` (array) Bug IDs to remove from this
"rememberlogin" : "on",
"requirelogin" : "0",
"urlbase" : "http://bugzilla.example.com/",
+ "use_regression_fields" : "1",
"use_see_also" : "1",
"useclassification" : "1",
"usemenuforusers" : "0",
* requirelogin
* search_allow_no_criteria
* urlbase
+* use_regression_fields
* use_see_also
* useclassification
* usemenuforusers
$vars->{'keywords'} = $cloned_bug->keywords;
$vars->{'dependson'} = join(", ", $cloned_bug_id, @{$cloned_bug->dependson});
$vars->{'blocked'} = join(", ", @{$cloned_bug->blocked});
+ $vars->{'regressed_by'} = join(", ", $cloned_bug_id, @{$cloned_bug->regressed_by});
$vars->{'deadline'} = $cloned_bug->deadline;
$vars->{'estimated_time'} = $cloned_bug->estimated_time;
$vars->{'status_whiteboard'} = $cloned_bug->status_whiteboard;
$vars->{'keywords'} = formvalue('keywords');
$vars->{'dependson'} = formvalue('dependson');
$vars->{'blocked'} = formvalue('blocked');
+ $vars->{'regressed_by'} = formvalue('regressed_by');
$vars->{'deadline'} = formvalue('deadline');
$vars->{'estimated_time'} = formvalue('estimated_time');
$vars->{'bug_ignored'} = formvalue('bug_ignored');
change.fieldname == 'work_time' %]
[% PROCESS formattimeunit time_unit=change_type %]
[% ELSIF change.fieldname == 'blocked' ||
- change.fieldname == 'dependson' %]
+ change.fieldname == 'dependson' ||
+ change.fieldname == 'regresses' ||
+ change.fieldname == 'regressed_by' %]
[% change_type FILTER bug_list_link FILTER none %]
[% ELSIF change.fieldname == 'assigned_to' ||
change.fieldname == 'reporter' ||
}
# identify buglist changes
- if ( $change->{fieldname} eq 'blocked'
- || $change->{fieldname} eq 'dependson'
- || $change->{fieldname} eq 'dupe'
+ if ($change->{fieldname} =~ /^(?:dependson|blocked|regress(?:ed_by|es)|dupe)$/
|| ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID))
{
$change->{buglist} = 1;
sub = [];
open_deps = bug.depends_on_obj.only("resolution", "").size;
IF open_deps;
- sub.push("Depends on: " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
+ sub.push("Depends on " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
END;
open_deps = bug.blocks_obj.only("resolution", "").size;
IF open_deps;
- sub.push("Blocks: " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
+ sub.push("Blocks " _ open_deps _ " bug" _ (open_deps == 1 ? "" : "s"));
+ END;
+ IF bug.regressed_by.size;
+ sub.push("Regression");
+ END;
+ open_regs = bug.regresses_obj.only("resolution", "").size;
+ IF open_regs;
+ sub.push("Regressed " _ open_regs _ " bug" _ (open_regs == 1 ? "" : "s"));
END;
IF bug.keyword_objects.size;
IF bug.keyword_objects.size <= 3;
[% END %]
[% END %]
+ [%# regressions %]
+ [% IF Param('use_regression_fields') %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.regresses
+ field_type = constants.FIELD_TYPE_BUG_LIST
+ values = bug.regresses_obj
+ hide_on_view = bug.regresses.size == 0
+ help = "https://wiki.mozilla.org/BMO/UserGuide/BugFields#regresses"
+ %]
+ [% INCLUDE bug_modal/field.html.tmpl
+ field = bug_fields.regressed_by
+ field_type = constants.FIELD_TYPE_BUG_LIST
+ values = bug.regressed_by_obj
+ hide_on_view = bug.regressed_by.size == 0
+ help = "https://wiki.mozilla.org/BMO/UserGuide/BugFields#regressed_by"
+ %]
+ [% END %]
+
[%# duplicates %]
[% IF bug.duplicates.size %]
[% INCLUDE bug_modal/field.html.tmpl
<a href="[% basepath FILTER none %]enter_bug.cgi?format=__default__&product=[% bug.product FILTER uri %]&dependson=[% bug.id FILTER uri %]"
role="menuitem" tabindex="-1" target="_blank">… that depends on this [% terms.bug %]</a>
</li>
+ <li role="presentation">
+ <a href="[% basepath FILTER none %]enter_bug.cgi?format=__default__&product=[% bug.product FILTER uri %]&regressed_by=[% bug.id FILTER uri %]"
+ role="menuitem" tabindex="-1" target="_blank">… that is regressed by this [% terms.bug %]</a>
+ </li>
<li role="separator"></li>
<li role="presentation">
<a href="[% basepath FILTER none %]enter_bug.cgi?format=__default__&product=[% bug.product FILTER uri %]&cloned_bug_id=[% bug.id FILTER uri %]"
}
}
- # Other fields such as keywords, blocks depends_on
+ # Other fields such as dependencies, regressions and keywords
# support 'set' which will make the list exactly what
# the user passes in.
- foreach my $field (qw(blocks depends_on dependson keywords)) {
+ foreach my $field (qw(blocks depends_on dependson regresses regressed_by keywords)) {
if (exists $params->{$field}) {
$params->{$field} = {set => $params->{$field}};
}
}
# identify buglist changes
- if ( $change->{fieldname} eq 'blocked'
- || $change->{fieldname} eq 'dependson'
- || $change->{fieldname} eq 'dupe'
+ if ($change->{fieldname} =~ /^(?:dependson|blocked|regress(?:ed_by|es)|dupe)$/
|| ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID))
{
$change->{buglist} = 1;
if ($changes->{bug_status}) {
my ($old_status, $new_status) = @{$changes->{bug_status}};
if (is_open_state($old_status) && !is_open_state($new_status)) {
- my @related_bugs = (@{$bug->blocks_obj}, @{$bug->depends_on_obj});
+ my @related_bugs = map { @{$bug->{$_} || []} }
+ qw(blocks_obj depends_on_obj regresses_obj regressed_by_obj);
my %involved;
foreach my $related_bug (@related_bugs) {
$comments .= "This bug blocked bug(s) "
. join(' ', _to_array($bug_fields{'blocked'})) . ".\n";
}
+ if (defined $bug_fields{'regressed_by'}) {
+ $comments .= "This bug is regressed by bug(s) "
+ . join(' ', _to_array($bug_fields{'regressed_by'})) . ".\n";
+ }
+ if (defined $bug_fields{'regresses'}) {
+ $comments .= "This bug regressed bug(s) "
+ . join(' ', _to_array($bug_fields{'regresses'})) . ".\n";
+ }
# Now we process each of the fields in turn and make sure they contain
# valid data. We will create two parallel arrays, one for the query
short_desc
op_sys
priority
+ regressed_by
+ regresses
rep_platform
version
target_milestone
$bug_sent->{id} = $id;
my @all_mail_results = ($bug_sent);
-foreach my $dep (@{$bug->dependson || []}, @{$bug->blocked || []}) {
+foreach my $dep (
+ map { @{$bug->{$_} || []} } qw(dependson blocked regressed_by regresses)
+) {
my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients);
$dep_sent->{type} = 'dep';
$dep_sent->{id} = $dep;
if (should_set('remove_see_also')) {
$set_all_fields{'see_also'}->{remove} = [$cgi->param('remove_see_also')];
}
-foreach my $dep_field (qw(dependson blocked)) {
+foreach my $dep_field (qw(dependson blocked regressed_by regresses)) {
if (should_set($dep_field)) {
if (my $dep_action = $cgi->param("${dep_field}_action")) {
$set_all_fields{$dep_field}->{$dep_action}
use QA::Util;
my ($config, @clients) = get_rpc_clients();
-plan tests => ($config->{test_extensions} ? 1338 : 1320);
+plan tests => ($config->{test_extensions} ? 1374 : 1356);
use constant INVALID_FIELD_NAME => 'invalid_field';
use constant INVALID_FIELD_ID => -1;
owner_idle_time
product
qa_contact
+ regressed_by
+ regresses
reporter
reporter_accessible
see_also
},
{
value => {add => [$public_id]},
- error => 'block itself or depend on itself',
+ error => 'block itself, depend on itself',
test => "can't add this bug itself in a dep field"
},
'cc/', 'dependencies/blocked',
'dependencies/dependson', 'duplicates/dupe',
'duplicates/dupe_of', 'flags/',
- 'keywords/', 'longdescs/'
+ 'keywords/', 'longdescs/',
+ 'regressions/regresses', 'regressions/regressed_by'
)
{
["longdescs", "bug_id"],
["dependencies", "blocked"],
["dependencies", "dependson"],
+ ["regressions", "regresses"],
+ ["regressions", "regressed_by"],
['flags', 'bug_id'],
["keywords", "bug_id"],
["duplicates", "dupe_of", "dupe"],
userid login_name realname is_enabled creation_ts
)
],
+ regressions => [
+ qw(
+ regresses regressed_by
+ )
+ ],
tracking_flags => [
qw(
id field_id name description type sortkey is_active
else {
my @blocker_stack = @stack;
foreach my $id (@blocker_stack) {
- my $blocker_ids = Bugzilla::Bug::EmitDependList('blocked', 'dependson', $id);
+ my $blocker_ids
+ = Bugzilla::Bug::list_relationship('dependencies', 'blocked', 'dependson', $id);
foreach my $blocker_id (@$blocker_ids) {
push(@blocker_stack, $blocker_id) unless $seen{$blocker_id};
$AddLink->($id, $blocker_id, $fh);
}
my @dependent_stack = @stack;
foreach my $id (@dependent_stack) {
- my $dep_bug_ids = Bugzilla::Bug::EmitDependList('dependson', 'blocked', $id);
+ my $dep_bug_ids
+ = Bugzilla::Bug::list_relationship('dependencies', 'dependson', 'blocked', $id);
foreach my $dep_bug_id (@$dep_bug_ids) {
push(@dependent_stack, $dep_bug_id) unless $seen{$dep_bug_id};
$AddLink->($dep_bug_id, $id, $fh);
my $cache = Bugzilla->request_cache->{dependency_cache} ||= {};
return $cache->{$bug_id}->{$relationship}
||= $relationship eq 'dependson'
- ? Bugzilla::Bug::EmitDependList('blocked', 'dependson', $bug_id,
- $hide_resolved)
- : Bugzilla::Bug::EmitDependList('dependson', 'blocked', $bug_id,
- $hide_resolved);
+ ? Bugzilla::Bug::list_relationship('dependencies', 'blocked', 'dependson',
+ $bug_id, $hide_resolved)
+ : Bugzilla::Bug::list_relationship('dependencies', 'dependson', 'blocked',
+ $bug_id, $hide_resolved);
}
use Bugzilla;
use Bugzilla::Constants; # LOGIN_*
-use Bugzilla::Bug; # EmitDependList
+use Bugzilla::Bug; # list_relationship
use Bugzilla::Util; # trim
use Bugzilla::Error;
sub get_blocker_ids {
my ($bug_id, $unique) = @_;
$unique ||= {$bug_id => 1};
- my $deps = Bugzilla::Bug::EmitDependList("blocked", "dependson", $bug_id);
+ my $deps
+ = Bugzilla::Bug::list_relationship('dependencies', 'blocked', 'dependson', $bug_id);
my @unseen = grep { !$unique->{$_}++ } @$deps;
foreach $bug_id (@unseen) {
get_blocker_ids($bug_id, $unique);
usebugaliases => "Do you wish to use $terms.bug aliases, which allow you to assign " _
"$terms.bugs an easy-to-remember name by which you can refer to them?",
+ use_regression_fields =>
+ "Do you wish to use the “Regressions” and “Regressed by” fields? These allow you to"
+ _ " efficiently track software regressions, which might previously be managed using"
+ _ " the “Depends on” and “Blocks” fields along with the “regression” keyword."
+
use_see_also =>
"Do you wish to use the See Also field? It allows you refer to"
_ " $terms.bugs in other installations. Even if you disable this field,"
change.fieldname == 'work_time' %]
[% PROCESS formattimeunit time_unit=change_type %]
[% ELSIF change.fieldname == 'blocked' ||
- change.fieldname == 'dependson' %]
+ change.fieldname == 'dependson' ||
+ change.fieldname == 'regresses' ||
+ change.fieldname == 'regressed_by' %]
[% change_type FILTER bug_list_link FILTER none %]
[% ELSIF change.fieldname == 'assigned_to' ||
change.fieldname == 'reporter' ||
</td>
</tr>
+ [% IF Param('use_regression_fields') %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.regressed_by
+ editable = 1
+ %]
+ <td>
+ <input name="regressed_by" value="[% regressed_by FILTER html %]" size="30">
+ </td>
+ <td colspan="2">
+ [%~# Regressions field is hidden from new bugs ~%]
+ </td>
+ </tr>
+ [% END %]
+
[% IF use_keywords %]
<tr>
[% INCLUDE bug/field.html.tmpl
[% PROCESS section_spacer %]
- [%# *** Dependencies and duplicates *** %]
+ [%# *** Duplicates, Dependencies and Regressions *** %]
[% PROCESS section_duplicates %]
[% PROCESS section_dependson_blocks %]
+ [% IF Param('use_regression_fields') %]
+ [% PROCESS section_regressions %]
+ [% END %]
+
[% IF user.id %]
<tr>
<td colspan="2">
</tr>
[% END %]
+[%############################################################################%]
+[%# Block for Regressed by / Regressions #%]
+[%############################################################################%]
+
+[% BLOCK section_regressions %]
+ <tr>
+ [% INCLUDE dependencies
+ field = bug_fields.regresses
+ deps = bug.regresses_obj
+ %]
+ </tr>
+ <tr>
+ [% INCLUDE dependencies
+ field = bug_fields.regressed_by
+ deps = bug.regressed_by_obj
+ %]
+ </tr>
+[% END %]
[%############################################################################%]
[%# Block for Restricting Visibility #%]
_ " unconfirmed, and for verifying the fix once the $terms.bug"
_ " has been resolved.",
+regressed_by =>
+ "This $terms.bug has been introduced by the $terms.bugs listed here.",
+
+regresses =>
+ "This $terms.bug has introduced the $terms.bugs listed here.",
+
remaining_time =>
"The number of hours of work left on this $terms.bug, calculated by
subtracting the $vars.field_descs.work_time from the
[% PROCESS dependencies name = "blocked" %]
[% END %]
+ [% IF (bug.regressed_by.size || bug.regresses.size) %]
+ [% PROCESS dependencies name = "regresses" %]
+ [% PROCESS dependencies name = "regressed_by" %]
+ [% END %]
+
[% IF user.is_timetracker %]
<tr>
<th>Time tracking:</th>
"product" => "Product",
"qa_contact" => "QA Contact",
"qa_contact_realname" => "QA Contact Real Name",
+ "regressed_by" => "Regressed by",
+ "regresses" => "Regressions",
"remaining_time" => "Hours Left",
"rep_platform" => "Hardware",
"reporter" => "Reporter",
[% ELSIF error == "dependency_loop_multi" %]
[% title = "Dependency Loop Detected" %]
- The following [% terms.bug %](s) would appear on both the "depends on"
- and "blocks" parts of the dependency tree if these changes
- are committed:
+ The following [% IF deps.size > 1; terms.bugs; ELSE; terms.bug; END %]
+ would appear on both the “Depends on” and “blocks” fields, or both the
+ “Regressions” and “Regressed by” fields, of the dependency tree if these
+ changes are committed:
[% FOREACH dep = deps %]
[%+ dep FILTER bug_link(dep) FILTER none %]
[% END %].
[% ELSIF error == "dependency_loop_single" %]
[% title = "Dependency Loop Detected" %]
- You can't make [% terms.abug %] block itself or depend on itself.
+ You can’t make [% terms.abug %] block itself, depend on itself, regress
+ itself, or regressed by itself.
[% ELSIF error == "dupe_id_required" %]
[% title = "Duplicate $terms.Bug Id Required" %]
</td>
</tr>
+ [% IF Param('use_regression_fields') %]
+ <tr>
+ <th>
+ <label for="regresses">
+ Regressions:
+ </label>
+ </th>
+ <td colspan="3">
+ <input id="regresses" name="regresses" size="40">
+ <select name="regresses_action">
+ <option value="add">Add these IDs</option>
+ <option value="remove">Delete these IDs</option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label for="regressed_by">
+ Regressed by:
+ </label>
+ </th>
+ <td colspan="3">
+ <input id="regressed_by" name="regressed_by" size="40">
+ <select name="regressed_by_action">
+ <option value="add">Add these IDs</option>
+ <option value="remove">Delete these IDs</option>
+ </select>
+ </td>
+ </tr>
+ [% END %]
+
[% IF Param('usestatuswhiteboard') %]
<tr>
<td align="right">
"keywords" => { maxlength => 0, wrap => 1 } ,
"dependson" => { maxlength => 0, wrap => 1 } ,
"blocked" => { maxlength => 0, wrap => 1 } ,
+ "regressed_by" => { maxlength => 0, wrap => 1 } ,
+ "regresses" => { maxlength => 0, wrap => 1 } ,
"flagtypes.name" => { maxlength => 0, wrap => 1 } ,
"component" => { maxlength => 8 , title => "Comp" } ,
"product" => { maxlength => 8 } ,
target_milestone?,
dependson*,
blocked*,
+ regressed_by*,
+ regresses*,
everconfirmed,
reporter,
assigned_to,
<!ELEMENT keywords (#PCDATA)>
<!ELEMENT dependson (#PCDATA)>
<!ELEMENT blocked (#PCDATA)>
+<!ELEMENT regressed_by (#PCDATA)>
+<!ELEMENT regresses (#PCDATA)>
<!ELEMENT everconfirmed (#PCDATA)>
<!ELEMENT cc (#PCDATA)>
<!ELEMENT see_also (#PCDATA)>
my $bug = $self->bug($number);
my @original_delta = ($bug2->delta_ts, $bug3->delta_ts);
Bugzilla->set_user($bug->reporter);
- $bug->set_dependencies([$bug2->id], [$bug3->id]);
+ $bug->set_relationship('dependencies',
+ ('dependson' => $bug2->id, 'blocked' => $bug3->id));
$bug->update($bug->delta_ts);
# Setting dependencies changed the delta_ts on bug2 and bug3, so