From: adefaria Date: Sat, 1 Nov 2014 00:41:27 +0000 (-0700) Subject: Some JIRA scripts X-Git-Url: https://defaria.com/gitweb/?a=commitdiff_plain;h=dbbd4da4b211ec44626801cbf41476980d211751;p=clearscm.git Some JIRA scripts --- diff --git a/audience/JIRA/importComments.pl b/audience/JIRA/importComments.pl new file mode 100644 index 0000000..6a1712c --- /dev/null +++ b/audience/JIRA/importComments.pl @@ -0,0 +1,237 @@ +#!/usr/bin/perl +use strict; +use warnings; + +=pod + +=head1 NAME importComments.pl + +This will import the comments from Bugzilla and update the corresponding JIRA +Issues. + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2014/05/23 $ + +=back + +=head1 SYNOPSIS + + $ importComments.pl [-bugzillaserver ] [-login ] + [-jiraserver ] + [-username ] [-password ] + [-bugids bugid,bugid,... | -file ] + [-[no]exec] + [-verbose] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -he|lp: Display full help + -usa|ge: Display usage + -[no]e|xec: Whether or not to update JIRA. -noexec says only + tell me what you would have updated. + -use|rname: Username to log into JIRA with (Default: jira-admin) + -p|assword: Password to log into JIRA with (Default: jira-admin's + password) + -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev) + -jiraserver: Machine where Jira lives (Default: jira-dev) + -bugi|ds: Comma separated list of BugIDs to process + -f|ile: File of BugIDs, one per line + +=head1 DESCRIPTION + +This will import the comments from Bugzilla and update the corresponding JIRA +Issues. + +=cut + +use FindBin; +use lib "$FindBin::Bin/lib"; + +$| = 1; + +use DBI; +use Display; +use Logger; +use TimeUtils; +use Utils; +use JIRAUtils; +use BugzillaUtils; + +use Getopt::Long; +use Pod::Usage; + +our %opts = ( + exec => 0, + bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev', + jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081', + username => $ENV{USERNAME}, + password => $ENV{PASSWORD}, + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, + verbose => sub { set_verbose }, + quiet => 0, +); + +our ($log, %total); + +sub sanitize ($) { + my ($str) = @_; + + my $p4web = 'http://p4web.audience.local:8080/@md=d&cd=//&c=vLW@/'; + my $bugzilla = 'http://bugs.audience.com/show_bug.cgi?id='; + + # 0x93 (147) and 0x94 (148) are "smart" quotes + $str =~ s/[\x93\x94]/"/gm; + # 0x91 (145) and 0x92 (146) are "smart" singlequotes + $str =~ s/[\x91\x92]/'/gm; + # 0x96 (150) and 0x97 (151) are emdashes + $str =~ s/[\x96\x97]/--/gm; + # 0x85 (133) is an ellipsis + $str =~ s/\x85/.../gm; + # 0x95 • replacement for unordered list + $str =~ s/\x95/*/gm; + + # Make P4Web links for "CL (\d{3,6}+)" + $str =~ s/CL\s*(\d{3,6}+)/CL \[$1|${p4web}$1\?ac=10\]/igm; + + # Make Bugzilla links for "Bug ID (\d{1,5}+)" + $str =~ s/Bug\s*ID\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm; + + # Make Bugzilla links for "Bug # (\d{1,5}+)" + $str =~ s/Bug\s*#\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm; + + # Make Bugzilla links for "Bug (\d{1,5}+)" + $str =~ s/Bug\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm; + + # Convert bug URLs to be more proper + $str =~ s/https\:\/\/bugs\.audience\.com\/show_bug\.cgi\?id=(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm; + + return $str; +} # sanitize + +sub addComments ($$) { + my ($jiraIssue, $bugid) = @_; + + my @comments = @{getBugComments ($bugid)}; + + # Note: In Bugzilla the first comment is considered the description. + my $description = shift @comments; + + my $result = addDescription $jiraIssue, sanitize $description; + + $total{'Descriptions added'}++; + + return $result if $result =~ /^Unable to add comment/; + + # Process the remaining comments + for (@comments) { + $result = addJIRAComment $jiraIssue, sanitize $_; + + if ($result =~ /Comment added/) { + $total{'Comments imported'}++; + } else { + return $result; + } # if + } # for + + $result = '' unless $result; + + return $result; +} # addComments + +sub main () { + my $startTime = time; + + GetOptions ( + \%opts, + 'verbose', + 'usage', + 'help', + 'exec!', + 'quiet', + 'username=s', + 'password=s', + 'bugids=s@', + 'file=s', + 'jiraserver=s', + 'bugzillaserver=s', + 'linkbugzilla', + 'relinkbugzilla' + ) or pod2usage; + + $log = Logger->new; + + if ($opts{file}) { + open my $file, '<', $opts{file} + or $log->err ("Unable to open $opts{file} - $!", 1); + + $opts{bugids} = [<$file>]; + + chomp @{$opts{bugids}}; + } else { + my @bugids; + + push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); + + $opts{bugids} = [@bugids]; + } # if + + pod2usage 'Must specify -bugids [,,...] or -file ' + unless $opts{bugids}; + + openBugzilla $opts{bugzillaserver} + or $log->err ("Unable to connect to $opts{bugzillaserver}", 1); + + Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver}) + or $log->err ("Unable to connect to $opts{jiraserver}", 1); + + $log->msg ("Processing comments"); + + for (@{$opts{bugids}}) { + my $jiraIssue = findIssue $_; + + if ($jiraIssue =~ /^[A-Z]{1,5}-\d+$/) { + my $result = addComments $jiraIssue, $_; + + if ($result =~ /^Unable/) { + $total{'Comment failures'}++; + + $log->err ("Unable to add comments for $jiraIssue ($_)\n$result"); + } elsif ($result =~ /^Comment added/) { + $log->msg ("Added comments for $jiraIssue ($_)"); + } elsif ($result =~ /^Would have linked/) { + $total{'Comments would be added'}++; + } # if + } else { + $total{'Missing JIRA Issues'}++; + + $log->err ("Unable to find JIRA Issue for Bug $_"); + } # if + } # for + + display_duration $startTime, $log; + + Stats (\%total, $log) unless $opts{quiet}; + + return 0; +} # main + +exit main; diff --git a/audience/JIRA/jiradep.pl b/audience/JIRA/jiradep.pl new file mode 100644 index 0000000..a8ce4c4 --- /dev/null +++ b/audience/JIRA/jiradep.pl @@ -0,0 +1,372 @@ +#!/usr/bin/perl +use strict; +use warnings; + +=pod + +=head1 NAME jiradep.pl + +Update Bugzilla dependencies (Dependencies/Blockers/Duplicates and Related), +transfering those relationships over to any matching JIRA issues. + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2014/05/23 $ + +=back + +=head1 SYNOPSIS + + $ jiradep.pl [-bugzillaserver ] [-login ] + [-jiraserver ] + [-username ] [-password ] + [-bugids bugid,bugid,... | -file ] + [-[no]exec] [-linkbugzilla] [-relinkbugzilla] + [-verbose] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -he|lp: Display full help + -usa|ge: Display usage + -[no]e|xec: Whether or not to update Bugilla. -noexec says only + tell me what you would have updated. + -use|rname: Username to log into JIRA with (Default: jira-admin) + -p|assword: Password to log into JIRA with (Default: jira-admin's + password) + -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev) + -jiraserver: Machine where Jira lives (Default: jira-dev) + -bugi|ds: Comma separated list of BugIDs to process + -f|ile: File of BugIDs, one per line + -linkbugzilla: If specified and we find that we cannot translate + a Bugzilla Bud ID to a JIRA Issue then create a + remote link for the Bugzilla Bug. (Default: + do not create Bugzilla remote links). + -relinkbugzilla: Scan current Remote Bugzilla links and if there + exists a corresponding JIRA issue, remove the + Remote Bugzilla link and make it a JIRA Issue + link. + -jiradbhost: Host name of the machine where the MySQL jiradb + database is located (Default: cm-db-ldev01) + +=head1 DESCRIPTION + +This script will process all BugIDs translating them into JIRA Issues, if +applicable. It will then determine the relationships of this BugID in Bugzilla - +what it blocks, what it depends on, if it's a duplicate of another bug or if +it has any related links. Those too will be translated to JIRA issues, again, +if applicable. Then the JIRA issue will be updates to reflect these +relationships. + +Note that it's not known at this time what to do for situations where BugIDs +cannot be translated into JIRA issues if such Bugzilla bugs have not yet been +migrated to JIRA. There's a though to simply make a Bugzilla Link but we will +need to keep that in mind and when we import the next project to JIRA these +old, no longer used Bugzilla Links should be converted to their corresponding +JIRA issue. Perhaps this script can do that too. + +=cut + +use FindBin; +use lib "$FindBin::Bin/lib"; + +$| = 1; + +use DBI; +use Display; +use Logger; +use TimeUtils; +use Utils; +use JIRAUtils; +use BugzillaUtils; + +use Getopt::Long; +use Pod::Usage; + +our %opts = ( + exec => 0, + bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev', + jiraserver => $ENV{JIRASERVER} || 'jira-dev', + jiradbhost => $ENV{JIRA_DB_HOST} || 'cm-db-ldev01', + username => 'jira-admin', + password => 'jira-admin', + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, + verbose => sub { set_verbose }, + quiet => 0, + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, +); + +our ($log, %total); + +my %relationshipMap = ( + Blocks => 'Dependencies Linked', + Duplicate => 'Duplicates Linked', + Related => 'Related Linked', +); + +sub callLink ($$$$) { + my ($from, $type, $to, $counter) = @_; + + my $bugzillaType; + + if ($from =~ /^\d+/) { + if ($type eq 'Blocks') { + $bugzillaType = 'is blocked by (Bugzilla)'; + } elsif ($type eq 'Duplicate') { + $bugzillaType = 'duplicate (Bugzilla)'; + } elsif ($type eq 'Related') { + $bugzillaType = 'related (Bugzilla)'; + } # if + } elsif ($to =~ /^\d+/) { + if ($type eq 'Blocks') { + $bugzillaType = 'blocks (Bugzilla)'; + } elsif ($type eq 'Duplicate') { + $bugzillaType = 'duplicate (Bugzilla)'; + } elsif ($type eq 'Related') { + $bugzillaType = 'related (Bugzilla)'; + } # if + } # if + + $total{$counter}++; + + if ($from =~ /^\d+/ && $to =~ /^\d+/) { + $total{'Skipped Bugzilla Links'}++; + + return "Refusing to link because both from ($from) and to ($to) links are still a Bugzilla link"; + } elsif ($from =~ /^\d+/) { + if ($opts{linkbugzilla}) { + my $result = addRemoteLink $from, $bugzillaType, $to; + + $total{'Bugzilla Links'}++ unless $result; + + if ($result eq '') { + return "Created remote $type link between Issue $to and Bug $from"; + } else { + return $result; + } # if + } else { + $total{'Skipped Bugzilla Links'}++; + + return "Refusing to link because from link ($from) is still a Bugzilla link"; + } # if + } elsif ($to =~ /^\d+/) { + if ($opts{linkbugzilla}) { + my $result = addRemoteLink $to, $bugzillaType, $from; + + $total{'Bugzilla Links'}++ unless $result; + + if (!defined $result) { + print "huh?"; + } + if ($result eq '') { + return "Created remote $type link between Issue $from and Bug $to"; + } else { + return $result; + } # if + } else { + $total{'Skipped Bugzilla Links'}++; + + return "Refusing to link because to link ($to) is still a Bugzilla link"; + } # if + } # if + + my $result = linkIssues $from, $type, $to; + + $log->msg ($result); + + if ($result =~ /^Unable/) { + $total{'Link Failures'}++; + } elsif ($result =~ /^Link made/) { + $total{'Links made'}++; + } elsif ($result =~ /^Would have linked/) { + $total{'Links would be made'}++; + } # if + + return; +} # callLink + +sub relinkBugzilla (@) { + my (@bugids) = @_; + + my %mapRelationships = ( + 'blocks (Bugzilla)' => 'Blocks', + 'is blocked by (Bugzilla)' => 'Blocks', + 'duplicates (Bugzilla)' => 'Duplicates', + 'is duplicated by (Bugzilla)' => 'Duplicates', + # old versions... + 'Bugzilla blocks' => 'Blocks', + 'Bugzilla is blocked by' => 'Blocks', + 'Bugzilla duplicates' => 'Duplicates', + 'Bugzilla is duplicated by' => 'Duplicates', + ); + + @bugids = getRemoteLinks unless @bugids; + + for my $bugid (@bugids) { + $total{'Remote Links Scanned'}++; + + my $links = findRemoteLinkByBugID $bugid; + + my $jirafrom = findIssue ($bugid); + + next if $jirafrom !~ /^[A-Z]{1,5}-\d+$/; + + for (@$links) { + my %link = %$_; + + # Found a link to JIRA. Remove remotelink and make an issuelink + if ($mapRelationships{$link{relationship}}) { + my ($fromIssue, $toIssue); + + if ($link{relationship} =~ / by/) { + $fromIssue = $jirafrom; + $toIssue = $link{issue}; + } else { + $fromIssue = $link{issue}; + $toIssue = $jirafrom; + } # if + + my $status = promoteBug2JIRAIssue $bugid, $fromIssue, $toIssue, + $mapRelationships{$link{relationship}}; + + $log->err ($status) if $status =~ /Unable to link/; + } else { + $log->err ("Unable to handle relationships of type $link{relationship}"); + } # if + } # for + } # for + + return; +} # relinkBugzilla + +sub main () { + my $startTime = time; + + GetOptions ( + \%opts, + 'verbose', + 'usage', + 'help', + 'exec!', + 'quiet', + 'username=s', + 'password=s', + 'bugids=s@', + 'file=s', + 'jiraserver=s', + 'bugzillaserver=s', + 'linkbugzilla', + 'relinkbugzilla', + 'jiradbhost=s', + ) or pod2usage; + + $log = Logger->new; + + if ($opts{file}) { + open my $file, '<', $opts{file} + or $log->err ("Unable to open $opts{file} - $!", 1); + + $opts{bugids} = [<$file>]; + + chomp @{$opts{bugids}}; + } else { + my @bugids; + + push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); + + $opts{bugids} = [@bugids]; + } # if + + pod2usage 'Must specify -bugids [,,...] or -file ' + unless ($opts{bugids} > 0 or $opts{relinkbugzilla}); + + openBugzilla $opts{bugzillaserver} + or $log->err ("Unable to connect to $opts{bugzillaserver}", 1); + + Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver}) + or $log->err ("Unable to connect to $opts{jiraserver}", 1); + + if ($opts{relinkbugzilla}) { + unless (@{$opts{bugids}}) { + relinkBugzilla; + } else { + relinkBugzilla $_ for @{$opts{bugids}} + } # unless + + Stats (\%total, $log); + + exit $log->errors; + } # if + + my %relationships; + + # The 'Blocks' IssueLinkType has two types of relationships in it - both + # blocks and dependson. Since JIRA has only one type - Blocks - we take + # the $dependson and flip the from and to. + my $blocks = getBlockers @{$opts{bugids}}; + my $dependson = getDependencies @{$opts{bugids}}; + + # Now merge them - we did it backwards! + for my $fromLink (keys %$dependson) { + for my $toLink (@{$dependson->{$fromLink}}) { + push @{$relationships{Blocks}{$toLink}}, $fromLink; + } # for + } # for + + #%{$relationships{Blocks}} = %$dependson; + + for my $fromLink (keys %$blocks) { + # Check to see if we already have the reverse of this link + for my $toLink (@{$blocks->{$fromLink}}) { + unless (grep {$toLink eq $_} keys %{$relationships{Blocks}}) { + push @{$relationships{Blocks}{$fromLink}}, $toLink; + } # unless + } # for + } # for + + $relationships{Duplicate} = getDuplicates @{$opts{bugids}}; + $relationships{Relates} = getRelated @{$opts{bugids}}; + + # Process relationships (social programming... ;-) + $log->msg ("Processing relationships"); + + for my $type (keys %relationshipMap) { + for my $from (keys %{$relationships{$type}}) { + for my $to (@{$relationships{$type}{$from}}) { + $total{'Relationships processed'}++; + + my $result = callLink $from, $type, $to, $relationshipMap{$type}; + + $log->msg ($result) if $result; + } # for + } # for + } # if + + display_duration $startTime, $log; + + Stats (\%total, $log) unless $opts{quiet}; + + return; +} # main + +main; + +exit; \ No newline at end of file diff --git a/audience/JIRA/lib/BugzillaUtils.pm b/audience/JIRA/lib/BugzillaUtils.pm new file mode 100644 index 0000000..899f506 --- /dev/null +++ b/audience/JIRA/lib/BugzillaUtils.pm @@ -0,0 +1,322 @@ +=pod + +=head1 NAME $RCSfile: BugzillaUtils.pm,v $ + +Some shared functions dealing with Bugzilla. Note this uses DBI to directly +access Bugzilla's database. This requires that your userid was granted access. +For this I setup adefaria with pretty much read only access. + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: 1.0 $ + +=item Created + +Fri Mar 12 10:17:44 PST 2004 + +=item Modified + +$Date: 2013/05/30 15:48:06 $ + +=head1 ROUTINES + +The following routines are exported: + +=cut + +package BugzillaUtils; + +use strict; +use warnings; + +use base 'Exporter'; + +use FindBin; +use Display; +use Carp; +use DBI; + +use lib 'lib'; + +use JIRAUtils; + +our $bugzilla; + +our @EXPORT = qw ( + openBugzilla + getRelationships + getDependencies + getBlockers + getDuplicates + getRelated + getBug + getBugComments + getWatchers +); + +sub _checkDBError ($$) { + my ($msg, $statement) = @_; + + my $dberr = $bugzilla->err; + my $dberrmsg = $bugzilla->errstr; + + $dberr ||= 0; + $dberrmsg ||= 'Success'; + + my $message = ''; + + if ($dberr) { + my $function = (caller (1)) [3]; + + $message = "$function: $msg\nError #$dberr: $dberrmsg\n" + . "SQL Statement: $statement"; + } # if + + $main::log->err ($message, $dberr) if $dberr; + + return; +} # _checkDBError + +sub openBugzilla (;$$$$) { + my ($dbhost, $dbname, $dbuser, $dbpass) = @_; + + $dbhost //= 'jira-dev'; + $dbname //= 'bugzilla'; + $dbuser //= 'adefaria'; + $dbpass //= 'reader'; + + $main::log->msg ("Connecting to Bugzilla ($dbuser\@$dbhost)"); + + $bugzilla = DBI->connect ( + "DBI:mysql:$dbname:$dbhost", + $dbuser, + $dbpass, { + PrintError => 0, + RaiseError => 1, + }, + ); + + _checkDBError 'Unable to execute statement', 'Connect'; + + return $bugzilla; +} # openBugzilla + +sub getBug ($;@) { + my ($bugid, @fields) = @_; + + push @fields, 'short_desc' unless @fields; + + my $statement = 'select ' . join (',', @fields) . + " from bugs where bug_id = $bugid"; + + my $sth = $bugzilla->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + _checkDBError 'Unable to execute statement', $statement; + + $sth->execute; + + return $sth->fetchrow_hashref; +} # getBug + +sub getBugComments ($) { + my ($bugid) = @_; + + my $statement = <<"END"; +select + bug_id, + bug_when, + substring_index(login_name,'\@',1) as username, + thetext +from + longdescs, + profiles +where + who = userid and + bug_id = $bugid +END + + my $sth = $bugzilla->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'Unable to execute statement', $statement; + + my @comments; + + while (my $comment = $sth->fetchrow_hashref) { + my $commentText = <<"END"; +The following comment was entered by [~$comment->{username}] on $comment->{bug_when}: + +$comment->{thetext} +END + + push @comments, $commentText; + } # while + + return \@comments; +} # getBugComments + +sub getRelationships ($$$$@) { + my ($table, $returnField, $testField, $relationshipType, @bugs) = @_; + + $main::log->msg ("Getting $relationshipType"); + + my $statement = "select $returnField from $table where $table.$testField = ?"; + + my $sth = $bugzilla->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + my %relationships; + + my %bugmap; + + map {$bugmap{$_} = 1} @bugs unless %bugmap; + + for my $bugid (@bugs) { + $sth->execute ($bugid); + + _checkDBError 'Unable to exit statement', $statement; + + my $result = JIRAUtils::findIssue ($bugid, %bugmap); + + if ($result =~ /^Unable/) { + $main::log->warn ($result); + + $main::total{'Missing JIRA Issues'}++; + + undef $result; + } elsif ($result =~ /^Future/) { + $main::total{'Future JIRA Issues'}++; + + undef $result; + } # if + + my $jiraIssue = $result; + my $key = $jiraIssue || $bugid; + + my @relationships; + my $relations = $sth->fetchall_arrayref; + my @relations; + + map {push @relations, $_->[0]} @$relations; + + for my $relation (@relations) { + $jiraIssue = JIRAUtils::findIssue ($relation); + + if ($jiraIssue =~ /^Unable/ || $jiraIssue =~ /^Future/) { + $main::log->warn ($jiraIssue); + + $main::total{'Missing JIRA Issues'}++ if $jiraIssue =~ /^Unable/; + $main::total{'Future JIRA Issues'}++ if $jiraIssue =~ /^Future/; + + push @relationships, $relation; + } else { + push @relationships, $jiraIssue; + } # if + } # for + + push @{$relationships{$key}}, @relationships if @relationships; + } # for + + $main::total{$relationshipType} = keys %relationships; + + return \%relationships; +} # getRelationships + +sub getDependencies (@) { + my (@bugs) = @_; + + return getRelationships ( + 'dependencies', # table + 'dependson', # returned field + 'blocked', # test field + 'Depends on', # relationship + @bugs + ); +} # getDependencies + +sub getBlockers (@) { + my (@bugs) = @_; + + return getRelationships ( + 'dependencies', + 'blocked', + 'dependson', + 'Blocks', + @bugs + ); +} # getBlockers + +sub getDuplicates (@) { + my (@bugs) = @_; + + return getRelationships ( + 'duplicates', + 'dupe', + 'dupe_of', + 'Duplicates', + @bugs + ); +} # getDuplicates + +sub getRelated (@) { + my (@bugs) = @_; + + return getRelationships ( + 'bug_see_also', + 'value', + 'bug_id', + 'Relates', + @bugs + ); +} # getRelated + +sub getWatchers ($) { + my ($bugid) = @_; + + my $statement = <<"END"; +select + profiles.login_name +from + cc, + profiles +where + cc.who = profiles.userid and + bug_id = ? +END + + my $sth = $bugzilla->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute ($bugid); + + _checkDBError 'Unable to execute statement', $statement; + + my @rows = @{$sth->fetchall_arrayref}; + + my %watchers; + + for (@rows) { + if ($$_[0] =~ /(.*)\@/) { + $watchers{$1} = 1; + } # if + + $main::total{'Watchers Processed'}++; + } # for + + return %watchers; +} # getWatchers \ No newline at end of file diff --git a/audience/JIRA/lib/JIRAUtils.pm b/audience/JIRA/lib/JIRAUtils.pm new file mode 100644 index 0000000..a60e01c --- /dev/null +++ b/audience/JIRA/lib/JIRAUtils.pm @@ -0,0 +1,655 @@ +=pod + +=head1 NAME $RCSfile: JIRAUtils.pm,v $ + +Some shared functions dealing with JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: 1.0 $ + +=item Created + +Fri Mar 12 10:17:44 PST 2004 + +=item Modified + +$Date: 2013/05/30 15:48:06 $ + +=back + +=head1 ROUTINES + +The following routines are exported: + +=cut + +package JIRAUtils; + +use strict; +use warnings; + +use base 'Exporter'; + +use FindBin; +use Display; +use Carp; +use DBI; + +use JIRA::REST; +use BugzillaUtils; + +our ($jira, %opts); + +our @EXPORT = qw ( + addJIRAComment + addDescription + Connect2JIRA + findIssue + getIssue + getIssueLinks + getIssueLinkTypes + getRemoteLinks + updateIssueWatchers + linkIssues + addRemoteLink + getRemoteLink + removeRemoteLink + getRemoteLinkByBugID + promoteBug2JIRAIssue + findRemoteLinkByBugID +); + +my (@issueLinkTypes, %total, %cache, $jiradb); + +sub _checkDBError ($;$) { + my ($msg, $statement) = @_; + + $statement //= 'Unknown'; + + $main::log->err ('JIRA database not opened!', 1) unless $jiradb; + + my $dberr = $jiradb->err; + my $dberrmsg = $jiradb->errstr; + + $dberr ||= 0; + $dberrmsg ||= 'Success'; + + my $message = ''; + + if ($dberr) { + my $function = (caller (1)) [3]; + + $message = "$function: $msg\nError #$dberr: $dberrmsg\n" + . "SQL Statement: $statement"; + } # if + + $main::log->err ($message, 1) if $dberr; + + return; +} # _checkDBError + +sub openJIRADB (;$$$$) { + my ($dbhost, $dbname, $dbuser, $dbpass) = @_; + + $dbhost //= $main::opts{jiradbhost}; + $dbname //= 'jiradb'; + $dbuser //= 'adefaria'; + $dbpass //= 'reader'; + + $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)..."); + + $jiradb = DBI->connect ( + "DBI:mysql:$dbname:$dbhost", + $dbuser, + $dbpass, { + PrintError => 0, + RaiseError => 1, + }, + ); + + _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)"; + + return $jiradb; +} # openJIRADB + +sub Connect2JIRA (;$$$) { + my ($username, $password, $server) = @_; + + my %opts; + + $opts{username} = $username || 'jira-admin'; + $opts{password} = $password || $ENV{PASSWORD} || 'jira-admin'; + $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev:8081'; + $opts{URL} = "http://$opts{server}/rest/api/latest"; + + $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})"); + + $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password}); + + # Store username as we might need it (see updateIssueWatchers) + $jira->{username} = $opts{username}; + + return $jira; +} # Connect2JIRA + +sub addDescription ($$) { + my ($issue, $description) = @_; + + if ($main::opts{exec}) { + eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})}; + + if ($@) { + return "Unable to add description\n$@"; + } else { + return 'Description added'; + } # if + } # if +} # addDescription + +sub addJIRAComment ($$) { + my ($issue, $comment) = @_; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })}; + + if ($@) { + return "Unable to add comment\n$@"; + } else { + return 'Comment added'; + } # if + } else { + return "Would have added comments to $issue"; + } # if +} # addJIRAComment + +sub findIssue ($%) { + my ($bugid, %bugmap) = @_; + +=pod + # Check the cache... + if ($cache{$bugid}) { + if ($cache{$bugid} =~ /^\d+/) { + # We have a cache hit but the contents here are a bugid. This means we had + # searched for the corresponding JIRA issue for this bug before and came + # up empty handed. In this situtaion we really have: + return "Unable to find a JIRA issue for Bug $bugid"; + } else { + return $cache{$bugid}; + } # if + } # if +=cut + my $issue; + + my %query = ( + jql => "\"Bugzilla Bug Number\" ~ $bugid", + fields => [ 'key' ], + ); + + eval {$issue = $jira->GET ("/search/", \%query)}; + + my $issueID = $issue->{issues}[0]{key}; + + if (@{$issue->{issues}} > 2) { + $main::log->err ("Found more than 2 issues for Bug ID $bugid"); + + return "Found more than 2 issues for Bug ID $bugid"; + } elsif (@{$issue->{issues}} == 2) { + my ($issueNum0, $issueNum1); + + if ($issue->{issues}[0]{key} =~ /(\d+)/) { + $issueNum0 = $1; + } # if + + if ($issue->{issues}[1]{key} =~ /(\d+)/) { + $issueNum1 = $1; + } # if + + if ($issueNum0 < $issueNum1) { + $issueID = $issue->{issues}[1]{key}; + } # if + + # Let's mark them as clones. See if this clone link already exists... + my $alreadyCloned; + + for (getIssueLinks ($issueID, 'Cloners')) { + my $inwardIssue = $_->{inwardIssue}{key} || ''; + my $outwardIssue = $_->{outwardIssue}{key} || ''; + + if ("RDBNK-$issueNum0" eq $inwardIssue || + "RDBNK-$issueNum0" eq $outwardIssue || + "RDBNK-$issueNum1" eq $inwardIssue || + "RDBNK-$issueNum1" eq $outwardIssue) { + $alreadyCloned = 1; + + last; + } # if + } # for + + unless ($alreadyCloned) { + my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1"); + + return $result if $result =~ /Unable to/; + + $main::log->msg ($result); + } # unless + } # if + + if ($issueID) { + $main::log->msg ("Found JIRA issue $issueID for Bug $bugid"); + + #$cache{$bugid} = $issueID; + + #return $cache{$bugid}; + return $issueID; + } else { + my $status = $bugmap{$bugid} ? 'Future JIRA Issue' + : "Unable to find a JIRA issue for Bug $bugid"; + + # Here we put this bugid into the cache but instead of a the JIRA issue + # id we put the bugid. This will stop us from adding up multiple hits on + # this bugid. + #$cache{$bugid} = $bugid; + + return $status; + } # if +} # findJIRA + +sub getIssue ($;@) { + my ($issue, @fields) = @_; + + my $fields = @fields ? "?fields=" . join ',', @fields : ''; + + return $jira->GET ("/issue/$issue$fields"); +} # getIssue + +sub getIssueLinkTypes () { + my $issueLinkTypes = $jira->GET ('/issueLinkType/'); + + map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}}; + + return @issueLinkTypes +} # getIssueLinkTypes + +sub linkIssues ($$$) { + my ($from, $type, $to) = @_; + + unless (@issueLinkTypes) { + getIssueLinkTypes; + } # unless + + unless (grep {$type eq $_} @issueLinkTypes) { + $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n" + . join "\n\t", @issueLinkTypes); + + return "Unable to $type link $from -> $to"; + } # unless + + my %link = ( + inwardIssue => { + key => $from, + }, + type => { + name => $type, + }, + outwardIssue => { + key => $to, + }, + comment => { + body => "Link ported as part of the migration from Bugzilla: $from <-> $to", + }, + ); + + $main::total{'IssueLinks Added'}++; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issueLink", undef, \%link)}; + + if ($@) { + return "Unable to $type link $from -> $to\n$@"; + } else { + return "Made $type link $from -> $to"; + } # if + } else { + return "Would have $type linked $from -> $to"; + } # if +} # linkIssue + +sub getRemoteLink ($;$) { + my ($jiraIssue, $id) = @_; + + $id //= ''; + + my $result; + + eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")}; + + return if $@; + + my %remoteLinks; + + if (ref $result eq 'ARRAY') { + map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result; + } else { + $remoteLinks{$result->{id}} = $result->{object}{title}; + } # if + + return \%remoteLinks; +} # getRemoteLink + +sub getRemoteLinks (;$) { + my ($bugid) = @_; + + $jiradb = openJIRADB unless $jiradb; + + my $statement = 'select url from remotelink'; + + $statement .= " where url like 'http://bugs%'"; + $statement .= " and url like '%$bugid'" if $bugid; + $statement .= " group by issueid desc"; + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'Unable to execute statement', $statement; + + my %bugids; + + while (my $record = $sth->fetchrow_array) { + if ($record =~ /(\d+)$/) { + $bugids{$1} = 1; + } # if + } # while + + return keys %bugids; +} # getRemoteLinks + +sub findRemoteLinkByBugID (;$) { + my ($bugid) = @_; + + my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id'; + + if ($bugid) { + $condition .= " and remotelink.url like '%id=$bugid'"; + } # unless + + $jiradb = openJIRADB unless $jiradb; + + my $statement = <<"END"; +select + remotelink.id, + concat (project.pkey, '-', issuenum) as issue, + relationship +from + remotelink, + jiraissue, + project +$condition +END + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'Unable to execute statement', $statement; + + my @records; + + while (my $row = $sth->fetchrow_hashref) { + $row->{bugid} = $bugid; + + push @records, $row; + } # while + + return \@records; +} # findRemoteLinkByBugID + +sub promoteBug2JIRAIssue ($$$$) { + my ($bugid, $jirafrom, $jirato, $relationship) = @_; + + my $result = linkIssues $jirafrom, $relationship, $jirato; + + return $result if $result =~ /Unable to link/; + + $main::log->msg ($result . " (BugID $bugid)"); + + for (@{findRemoteLinkByBugID $bugid}) { + my %record = %$_; + + $result = removeRemoteLink ($record{issue}, $record{id}); + + # We may not care if we couldn't remove this link because it may have been + # removed by a prior pass. + return $result if $result =~ /Unable to remove link/; + + $main::log->msg ($result) unless $result eq ''; + } # for + + return $result; +} # promoteBug2JIRAIssue + +sub addRemoteLink ($$$) { + my ($bugid, $relationship, $jiraIssue) = @_; + + my $bug = getBug $bugid; + + # Check to see if this Bug ID already exists on this JIRA Issue, otherwise + # JIRA will duplicate it! + my $remoteLinks = getRemoteLink $jiraIssue; + + for (keys %$remoteLinks) { + if ($remoteLinks->{$_} =~ /Bug (\d+)/) { + return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1; + } # if + } # for + + # Note this globalid thing is NOT working! ALl I see is null in the database + my %remoteLink = ( +# globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid", +# application => { +# type => 'Bugzilla', +# name => 'Bugzilla', +# }, + relationship => $relationship, + object => { + url => "http://bugs.audience.com/show_bug.cgi?id=$bugid", + title => "Bug $bugid", + summary => $bug->{short_desc}, + icon => { + url16x16 => 'http://bugs.audience.local/favicon.png', + title => 'Bugzilla Bug', + }, + }, + ); + + $main::total{'RemoteLink Added'}++; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)}; + + return $@; + } else { + return "Would have linked $bugid -> $jiraIssue"; + } # if +} # addRemoteLink + +sub removeRemoteLink ($;$) { + my ($jiraIssue, $id) = @_; + + $id //= ''; + + my $remoteLinks = getRemoteLink ($jiraIssue, $id); + + for (keys %$remoteLinks) { + my $result; + + $main::total{'RemoteLink Removed'}++; + + if ($main::opts{exec}) { + eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")}; + + if ($@) { + return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@; + } else { + my $bugid; + + if ($remoteLinks->{$_} =~ /(\d+)/) { + return "Removed remote link $jiraIssue (Bug ID $1)"; + } # if + } # if + + $main::total{'Remote Links Removed'}++; + } else { + if ($remoteLinks->{$_} =~ /(\d+)/) { + return "Would have removed remote link $jiraIssue (Bug ID $1)"; + } # if + } # if + } # for +} # removeRemoteLink + +sub getIssueLinks ($;$) { + my ($issue, $type) = @_; + + my @links = getIssue ($issue, ('issuelinks')); + + my @issueLinks; + + for (@{$links[0]->{fields}{issuelinks}}) { + my %issueLink = %$_; + + next if ($type && $type ne $issueLink{type}{name}); + + push @issueLinks, \%issueLink; + } + + return @issueLinks; +} # getIssueLinks + +sub updateIssueWatchers ($%) { + my ($issue, %watchers) = @_; + + my $existingWatchers; + + eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")}; + + return "Unable to get issue $issue\n$@" if $@; + + for (@{$existingWatchers->{watchers}}) { + # Cleanup: Remove the current user from the watchers list. + # If he's on the list then remove him. + if ($_->{name} eq $jira->{username}) { + $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}"); + + $total{"Admins destroyed"}++; + } # if + + # Delete any matching watchers + delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})}; + } # for + + return '' if keys %watchers == 0; + + my $issueUpdated; + + for (keys %watchers) { + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$issue/watchers", undef, $_)}; + + if ($@) { + $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue"); + + $main::total{'Watchers skipped'}++; + } else { + $issueUpdated = 1; + + $main::total{'Watchers added'}++; + } # if + } else { + $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue"); + + $main::total{'Watchers that would have been added'}++; + } # if + } # for + + $main::total{'Issues updated'}++ if $issueUpdated; + + return ''; +} # updateIssueWatchers + +=pod + +I'm pretty sure I'm not using this routine anymore and I don't think it works. +If you wish to reserect this then please test. + +sub updateWatchers ($%) { + my ($issue, %watchers) = @_; + + my $existingWatchers; + + eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")}; + + if ($@) { + error "Unable to get issue $issue"; + + $main::total{'Missing JIRA Issues'}++; + + return; + } # if + + for (@{$existingWatchers->{watchers}}) { + # Cleanup: Mike Admin Cogan was added as a watcher for each issue imported. + # If he's on the list then remove him. + if ($_->{name} eq 'mcoganAdmin') { + $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}"); + + $main::total{"mcoganAdmin's destroyed"}++; + } # if + + # Delete any matching watchers + delete $watchers{$_->{name}} if $watchers{$_->{name}}; + } # for + + return if keys %watchers == 0; + + my $issueUpdated; + + for (keys %watchers) { + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$issue/watchers", undef, $_)}; + + if ($@) { + error "Unable to add user $_ as a watcher to JIRA Issue $issue"; + + $main::total{'Watchers skipped'}++; + } else { + $main::total{'Watchers added'}++; + + $issueUpdated = 1; + } # if + } else { + $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue"); + + $main::total{'Watchers that would have been added'}++; + } # if + } # for + + $main::total{'Issues updated'}++ if $issueUpdated; + + return; +} # updateWatchers +=cut + +1; diff --git a/audience/JIRA/updateWatchLists.pl b/audience/JIRA/updateWatchLists.pl new file mode 100644 index 0000000..5a5c4a0 --- /dev/null +++ b/audience/JIRA/updateWatchLists.pl @@ -0,0 +1,170 @@ +#!/usr/bin/perl +use strict; +use warnings; + +=pod + +=head1 NAME updateWatchLists.pl + +Copy CC lists from Bugzilla -> JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2014/05/23 $ + +=back + +=head1 SYNOPSIS + + Updates JIRA watchlists by copying the CC list information from Bugzilla + + $ updateWatchLists.pl [-login ] [-products product1, + product2,...] [-[no]exec] + [-verbose] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -he|lp: Display full help + -usa|ge: Display usage + -[no]e|xec: Whether or not to update JIRA. -noexec says only + tell me what you would have updated. + -use|rname: Username to log into JIRA with (Default: jira-admin) + -p|assword: Password to log into JIRA with (Default: jira-admin's + password) + -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev) + -jiraserver: Machine where Jira lives (Default: jira-dev) + -bugi|ds: Comma separated list of BugIDs to process + -f|ile: File of BugIDs, one per line + +=head1 DESCRIPTION + +This script updates JIRA watchlists by copying the CC List information from +Bugzilla to JIRA. + +=cut + +use FindBin; +use lib "$FindBin::Bin/lib"; + +$| = 1; + +use DBI; +use Display; +use Logger; +use TimeUtils; +use Utils; +use JIRAUtils; +use BugzillaUtils; +use JIRA::REST; + +use Getopt::Long; +use Pod::Usage; + +# Login should be the email address of the bugzilla account which has +# priviledges to create products and components +our %opts = ( + exec => 0, + bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev', + jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081', + username => 'jira-admin', + password => 'jira-admin', + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, + verbose => sub { set_verbose }, +); + +our ($log, %total); + +my ($bugzilla, $jira); + +sub main () { + my $startTime = time; + + GetOptions ( + \%opts, + 'verbose', + 'usage', + 'help', + 'exec!', + 'quiet', + 'username=s', + 'password=s', + 'bugids=s@', + 'file=s', + 'jiraserver=s', + 'bugzillaserver=s', + ) or pod2usage; + + $log = Logger->new; + + if ($opts{file}) { + open my $file, '<', $opts{file} + or die "Unable to open $opts{file} - $!"; + + $opts{bugids} = [<$file>]; + + chomp @{$opts{bugids}}; + } else { + my @bugids; + + push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); + + $opts{bugids} = [@bugids]; + } # if + + pod2usage 'Must specify -bugids [,,...] or -file ' + unless $opts{bugids}; + + openBugzilla $opts{bugzillaserver} + or $log->err ("Unable to connect to $opts{bugzillaserver}", 1); + + Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver}) + or $log->err ("Unable to connect to $opts{jiraserver}", 1); + + for (@{$opts{bugids}}) { + my $issue = findIssue $_; + + if ($issue =~ /^Future JIRA Issue/ or $issue =~ /^Unable to find/) { + $log->msg ($issue); + } else { + my %watchers = getWatchers $_; + + $log->msg ('Found ' . scalar (keys %watchers) . " watchers for JIRA Issue $issue"); + + my $result = updateIssueWatchers ($issue, %watchers); + + if ($result =~ /^Unable to/) { + $total{'Missing JIRA Issues'}++; + + $log->err ($result); + } else { + $total{'Issues updated'}++; + } # if + } # if + } # for + + display_duration $startTime, $log; + + Stats (\%total, $log) unless $opts{quiet}; + + return 0; +} # main + +exit main;