70629aa7dbdd4d07aa11c74830a6dbc51cee3519
[clearscm.git] / 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;
51
52 our @EXPORT = qw (
53   Connect2JIRA
54   addDescription
55   addJIRAComment
56   addRemoteLink
57   attachFiles2Issue
58   attachmentExists
59   blankBugzillaNbr
60   copyGroupMembership
61   count
62   findIssue
63   findIssues
64   findRemoteLinkByBugID
65   getIssue
66   getIssueFromBugID
67   getIssueLinkTypes
68   getIssueLinks
69   getIssueWatchers
70   getIssues
71   getNextIssue
72   getRemoteLink
73   getRemoteLinkByBugID
74   getRemoteLinks
75   getUsersGroups
76   linkIssues
77   promoteBug2JIRAIssue
78   removeRemoteLink
79   renameUsers
80   updateColumn
81   updateIssueWatchers
82   updateUsersGroups
83 );
84
85 my (@issueLinkTypes, %cache, $jiradb, %findQuery);
86
87 my %tables = (
88   ao_08d66b_filter_display_conf => [
89                                      {column    => 'user_name'}
90                                    ],
91   ao_0c0737_vote_info           => [
92                                      {column    => 'user_name'}
93                                    ],
94   ao_3a112f_audit_log_entry     => [
95                                      {column    => 'user'}
96                                    ],                                   
97   ao_563aee_activity_entity     => [
98                                      {column    => 'username'}
99                                    ],
100   ao_60db71_auditentry          => [
101                                      {column    => 'user'}
102                                    ],
103   ao_60db71_boardadmins         => [
104                                      {column    => "'key'"}
105                                    ],
106   ao_60db71_rapidview           => [
107                                      {column    => 'owner_user_name'}
108                                    ],
109   ao_caff30_favourite_issue     => [
110                                      {column    => 'user_key'}
111                                    ],
112 #  app_user                      => [
113 #                                     {column    => 'user_key'},
114 #                                     {column    => 'lower_user_name'},
115 #                                   ],
116   audit_log                     => [
117                                      {column    => 'author_key'}
118                                    ],
119   avatar                        => [
120                                      {column    => 'owner'}
121                                    ],
122   changegroup                   => [
123                                      {column    => 'author'}
124                                    ],
125   changeitem                    => [
126                                      {column    => 'oldvalue',
127                                       condition => 'field = "assignee"'},
128                                      {column    => 'newvalue',
129                                       condition => 'field = "assignee"'},
130                                    ],
131   columnlayout                  => [
132                                      {column    => 'username'},
133                                    ],
134   component                     => [
135                                      {column    => 'lead'},
136                                    ],
137   customfieldvalue              => [
138                                      {column    => 'stringvalue'},
139                                    ],
140   favouriteassociations         => [
141                                      {column    => 'username'},
142                                    ],                                   
143 #  cwd_membership                => [
144 #                                     {column    => 'child_name'},
145 #                                     {column    => 'lower_child_name'},
146 #                                   ],
147   fileattachment                => [
148                                      {column    => 'author'},
149                                    ],
150   filtersubscription            => [
151                                      {column    => 'username'},
152                                    ],
153   jiraaction                    => [
154                                      {column    => 'author'},
155                                    ],
156   jiraissue                     => [
157                                      {column    => 'reporter'},
158                                      {column    => 'assignee'},
159                                    ],
160   jiraworkflows                 => [
161                                      {column    => 'creatorname'},
162                                    ],
163   membershipbase                => [
164                                      {column    => 'user_name'},
165                                    ],
166   os_currentstep                => [
167                                      {column    => 'owner'},
168                                      {column    => 'caller'},
169                                    ],
170   os_historystep                => [
171                                      {column    => 'owner'},
172                                      {column    => 'caller'},
173                                    ],
174   project                       => [
175                                      {column    => 'lead'},
176                                    ],
177   portalpage                    => [
178                                      {column    => 'username'},
179                                    ],
180   schemepermissions             => [
181                                      {column    => 'perm_parameter',
182                                       condition => 'perm_type = "user"'},
183                                    ],
184   searchrequest                 => [
185                                      {column    => 'authorname'},
186                                      {column    => 'username'},
187                                    ],
188   userassociation               => [
189                                      {column    => 'source_name'},
190                                    ],
191   userbase                      => [
192                                      {column    => 'username'},
193                                    ],
194   userhistoryitem               => [
195                                      {column    => 'username'}
196                                    ],
197   worklog                       => [
198                                      {column    => 'author'},
199                                    ],
200 );
201
202 sub _checkDBError ($;$) {
203   my ($msg, $statement) = @_;
204
205   $statement //= 'Unknown';
206   
207   if ($main::log) {
208    $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
209   } # if
210   
211   my $dberr    = $jiradb->err;
212   my $dberrmsg = $jiradb->errstr;
213   
214   $dberr    ||= 0;
215   $dberrmsg ||= 'Success';
216
217   my $message = '';
218   
219   if ($dberr) {
220     my $function = (caller (1)) [3];
221
222     $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
223              . "SQL Statement: $statement";
224   } # if
225
226   if ($main::log) {
227     $main::log->err ($message, 1) if $dberr;
228   } # if
229
230   return;
231 } # _checkDBError
232
233 sub openJIRADB (;$$$$) {
234   my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
235
236 =pod
237
238 =head2 openJIRADB ()
239
240 Opens the JIRA database directly using MySQL. This is only for certain 
241 operations for which there is no corresponding REST interface
242
243 Parameters:
244
245 =for html <blockquote>
246
247 =over
248
249 =item $dbhost
250
251 Name of the database host
252
253 =item $dbname
254
255 database name
256
257 =item $dbuser
258
259 Database user name
260
261 =item $dbpass
262
263 Database user's password
264
265 =back
266
267 =for html </blockquote>
268
269 Returns:
270
271 =for html <blockquote>
272
273 =over
274
275 =item $dbhandle
276
277 Handle for database
278
279 =back
280
281 =for html </blockquote>
282
283 =cut
284
285   $dbhost //= $main::opts{jiradbhost};
286   $dbname //= 'jiradb';
287   $dbuser //= 'root';
288   $dbpass //= 'r00t';
289   
290   $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...") if $main::log;
291   
292   $jiradb = DBI->connect (
293     "DBI:mysql:$dbname:$dbhost",
294     $dbuser,
295     $dbpass, {
296       PrintError => 0,
297       RaiseError => 1,
298     },
299   );
300
301   _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
302   
303   return $jiradb;
304 } # openJIRADB
305
306 sub Connect2JIRA (;$$$) {  
307   my ($username, $password, $server) = @_;
308
309 =pod
310
311 =head2 Connect2JIRA ()
312
313 Establishes a connection to the JIRA instance using the REST API
314
315 Parameters:
316
317 =for html <blockquote>
318
319 =over
320
321 =item $username
322
323 Username to authenticate with
324
325 =item $password
326
327 Password to authenticate with
328
329 =item $server
330
331 JIRA server to connect to
332
333 =back
334
335 =for html </blockquote>
336
337 Returns:
338
339 =for html <blockquote>
340
341 =over
342
343 =item $jira
344
345 JIRA REST handle
346
347 =back
348
349 =for html </blockquote>
350
351 =cut
352
353   my %opts;
354   
355   $opts{username} = $username || 'jira-admin';
356   $opts{password} = $password || $ENV{PASSWORD}    || 'jira-admin';
357   $opts{server}   = $server   || $ENV{JIRA_SERVER} || 'jira-dev';
358   $opts{URL}      = "http://$opts{server}/rest/api/latest";
359   
360   $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})") if $main::log;
361   
362   $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
363
364   # Store username as we might need it (see updateIssueWatchers)
365   $jira->{username} = $opts{username};
366   
367   return $jira;  
368 } # Connect2JIRA
369
370 sub count ($$) {
371   my ($table, $condition) = @_;
372
373 =pod
374
375 =head2 count ()
376
377 Return the count of a table in the JIRA database given a condition
378
379 Parameters:
380
381 =for html <blockquote>
382
383 =over
384
385 =item $table
386
387 Name of table to perform count of
388
389 =item $condition
390
391 MySQL condition to apply
392
393 =back
394
395 =for html </blockquote>
396
397 Returns:
398
399 =for html <blockquote>
400
401 =over
402
403 =item $count
404
405 Count of qualifying entries
406
407 =back
408
409 =for html </blockquote>
410
411 =cut
412
413   my $statement;
414   
415   $jiradb = openJIRADB unless $jiradb;
416
417   if ($condition) {
418     $statement = "select count(*) from $table where $condition";
419   } else {
420     $statement = "select count(*) from $table";
421   } # if
422
423   my $sth = $jiradb->prepare ($statement);
424   
425   _checkDBError 'count: Unable to prepare statement', $statement;
426
427   $sth->execute;
428   
429   _checkDBError 'count: Unable to execute statement', $statement;
430
431   # Get return value, which should be how many message there are
432   my @row = $sth->fetchrow_array;
433
434   # Done with $sth
435   $sth->finish;
436
437   my $count;
438
439   # Retrieve returned value
440   unless ($row[0]) {
441     $count = 0
442   } else {
443     $count = $row[0];
444   } # unless
445
446   return $count
447 } # count
448
449 sub addDescription ($$) {
450   my ($issue, $description) = @_;
451   
452 =pod
453
454 =head2 addDescription ()
455
456 Add a description to a JIRA issue
457
458 Parameters:
459
460 =for html <blockquote>
461
462 =over
463
464 =item $issue
465
466 Issue ID
467
468 =item $description
469
470 Description to add
471
472 =back
473
474 =for html </blockquote>
475
476 Returns:
477
478 =for html <blockquote>
479
480 =over
481
482 =item <nothing>
483
484 =back
485
486 =for html </blockquote>
487
488 =cut
489
490   if ($main::opts{exec}) {
491     eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
492   
493     if ($@) {
494       return "Unable to add description\n$@";
495     } else {
496       return 'Description added';
497     } # if
498   } # if
499 } # addDescription
500
501 sub addJIRAComment ($$) {
502   my ($issue, $comment) = @_;
503
504 =pod
505
506 =head2 addJIRAComment ()
507
508 Add a comment to a JIRA issue
509
510 Parameters:
511
512 =for html <blockquote>
513
514 =over
515
516 =item $issue
517
518 Issue ID
519
520 =item $comment
521
522 Comment to add
523
524 =back
525
526 =for html </blockquote>
527
528 Returns:
529
530 =for html <blockquote>
531
532 =over
533
534 =item <nothing>
535
536 =back
537
538 =for html </blockquote>
539
540 =cut
541   
542   if ($main::opts{exec}) {
543     eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
544   
545     if ($@) {
546       return "Unable to add comment\n$@";
547     } else {
548       return 'Comment added';
549     } # if
550   } else {
551     return "Would have added comments to $issue";
552   } # if
553 } # addJIRAComment
554
555 sub blankBugzillaNbr ($) {
556   my ($issue) = @_;
557   
558   eval {$jira->PUT ("/issue/$issue", undef, {fields => {'Bugzilla Bug Origin' => ''}})};
559   #eval {$jira->PUT ("/issue/$issue", undef, {fields => {'customfield_10132' => ''}})};
560   
561   if ($@) {
562     return "Unable to blank Bugzilla number$@\n"
563   } else {
564     return 'Corrected'
565   } # if
566 } # blankBugzillaNbr
567
568 sub attachmentExists ($$) {
569   my ($issue, $filename) = @_;
570   
571 =pod
572
573 =head2 attachmentExists ()
574
575 Determine if an attachment to a JIRA issue exists
576
577 Parameters:
578
579 =for html <blockquote>
580
581 =over
582
583 =item $issue
584
585 Issue ID
586
587 =item $filename
588
589 Filename of attachment
590
591 =back
592
593 =for html </blockquote>
594
595 Returns:
596
597 =for html <blockquote>
598
599 =over
600
601 =item <nothing>
602
603 =back
604
605 =for html </blockquote>
606
607 =cut
608
609   my $attachments = getIssue ($issue, qw(attachment));
610   
611   for (@{$attachments->{fields}{attachment}}) {
612     return 1 if $filename eq $_->{filename};
613   } # for
614   
615   return 0;
616 } # attachmentExists
617
618 sub attachFiles2Issue ($@) {
619   my ($issue, @files) = @_;
620
621 =pod
622
623 =head2 attachFiles2Issue ()
624
625 Attach a list of files to a JIRA issue
626
627 Parameters:
628
629 =for html <blockquote>
630
631 =over
632
633 =item $issue
634
635 Issue ID
636
637 =item @files
638
639 List of filenames
640
641 =back
642
643 =for html </blockquote>
644
645 Returns:
646
647 =for html <blockquote>
648
649 =over
650
651 =item <nothing>
652
653 =back
654
655 =for html </blockquote>
656
657 =cut  
658
659   my $status = $jira->attach_files ($issue, @files);
660   
661   return $status;
662 } # attachFiles2Issue
663
664 sub getIssueFromBugID ($) {
665   my ($bugid) = @_;
666   
667   my $issue;
668   
669   my %query = (
670     jql    => "\"Bugzilla Bug Origin\" ~ $bugid",
671     fields => [ 'key' ],
672   );
673   
674   eval {$issue = $jira->GET ("/search/", \%query)};
675
676   my $issueID = $issue->{issues}[0]{key};
677   
678   return $issue->{issues} if @{$issue->{issues}} > 1;
679   return $issueID;
680 } # getIssueFromBugID
681
682 sub findIssue ($%) {
683   my ($bugid, %bugmap) = @_;
684   
685 =pod
686
687   # Check the cache...
688   if ($cache{$bugid}) {
689     if ($cache{$bugid} =~ /^\d+/) {
690       # We have a cache hit but the contents here are a bugid. This means we had
691       # searched for the corresponding JIRA issue for this bug before and came
692       # up empty handed. In this situtaion we really have:
693       return "Unable to find a JIRA issue for Bug $bugid"; 
694     } else {
695       return $cache{$bugid};
696     } # if
697   } # if
698
699 =cut
700
701   my $issue;
702
703   my %query = (
704     jql    => "\"Bugzilla Bug Origin\" ~ $bugid",
705     fields => [ 'key' ],
706   );
707   
708   eval {$issue = $jira->GET ("/search/", \%query)};
709
710   my $issueID = $issue->{issues}[0]{key};
711   
712   if (@{$issue->{issues}} > 2) {
713     $main::log->err ("Found more than 2 issues for Bug ID $bugid") if $main::log;
714     
715     return "Found more than 2 issues for Bug ID $bugid";
716   } elsif (@{$issue->{issues}} == 2) {
717     my ($issueNum0, $issueNum1, $projectName0, $projectName1);
718     
719     if ($issue->{issues}[0]{key} =~ /(.*)-(\d+)/) {
720       $projectName0 = $1;
721       $issueNum0    = $2;
722     } # if
723     
724     if ($issue->{issues}[1]{key} =~ /(.*)-(\d+)/) {
725       $projectName1 = $1;
726       $issueNum1    = $2;
727     } # if
728     
729     if ($issueNum0 < $issueNum1) {
730       $issueID = $issue->{issues}[1]{key};
731     } # if
732     
733     # Let's mark them as clones. See if this clone link already exists...
734     my $alreadyCloned;
735     
736     for (getIssueLinks ($issueID, 'Cloners')) {
737       my $inwardIssue  = $_->{inwardIssue}{key}  || '';
738       my $outwardIssue = $_->{outwardIssue}{key} || '';
739       
740       if ("$projectName0-$issueNum0" eq $inwardIssue  ||
741           "$projectName0-$issueNum0" eq $outwardIssue ||
742           "$projectName1-$issueNum1" eq $inwardIssue  ||
743           "$projectName1-$issueNum1" eq $outwardIssue) {
744          $alreadyCloned = 1;
745          
746          last;
747       } # if
748     } # for
749
750     unless ($alreadyCloned) {
751       my $result = linkIssues ("$projectName0-$issueNum0", 'Cloners', "$projectName1-$issueNum1");
752     
753       return $result if $result =~ /Unable to/;
754     
755       $main::log->msg ($result) if $main::log;
756     } # unless
757   } # if
758
759   if ($issueID) {
760     $main::log->msg ("Found JIRA issue $issueID for Bug $bugid") if $main::log;
761   
762     #$cache{$bugid} = $issueID;
763       
764     #return $cache{$bugid};
765     return $issueID;
766   } else {
767     my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
768                                  : "Unable to find a JIRA issue for Bug $bugid";
769     
770     # Here we put this bugid into the cache but instead of a the JIRA issue
771     # id we put the bugid. This will stop us from adding up multiple hits on
772     # this bugid.
773     #$cache{$bugid} = $bugid;
774
775     return $status;
776   } # if
777 } # findJIRA
778
779 sub findIssues (;$@) {
780   my ($condition, @fields) = @_;
781
782 =pod
783
784 =head2 findIssues ()
785
786 Set up a find for JIRA issues based on a condition
787
788 Parameters:
789
790 =for html <blockquote>
791
792 =over
793
794 =item $condition
795
796 Condition to use. JQL is supported
797
798 =item @fields
799
800 List of fields to retrieve data for
801
802 =back
803
804 =for html </blockquote>
805
806 Returns:
807
808 =for html <blockquote>
809
810 =over
811
812 =item <nothing>
813
814 =back
815
816 =for html </blockquote>
817
818 =cut  
819
820   push @fields, '*all' unless @fields;
821   
822   $findQuery{jql}        = $condition || '';
823   $findQuery{startAt}    = 0;
824   $findQuery{maxResults} = 1;
825   $findQuery{fields}     = join ',', @fields;
826   
827   return;
828 } # findIssues
829
830 sub getNextIssue () {
831   my $result;
832   
833 =pod
834
835 =head2 getNextIssue ()
836
837 Get next qualifying issue. Call findIssues first
838
839 Parameters:
840
841 =for html <blockquote>
842
843 =over
844
845 =item <none>
846
847 =back
848
849 =for html </blockquote>
850
851 Returns:
852
853 =for html <blockquote>
854
855 =over
856
857 =item %issue
858
859 Perl hash of the fields in the next JIRA issue
860
861 =back
862
863 =for html </blockquote>
864
865 =cut
866   
867   eval {$result = $jira->GET ('/search/', \%findQuery)};
868   
869   $findQuery{startAt}++;
870   
871   # Move id and key into fields
872   return unless @{$result->{issues}};
873   
874   $result->{issues}[0]{fields}{id} = $result->{issues}[0]{id};
875   $result->{issues}[0]{fields}{key} = $result->{issues}[0]{key};
876     
877   return %{$result->{issues}[0]{fields}};
878 } # getNextIssue
879
880 sub getIssues (;$$$@) {
881   my ($condition, $start, $max, @fields) = @_;
882   
883 =pod
884
885 =head2 getIssues ()
886
887 Get the @fields of JIRA issues based on a condition. Note that JIRA limits the
888 amount of entries returned to 1000. You can get fewer. Or you can use $start
889 to continue from where you've left off. 
890
891 Parameters:
892
893 =for html <blockquote>
894
895 =over
896
897 =item $condition
898
899 JQL condition to apply
900
901 =item $start
902
903 Starting point to get issues from
904
905 =item $max
906
907 Max number of entrist to get
908
909 =item @fields
910
911 List of fields to retrieve
912
913 =back
914
915 =for html </blockquote>
916
917 Returns:
918
919 =for html <blockquote>
920
921 =over
922
923 =item @issues
924
925 Perl array of hashes of JIRA issue records
926
927 =back
928
929 =for html </blockquote>
930
931 =cut
932   
933   push @fields, '*all' unless @fields;
934   
935   my ($result, %query);
936   
937   $query{jql}        = $condition || '';
938   $query{startAt}    = $start     || 0;
939   $query{maxResults} = $max       || 50;
940   $query{fields}     = join ',', @fields;
941   
942   eval {$result = $jira->GET ('/search/', \%query)};
943
944   # We sometimes get an error here when $result->{issues} is undef.
945   # I suspect this is when the number of issues just happens to be
946   # an even number like on a $query{maxResults} boundry. So when
947   # $result->{issues} is undef we assume it's the last of the issues.
948   # (I should really verify this).
949   if ($result->{issues}) {
950     return @{$result->{issues}};
951   } else {
952     return;
953   } # if
954 } # getIssues
955
956 sub getIssue ($;@) {
957   my ($issue, @fields) = @_;
958
959 =pod
960
961 =head2 getIssue ()
962
963 Get individual JIRA issue
964
965 Parameters:
966
967 =for html <blockquote>
968
969 =over
970
971 =item $issue
972
973 Issue ID
974
975 =item @fields
976
977 List of fields to retrieve
978
979 =back
980
981 =for html </blockquote>
982
983 Returns:
984
985 =for html <blockquote>
986
987 =over
988
989 =item %issue
990
991 Perl hash of JIRA issue
992
993 =back
994
995 =for html </blockquote>
996
997 =cut
998   
999   my $fields = @fields ? "?fields=" . join ',', @fields : '';
1000
1001   return $jira->GET ("/issue/$issue$fields");
1002 } # getIssue
1003
1004 sub getIssueLinkTypes () {
1005   my $issueLinkTypes = $jira->GET ('/issueLinkType/');
1006   
1007   map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
1008   
1009   return @issueLinkTypes
1010 } # getIssueLinkTypes
1011
1012 sub linkIssues ($$$) {
1013   my ($from, $type, $to) = @_;
1014   
1015   unless (@issueLinkTypes) {
1016     getIssueLinkTypes;
1017   } # unless
1018   
1019   unless (grep {$type eq $_} @issueLinkTypes) {
1020     $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n\t" 
1021                . join "\n\t", @issueLinkTypes) if $main::log;
1022                
1023     return "Unable to $type link $from -> $to";           
1024   } # unless  
1025   
1026   my %link = (
1027     inwardIssue  => {
1028       key        => $from,
1029     },
1030     type         => {
1031       name       => $type,
1032     },
1033     outwardIssue => {
1034       key        => $to,
1035     },
1036     comment      => {
1037       body       => "Link ported as part of the migration from Bugzilla: $from <-> $to",
1038     },
1039   );
1040   
1041   $main::total{'IssueLinks Added'}++;
1042   
1043   if ($main::opts{exec}) {
1044     eval {$jira->POST ("/issueLink", undef, \%link)};
1045     
1046     if ($@) {
1047       return "Unable to $type link $from -> $to\n$@";
1048     } else {
1049       return "Made $type link $from -> $to";
1050     } # if
1051   } else {
1052     return "Would have $type linked $from -> $to";
1053   } # if
1054 } # linkIssue
1055
1056 sub getRemoteLink ($;$) {
1057   my ($jiraIssue, $id) = @_;
1058   
1059 =pod
1060
1061 =head2 getRemoteLink ()
1062
1063 Retrieve a remote link
1064
1065 Parameters:
1066
1067 =for html <blockquote>
1068
1069 =over
1070
1071 =item $jiraIssue
1072
1073 Issue ID
1074
1075 =item $id
1076
1077 Which ID to retrieve
1078
1079 =back
1080
1081 =for html </blockquote>
1082
1083 Returns:
1084
1085 =for html <blockquote>
1086
1087 =over
1088
1089 =item %issue
1090
1091 Perl hash of remote links
1092
1093 =back
1094
1095 =for html </blockquote>
1096
1097 =cut
1098   
1099   $id //= '';
1100   
1101   my $result;
1102   
1103   eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
1104   
1105   return if $@;
1106   
1107   my %remoteLinks;
1108
1109   if (ref $result eq 'ARRAY') {
1110     map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;  
1111   } else {
1112     $remoteLinks{$result->{id}} = $result->{object}{title};
1113   } # if
1114     
1115   return \%remoteLinks;
1116 } # getRemoteLink
1117
1118 sub getRemoteLinks (;$) {
1119   my ($bugid) = @_;
1120   
1121   $jiradb = openJIRADB unless $jiradb;
1122   
1123   my $statement = 'select url from remotelink';
1124
1125   $statement .= " where url like 'http://bugs%'";  
1126   $statement .= " and url like '%$bugid'" if $bugid; 
1127   $statement .= " group by issueid desc";
1128   
1129   my $sth = $jiradb->prepare ($statement);
1130   
1131   _checkDBError 'Unable to prepare statement', $statement;
1132   
1133   $sth->execute;
1134   
1135   _checkDBError 'Unable to execute statement', $statement;
1136
1137   my %bugids;
1138   
1139   while (my $record = $sth->fetchrow_array) {
1140     if ($record =~ /(\d+)$/) {
1141       $bugids{$1} = 1;
1142     } # if 
1143   } # while
1144   
1145   return keys %bugids;
1146 } # getRemoteLinks
1147
1148 sub findRemoteLinkByBugID (;$) {
1149   my ($bugid) = @_;
1150   
1151   my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
1152   
1153   if ($bugid) {
1154     $condition .= " and remotelink.url like '%id=$bugid'";
1155   } # unless
1156   
1157   $jiradb = openJIRADB unless $jiradb;
1158
1159   my $statement = <<"END";
1160 select 
1161   remotelink.id, 
1162   concat (project.pkey, '-', issuenum) as issue,
1163   relationship
1164 from
1165   remotelink,
1166   jiraissue,
1167   project
1168 $condition
1169 END
1170
1171   my $sth = $jiradb->prepare ($statement);
1172   
1173   _checkDBError 'Unable to prepare statement', $statement;
1174   
1175   $sth->execute;
1176   
1177   _checkDBError 'Unable to execute statement', $statement;
1178   
1179   my @records;
1180   
1181   while (my $row = $sth->fetchrow_hashref) {
1182     $row->{bugid} = $bugid;
1183         
1184     push @records, $row;
1185   } # while
1186   
1187   return \@records;
1188 } # findRemoteLinkByBugID
1189
1190 sub promoteBug2JIRAIssue ($$$$) {
1191   my ($bugid, $jirafrom, $jirato, $relationship) = @_;
1192
1193   my $result = linkIssues $jirafrom, $relationship, $jirato;
1194         
1195   return $result if $result =~ /Unable to link/;
1196   
1197   $main::log->msg ($result . " (BugID $bugid)") if $main::log;
1198   
1199   for (@{findRemoteLinkByBugID $bugid}) {
1200     my %record = %$_;
1201     
1202     $result = removeRemoteLink ($record{issue}, $record{id});
1203     
1204     # We may not care if we couldn't remove this link because it may have been
1205     # removed by a prior pass.
1206     return $result if $result =~ /Unable to remove link/;
1207     
1208     if ($main::log) {
1209       $main::log->msg ($result) unless $result eq '';
1210     } # if
1211   } # for
1212   
1213   return $result;
1214 } # promoteBug2JIRAIssue
1215
1216 sub addRemoteLink ($$$) {
1217   my ($bugid, $relationship, $jiraIssue) = @_;
1218   
1219   my $bug = getBug $bugid;
1220   
1221   # Check to see if this Bug ID already exists on this JIRA Issue, otherwise
1222   # JIRA will duplicate it! 
1223   my $remoteLinks = getRemoteLink $jiraIssue;
1224   
1225   for (keys %$remoteLinks) {
1226     if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
1227       return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
1228     } # if
1229   } # for
1230   
1231   # Note this globalid thing is NOT working! ALl I see is null in the database
1232   my %remoteLink = (
1233 #    globalid     => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
1234 #    application  => {
1235 #      type       => 'Bugzilla',
1236 #      name       => 'Bugzilla',
1237 #    },
1238     relationship => $relationship, 
1239     object       => {
1240       url        => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
1241       title      => "Bug $bugid",
1242       summary    => $bug->{short_desc},
1243       icon       => {
1244         url16x16 => 'http://bugs.audience.local/favicon.png',
1245         title    => 'Bugzilla Bug',
1246       },
1247     },
1248   );
1249   
1250   $main::total{'RemoteLink Added'}++;
1251   
1252   if ($main::opts{exec}) {
1253     eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
1254   
1255     return $@;
1256   } else {
1257     return "Would have linked $bugid -> $jiraIssue";
1258   } # if
1259 } # addRemoteLink
1260
1261 sub removeRemoteLink ($;$) {
1262   my ($jiraIssue, $id) = @_;
1263   
1264   $id //= '';
1265   
1266   my $remoteLinks = getRemoteLink ($jiraIssue, $id);
1267   
1268   for (keys %$remoteLinks) {
1269     my $result;
1270     
1271     $main::total{'RemoteLink Removed'}++;
1272   
1273     if ($main::opts{exec}) {
1274       eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
1275
1276       if ($@) {  
1277         return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
1278       } else {
1279         my $bugid;
1280         
1281         if ($remoteLinks->{$_} =~ /(\d+)/) {
1282           return "Removed remote link $jiraIssue (Bug ID $1)";
1283         } # if
1284       } # if
1285       
1286       $main::total{'Remote Links Removed'}++;
1287     } else {
1288       if ($remoteLinks->{$_} =~ /(\d+)/) {
1289         return "Would have removed remote link $jiraIssue (Bug ID $1)";
1290       } # if
1291     } # if
1292   } # for  
1293 } # removeRemoteLink
1294
1295 sub getIssueLinks ($;$) {
1296   my ($issue, $type) = @_;
1297   
1298   my @links = getIssue ($issue, ('issuelinks'));
1299   
1300   my @issueLinks;
1301
1302   for (@{$links[0]->{fields}{issuelinks}}) {
1303      my %issueLink = %$_;
1304      
1305      next if ($type && $type ne $issueLink{type}{name});
1306      
1307      push @issueLinks, \%issueLink;  
1308   }
1309   
1310   return @issueLinks;
1311 } # getIssueLinks
1312
1313 sub getIssueWatchers ($) {
1314   my ($issue) = @_;
1315   
1316   my $watchers;
1317   
1318   eval {$watchers = $jira->GET ("/issue/$issue/watchers")};
1319   
1320   return if $@;
1321   
1322   # The watcher information returned by the above is incomplete. Let's complete
1323   # it.
1324   my @watchers;
1325   
1326   for (@{$watchers->{watchers}}) {
1327     my $user;
1328     
1329     eval {$user = $jira->GET ("/user?username=$_->{key}")};
1330     
1331     unless ($@) {
1332       push @watchers, $user;
1333     } else {
1334       if ($main::log) {
1335         $main::log->err ("Unable to find user record for $_->{name}")
1336           unless $_->{name} eq 'jira-admin';
1337       }# if
1338     } # unless
1339   } # for
1340   
1341   return @watchers;
1342 } # getIssueWatchers
1343
1344 sub updateIssueWatchers ($%) {
1345   my ($issue, %watchers) = @_;
1346   
1347 =pod
1348
1349 =head2 updateIssueWatchers ()
1350
1351 Updates the issue watchers list
1352
1353 Parameters:
1354
1355 =for html <blockquote>
1356
1357 =over
1358
1359 =item $issue
1360
1361 Issue ID
1362
1363 =item %watchers
1364
1365 List of watchers to add
1366
1367 =back
1368
1369 =for html </blockquote>
1370
1371 Returns:
1372
1373 =for html <blockquote>
1374
1375 =over
1376
1377 =item $error 
1378
1379 Error message or '' to indicate no error
1380
1381 =back
1382
1383 =for html </blockquote>
1384
1385 =cut
1386
1387   my $existingWatchers;
1388   
1389   eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
1390   
1391   return "Unable to get issue $issue\n$@" if $@;
1392   
1393   for (@{$existingWatchers->{watchers}}) {
1394     # Cleanup: Remove the current user from the watchers list.
1395     # If he's on the list then remove him.
1396     if ($_->{name} eq $jira->{username}) {
1397       $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
1398       
1399       $main::total{"Admins destroyed"}++;
1400     } # if
1401     
1402     # Delete any matching watchers
1403     delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
1404   } # for
1405
1406   return '' if keys %watchers == 0;
1407   
1408   my $issueUpdated;
1409   
1410   for (keys %watchers) {
1411     if ($main::opts{exec}) {
1412       eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
1413     
1414       if ($@) {
1415         $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue") if $main::log;
1416       
1417         $main::total{'Watchers skipped'}++;
1418       } else {
1419         $issueUpdated = 1;
1420         
1421         $main::total{'Watchers added'}++;
1422       } # if
1423     } else {
1424       $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue") if $main::log;
1425       
1426       $main::total{'Watchers that would have been added'}++;
1427     } # if
1428   } # for
1429   
1430   $main::total{'Issues updated'}++ if $issueUpdated;
1431   
1432   return '';
1433 } # updateIssueWatchers
1434
1435 sub getUsersGroups ($) {
1436   my ($username) = @_;
1437   
1438 =pod
1439
1440 =head2 getUsersGroups ()
1441
1442 Returns the groups that the user is a member of
1443
1444 Parameters:
1445
1446 =for html <blockquote>
1447
1448 =over
1449
1450 =item $username
1451
1452 Username
1453
1454 =back
1455
1456 =for html </blockquote>
1457
1458 Returns:
1459
1460 =for html <blockquote>
1461
1462 =over
1463
1464 =item @groups
1465
1466 List of groups
1467
1468 =back
1469
1470 =for html </blockquote>
1471
1472 =cut
1473   
1474   my ($result, %query);
1475   
1476   %query = (
1477     username => $username,
1478     expand   => 'groups',
1479   );
1480   
1481   eval {$result = $jira->GET ('/user/', \%query)};
1482   
1483   my @groups;
1484   
1485   for (@{$result->{groups}{items}}) {
1486     push @groups, $_->{name};
1487   } # for
1488   
1489   return @groups;
1490 } # getusersGroups
1491
1492 sub updateUsersGroups ($@) {
1493   my ($username, @groups) = @_;
1494
1495 =pod
1496
1497 =head2 updateUsersGroups ()
1498
1499 Updates the users group membership
1500
1501 Parameters:
1502
1503 =for html <blockquote>
1504
1505 =over
1506
1507 =item $username
1508
1509 Username to operate on
1510
1511 =item @groups
1512
1513 List of groups the user should be a member of
1514
1515 =back
1516
1517 =for html </blockquote>
1518
1519 Returns:
1520
1521 =for html <blockquote>
1522
1523 =over
1524
1525 =item @errors
1526
1527 List of errors (if any)
1528
1529 =back
1530
1531 =for html </blockquote>
1532
1533 =cut
1534   
1535   my ($result, @errors);
1536   
1537   my @oldgroups = getUsersGroups $username;
1538   
1539   # We can't always add groups to the new user due to either the group not being
1540   # in the new LDAP directory or we are unable to see it. If we attempt to JIRA
1541   # will try to add the group and we don't have write permission to the 
1542   # directory. So we'll just return @errors and let the caller deal with it.  
1543   for my $group (@groups) {
1544     next if grep {$_ eq $group} @oldgroups;
1545     
1546     eval {$result = $jira->POST ('/group/user', {groupname => $group}, {name => $username})};
1547   
1548     push @errors, $@ if $@;  
1549   } # for
1550   
1551   return @errors;
1552 } # updateUsersGroups
1553
1554 sub copyGroupMembership ($$) {
1555   my ($from_username, $to_username) = @_;
1556   
1557   return updateUsersGroups $to_username, getUsersGroups $from_username;
1558 } # copyGroupMembership
1559
1560 sub updateColumn ($$$%) {
1561   my ($table, $oldvalue, $newvalue, %info) = @_;
1562   
1563 =pod
1564
1565 =head2 updateColumn ()
1566
1567 Updates a column in the MySQL JIRA database (SQL surgery)
1568
1569 Parameters:
1570
1571 =for html <blockquote>
1572
1573 =over
1574
1575 =item $table
1576
1577 Table to operate on
1578
1579 =item $oldvalue
1580
1581 Old value
1582
1583 =item $newvalue
1584
1585 New value
1586
1587 =item %info
1588
1589 Hash of column names and optional conditions
1590
1591 =back
1592
1593 =for html </blockquote>
1594
1595 Returns:
1596
1597 =for html <blockquote>
1598
1599 =over
1600
1601 =item $numrows
1602
1603 Number of rows updated
1604
1605 =back
1606
1607 =for html </blockquote>
1608
1609 =cut
1610   
1611   # UGH! Sometimes values need to be quoted
1612   $oldvalue = quotemeta $oldvalue;
1613   $newvalue = quotemeta $newvalue;
1614   
1615   my $condition  =  "$info{column} = '$oldvalue'";
1616      $condition .= " and $info{condition}" if $info{condition};
1617   my $statement  = "update $table set $info{column} = '$newvalue' where $condition";
1618
1619   my $nbrRows = count $table, $condition;
1620   
1621   if ($nbrRows) {
1622     if ($main::opts{exec}) {
1623       $main::total{'Rows updated'}++;
1624     
1625       $jiradb->do ($statement);
1626       
1627       _checkDBError 'Unable to execute statement', $statement;
1628     } else {
1629       $main::total{'Rows would be updated'}++;
1630
1631       $main::log->msg ("Would have executed $statement") if $main::log;
1632     } # if
1633   } # if 
1634   
1635   return $nbrRows;
1636 } # updateColumn
1637
1638 sub renameUsers (%) {
1639   my (%users) = @_;
1640
1641 =pod
1642
1643 =head2 renameUsers ()
1644
1645 Renames users
1646
1647 Parameters:
1648
1649 =for html <blockquote>
1650
1651 =over
1652
1653 =item %users
1654
1655 Hash of old -> new usernames
1656
1657 =back
1658
1659 =for html </blockquote>
1660
1661 Returns:
1662
1663 =for html <blockquote>
1664
1665 =over
1666
1667 =item $errors
1668
1669 Number of errors
1670
1671 =back
1672
1673 =for html </blockquote>
1674
1675 =cut
1676
1677   for my $olduser (sort keys %users) {
1678     my $newuser = $users{$olduser};
1679     
1680     $main::log->msg ("Renaming $olduser -> $newuser") if $main::log;
1681     display ("Renaming $olduser -> $newuser");
1682     
1683     if ($main::opts{exec}) {
1684       $main::total{'Users renamed'}++;
1685     } else {
1686       $main::total{'Users would be updated'}++;
1687     } # if
1688     
1689     for my $table (sort keys %tables) {
1690       $main::log->msg ("\tTable: $table Column: ", 1) if $main::log;
1691       
1692       my @columns = @{$tables{$table}};
1693       
1694       for my $column (@columns) {
1695         my %info = %$column;
1696         
1697         $main::log->msg ("$info{column} ", 1) if $main::log;
1698         
1699         my $rowsUpdated = updateColumn ($table, $olduser, $newuser, %info);
1700         
1701         if ($rowsUpdated) {
1702           my $msg  = " $rowsUpdated row";
1703              $msg .= 's' if $rowsUpdated > 1;
1704              $msg .= ' would have been' unless $main::opts{exec};
1705              $msg .= ' updated';
1706              
1707           $main::log->msg ($msg, 1) if $main::log;
1708         } # if
1709       } # for
1710       
1711       $main::log->msg ('') if $main::log;
1712     } # for
1713     
1714     if (my @result = copyGroupMembership ($olduser, $newuser)) {
1715       # Skip errors of the form 'Could not add user... group is read-only
1716       @result = grep {!/Could not add user.*group is read-only/} @result;
1717       
1718       if ($main::log) {
1719         $main::log->err ("Unable to copy group membership from $olduser -> $newuser\n@result", 1) if @result;
1720       } # if
1721     } # if
1722   } # for
1723   
1724   
1725   return $main::log ? $main::log->errors : 0;
1726 } # renameUsers
1727
1728 1;