3 =head1 NAME $RCSfile: JIRAUtils.pm,v $
5 Some shared functions dealing with JIRA
13 Andrew DeFaria <Andrew@ClearSCM.com>
21 Fri Mar 12 10:17:44 PST 2004
25 $Date: 2013/05/30 15:48:06 $
31 The following routines are exported:
71 my (@issueLinkTypes, %total, %cache, $jiradb);
73 sub _checkDBError ($;$) {
74 my ($msg, $statement) = @_;
76 $statement //= 'Unknown';
78 $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
80 my $dberr = $jiradb->err;
81 my $dberrmsg = $jiradb->errstr;
84 $dberrmsg ||= 'Success';
89 my $function = (caller (1)) [3];
91 $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
92 . "SQL Statement: $statement";
95 $main::log->err ($message, 1) if $dberr;
100 sub openJIRADB (;$$$$) {
101 my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
103 $dbhost //= $main::opts{jiradbhost};
104 $dbname //= 'jiradb';
105 $dbuser //= 'adefaria';
106 $dbpass //= 'reader';
108 $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...");
110 $jiradb = DBI->connect (
111 "DBI:mysql:$dbname:$dbhost",
119 _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
124 sub Connect2JIRA (;$$$) {
125 my ($username, $password, $server) = @_;
129 $opts{username} = $username || 'jira-admin';
130 $opts{password} = $password || $ENV{PASSWORD} || 'jira-admin';
131 $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev:8081';
132 $opts{URL} = "http://$opts{server}/rest/api/latest";
134 $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})");
136 $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
138 # Store username as we might need it (see updateIssueWatchers)
139 $jira->{username} = $opts{username};
144 sub addDescription ($$) {
145 my ($issue, $description) = @_;
147 if ($main::opts{exec}) {
148 eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
151 return "Unable to add description\n$@";
153 return 'Description added';
158 sub addJIRAComment ($$) {
159 my ($issue, $comment) = @_;
161 if ($main::opts{exec}) {
162 eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
165 return "Unable to add comment\n$@";
167 return 'Comment added';
170 return "Would have added comments to $issue";
175 my ($bugid, %bugmap) = @_;
179 if ($cache{$bugid}) {
180 if ($cache{$bugid} =~ /^\d+/) {
181 # We have a cache hit but the contents here are a bugid. This means we had
182 # searched for the corresponding JIRA issue for this bug before and came
183 # up empty handed. In this situtaion we really have:
184 return "Unable to find a JIRA issue for Bug $bugid";
186 return $cache{$bugid};
193 jql => "\"Bugzilla Bug Number\" ~ $bugid",
197 eval {$issue = $jira->GET ("/search/", \%query)};
199 my $issueID = $issue->{issues}[0]{key};
201 if (@{$issue->{issues}} > 2) {
202 $main::log->err ("Found more than 2 issues for Bug ID $bugid");
204 return "Found more than 2 issues for Bug ID $bugid";
205 } elsif (@{$issue->{issues}} == 2) {
206 my ($issueNum0, $issueNum1);
208 if ($issue->{issues}[0]{key} =~ /(\d+)/) {
212 if ($issue->{issues}[1]{key} =~ /(\d+)/) {
216 if ($issueNum0 < $issueNum1) {
217 $issueID = $issue->{issues}[1]{key};
220 # Let's mark them as clones. See if this clone link already exists...
223 for (getIssueLinks ($issueID, 'Cloners')) {
224 my $inwardIssue = $_->{inwardIssue}{key} || '';
225 my $outwardIssue = $_->{outwardIssue}{key} || '';
227 if ("RDBNK-$issueNum0" eq $inwardIssue ||
228 "RDBNK-$issueNum0" eq $outwardIssue ||
229 "RDBNK-$issueNum1" eq $inwardIssue ||
230 "RDBNK-$issueNum1" eq $outwardIssue) {
237 unless ($alreadyCloned) {
238 my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1");
240 return $result if $result =~ /Unable to/;
242 $main::log->msg ($result);
247 $main::log->msg ("Found JIRA issue $issueID for Bug $bugid");
249 #$cache{$bugid} = $issueID;
251 #return $cache{$bugid};
254 my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
255 : "Unable to find a JIRA issue for Bug $bugid";
257 # Here we put this bugid into the cache but instead of a the JIRA issue
258 # id we put the bugid. This will stop us from adding up multiple hits on
260 #$cache{$bugid} = $bugid;
267 my ($issue, @fields) = @_;
269 my $fields = @fields ? "?fields=" . join ',', @fields : '';
271 return $jira->GET ("/issue/$issue$fields");
274 sub getIssueLinkTypes () {
275 my $issueLinkTypes = $jira->GET ('/issueLinkType/');
277 map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
279 return @issueLinkTypes
280 } # getIssueLinkTypes
282 sub linkIssues ($$$) {
283 my ($from, $type, $to) = @_;
285 unless (@issueLinkTypes) {
289 unless (grep {$type eq $_} @issueLinkTypes) {
290 $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n"
291 . join "\n\t", @issueLinkTypes);
293 return "Unable to $type link $from -> $to";
307 body => "Link ported as part of the migration from Bugzilla: $from <-> $to",
311 $main::total{'IssueLinks Added'}++;
313 if ($main::opts{exec}) {
314 eval {$jira->POST ("/issueLink", undef, \%link)};
317 return "Unable to $type link $from -> $to\n$@";
319 return "Made $type link $from -> $to";
322 return "Would have $type linked $from -> $to";
326 sub getRemoteLink ($;$) {
327 my ($jiraIssue, $id) = @_;
333 eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
339 if (ref $result eq 'ARRAY') {
340 map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;
342 $remoteLinks{$result->{id}} = $result->{object}{title};
345 return \%remoteLinks;
348 sub getRemoteLinks (;$) {
351 $jiradb = openJIRADB unless $jiradb;
353 my $statement = 'select url from remotelink';
355 $statement .= " where url like 'http://bugs%'";
356 $statement .= " and url like '%$bugid'" if $bugid;
357 $statement .= " group by issueid desc";
359 my $sth = $jiradb->prepare ($statement);
361 _checkDBError 'Unable to prepare statement', $statement;
365 _checkDBError 'Unable to execute statement', $statement;
369 while (my $record = $sth->fetchrow_array) {
370 if ($record =~ /(\d+)$/) {
378 sub findRemoteLinkByBugID (;$) {
381 my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
384 $condition .= " and remotelink.url like '%id=$bugid'";
387 $jiradb = openJIRADB unless $jiradb;
389 my $statement = <<"END";
392 concat (project.pkey, '-', issuenum) as issue,
401 my $sth = $jiradb->prepare ($statement);
403 _checkDBError 'Unable to prepare statement', $statement;
407 _checkDBError 'Unable to execute statement', $statement;
411 while (my $row = $sth->fetchrow_hashref) {
412 $row->{bugid} = $bugid;
418 } # findRemoteLinkByBugID
420 sub promoteBug2JIRAIssue ($$$$) {
421 my ($bugid, $jirafrom, $jirato, $relationship) = @_;
423 my $result = linkIssues $jirafrom, $relationship, $jirato;
425 return $result if $result =~ /Unable to link/;
427 $main::log->msg ($result . " (BugID $bugid)");
429 for (@{findRemoteLinkByBugID $bugid}) {
432 $result = removeRemoteLink ($record{issue}, $record{id});
434 # We may not care if we couldn't remove this link because it may have been
435 # removed by a prior pass.
436 return $result if $result =~ /Unable to remove link/;
438 $main::log->msg ($result) unless $result eq '';
442 } # promoteBug2JIRAIssue
444 sub addRemoteLink ($$$) {
445 my ($bugid, $relationship, $jiraIssue) = @_;
447 my $bug = getBug $bugid;
449 # Check to see if this Bug ID already exists on this JIRA Issue, otherwise
450 # JIRA will duplicate it!
451 my $remoteLinks = getRemoteLink $jiraIssue;
453 for (keys %$remoteLinks) {
454 if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
455 return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
459 # Note this globalid thing is NOT working! ALl I see is null in the database
461 # globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
463 # type => 'Bugzilla',
464 # name => 'Bugzilla',
466 relationship => $relationship,
468 url => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
469 title => "Bug $bugid",
470 summary => $bug->{short_desc},
472 url16x16 => 'http://bugs.audience.local/favicon.png',
473 title => 'Bugzilla Bug',
478 $main::total{'RemoteLink Added'}++;
480 if ($main::opts{exec}) {
481 eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
485 return "Would have linked $bugid -> $jiraIssue";
489 sub removeRemoteLink ($;$) {
490 my ($jiraIssue, $id) = @_;
494 my $remoteLinks = getRemoteLink ($jiraIssue, $id);
496 for (keys %$remoteLinks) {
499 $main::total{'RemoteLink Removed'}++;
501 if ($main::opts{exec}) {
502 eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
505 return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
509 if ($remoteLinks->{$_} =~ /(\d+)/) {
510 return "Removed remote link $jiraIssue (Bug ID $1)";
514 $main::total{'Remote Links Removed'}++;
516 if ($remoteLinks->{$_} =~ /(\d+)/) {
517 return "Would have removed remote link $jiraIssue (Bug ID $1)";
523 sub getIssueLinks ($;$) {
524 my ($issue, $type) = @_;
526 my @links = getIssue ($issue, ('issuelinks'));
530 for (@{$links[0]->{fields}{issuelinks}}) {
533 next if ($type && $type ne $issueLink{type}{name});
535 push @issueLinks, \%issueLink;
541 sub updateIssueWatchers ($%) {
542 my ($issue, %watchers) = @_;
544 my $existingWatchers;
546 eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
548 return "Unable to get issue $issue\n$@" if $@;
550 for (@{$existingWatchers->{watchers}}) {
551 # Cleanup: Remove the current user from the watchers list.
552 # If he's on the list then remove him.
553 if ($_->{name} eq $jira->{username}) {
554 $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
556 $total{"Admins destroyed"}++;
559 # Delete any matching watchers
560 delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
563 return '' if keys %watchers == 0;
567 for (keys %watchers) {
568 if ($main::opts{exec}) {
569 eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
572 $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue");
574 $main::total{'Watchers skipped'}++;
578 $main::total{'Watchers added'}++;
581 $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
583 $main::total{'Watchers that would have been added'}++;
587 $main::total{'Issues updated'}++ if $issueUpdated;
590 } # updateIssueWatchers
594 I'm pretty sure I'm not using this routine anymore and I don't think it works.
595 If you wish to reserect this then please test.
597 sub updateWatchers ($%) {
598 my ($issue, %watchers) = @_;
600 my $existingWatchers;
602 eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
605 error "Unable to get issue $issue";
607 $main::total{'Missing JIRA Issues'}++;
612 for (@{$existingWatchers->{watchers}}) {
613 # Cleanup: Mike Admin Cogan was added as a watcher for each issue imported.
614 # If he's on the list then remove him.
615 if ($_->{name} eq 'mcoganAdmin') {
616 $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
618 $main::total{"mcoganAdmin's destroyed"}++;
621 # Delete any matching watchers
622 delete $watchers{$_->{name}} if $watchers{$_->{name}};
625 return if keys %watchers == 0;
629 for (keys %watchers) {
630 if ($main::opts{exec}) {
631 eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
634 error "Unable to add user $_ as a watcher to JIRA Issue $issue";
636 $main::total{'Watchers skipped'}++;
638 $main::total{'Watchers added'}++;
643 $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
645 $main::total{'Watchers that would have been added'}++;
649 $main::total{'Issues updated'}++ if $issueUpdated;