duplicates.cgi 8.23 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
21 22 23
# Contributor(s): 
#   Gervase Markham <gerv@gerv.net>
#   Max Kanat-Alexander <mkanat@bugzilla.org>
24

25
use strict;
26
use lib qw(. lib);
27

28
use Bugzilla;
29
use Bugzilla::Constants;
30 31 32
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Search;
33
use Bugzilla::Field;
34
use Bugzilla::Product;
35

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
use constant DEFAULTS => {
    # 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',
};

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
###############
# 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 {
    my ($counts, $dups) = @_;

    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;
    }
}

sub walk_dup_chain {
    my ($dups, $from_id) = @_;
    my $to_id = $dups->{$from_id};
76
    my %seen;
77
    while (my $bug_id = $dups->{$to_id}) {
78 79 80 81 82
        if ($seen{$bug_id}) {
            warn "Duplicate loop: $to_id -> $bug_id\n";
            last;
        }
        $seen{$bug_id} = 1;
83 84 85 86 87 88 89
        $to_id = $bug_id;
    }
    # Optimize for future calls to add_indirect_dups.
    $dups->{$from_id} = $to_id;
    return $to_id;
}

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
# Get params from URL
sub formvalue {
    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;
}

sub sort_duplicates {
    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;
    
}

116 117 118 119
###############
# Main Script #
###############

120
my $cgi = Bugzilla->cgi;
121
my $template = Bugzilla->template;
122
my $user = Bugzilla->login();
123

124
my $dbh = Bugzilla->switch_to_shadow_db();
125

126 127
my $changedsince = formvalue("changedsince");
my $maxrows = formvalue("maxrows");
128
my $openonly = formvalue("openonly");
129 130 131 132 133 134 135 136 137 138 139 140 141 142
my $sortby = formvalue("sortby");
if (!grep(lc($_) eq lc($sortby), qw(count delta id))) {
    Bugzilla::Field->check($sortby);
}
my $reverse = formvalue("reverse");
# Reverse count and delta by default.
if (!defined $reverse) {
    if ($sortby eq 'count' or $sortby eq 'delta') {
        $reverse = 1;
    }
    else {
        $reverse = 0;
    }
}
143
my @query_products = $cgi->param('product');
144
my $sortvisible = formvalue("sortvisible");
145 146 147 148 149 150
my @bugs;
if ($sortvisible) {
    my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || ''));
    @bugs = @{ Bugzilla::Bug->new_from_list(\@limit_to_ids) };
    @bugs = @{ $user->visible_bugs(\@bugs) };
}
151

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

155 156
# Small backwards-compatibility hack, dated 2002-04-10.
$sortby = "count" if $sortby eq "dup_count";
157

158 159 160 161 162 163 164 165 166
my $origmaxrows = $maxrows;
detaint_natural($maxrows)
  || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});

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

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
my %total_dups = @{$dbh->selectcol_arrayref(
    "SELECT dupe_of, COUNT(dupe)
       FROM duplicates
   GROUP BY dupe_of", {Columns => [1,2]})};

my %dupe_relation = @{$dbh->selectcol_arrayref(
    "SELECT dupe, dupe_of FROM duplicates
      WHERE dupe IN (SELECT dupe_of FROM duplicates)",
    {Columns => [1,2]})};
add_indirect_dups(\%total_dups, \%dupe_relation);

my $reso_field_id = get_field_id('resolution');
my %since_dups = @{$dbh->selectcol_arrayref(
    "SELECT dupe_of, COUNT(dupe)
       FROM duplicates INNER JOIN bugs_activity 
                       ON bugs_activity.bug_id = duplicates.dupe 
183 184 185 186
      WHERE added = 'DUPLICATE' AND fieldid = ? 
            AND bug_when >= LOCALTIMESTAMP(0) - "
                . $dbh->sql_interval('?', 'DAY') .
 " GROUP BY dupe_of", {Columns=>[1,2]},
187 188 189
    $reso_field_id, $changedsince)};
add_indirect_dups(\%since_dups, \%dupe_relation);

190
# Enforce the mostfreqthreshold parameter and the "bug_id" cgi param.
191
my $mostfreq = Bugzilla->params->{'mostfreqthreshold'};
192
foreach my $id (keys %total_dups) {
193
    if ($total_dups{$id} < $mostfreq) {
194 195 196
        delete $total_dups{$id};
        next;
    }
197
    if ($sortvisible and !grep($_->id == $id, @bugs)) {
198 199
        delete $total_dups{$id};
    }
200 201
}

202 203 204 205
if (!@bugs) {
    @bugs = @{ Bugzilla::Bug->new_from_list([keys %total_dups]) };
    @bugs = @{ $user->visible_bugs(\@bugs) };
}
206

207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
my @fully_exclude_status = formvalue('fully_exclude_status');
my @partly_exclude_status = formvalue('partly_exclude_status');
my @except_resolution = formvalue('except_resolution');

# Filter bugs by criteria
my @result_bugs;
foreach my $bug (@bugs) {
    # 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};

    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);
227 228
    }

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

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    # Note: maximum row count is dealt with later.
    push (@result_bugs, { bug => $bug,
                          count => $total_dups{$bug->id},
                          delta => $since_dups{$bug->id} || 0 });
}
@bugs = @result_bugs;
@bugs = sort { sort_duplicates($a, $b, $sortby) } @bugs;
if ($reverse) {
    @bugs = reverse @bugs;
}
@bugs = @bugs[0..$maxrows-1] if scalar(@bugs) > $maxrows;

my %vars = (
    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,
256
);
257

258 259 260
my $format = $template->get_format("reports/duplicates", $vars{'format'});
print $cgi->header;

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