[bugzilla-gnome-org-extensions] Add automatic duplicate handling.



commit a45320a1f89c20a9b5d6cdf851eb8243cbabaeb1
Author: Max Kanat-Alexander <mkanat everythingsolved com>
Date:   Fri Aug 7 18:51:35 2009 -0500

    Add automatic duplicate handling.

 code/db_schema-abstract_schema.pl              |   15 +++
 code/install-before_final_checks.pl            |   33 ++++++
 lib/TraceParser/Hooks.pm                       |  126 +++++++++++++++++++++--
 lib/TraceParser/Trace.pm                       |  103 +++++++++++++++++++-
 template/en/default/pages/trace.html.tmpl      |   23 +++++
 template/en/global/messages-messages.html.tmpl |    7 ++
 template/en/global/user-error-errors.html.tmpl |   24 +++++
 7 files changed, 319 insertions(+), 12 deletions(-)
---
diff --git a/code/db_schema-abstract_schema.pl b/code/db_schema-abstract_schema.pl
index fd3a04d..4fe22b4 100644
--- a/code/db_schema-abstract_schema.pl
+++ b/code/db_schema-abstract_schema.pl
@@ -46,3 +46,18 @@ $schema->{trace} = {
         trace_comment_id_idx => {TYPE => 'UNIQUE', FIELDS => ['comment_id']},
     ],
 };
+
+$schema->{trace_dup} = {
+    FIELDS => [
+        hash      => {TYPE => 'char(22)', NOTNULL => 1},
+        identical => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0},
+        bug_id    => {TYPE => 'INT3', NOTNULL => 1, 
+                      REFERENCES => {TABLE  => 'bugs',
+                                     COLUMN => 'bug_id'}},
+    ],
+    INDEXES => [
+        trace_dup_hash_idx => {TYPE => 'UNIQUE', 
+                               FIELDS => [qw(hash identical)]},
+        trace_bug_id_idx   => ['bug_id'],
+    ],
+};
diff --git a/code/install-before_final_checks.pl b/code/install-before_final_checks.pl
new file mode 100644
index 0000000..736ddb7
--- /dev/null
+++ b/code/install-before_final_checks.pl
@@ -0,0 +1,33 @@
+# -*- 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 Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical Ltd. are Copyright (C) 2009 
+# Canonical Ltd. All Rights Reserved.
+#
+# Contributor(s): 
+#   Max Kanat-Alexander <mkanat bugzilla org>
+
+
+use strict;
+use warnings;
+use Bugzilla;
+use Bugzilla::Group;
+
+if (!new Bugzilla::Group({ name => 'traceparser_edit' })) {
+    Bugzilla::Group->create({
+        name        => 'traceparser_edit',
+        description => 'Can edit properties of traces',
+        isbuggroup  => 0 });
+}
diff --git a/lib/TraceParser/Hooks.pm b/lib/TraceParser/Hooks.pm
index 65a93c3..dc9ece5 100644
--- a/lib/TraceParser/Hooks.pm
+++ b/lib/TraceParser/Hooks.pm
@@ -22,11 +22,16 @@
 package TraceParser::Hooks;
 use strict;
 use base qw(Exporter);
+use Bugzilla::Bug;
+use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Install::Util qw(indicate_progress);
 use Bugzilla::Util qw(detaint_natural);
+
 use TraceParser::Trace;
 
+use List::Util;
+
 our @EXPORT = qw(
     bug_create
     bug_update
@@ -43,7 +48,93 @@ sub bug_create {
     my $comment = $bug->longdescs->[0];
     my $data = TraceParser::Trace->parse_from_text($comment->{body});
     return if !$data;
-    TraceParser::Trace->create({ %$data, comment_id => $comment->{id} });
+    my $trace = TraceParser::Trace->create(
+        { %$data, comment_id => $comment->{id} });
+    _check_duplicate_trace($trace, $bug, $comment);
+}
+
+sub _check_duplicate_trace {
+    my ($trace, $bug, $comment) = @_;
+    my $dbh = Bugzilla->dbh;
+    my $user = Bugzilla->user;
+
+    if (my $dup_to = $trace->must_dup_to) {
+        $dbh->bz_rollback_transaction if $dbh->bz_in_transaction;
+        if ($user->can_edit_product($dup_to->product_id)
+            and $user->can_see_bug($dup_to))
+        {
+            _handle_dup_to($trace, $dup_to, $comment);
+        }
+        else {
+            ThrowUserError('traceparser_dup_to_hidden',
+                           { dup_to => $dup_to });
+        }
+    }
+
+    my $identical = $trace->identical_traces;
+    my $similar   = $trace->similar_traces;
+    my $product = $bug->product;
+    my @prod_identical = grep { $_->bug->product eq $product } @$identical;
+    my @prod_similar   = grep { $_->bug->product eq $product } @$identical;
+}
+
+sub _handle_dup_to {
+    my ($trace, $dup_to, $comment) = @_;
+    my $user = Bugzilla->user;
+
+    if ($dup_to->isopened) {
+        $dup_to->add_cc($user);
+
+        # If this trace is higher quality than any other trace on the
+        # bug, then we add the comment. Otherwise we just skip the
+        # comment entirely.
+        my $bug_traces = TraceParser::Trace->traces_on_bug($dup_to);
+        my $higher_quality_traces;
+        foreach my $t (@$bug_traces) {
+            if ($t->quality >= $trace->quality) {
+                $higher_quality_traces = 1;
+                last;
+            }
+        }
+
+        if (!$higher_quality_traces) {
+            $dup_to->add_comment($comment->{thetext}, $comment);
+        }
+
+        $dup_to->update();
+        if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
+            my $template = Bugzilla->template;
+            my $cgi = Bugzilla->cgi;
+            my $vars = {};
+            # Do the various silly things required to display show_bug.cgi
+            # in Bugzilla 3.4.
+            $vars->{use_keywords} = 1 if Bugzilla::Keyword::keyword_count();
+            $vars->{bugs} = [$dup_to];
+            $vars->{bugids} = [$dup_to->id];
+            if ($cgi->cookie("BUGLIST")) {
+                $vars->{bug_list} = [split(/:/, $cgi->cookie("BUGLIST"))];
+            }
+            eval {
+                require PatchReader;
+                $vars->{'patchviewerinstalled'} = 1;
+            };
+            $vars->{added_comment} = !$higher_quality_traces;
+            $vars->{message} = 'traceparser_dup_to';
+            print $cgi->header;
+            $template->process('bug/show.html.tmpl', $vars)
+                or ThrowTemplateError($template->error);
+            exit;
+        }
+        else {
+            ThrowUserError('traceparser_dup_to',
+                           { dup_to => $dup_to, 
+                             comment_added => !$higher_quality_traces });
+        }
+    }
+    else {
+        ThrowUserError('traceparser_dup_to_closed',
+                       { dup_to => $dup_to });
+    }
 }
 
 sub bug_update {
@@ -148,21 +239,34 @@ sub page {
 
 sub _page_trace {
     my $vars = shift;
+    my $cgi = Bugzilla->cgi;
+    my $dbh = Bugzilla->dbh;
+    my $user = Bugzilla->user;
 
-    my $trace_id = Bugzilla->cgi->param('trace_id');
+    my $trace_id = $cgi->param('trace_id');
     my $trace = TraceParser::Trace->check({ id => $trace_id });
     $trace->bug->check_is_visible;
 
+    my $action = $cgi->param('action') || '';
+    if ($action eq 'update') {
+        $user->in_group('traceparser_edit')
+          or ThrowUserError('auth_failure', 
+                 { action => 'modify', group => 'traceparser_edit',
+                   object => 'settings' });
+        if (!$trace->stack_hash) {
+            ThrowUserError('traceparser_trace_too_short');
+        }
+        my $ident_dup = $cgi->param('identical_dup');
+        my $similar_dup = $cgi->param('similar_dup');
+        $dbh->bz_start_transaction();
+        $trace->update_identical_dup($ident_dup);
+        $trace->update_similar_dup($similar_dup);
+        $dbh->bz_commit_transaction();
+    }
+
     if ($trace->stack_hash) {
-        my $identical_traces = TraceParser::Trace->match(
-            { stack_hash => $trace->stack_hash });
-        my $similar_traces = TraceParser::Trace->match(
-            { short_hash => $trace->short_hash });
-        # Remove identical traces.
-        my %identical = map { $_->id => 1 } @$identical_traces;
-        @$similar_traces = grep { !$identical{$_->id} } @$similar_traces;
-        # Remove this trace from the identical traces.
-        @$identical_traces = grep { $_->id != $trace->id } @$identical_traces;
+        my $identical_traces = $trace->identical_traces;
+        my $similar_traces = $trace->similar_traces;
 
         my %ungrouped = ( identical => $identical_traces, 
                           similar   => $similar_traces );
diff --git a/lib/TraceParser/Trace.pm b/lib/TraceParser/Trace.pm
index 9efbfc7..735f87c 100644
--- a/lib/TraceParser/Trace.pm
+++ b/lib/TraceParser/Trace.pm
@@ -26,6 +26,7 @@ use base qw(Bugzilla::Object);
 use Bugzilla::Bug;
 use Bugzilla::Error;
 use Bugzilla::Util;
+use Scalar::Util qw(blessed);
 
 use Parse::StackTrace;
 use Digest::MD5 qw(md5_base64);
@@ -173,11 +174,24 @@ sub parse_from_text {
 }
 
 sub _hash {
-    my $str = shift;
+    my ($str) = @_;
     utf8::encode($str) if utf8::is_utf8($str);
     return md5_base64($str);
 }
 
+#################
+# Class Methods #
+#################
+
+sub traces_on_bug {
+    my ($class, $bug) = @_;
+    my $bug_id = blessed $bug ? $bug->id : $bug;
+    my $comment_ids = Bugzilla->dbh->selectcol_arrayref(
+        'SELECT comment_id FROM longdescs WHERE bug_id = ?',
+        undef, $bug_id);
+    return $class->match({ comment_id => $comment_ids });
+}
+
 ###############################
 ####      Accessors      ######
 ###############################
@@ -216,6 +230,22 @@ sub crash_thread {
     return $st->thread_with_crash || $st->threads->[0];
 }
 
+sub identical_traces {
+    my $self = shift;
+    return $self->{identical_traces} if exists $self->{identical_traces};
+    my $class = ref $self;
+    my $identical ||= $class->match({ stack_hash => $self->stack_hash });
+    @$identical = grep { $_->id != $self->id } @$identical;
+    $self->{identical_traces} = $identical;
+    return $self->{identical_traces};
+}
+
+sub must_dup_to {
+    my $self = shift;
+    my $id = $self->identical_dup_id || $self->similar_dup_id;
+    return new Bugzilla::Bug($id);
+}
+
 sub _important_stack_frames {
     my ($invocant, $st) = @_;
     $st ||= $invocant->stack;
@@ -268,6 +298,18 @@ sub short_stack {
     return \ short_stack;
 }
 
+# Gets similar traces without also listing identical traces in the list.
+sub similar_traces {
+    my $self = shift;
+    return $self->{similar_traces} if exists $self->{similar_traces};
+    my $class = ref $self;
+    my $similar = $class->match({ short_hash => $self->short_hash });
+    my %identical = map { $_->id => 1 } @{ $self->identical_traces };
+    @$similar = grep { !$identical{$_->id} and $_->id != $self->id } @$similar;
+    $self->{similar_traces} = $similar;
+    return $similar;
+}
+
 sub stack {
     my $self = shift;
     my $type = $self->type;
@@ -276,6 +318,65 @@ sub stack {
     return $self->{stack};
 }
 
+###########################
+# Trace Duplicate Methods #
+###########################
+
+sub identical_dup_id {
+    my $self = shift;
+    return $self->{identical_dup_id} if exists $self->{identical_dup_id};
+    $self->{identical_dup_id} = Bugzilla->dbh->selectrow_array(
+        'SELECT bug_id FROM trace_dup WHERE hash = ? AND identical = 1',
+        undef, $self->stack_hash);
+    return $self->{identical_dup_id};
+}
+
+sub similar_dup_id {
+    my $self = shift;
+    return $self->{similar_dup_id} if exists $self->{similar_dup_id};
+    $self->{similar_dup_id} = Bugzilla->dbh->selectrow_array(
+        'SELECT bug_id FROM trace_dup WHERE hash = ? AND identical = 0',
+        undef, $self->short_hash);
+    return $self->{similar_dup_id};
+}
+
+sub update_identical_dup {
+    my ($self, $bug_id) = @_;
+    _update_dup($self->stack_hash, 1, $bug_id);
+}
+
+sub update_similar_dup {
+    my ($self, $bug_id) = @_;
+    _update_dup($self->short_hash, 0, $bug_id);
+}
+
+sub _update_dup {
+    my ($hash, $identical, $bug_id) = @_;
+    my $dbh = Bugzilla->dbh;
+    if (!$bug_id) {
+        $dbh->do("DELETE FROM trace_dup WHERE hash = ? AND identical = ?",
+                 undef, $hash, $identical);
+        return;
+    }
+
+    my $bug = Bugzilla::Bug->check($bug_id);
+    $bug_id = $bug->id; # detaint $bug_id
+
+    my $exists = $dbh->selectrow_array(
+        'SELECT 1 FROM trace_dup WHERE hash = ? AND identical = ?',
+        undef, $hash, $identical);
+    if ($exists) {
+        $dbh->do('UPDATE trace_dup SET bug_id = ? 
+                   WHERE hash = ? AND identical = ?', 
+                 undef, $bug_id, $hash, $identical);
+    }
+    else {
+        $dbh->do('INSERT INTO trace_dup (bug_id, hash, identical)
+                       VALUES (?,?,?)', undef, $bug_id, $hash, $identical);
+    }
+}
+
+
 ###############################
 ###       Validators        ###
 ###############################
diff --git a/template/en/default/pages/trace.html.tmpl b/template/en/default/pages/trace.html.tmpl
index 8b87813..7e1b2cb 100644
--- a/template/en/default/pages/trace.html.tmpl
+++ b/template/en/default/pages/trace.html.tmpl
@@ -23,6 +23,29 @@
    title = "Trace $trace.id From Bug $trace.bug.id" 
 %]
 
+[% IF user.in_group('traceparser_edit') %]
+  <h2>Properties of trace [% trace.id FILTER html %]</h2>
+  <form action="page.cgi?id=trace.html&amp;trace_id=
+                [%- trace.id FILTER url_quote %]"
+        method="POST">
+  <div>
+    <p>If a trace with an <em>identical</em> function stack is submitted,
+    automatically refer the user to this [% terms.bug %]:
+    <input type="text" size="5" id="identical_dup" name="identical_dup"
+           value="[% trace.identical_dup_id FILTER html %]"></p>
+
+    <p>If a trace with a <em>similar</em> (but <strong>not</strong>
+      identical) function stack is submitted, automatically refer
+      to user to this [% terms.bug %]:
+      <input type="text" size="5" id="similar_dup" name="similar_dup"
+             value="[% trace.similar_dup_id FILTER html %]"></p>
+
+    <input type="hidden" name="action" value="update">
+    <input type="submit" value="Submit" id="submit_trace">
+  </div>
+  </form>
+[% END %]
+
 [% IF identical_traces.size %]
   <h2>Traces with an identical stack:</h2>
   [% PROCESS trace_list list = identical_traces %]
diff --git a/template/en/global/messages-messages.html.tmpl b/template/en/global/messages-messages.html.tmpl
new file mode 100644
index 0000000..23265ae
--- /dev/null
+++ b/template/en/global/messages-messages.html.tmpl
@@ -0,0 +1,7 @@
+[% IF message_tag == "traceparser_dup_to" %]
+    Your crash is a duplicate of the [% terms.bug %] you see below.
+    You have been added to the CC list of this [% terms.bug %]
+    [%~ IF comment_added ~%]
+       , and your crash information has been added to it.
+    [%~ END %].
+[% END %]
diff --git a/template/en/global/user-error-errors.html.tmpl b/template/en/global/user-error-errors.html.tmpl
new file mode 100644
index 0000000..8fc273a
--- /dev/null
+++ b/template/en/global/user-error-errors.html.tmpl
@@ -0,0 +1,24 @@
+[% IF error == "traceparser_dup_to" %]
+    [% title = "Stack Trace Is a Duplicate" %]
+    Thank you for submitting your crash. This crash is a duplicate of
+    [%+ "$terms.bug $dup_to.id" FILTER bug_link(dup_to) %].
+    You have been added to the CC list of that [% terms.bug %]
+    [%~ IF comment_added ~%]
+       , and your crash information has been added to it.
+    [%~ END %].
+
+[% ELSIF error == "traceparser_dup_to_closed" %]
+    [% title = "Stack Trace Is a Duplicate" %]
+    The crash you submitted appears to be a duplicate of 
+    [%+ "$terms.bug $dup_to.id" FILTER bug_link(dup_to) %].
+
+    [% IF dup_to.resolution == 'FIXED' %]
+      This [% terms.bug %] has already been fixed.
+    [% END %]
+
+    See [% "the $terms.bug" FILTER bug_link(dup_to) %] for more information.
+
+[% ELSIF error == "traceparser_trace_too_short" %]
+    [% title = "Trace Too Short To Edit" %]
+    This trace is too short to set properties for it.
+[% END %]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]