Corrected Tellabs
[clearscm.git] / audience / JIRA / lib / JIRAUtils.pm
1 =pod
2
3 =head1 NAME $RCSfile: JIRAUtils.pm,v $
4
5 Some shared functions dealing with JIRA
6
7 =head1 VERSION
8
9 =over
10
11 =item Author
12
13 Andrew DeFaria <Andrew@ClearSCM.com>
14
15 =item Revision
16
17 $Revision: 1.0 $
18
19 =item Created
20
21 Fri Mar 12 10:17:44 PST 2004
22
23 =item Modified
24
25 $Date: 2013/05/30 15:48:06 $
26
27 =back
28
29 =head1 ROUTINES
30
31 The following routines are exported:
32
33 =cut
34
35 package JIRAUtils;
36
37 use strict;
38 use warnings;
39
40 use base 'Exporter';
41
42 use FindBin;
43 use Display;
44 use Carp;
45 use DBI;
46
47 use JIRA::REST;
48 use BugzillaUtils;
49
50 our ($jira, %opts);
51
52 our @EXPORT = qw (
53   addJIRAComment
54   addDescription
55   Connect2JIRA
56   findIssue
57   getIssue
58   getIssueLinks
59   getIssueLinkTypes
60   getRemoteLinks
61   updateIssueWatchers
62   linkIssues
63   addRemoteLink
64   getRemoteLink
65   removeRemoteLink
66   getRemoteLinkByBugID
67   promoteBug2JIRAIssue
68   findRemoteLinkByBugID
69 );
70
71 my (@issueLinkTypes, %total, %cache, $jiradb);
72
73 sub _checkDBError ($;$) {
74   my ($msg, $statement) = @_;
75
76   $statement //= 'Unknown';
77   
78   $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
79   
80   my $dberr    = $jiradb->err;
81   my $dberrmsg = $jiradb->errstr;
82   
83   $dberr    ||= 0;
84   $dberrmsg ||= 'Success';
85
86   my $message = '';
87   
88   if ($dberr) {
89     my $function = (caller (1)) [3];
90
91     $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
92              . "SQL Statement: $statement";
93   } # if
94
95   $main::log->err ($message, 1) if $dberr;
96
97   return;
98 } # _checkDBError
99
100 sub openJIRADB (;$$$$) {
101   my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
102
103   $dbhost //= $main::opts{jiradbhost};
104   $dbname //= 'jiradb';
105   $dbuser //= 'adefaria';
106   $dbpass //= 'reader';
107   
108   $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...");
109   
110   $jiradb = DBI->connect (
111     "DBI:mysql:$dbname:$dbhost",
112     $dbuser,
113     $dbpass, {
114       PrintError => 0,
115       RaiseError => 1,
116     },
117   );
118
119   _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
120   
121   return $jiradb;
122 } # openJIRADB
123
124 sub Connect2JIRA (;$$$) {
125   my ($username, $password, $server) = @_;
126
127   my %opts;
128   
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";
133   
134   $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})");
135   
136   $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
137
138   # Store username as we might need it (see updateIssueWatchers)
139   $jira->{username} = $opts{username};
140   
141   return $jira;  
142 } # Connect2JIRA
143
144 sub addDescription ($$) {
145   my ($issue, $description) = @_;
146   
147   if ($main::opts{exec}) {
148     eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
149   
150     if ($@) {
151       return "Unable to add description\n$@";
152     } else {
153       return 'Description added';
154     } # if
155   } # if
156 } # addDescription
157
158 sub addJIRAComment ($$) {
159   my ($issue, $comment) = @_;
160   
161   if ($main::opts{exec}) {
162     eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
163   
164     if ($@) {
165       return "Unable to add comment\n$@";
166     } else {
167       return 'Comment added';
168     } # if
169   } else {
170     return "Would have added comments to $issue";
171   } # if
172 } # addJIRAComment
173
174 sub findIssue ($%) {
175   my ($bugid, %bugmap) = @_;
176   
177 =pod
178   # Check the cache...
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"; 
185     } else {
186       return $cache{$bugid};
187     } # if
188   } # if
189 =cut  
190   my $issue;
191
192   my %query = (
193     jql    => "\"Bugzilla Bug Number\" ~ $bugid",
194     fields => [ 'key' ],
195   );
196   
197   eval {$issue = $jira->GET ("/search/", \%query)};
198
199   my $issueID = $issue->{issues}[0]{key};
200   
201   if (@{$issue->{issues}} > 2) {
202     $main::log->err ("Found more than 2 issues for Bug ID $bugid");
203     
204     return "Found more than 2 issues for Bug ID $bugid";
205   } elsif (@{$issue->{issues}} == 2) {
206     my ($issueNum0, $issueNum1);
207     
208     if ($issue->{issues}[0]{key} =~ /(\d+)/) {
209       $issueNum0 = $1;
210     } # if
211     
212     if ($issue->{issues}[1]{key} =~ /(\d+)/) {
213       $issueNum1 = $1;
214     } # if
215     
216     if ($issueNum0 < $issueNum1) {
217       $issueID = $issue->{issues}[1]{key};
218     } # if
219     
220     # Let's mark them as clones. See if this clone link already exists...
221     my $alreadyCloned;
222     
223     for (getIssueLinks ($issueID, 'Cloners')) {
224       my $inwardIssue  = $_->{inwardIssue}{key}  || '';
225       my $outwardIssue = $_->{outwardIssue}{key} || '';
226       
227       if ("RDBNK-$issueNum0" eq $inwardIssue  ||
228           "RDBNK-$issueNum0" eq $outwardIssue ||
229           "RDBNK-$issueNum1" eq $inwardIssue  ||
230           "RDBNK-$issueNum1" eq $outwardIssue) {
231          $alreadyCloned = 1;
232          
233          last;
234       } # if
235     } # for
236
237     unless ($alreadyCloned) {
238       my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1");
239     
240       return $result if $result =~ /Unable to/;
241     
242       $main::log->msg ($result);
243     } # unless
244   } # if
245
246   if ($issueID) {
247     $main::log->msg ("Found JIRA issue $issueID for Bug $bugid");
248   
249     #$cache{$bugid} = $issueID;
250       
251     #return $cache{$bugid};
252     return $issueID;
253   } else {
254     my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
255                                  : "Unable to find a JIRA issue for Bug $bugid";
256     
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
259     # this bugid.
260     #$cache{$bugid} = $bugid;
261
262     return $status;
263   } # if
264 } # findJIRA
265
266 sub getIssue ($;@) {
267   my ($issue, @fields) = @_;
268   
269   my $fields = @fields ? "?fields=" . join ',', @fields : '';
270
271   return $jira->GET ("/issue/$issue$fields");
272 } # getIssue
273
274 sub getIssueLinkTypes () {
275   my $issueLinkTypes = $jira->GET ('/issueLinkType/');
276   
277   map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
278   
279   return @issueLinkTypes
280 } # getIssueLinkTypes
281
282 sub linkIssues ($$$) {
283   my ($from, $type, $to) = @_;
284   
285   unless (@issueLinkTypes) {
286     getIssueLinkTypes;
287   } # unless
288   
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);
292                
293     return "Unable to $type link $from -> $to";           
294   } # unless  
295   
296   my %link = (
297     inwardIssue  => {
298       key        => $from,
299     },
300     type         => {
301       name       => $type,
302     },
303     outwardIssue => {
304       key        => $to,
305     },
306     comment      => {
307       body       => "Link ported as part of the migration from Bugzilla: $from <-> $to",
308     },
309   );
310   
311   $main::total{'IssueLinks Added'}++;
312   
313   if ($main::opts{exec}) {
314     eval {$jira->POST ("/issueLink", undef, \%link)};
315     
316     if ($@) {
317       return "Unable to $type link $from -> $to\n$@";
318     } else {
319       return "Made $type link $from -> $to";
320     } # if
321   } else {
322     return "Would have $type linked $from -> $to";
323   } # if
324 } # linkIssue
325
326 sub getRemoteLink ($;$) {
327   my ($jiraIssue, $id) = @_;
328   
329   $id //= '';
330   
331   my $result;
332   
333   eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
334   
335   return if $@;
336   
337   my %remoteLinks;
338
339   if (ref $result eq 'ARRAY') {
340     map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;  
341   } else {
342     $remoteLinks{$result->{id}} = $result->{object}{title};
343   } # if
344     
345   return \%remoteLinks;
346 } # getRemoteLink
347
348 sub getRemoteLinks (;$) {
349   my ($bugid) = @_;
350   
351   $jiradb = openJIRADB unless $jiradb;
352   
353   my $statement = 'select url from remotelink';
354
355   $statement .= " where url like 'http://bugs%'";  
356   $statement .= " and url like '%$bugid'" if $bugid; 
357   $statement .= " group by issueid desc";
358   
359   my $sth = $jiradb->prepare ($statement);
360   
361   _checkDBError 'Unable to prepare statement', $statement;
362   
363   $sth->execute;
364   
365   _checkDBError 'Unable to execute statement', $statement;
366
367   my %bugids;
368   
369   while (my $record = $sth->fetchrow_array) {
370     if ($record =~ /(\d+)$/) {
371       $bugids{$1} = 1;
372     } # if 
373   } # while
374   
375   return keys %bugids;
376 } # getRemoteLinks
377
378 sub findRemoteLinkByBugID (;$) {
379   my ($bugid) = @_;
380   
381   my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
382   
383   if ($bugid) {
384     $condition .= " and remotelink.url like '%id=$bugid'";
385   } # unless
386   
387   $jiradb = openJIRADB unless $jiradb;
388
389   my $statement = <<"END";
390 select 
391   remotelink.id, 
392   concat (project.pkey, '-', issuenum) as issue,
393   relationship
394 from
395   remotelink,
396   jiraissue,
397   project
398 $condition
399 END
400
401   my $sth = $jiradb->prepare ($statement);
402   
403   _checkDBError 'Unable to prepare statement', $statement;
404   
405   $sth->execute;
406   
407   _checkDBError 'Unable to execute statement', $statement;
408   
409   my @records;
410   
411   while (my $row = $sth->fetchrow_hashref) {
412     $row->{bugid} = $bugid;
413         
414     push @records, $row;
415   } # while
416   
417   return \@records;
418 } # findRemoteLinkByBugID
419
420 sub promoteBug2JIRAIssue ($$$$) {
421   my ($bugid, $jirafrom, $jirato, $relationship) = @_;
422
423   my $result = linkIssues $jirafrom, $relationship, $jirato;
424         
425   return $result if $result =~ /Unable to link/;
426   
427   $main::log->msg ($result . " (BugID $bugid)");
428   
429   for (@{findRemoteLinkByBugID $bugid}) {
430     my %record = %$_;
431     
432     $result = removeRemoteLink ($record{issue}, $record{id});
433     
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/;
437     
438     $main::log->msg ($result) unless $result eq '';
439   } # for
440   
441   return $result;
442 } # promoteBug2JIRAIssue
443
444 sub addRemoteLink ($$$) {
445   my ($bugid, $relationship, $jiraIssue) = @_;
446   
447   my $bug = getBug $bugid;
448   
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;
452   
453   for (keys %$remoteLinks) {
454     if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
455       return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
456     } # if
457   } # for
458   
459   # Note this globalid thing is NOT working! ALl I see is null in the database
460   my %remoteLink = (
461 #    globalid     => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
462 #    application  => {
463 #      type       => 'Bugzilla',
464 #      name       => 'Bugzilla',
465 #    },
466     relationship => $relationship, 
467     object       => {
468       url        => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
469       title      => "Bug $bugid",
470       summary    => $bug->{short_desc},
471       icon       => {
472         url16x16 => 'http://bugs.audience.local/favicon.png',
473         title    => 'Bugzilla Bug',
474       },
475     },
476   );
477   
478   $main::total{'RemoteLink Added'}++;
479   
480   if ($main::opts{exec}) {
481     eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
482   
483     return $@;
484   } else {
485     return "Would have linked $bugid -> $jiraIssue";
486   } # if
487 } # addRemoteLink
488
489 sub removeRemoteLink ($;$) {
490   my ($jiraIssue, $id) = @_;
491   
492   $id //= '';
493   
494   my $remoteLinks = getRemoteLink ($jiraIssue, $id);
495   
496   for (keys %$remoteLinks) {
497     my $result;
498     
499     $main::total{'RemoteLink Removed'}++;
500   
501     if ($main::opts{exec}) {
502       eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
503
504       if ($@) {  
505         return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
506       } else {
507         my $bugid;
508         
509         if ($remoteLinks->{$_} =~ /(\d+)/) {
510           return "Removed remote link $jiraIssue (Bug ID $1)";
511         } # if
512       } # if
513       
514       $main::total{'Remote Links Removed'}++;
515     } else {
516       if ($remoteLinks->{$_} =~ /(\d+)/) {
517         return "Would have removed remote link $jiraIssue (Bug ID $1)";
518       } # if
519     } # if
520   } # for  
521 } # removeRemoteLink
522
523 sub getIssueLinks ($;$) {
524   my ($issue, $type) = @_;
525   
526   my @links = getIssue ($issue, ('issuelinks'));
527   
528   my @issueLinks;
529
530   for (@{$links[0]->{fields}{issuelinks}}) {
531      my %issueLink = %$_;
532      
533      next if ($type && $type ne $issueLink{type}{name});
534      
535      push @issueLinks, \%issueLink;  
536   }
537   
538   return @issueLinks;
539 } # getIssueLinks
540
541 sub updateIssueWatchers ($%) {
542   my ($issue, %watchers) = @_;
543
544   my $existingWatchers;
545   
546   eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
547   
548   return "Unable to get issue $issue\n$@" if $@;
549   
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}");
555       
556       $total{"Admins destroyed"}++;
557     } # if
558     
559     # Delete any matching watchers
560     delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
561   } # for
562
563   return '' if keys %watchers == 0;
564   
565   my $issueUpdated;
566   
567   for (keys %watchers) {
568     if ($main::opts{exec}) {
569       eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
570     
571       if ($@) {
572         $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue");
573       
574         $main::total{'Watchers skipped'}++;
575       } else {
576         $issueUpdated = 1;
577         
578         $main::total{'Watchers added'}++;
579       } # if
580     } else {
581       $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
582       
583       $main::total{'Watchers that would have been added'}++;
584     } # if
585   } # for
586   
587   $main::total{'Issues updated'}++ if $issueUpdated;
588   
589   return '';
590 } # updateIssueWatchers
591
592 =pod
593
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.
596
597 sub updateWatchers ($%) {
598   my ($issue, %watchers) = @_;
599
600   my $existingWatchers;
601   
602   eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
603   
604   if ($@) {
605     error "Unable to get issue $issue";
606     
607     $main::total{'Missing JIRA Issues'}++;
608     
609     return;
610   } # if
611   
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}");
617       
618       $main::total{"mcoganAdmin's destroyed"}++;
619     } # if
620     
621     # Delete any matching watchers
622     delete $watchers{$_->{name}} if $watchers{$_->{name}};
623   } # for
624
625   return if keys %watchers == 0;
626   
627   my $issueUpdated;
628   
629   for (keys %watchers) {
630     if ($main::opts{exec}) {
631       eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
632     
633       if ($@) {
634         error "Unable to add user $_ as a watcher to JIRA Issue $issue";
635       
636         $main::total{'Watchers skipped'}++;
637       } else {
638         $main::total{'Watchers added'}++;
639         
640         $issueUpdated = 1;
641       } # if
642     } else {
643       $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
644       
645       $main::total{'Watchers that would have been added'}++;
646     } # if
647   } # for
648   
649   $main::total{'Issues updated'}++ if $issueUpdated;
650   
651   return;
652 } # updateWatchers
653 =cut
654
655 1;