]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1461492 - Add an optional regressed-by field in bugs
authorKohei Yoshino <kohei.yoshino@gmail.com>
Wed, 27 Mar 2019 23:21:30 +0000 (19:21 -0400)
committerGitHub <noreply@github.com>
Wed, 27 Mar 2019 23:21:30 +0000 (19:21 -0400)
42 files changed:
Bugzilla/Bug.pm
Bugzilla/BugMail.pm
Bugzilla/Config/BugFields.pm
Bugzilla/Field.pm
Bugzilla/Search.pm
Bugzilla/Search/Quicksearch.pm
Bugzilla/Test/Util.pm
Bugzilla/User.pm
Bugzilla/WebService/Bug.pm
bugzilla.dtd
docs/en/rst/administering/parameters.rst
docs/en/rst/api/core/v1/bug.rst
docs/en/rst/api/core/v1/bugzilla.rst
enter_bug.cgi
extensions/BMO/template/en/default/pages/user_activity.html.tmpl
extensions/BugModal/lib/ActivityStream.pm
extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl
extensions/BzAPI/lib/Resources/Bug.pm
extensions/InlineHistory/Extension.pm
extensions/MyDashboard/Extension.pm
importxml.pl
post_bug.cgi
process_bug.cgi
qa/t/webservice_bug_fields.t
qa/t/webservice_bug_update.t
sanitycheck.cgi
scripts/remove-non-public-data.pl
showdependencygraph.cgi
showdependencytree.cgi
summarize_time.cgi
template/en/default/admin/params/bugfields.html.tmpl
template/en/default/bug/activity/table.html.tmpl
template/en/default/bug/create/create.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/bug/field-help.none.tmpl
template/en/default/bug/show-multiple.html.tmpl
template/en/default/global/field-descs.none.tmpl
template/en/default/global/user-error.html.tmpl
template/en/default/list/edit-multiple.html.tmpl
template/en/default/list/table.html.tmpl
template/en/default/pages/bugzilla.dtd.tmpl
xt/lib/Bugzilla/Test/Search.pm

index b76aa7edb9a18dbf6fa5ade09392524ea137cc8d..fca29177019fd1149c9ded1f7b5eb1ef4270c6cf 100644 (file)
@@ -57,6 +57,12 @@ use constant ID_FIELD   => 'bug_id';
 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;
@@ -272,6 +278,7 @@ use constant FIELD_MAP => {
   last_change_time      => 'delta_ts',
   comment_count         => 'longdescs.count',
   platform              => 'rep_platform',
+  regressions           => 'regresses',
   severity              => 'bug_severity',
   status                => 'bug_status',
   summary               => 'short_desc',
@@ -549,6 +556,8 @@ sub CLEANUP {
     next unless $bug;
     delete $bug->{depends_on_obj};
     delete $bug->{blocks_obj};
+    delete $bug->{regressed_by_obj};
+    delete $bug->{regresses_obj};
   }
   %CLEANUP = ();
 }
@@ -580,7 +589,14 @@ sub check {
     }
   }
 
-  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;
@@ -677,7 +693,8 @@ sub preload {
   # 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;
 
@@ -902,12 +919,12 @@ sub create {
   # 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.
@@ -942,25 +959,27 @@ sub create {
     $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
@@ -1057,9 +1076,15 @@ sub run_create_validators {
   $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};
@@ -1165,36 +1190,39 @@ sub update {
     $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)];
+      }
     }
   }
 
@@ -1533,6 +1561,8 @@ sub remove_from_db {
   $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);
@@ -1616,12 +1646,16 @@ sub send_changes {
     }
   }
 
-  # 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
@@ -1924,10 +1958,11 @@ sub _check_deadline {
   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) {
 
@@ -1935,13 +1970,11 @@ sub _check_dependencies {
     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;
@@ -1996,10 +2029,9 @@ sub _check_dependencies {
 
   # 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 {
@@ -2596,9 +2628,10 @@ sub fields {
       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" : (),
@@ -2681,9 +2714,14 @@ sub set_all {
   # 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
@@ -2709,7 +2747,7 @@ sub set_all {
       $set_deps{$name} = \@dep_ids;
     }
 
-    $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'});
+    $self->set_relationship($table, %set_deps);
   }
 
   if (exists $params->{'keywords'}) {
@@ -2863,18 +2901,20 @@ sub set_custom_field {
 }
 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; }
 
@@ -3754,7 +3794,8 @@ sub blocked {
   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'};
 }
 
@@ -3853,7 +3894,8 @@ sub dependson {
   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'};
 }
 
@@ -4042,6 +4084,38 @@ sub qa_contact {
   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'};
@@ -4381,13 +4455,11 @@ sub editable_bug_fields {
   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;
@@ -4395,8 +4467,8 @@ sub EmitDependList {
 
   $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"
@@ -4618,7 +4690,7 @@ sub _join_activity_entries {
   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;
     }
@@ -4945,35 +5017,29 @@ sub _changes_everconfirmed {
 # 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}) {
@@ -4987,17 +5053,15 @@ sub ValidateDependencies {
     @{$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;
@@ -5006,8 +5070,8 @@ sub ValidateDependencies {
     }
   }
 
-  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}++ }
index 47edac7511439dd39d5892197f25da591db785c0..28ff871dae9e5fa79da34b164aa1a7021cd3bedf 100644 (file)
@@ -133,10 +133,10 @@ sub Send {
     }
   }
 
-  # 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});
   }
 
@@ -596,7 +596,7 @@ sub _get_diffs {
       $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});
     }
index 87ecc61220de7ee1fee708a989674e7fa840fb1d..9c08065e89c66c8f67629d51de494ee110887086 100644 (file)
@@ -35,6 +35,8 @@ sub get_param_list {
 
     {name => 'usebugaliases', type => 'b', default => 0},
 
+    {name => 'use_regression_fields', type => 'b', default => 1},
+
     {name => 'use_see_also', type => 'b', default => 1},
 
     {
index d94d92f4e56d85100feba53e9e0e518fc7a313e5..7a061b43b119e067eb5cbfeef9da9b9fdd528502 100644 (file)
@@ -285,6 +285,20 @@ use constant DEFAULT_FIELDS => (
     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',
index dabe52efd6ec672efeeb6b33eb76cfa7007ed945..c61f448e752b0b360f0be02cd4ed734094d827ab 100644 (file)
@@ -287,6 +287,8 @@ use constant OPERATOR_FIELD_OVERRIDE => {
     _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,
 
@@ -483,6 +485,8 @@ sub COLUMN_JOINS {
     },
     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',
@@ -566,6 +570,9 @@ sub COLUMNS {
     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',
@@ -677,6 +684,8 @@ use constant GROUP_BY_SKIP => qw(
   keywords
   longdescs.count
   percentage_complete
+  regressed_by
+  regresses
 );
 
 ###############
@@ -3097,6 +3106,12 @@ sub _multiselect_table {
     $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';
@@ -3198,6 +3213,17 @@ sub _multiselect_isempty {
       };
     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"
index 7d152aafbaa006dc7a12e6b8d0670950283b4f9c..02d1cf6e5e5d60c3b55b32e8b0bc5e08d8b8250d 100644 (file)
@@ -34,6 +34,9 @@ use constant MAPPINGS => {
   "os"       => "op_sys",
   "severity" => "bug_severity",
 
+  # Dependencies, Regressions
+  "regressions" => "regresses",
+
   # People: AssignedTo, Reporter, QA Contact, CC, etc.
   "assignee" => "assigned_to",
   "owner"    => "assigned_to",
index ed46ec10473cae3df1834d9e6231cb7d04a601e8..b668aedb3c30eb09605fb995f55b694e65080c32 100644 (file)
@@ -129,6 +129,10 @@ sub create_bug {
     {default => ""},
     blocked => Str,
     {default => ""},
+    regressed_by => Str,
+    {default => ""},
+    regresses => Str,
+    {default => ""},
     assigned_to => Str,
     bug_mentors => ArrayRef [Str],
     {
index bf5211f11fa890b40c6bc105d5bd43d7bd11fcbb..ad929f1b4450c2b3f06a0e3430cda4131ffb99f8 100644 (file)
@@ -2374,6 +2374,8 @@ our %names_to_events = (
   '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
 );
index 44b68eaa43931b1a4b424d219b9707ce48334c10..65d5d78af5fee0075f63201333cfc38e873699c2 100644 (file)
@@ -1531,6 +1531,18 @@ sub _bug_to_hash {
     $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(
     {
@@ -2882,6 +2894,14 @@ C<string> The login name of the current QA Contact on the bug.
 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
@@ -3107,8 +3127,9 @@ and all custom fields.
 =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
 
@@ -3712,8 +3733,9 @@ You didn't specify a summary for the bug.
 
 =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)
 
@@ -4541,6 +4563,14 @@ C<boolean> Whether or not the bug's reporter is allowed to access
 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.
@@ -4742,8 +4772,9 @@ The error message will have more detail.
 
 =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)
 
index e22a2bfd18726733b96a8a64f7f7cb39842b0ec9..57fe3aadbb7471e54cfcb20b40525ecddb686684 100644 (file)
@@ -9,9 +9,9 @@
     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
 >
@@ -54,6 +54,8 @@
 <!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)>
index 1b657ff3cf4b9ad23038f739d65510f4bff29b6a..f5e875f699296df243935a2c79075f8144dda639 100644 (file)
@@ -245,6 +245,12 @@ usestatuswhiteboard
     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
index 91adb33267d7e2d446833de7c5275a28040f3de3..7da9018cb2018c40a6de2b4a1a7368f12ea5cbe9 100644 (file)
@@ -121,6 +121,8 @@ name              type   description
          },
          "cf_free_text": "",
          "blocks": [],
+         "regressed_by": [],
+         "regressions": [],
          "comment_count": 12
        }
      ]
@@ -205,6 +207,8 @@ qa_contact             string    The login name of the current QA Contact on the
 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
@@ -673,6 +677,7 @@ flags               array    Flags objects to add to the bug. The object format
 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:
@@ -728,8 +733,9 @@ id    int   This is the ID of the newly-filed bug.
 * 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.
@@ -818,11 +824,14 @@ alias                  object   These specify the aliases of a bug that can be
                                 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
index 220ee5fc5420156630233faee84376262066f01c..571f274e17f30843469fc56e74e834f813a002b5 100644 (file)
@@ -208,6 +208,7 @@ Example response for authenticated user:
           "rememberlogin" : "on",
           "requirelogin" : "0",
           "urlbase" : "http://bugzilla.example.com/",
+          "use_regression_fields" : "1",
           "use_see_also" : "1",
           "useclassification" : "1",
           "usemenuforusers" : "0",
@@ -247,6 +248,7 @@ A logged-in user can access the following parameters (listed alphabetically):
 * requirelogin
 * search_allow_no_criteria
 * urlbase
+* use_regression_fields
 * use_see_also
 * useclassification
 * usemenuforusers
index c721c048e66e488128d659d487c6aaa728b72a79..aa07f679a2409683bb315da39813e26f1b5cd4b3 100755 (executable)
@@ -262,6 +262,7 @@ if ($cloned_bug_id) {
   $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;
@@ -328,6 +329,7 @@ else {
   $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');
index 740f8129efc48b45487cd7c6e6166daf42ba9fab..993f59314f81764ca6c7424bf9c5d698ac975b11 100644 (file)
             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' ||
index 0035d7ab08e6b80fb20c34c536a1d5eb076416d2..a0e1b18fdd5bc70d6961c2759d9b4088164980ed 100644 (file)
@@ -268,9 +268,7 @@ sub _add_activities_to_stream {
       }
 
       # 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;
index d3d5394267875fccc9d3ee496b26b0afaa656fa8..51473ac7cf2d964b526206d2126ea6b0b6090e3d 100644 (file)
   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__&amp;product=[% bug.product FILTER uri %]&amp;dependson=[% bug.id FILTER uri %]"
                role="menuitem" tabindex="-1" target="_blank">&#8230; that depends on this [% terms.bug %]</a>
           </li>
+          <li role="presentation">
+            <a href="[% basepath FILTER none %]enter_bug.cgi?format=__default__&amp;product=[% bug.product FILTER uri %]&amp;regressed_by=[% bug.id FILTER uri %]"
+               role="menuitem" tabindex="-1" target="_blank">&#8230; 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__&amp;product=[% bug.product FILTER uri %]&amp;cloned_bug_id=[% bug.id FILTER uri %]"
index 8c5881874b175ced534136d688d17587289721a0..d95d8e9463453b33b9c7815707072fc5eac84e37 100644 (file)
@@ -446,10 +446,10 @@ sub update_bug_request {
     }
   }
 
-  # 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}};
     }
index 45f1120f8e5c15aec633e080d47b9b811c3b37fb..7ccaf31e6792dff8d8ed69dc0b204cb2a68039fd 100644 (file)
@@ -118,9 +118,7 @@ sub template_before_process {
       }
 
       # 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;
index ae7921af717addfed4f3e8b3845789c4c825382b..c94ecba4a8f8287a48e8ba6f5037ab2f9417a435 100644 (file)
@@ -223,7 +223,8 @@ sub bug_end_of_update {
   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) {
index 7e8ca0cfdb18247ad576958fffb5c1ce50b925fb..2c5aebdf47cc64126118c99df64b59b620df9b34 100755 (executable)
@@ -548,6 +548,14 @@ sub process_bug {
     $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
index cb4e5db9904c4d730f824efb20f2ac651c7885ef..cbcfc68e6247648636dfd2fed376fabf1dc1a953 100755 (executable)
@@ -120,6 +120,8 @@ push(
     short_desc
     op_sys
     priority
+    regressed_by
+    regresses
     rep_platform
     version
     target_milestone
@@ -276,7 +278,9 @@ $bug_sent->{type} = 'created';
 $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;
index 062af5008be45947358a467202a7464eac2d965b..4741dfc57258d084c05141b7f0ca9c294dd97fee 100755 (executable)
@@ -284,7 +284,7 @@ if (should_set('see_also')) {
 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}
index 110ed5741d0dd13f1eec2bcdbeffe36af781feff..244a92514aa338f6c471b6f1fb4e1efc7d95473a 100644 (file)
@@ -14,7 +14,7 @@ use List::Util qw(first);
 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;
@@ -56,6 +56,8 @@ sub GLOBAL_GENERAL_FIELDS {
     owner_idle_time
     product
     qa_contact
+    regressed_by
+    regresses
     reporter
     reporter_accessible
     see_also
index 4012dccc674e02555807965b88d11954c1c98562..c92debbbb35854c9176f120d7a9f8ffc53666b9e 100644 (file)
@@ -399,7 +399,7 @@ sub invalid_values {
       },
       {
         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"
       },
 
index 71ad9eab0e135f340867cb19a91faf3a2308fd66..cf95950b2a8c28fb575aece281c6099e6dd71305 100755 (executable)
@@ -306,7 +306,8 @@ if ($cgi->param('remove_invalid_bug_references')) {
     'cc/',                    'dependencies/blocked',
     'dependencies/dependson', 'duplicates/dupe',
     'duplicates/dupe_of',     'flags/',
-    'keywords/',              'longdescs/'
+    'keywords/',              'longdescs/',
+    'regressions/regresses',  'regressions/regressed_by'
     )
   {
 
@@ -516,6 +517,8 @@ CrossCheck(
   ["longdescs",     "bug_id"],
   ["dependencies",  "blocked"],
   ["dependencies",  "dependson"],
+  ["regressions",   "regresses"],
+  ["regressions",   "regressed_by"],
   ['flags',         'bug_id'],
   ["keywords",      "bug_id"],
   ["duplicates", "dupe_of", "dupe"],
index bca2440ea86e1a0dc43555e1b3694fd344f5b976..e72c49d447e8b394dd2831d1e48bd25931fd8470 100755 (executable)
@@ -150,6 +150,11 @@ my %whitelist = (
       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
index 40de2af9e6e3b64ccf6da1a98cfcac64243fc1c6..be0699412cc9661ea1dd7c106dcef77888ac15a4 100755 (executable)
@@ -165,7 +165,8 @@ if ($display eq 'web') {
 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);
@@ -173,7 +174,8 @@ else {
   }
   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);
index 08fee75b68780fc5faea3ac249fe407ff1c2d47e..dd8fce7a829aa8458d95ef58e66b02ca5ff9e22d 100755 (executable)
@@ -143,9 +143,9 @@ sub _get_dependencies {
   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);
 }
 
index 979629729e036550c55fcbd2e4e7ee5d31e17806..b6a87830af0d64f3c58bb9f2cc08f3fb97ab184e 100755 (executable)
@@ -16,7 +16,7 @@ use Date::Parse;    # strptime
 
 use Bugzilla;
 use Bugzilla::Constants;    # LOGIN_*
-use Bugzilla::Bug;          # EmitDependList
+use Bugzilla::Bug;          # list_relationship
 use Bugzilla::Util;         # trim
 use Bugzilla::Error;
 
@@ -170,7 +170,8 @@ sub sqlize_dates {
 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);
index 9c09610e1c1a7294823615f2a1df030d2a3fb748..ef5ec55e5313c9f929e3628cbf7ba8ab9bd98e53 100644 (file)
   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,"
index 17fbd9f33ce1b503ba3485c0dedb8c8d766eaab1..97683590ba49008a7fc6ff26415af7371c0b98ab 100644 (file)
             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' ||
index 223209efd17edb0f732d4cdaa736ceab4f1511c4..3383954f1a700be6dcebc6e79b0984ad1e6ed4a5 100644 (file)
@@ -608,6 +608,21 @@ TUI_hide_default('expert_fields');
       </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
index 0788b2d7fa87e1c968251aaf5924f9882f2f6e48..9e51d9fba9c61b3dbfe366e17dd7106b50800e7d 100644 (file)
 
           [% 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                                         #%]
index 782318c8fd4eedc4176c4cf11e5bfb0a2dd8df0d..9209525b96590d41d731018512fb162e55572d24 100644 (file)
@@ -121,6 +121,12 @@ qa_contact =>
   _ " 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
index 288af30af1aa561e0d3c961ac7388f8da1d11e05..d335c5559b7d0bcf244a6389ff8ab3ec24f898de 100644 (file)
       [% 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>
index cb6240c291d0ab58dc38df2590f525c2feea12c8..99584f8fdcd9e15637d1def21c4ea09e0f47006e 100644 (file)
@@ -136,6 +136,8 @@ if ( $stash->get("in_template_var") ) {
         "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",
index 40ac8c5b0382ce6e5a485509a8583514c0048b41..3bdb5c6a856a381b727e4dc767411dee82e4aaa9 100644 (file)
 
   [% 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" %]
index 88525de12351d9103f17d2ed8621ca3f89c55329..0637eb159ecff53e909298de923652269e6eebdb 100644 (file)
     </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">
index c4a853624d2cf84919bc3a29c6e18ec5d709fb5d..fd572f4337c54c0ff550d9a9b71395f49e580a8f 100644 (file)
@@ -62,6 +62,8 @@
     "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 } ,
index 09d83fb7f93e3e4e9e517aedfdc26368e2186273..f0da8d78ee0bcdfb397b93de3a06302f1a662f12 100644 (file)
@@ -61,6 +61,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)>
index b1651916e9c5fd96af6f92621efcd976fc93bc23..30dbfb47d360c0e863649ec8babb2d8be5b8bce4 100644 (file)
@@ -987,7 +987,8 @@ sub _setup_dependencies {
     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