You need to sign in or sign up before continuing.
duplicates.cgi 7.46 KB
Newer Older
1
#!/usr/bin/perl -T
2 3 4
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
#
6 7
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
8

9
use 5.10.1;
10
use strict;
11 12
use warnings;

13
use lib qw(. lib);
14

15
use Bugzilla;
16
use Bugzilla::Constants;
17 18
use Bugzilla::Util;
use Bugzilla::Error;
19
use Bugzilla::Bug;
20
use Bugzilla::Field;
21
use Bugzilla::Product;
22

23
use constant DEFAULTS => {
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

  # We want to show bugs which:
  # a) Aren't CLOSED; and
  # b)  i) Aren't VERIFIED; OR
  #    ii) Were resolved INVALID/WONTFIX
  #
  # The rationale behind this is that people will eventually stop
  # reporting fixed bugs when they get newer versions of the software,
  # but if the bug is determined to be erroneous, people will still
  # keep reporting it, so we do need to show it here.
  fully_exclude_status  => ['CLOSED'],
  partly_exclude_status => ['VERIFIED'],
  except_resolution     => ['INVALID', 'WONTFIX'],
  changedsince          => 7,
  maxrows               => 20,
  sortby                => 'count',
40 41
};

42 43 44 45 46 47 48 49 50 51
###############
# Subroutines #
###############

# $counts is a count of exactly how many direct duplicates there are for
# each bug we're considering. $dups is a map of duplicates, from one
# bug_id to another. We go through the duplicates map ($dups) and if one bug
# in $count is a duplicate of another bug in $count, we add their counts
# together under the target bug.
sub add_indirect_dups {
52
  my ($counts, $dups) = @_;
53

54 55 56 57 58
  foreach my $add_from (keys %$dups) {
    my $add_to = walk_dup_chain($dups, $add_from);
    my $add_amount = delete $counts->{$add_from} || 0;
    $counts->{$add_to} += $add_amount;
  }
59 60 61
}

sub walk_dup_chain {
62 63 64 65 66 67 68
  my ($dups, $from_id) = @_;
  my $to_id = $dups->{$from_id};
  my %seen;
  while (my $bug_id = $dups->{$to_id}) {
    if ($seen{$bug_id}) {
      warn "Duplicate loop: $to_id -> $bug_id\n";
      last;
69
    }
70 71 72 73 74 75 76
    $seen{$bug_id} = 1;
    $to_id = $bug_id;
  }

  # Optimize for future calls to add_indirect_dups.
  $dups->{$from_id} = $to_id;
  return $to_id;
77 78
}

79 80
# Get params from URL
sub formvalue {
81 82 83 84 85 86 87 88 89
  my ($name) = (@_);
  my $cgi = Bugzilla->cgi;
  if (defined $cgi->param($name)) {
    return $cgi->param($name);
  }
  elsif (exists DEFAULTS->{$name}) {
    return ref DEFAULTS->{$name} ? @{DEFAULTS->{$name}} : DEFAULTS->{$name};
  }
  return undef;
90 91 92
}

sub sort_duplicates {
93 94 95 96 97 98 99 100 101
  my ($a, $b, $sort_by) = @_;
  if ($sort_by eq 'count' or $sort_by eq 'delta') {
    return $a->{$sort_by} <=> $b->{$sort_by};
  }
  if ($sort_by =~ /^(bug_)?id$/) {
    return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by;
  }
  return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by;

102 103
}

104 105 106 107
###############
# Main Script #
###############

108
my $cgi      = Bugzilla->cgi;
109
my $template = Bugzilla->template;
110
my $user     = Bugzilla->login();
111

112
my $dbh = Bugzilla->switch_to_shadow_db();
113

114
my $changedsince = formvalue("changedsince");
115 116 117
my $maxrows      = formvalue("maxrows");
my $openonly     = formvalue("openonly");
my $sortby       = formvalue("sortby");
118
if (!grep(lc($_) eq lc($sortby), qw(count delta id))) {
119
  Bugzilla::Field->check($sortby);
120 121
}
my $reverse = formvalue("reverse");
122

123 124
# Reverse count and delta by default.
if (!defined $reverse) {
125 126 127 128 129 130
  if ($sortby eq 'count' or $sortby eq 'delta') {
    $reverse = 1;
  }
  else {
    $reverse = 0;
  }
131
}
132
my @query_products = $cgi->param('product');
133
my $sortvisible    = formvalue("sortvisible");
134 135
my @bugs;
if ($sortvisible) {
136 137 138
  my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || ''));
  @bugs = @{Bugzilla::Bug->new_from_list(\@limit_to_ids)};
  @bugs = @{$user->visible_bugs(\@bugs)};
139
}
140

141
# Make sure all products are valid.
142
@query_products = map { Bugzilla::Product->check($_) } @query_products;
143

144 145
# Small backwards-compatibility hack, dated 2002-04-10.
$sortby = "count" if $sortby eq "dup_count";
146

147 148
my $origmaxrows = $maxrows;
detaint_natural($maxrows)
149
  || ThrowUserError("invalid_maxrows", {maxrows => $origmaxrows});
150 151 152

my $origchangedsince = $changedsince;
detaint_natural($changedsince)
153 154
  || ThrowUserError("invalid_changedsince",
  {changedsince => $origchangedsince});
155

156 157
my %total_dups = @{
  $dbh->selectcol_arrayref(
158 159
    "SELECT dupe_of, COUNT(dupe)
       FROM duplicates
160 161 162
   GROUP BY dupe_of", {Columns => [1, 2]}
  )
};
163

164 165
my %dupe_relation = @{
  $dbh->selectcol_arrayref(
166
    "SELECT dupe, dupe_of FROM duplicates
167 168 169
      WHERE dupe IN (SELECT dupe_of FROM duplicates)", {Columns => [1, 2]}
  )
};
170 171 172
add_indirect_dups(\%total_dups, \%dupe_relation);

my $reso_field_id = get_field_id('resolution');
173 174
my %since_dups    = @{
  $dbh->selectcol_arrayref(
175 176 177
    "SELECT dupe_of, COUNT(dupe)
       FROM duplicates INNER JOIN bugs_activity 
                       ON bugs_activity.bug_id = duplicates.dupe 
178
      WHERE added = 'DUPLICATE' AND fieldid = ? 
179
            AND bug_when >= "
180 181 182 183
      . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY')
      . " GROUP BY dupe_of", {Columns => [1, 2]}, $reso_field_id, $changedsince
  )
};
184 185
add_indirect_dups(\%since_dups, \%dupe_relation);

186
# Enforce the MOST_FREQUENT_THRESHOLD constant and the "bug_id" cgi param.
187
foreach my $id (keys %total_dups) {
188 189 190 191 192 193 194
  if ($total_dups{$id} < MOST_FREQUENT_THRESHOLD) {
    delete $total_dups{$id};
    next;
  }
  if ($sortvisible and !grep($_->id == $id, @bugs)) {
    delete $total_dups{$id};
  }
195 196
}

197
if (!@bugs) {
198 199
  @bugs = @{Bugzilla::Bug->new_from_list([keys %total_dups])};
  @bugs = @{$user->visible_bugs(\@bugs)};
200
}
201

202
my @fully_exclude_status  = formvalue('fully_exclude_status');
203
my @partly_exclude_status = formvalue('partly_exclude_status');
204
my @except_resolution     = formvalue('except_resolution');
205 206 207 208

# Filter bugs by criteria
my @result_bugs;
foreach my $bug (@bugs) {
209

210 211 212 213
  # It's possible, if somebody specified a bug ID that wasn't a dup
  # in the "buglist" parameter and specified $sortvisible that there
  # would be bugs in the list with 0 dups, so we want to avoid that.
  next if !$total_dups{$bug->id};
214

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  next if ($openonly and !$bug->isopened);

  # If the bug has a status in @fully_exclude_status, we skip it,
  # no question.
  next if grep($_ eq $bug->bug_status, @fully_exclude_status);

  # If the bug has a status in @partly_exclude_status, we skip it...
  if (grep($_ eq $bug->bug_status, @partly_exclude_status)) {

    # ...unless it has a resolution in @except_resolution.
    next if !grep($_ eq $bug->resolution, @except_resolution);
  }

  if (scalar @query_products) {
    next if !grep($_->id == $bug->product_id, @query_products);
  }

  # Note: maximum row count is dealt with later.
  push(
    @result_bugs,
    {
      bug   => $bug,
      count => $total_dups{$bug->id},
      delta => $since_dups{$bug->id} || 0
    }
  );
241 242 243 244
}
@bugs = @result_bugs;
@bugs = sort { sort_duplicates($a, $b, $sortby) } @bugs;
if ($reverse) {
245
  @bugs = reverse @bugs;
246
}
247
@bugs = @bugs[0 .. $maxrows - 1] if scalar(@bugs) > $maxrows;
248 249

my %vars = (
250 251 252 253 254 255 256 257 258 259
  bugs         => \@bugs,
  bug_ids      => [map { $_->{'bug'}->id } @bugs],
  sortby       => $sortby,
  openonly     => $openonly,
  maxrows      => $maxrows,
  reverse      => $reverse,
  format       => scalar $cgi->param('format'),
  product      => [map { $_->name } @query_products],
  sortvisible  => $sortvisible,
  changedsince => $changedsince,
260
);
261

262 263 264
my $format = $template->get_format("reports/duplicates", $vars{'format'});
print $cgi->header;

265
# Generate and return the UI (HTML page) from the appropriate template.
266
$template->process($format->{'template'}, \%vars)
267
  || ThrowTemplateError($template->error());