#!/usr/bin/perl -wT # 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. # # Contributor(s): # Gervase Markham # Max Kanat-Alexander use strict; use lib qw(. lib); use Bugzilla; use Bugzilla::Constants; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::Search; use Bugzilla::Field; use Bugzilla::Product; 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', }; ############### # 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}; my %seen; while (my $bug_id = $dups->{$to_id}) { if ($seen{$bug_id}) { warn "Duplicate loop: $to_id -> $bug_id\n"; last; } $seen{$bug_id} = 1; $to_id = $bug_id; } # Optimize for future calls to add_indirect_dups. $dups->{$from_id} = $to_id; return $to_id; } 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; } ############### # Main Script # ############### my $ARGS = Bugzilla->input_params; my $template = Bugzilla->template; my $user = Bugzilla->login(); my $dbh = Bugzilla->switch_to_shadow_db(); my $changedsince = $ARGS->{changedsince} || DEFAULTS->{changedsince}; my $maxrows = $ARGS->{maxrows} || DEFAULTS->{maxrows}; my $openonly = $ARGS->{openonly} || DEFAULTS->{openonly}; my $sortby = $ARGS->{sortby} || DEFAULTS->{sortby}; if (!grep(lc($_) eq lc($sortby), qw(count delta id))) { Bugzilla->get_field($sortby, THROW_ERROR); } my $reverse = $ARGS->{reverse} || DEFAULTS->{reverse}; # Reverse count and delta by default. if (!defined $reverse) { $reverse = $sortby eq 'count' || $sortby eq 'delta' ? 1 : 0; } my @query_products = $ARGS->{product}; my $sortvisible = $ARGS->{sortvisible} || DEFAULTS->{sortvisible}; my @bugs; if ($sortvisible) { my @limit_to_ids = split(/[:,]/, $ARGS->{bug_id} || ''); @bugs = @{ Bugzilla::Bug->new_from_list(\@limit_to_ids) }; @bugs = @{ $user->visible_bugs(\@bugs) }; } # Make sure all products are valid. @query_products = map { Bugzilla::Product->check($_) } @query_products; # Small backwards-compatibility hack, dated 2002-04-10. $sortby = "count" if $sortby eq "dup_count"; my $origmaxrows = $maxrows; detaint_natural($maxrows) || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows }); my $origchangedsince = $changedsince; detaint_natural($changedsince) || ThrowUserError("invalid_changedsince", { changedsince => $origchangedsince }); 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 = Bugzilla->get_field('resolution')->id; my %since_dups = @{$dbh->selectcol_arrayref( "SELECT dupe_of, COUNT(dupe) FROM duplicates". " INNER JOIN bugs_activity ON bugs_activity.bug_id = duplicates.dupe". " WHERE added = ? AND fieldid = ? AND bug_when >= ". $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY')." GROUP BY dupe_of", {Columns=>[1,2]}, Bugzilla->params->{duplicate_resolution}, $reso_field_id, $changedsince )}; add_indirect_dups(\%since_dups, \%dupe_relation); # Enforce the mostfreqthreshold parameter and the "bug_id" URL param. my $mostfreq = Bugzilla->params->{mostfreqthreshold}; foreach my $id (keys %total_dups) { if ($total_dups{$id} < $mostfreq) { delete $total_dups{$id}; next; } if ($sortvisible && !grep($_->id == $id, @bugs)) { delete $total_dups{$id}; } } if (!@bugs) { @bugs = @{ Bugzilla::Bug->new_from_list([keys %total_dups]) }; @bugs = @{ $user->visible_bugs(\@bugs) }; } my @fully_exclude_status = list($ARGS->{fully_exclude_status} || DEFAULTS->{fully_exclude_status}); my @partly_exclude_status = list($ARGS->{partly_exclude_status} || DEFAULTS->{partly_exclude_status}); my @except_resolution = list($ARGS->{except_resolution} || DEFAULTS->{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_obj->name, @fully_exclude_status); # If the bug has a status in @partly_exclude_status, we skip it... if (grep($_ eq $bug->bug_status_obj->name, @partly_exclude_status)) { # ...unless it has a resolution in @except_resolution. next if !grep($_ eq $bug->resolution_obj->name, @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, }); } @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 => $ARGS->{format}, product => [map { $_->name } @query_products], sortvisible => $sortvisible, changedsince => $changedsince, ); my $format = $template->get_format("reports/duplicates", $vars{format}); # Generate and return the UI (HTML page) from the appropriate template. $template->process($format->{template}, \%vars) || ThrowTemplateError($template->error()); exit;