Adding some files of recent work.
[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   $dbhost //= $main::opts{jiradbhost};
237   $dbname //= 'jiradb';
238   $dbuser //= 'root';
239   $dbpass //= 'r00t';
240   
241   $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...") if $main::log;
242   
243   $jiradb = DBI->connect (
244     "DBI:mysql:$dbname:$dbhost",
245     $dbuser,
246     $dbpass, {
247       PrintError => 0,
248       RaiseError => 1,
249     },
250   );
251
252   _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
253   
254   return $jiradb;
255 } # openJIRADB
256
257 sub Connect2JIRA (;$$$) {
258   my ($username, $password, $server) = @_;
259
260   my %opts;
261   
262   $opts{username} = $username || 'jira-admin';
263   $opts{password} = $password || $ENV{PASSWORD}    || 'jira-admin';
264   $opts{server}   = $server   || $ENV{JIRA_SERVER} || 'jira-dev';
265   $opts{URL}      = "http://$opts{server}/rest/api/latest";
266   
267   $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})") if $main::log;
268   
269   $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
270
271   # Store username as we might need it (see updateIssueWatchers)
272   $jira->{username} = $opts{username};
273   
274   return $jira;  
275 } # Connect2JIRA
276
277 sub count ($$) {
278   my ($table, $condition) = @_;
279
280   my $statement;
281   
282   $jiradb = openJIRADB unless $jiradb;
283
284   if ($condition) {
285     $statement = "select count(*) from $table where $condition";
286   } else {
287     $statement = "select count(*) from $table";
288   } # if
289
290   my $sth = $jiradb->prepare ($statement);
291   
292   _checkDBError 'count: Unable to prepare statement', $statement;
293
294   $sth->execute;
295   
296   _checkDBError 'count: Unable to execute statement', $statement;
297
298   # Get return value, which should be how many message there are
299   my @row = $sth->fetchrow_array;
300
301   # Done with $sth
302   $sth->finish;
303
304   my $count;
305
306   # Retrieve returned value
307   unless ($row[0]) {
308     $count = 0
309   } else {
310     $count = $row[0];
311   } # unless
312
313   return $count
314 } # count
315
316 sub addDescription ($$) {
317   my ($issue, $description) = @_;
318   
319   if ($main::opts{exec}) {
320     eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
321   
322     if ($@) {
323       return "Unable to add description\n$@";
324     } else {
325       return 'Description added';
326     } # if
327   } # if
328 } # addDescription
329
330 sub addJIRAComment ($$) {
331   my ($issue, $comment) = @_;
332   
333   if ($main::opts{exec}) {
334     eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
335   
336     if ($@) {
337       return "Unable to add comment\n$@";
338     } else {
339       return 'Comment added';
340     } # if
341   } else {
342     return "Would have added comments to $issue";
343   } # if
344 } # addJIRAComment
345
346 sub blankBugzillaNbr ($) {
347   my ($issue) = @_;
348   
349   eval {$jira->PUT ("/issue/$issue", undef, {fields => {'Bugzilla Bug Origin' => ''}})};
350   #eval {$jira->PUT ("/issue/$issue", undef, {fields => {'customfield_10132' => ''}})};
351   
352   if ($@) {
353     return "Unable to blank Bugzilla number$@\n"
354   } else {
355     return 'Corrected'
356   } # if
357 } # blankBugzillaNbr
358
359 sub attachmentExists ($$) {
360   my ($issue, $filename) = @_;
361   
362   my $attachments = getIssue ($issue, qw(attachment));
363   
364   for (@{$attachments->{fields}{attachment}}) {
365     return 1 if $filename eq $_->{filename};
366   } # for
367   
368   return 0;
369 } # attachmentExists
370
371 sub attachFiles2Issue ($@) {
372   my ($issue, @files) = @_;
373
374   my $status = $jira->attach_files ($issue, @files);
375   
376   return $status;
377 } # attachFiles2Issue
378
379 sub getIssueFromBugID ($) {
380   my ($bugid) = @_;
381   
382   my $issue;
383   
384   my %query = (
385     jql    => "\"Bugzilla Bug Origin\" ~ $bugid",
386     fields => [ 'key' ],
387   );
388   
389   eval {$issue = $jira->GET ("/search/", \%query)};
390
391   my $issueID = $issue->{issues}[0]{key};
392   
393   return $issue->{issues} if @{$issue->{issues}} > 1;
394   return $issueID;
395 } # getIssueFromBugID
396
397 sub findIssue ($%) {
398   my ($bugid, %bugmap) = @_;
399   
400 =pod
401   # Check the cache...
402   if ($cache{$bugid}) {
403     if ($cache{$bugid} =~ /^\d+/) {
404       # We have a cache hit but the contents here are a bugid. This means we had
405       # searched for the corresponding JIRA issue for this bug before and came
406       # up empty handed. In this situtaion we really have:
407       return "Unable to find a JIRA issue for Bug $bugid"; 
408     } else {
409       return $cache{$bugid};
410     } # if
411   } # if
412 =cut  
413   my $issue;
414
415   my %query = (
416     jql    => "\"Bugzilla Bug Origin\" ~ $bugid",
417     fields => [ 'key' ],
418   );
419   
420   eval {$issue = $jira->GET ("/search/", \%query)};
421
422   my $issueID = $issue->{issues}[0]{key};
423   
424   if (@{$issue->{issues}} > 2) {
425     $main::log->err ("Found more than 2 issues for Bug ID $bugid") if $main::log;
426     
427     return "Found more than 2 issues for Bug ID $bugid";
428   } elsif (@{$issue->{issues}} == 2) {
429     my ($issueNum0, $issueNum1, $projectName0, $projectName1);
430     
431     if ($issue->{issues}[0]{key} =~ /(.*)-(\d+)/) {
432       $projectName0 = $1;
433       $issueNum0    = $2;
434     } # if
435     
436     if ($issue->{issues}[1]{key} =~ /(.*)-(\d+)/) {
437       $projectName1 = $1;
438       $issueNum1    = $2;
439     } # if
440     
441     if ($issueNum0 < $issueNum1) {
442       $issueID = $issue->{issues}[1]{key};
443     } # if
444     
445     # Let's mark them as clones. See if this clone link already exists...
446     my $alreadyCloned;
447     
448     for (getIssueLinks ($issueID, 'Cloners')) {
449       my $inwardIssue  = $_->{inwardIssue}{key}  || '';
450       my $outwardIssue = $_->{outwardIssue}{key} || '';
451       
452       if ("$projectName0-$issueNum0" eq $inwardIssue  ||
453           "$projectName0-$issueNum0" eq $outwardIssue ||
454           "$projectName1-$issueNum1" eq $inwardIssue  ||
455           "$projectName1-$issueNum1" eq $outwardIssue) {
456          $alreadyCloned = 1;
457          
458          last;
459       } # if
460     } # for
461
462     unless ($alreadyCloned) {
463       my $result = linkIssues ("$projectName0-$issueNum0", 'Cloners', "$projectName1-$issueNum1");
464     
465       return $result if $result =~ /Unable to/;
466     
467       $main::log->msg ($result) if $main::log;
468     } # unless
469   } # if
470
471   if ($issueID) {
472     $main::log->msg ("Found JIRA issue $issueID for Bug $bugid") if $main::log;
473   
474     #$cache{$bugid} = $issueID;
475       
476     #return $cache{$bugid};
477     return $issueID;
478   } else {
479     my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
480                                  : "Unable to find a JIRA issue for Bug $bugid";
481     
482     # Here we put this bugid into the cache but instead of a the JIRA issue
483     # id we put the bugid. This will stop us from adding up multiple hits on
484     # this bugid.
485     #$cache{$bugid} = $bugid;
486
487     return $status;
488   } # if
489 } # findJIRA
490
491 sub findIssues (;$@) {
492   my ($condition, @fields) = @_;
493   
494   push @fields, '*all' unless @fields;
495   
496   $findQuery{jql}        = $condition || '';
497   $findQuery{startAt}    = 0;
498   $findQuery{maxResults} = 1;
499   $findQuery{fields}     = join ',', @fields;
500   
501   return;
502 } # findIssues
503
504 sub getNextIssue () {
505   my $result;
506   
507   eval {$result = $jira->GET ('/search/', \%findQuery)};
508   
509   $findQuery{startAt}++;
510   
511   # Move id and key into fields
512   return unless @{$result->{issues}};
513   
514   $result->{issues}[0]{fields}{id} = $result->{issues}[0]{id};
515   $result->{issues}[0]{fields}{key} = $result->{issues}[0]{key};
516     
517   return %{$result->{issues}[0]{fields}};
518 } # getNextIssue
519
520 sub getIssues (;$$$@) {
521   my ($condition, $start, $max, @fields) = @_;
522   
523   push @fields, '*all' unless @fields;
524   
525   my ($result, %query);
526   
527   $query{jql}        = $condition || '';
528   $query{startAt}    = $start     || 0;
529   $query{maxResults} = $max       || 50;
530   $query{fields}     = join ',', @fields;
531   
532   eval {$result = $jira->GET ('/search/', \%query)};
533
534   # We sometimes get an error here when $result->{issues} is undef.
535   # I suspect this is when the number of issues just happens to be
536   # an even number like on a $query{maxResults} boundry. So when
537   # $result->{issues} is undef we assume it's the last of the issues.
538   # (I should really verify this).
539   if ($result->{issues}) {
540     return @{$result->{issues}};
541   } else {
542     return;
543   } # if
544 } # getIssues
545
546 sub getIssue ($;@) {
547   my ($issue, @fields) = @_;
548   
549   my $fields = @fields ? "?fields=" . join ',', @fields : '';
550
551   return $jira->GET ("/issue/$issue$fields");
552 } # getIssue
553
554 sub getIssueLinkTypes () {
555   my $issueLinkTypes = $jira->GET ('/issueLinkType/');
556   
557   map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
558   
559   return @issueLinkTypes
560 } # getIssueLinkTypes
561
562 sub linkIssues ($$$) {
563   my ($from, $type, $to) = @_;
564   
565   unless (@issueLinkTypes) {
566     getIssueLinkTypes;
567   } # unless
568   
569   unless (grep {$type eq $_} @issueLinkTypes) {
570     $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n\t" 
571                . join "\n\t", @issueLinkTypes) if $main::log;
572                
573     return "Unable to $type link $from -> $to";           
574   } # unless  
575   
576   my %link = (
577     inwardIssue  => {
578       key        => $from,
579     },
580     type         => {
581       name       => $type,
582     },
583     outwardIssue => {
584       key        => $to,
585     },
586     comment      => {
587       body       => "Link ported as part of the migration from Bugzilla: $from <-> $to",
588     },
589   );
590   
591   $main::total{'IssueLinks Added'}++;
592   
593   if ($main::opts{exec}) {
594     eval {$jira->POST ("/issueLink", undef, \%link)};
595     
596     if ($@) {
597       return "Unable to $type link $from -> $to\n$@";
598     } else {
599       return "Made $type link $from -> $to";
600     } # if
601   } else {
602     return "Would have $type linked $from -> $to";
603   } # if
604 } # linkIssue
605
606 sub getRemoteLink ($;$) {
607   my ($jiraIssue, $id) = @_;
608   
609   $id //= '';
610   
611   my $result;
612   
613   eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
614   
615   return if $@;
616   
617   my %remoteLinks;
618
619   if (ref $result eq 'ARRAY') {
620     map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;  
621   } else {
622     $remoteLinks{$result->{id}} = $result->{object}{title};
623   } # if
624     
625   return \%remoteLinks;
626 } # getRemoteLink
627
628 sub getRemoteLinks (;$) {
629   my ($bugid) = @_;
630   
631   $jiradb = openJIRADB unless $jiradb;
632   
633   my $statement = 'select url from remotelink';
634
635   $statement .= " where url like 'http://bugs%'";  
636   $statement .= " and url like '%$bugid'" if $bugid; 
637   $statement .= " group by issueid desc";
638   
639   my $sth = $jiradb->prepare ($statement);
640   
641   _checkDBError 'Unable to prepare statement', $statement;
642   
643   $sth->execute;
644   
645   _checkDBError 'Unable to execute statement', $statement;
646
647   my %bugids;
648   
649   while (my $record = $sth->fetchrow_array) {
650     if ($record =~ /(\d+)$/) {
651       $bugids{$1} = 1;
652     } # if 
653   } # while
654   
655   return keys %bugids;
656 } # getRemoteLinks
657
658 sub findRemoteLinkByBugID (;$) {
659   my ($bugid) = @_;
660   
661   my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
662   
663   if ($bugid) {
664     $condition .= " and remotelink.url like '%id=$bugid'";
665   } # unless
666   
667   $jiradb = openJIRADB unless $jiradb;
668
669   my $statement = <<"END";
670 select 
671   remotelink.id, 
672   concat (project.pkey, '-', issuenum) as issue,
673   relationship
674 from
675   remotelink,
676   jiraissue,
677   project
678 $condition
679 END
680
681   my $sth = $jiradb->prepare ($statement);
682   
683   _checkDBError 'Unable to prepare statement', $statement;
684   
685   $sth->execute;
686   
687   _checkDBError 'Unable to execute statement', $statement;
688   
689   my @records;
690   
691   while (my $row = $sth->fetchrow_hashref) {
692     $row->{bugid} = $bugid;
693         
694     push @records, $row;
695   } # while
696   
697   return \@records;
698 } # findRemoteLinkByBugID
699
700 sub promoteBug2JIRAIssue ($$$$) {
701   my ($bugid, $jirafrom, $jirato, $relationship) = @_;
702
703   my $result = linkIssues $jirafrom, $relationship, $jirato;
704         
705   return $result if $result =~ /Unable to link/;
706   
707   $main::log->msg ($result . " (BugID $bugid)") if $main::log;
708   
709   for (@{findRemoteLinkByBugID $bugid}) {
710     my %record = %$_;
711     
712     $result = removeRemoteLink ($record{issue}, $record{id});
713     
714     # We may not care if we couldn't remove this link because it may have been
715     # removed by a prior pass.
716     return $result if $result =~ /Unable to remove link/;
717     
718     if ($main::log) {
719       $main::log->msg ($result) unless $result eq '';
720     } # if
721   } # for
722   
723   return $result;
724 } # promoteBug2JIRAIssue
725
726 sub addRemoteLink ($$$) {
727   my ($bugid, $relationship, $jiraIssue) = @_;
728   
729   my $bug = getBug $bugid;
730   
731   # Check to see if this Bug ID already exists on this JIRA Issue, otherwise
732   # JIRA will duplicate it! 
733   my $remoteLinks = getRemoteLink $jiraIssue;
734   
735   for (keys %$remoteLinks) {
736     if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
737       return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
738     } # if
739   } # for
740   
741   # Note this globalid thing is NOT working! ALl I see is null in the database
742   my %remoteLink = (
743 #    globalid     => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
744 #    application  => {
745 #      type       => 'Bugzilla',
746 #      name       => 'Bugzilla',
747 #    },
748     relationship => $relationship, 
749     object       => {
750       url        => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
751       title      => "Bug $bugid",
752       summary    => $bug->{short_desc},
753       icon       => {
754         url16x16 => 'http://bugs.audience.local/favicon.png',
755         title    => 'Bugzilla Bug',
756       },
757     },
758   );
759   
760   $main::total{'RemoteLink Added'}++;
761   
762   if ($main::opts{exec}) {
763     eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
764   
765     return $@;
766   } else {
767     return "Would have linked $bugid -> $jiraIssue";
768   } # if
769 } # addRemoteLink
770
771 sub removeRemoteLink ($;$) {
772   my ($jiraIssue, $id) = @_;
773   
774   $id //= '';
775   
776   my $remoteLinks = getRemoteLink ($jiraIssue, $id);
777   
778   for (keys %$remoteLinks) {
779     my $result;
780     
781     $main::total{'RemoteLink Removed'}++;
782   
783     if ($main::opts{exec}) {
784       eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
785
786       if ($@) {  
787         return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
788       } else {
789         my $bugid;
790         
791         if ($remoteLinks->{$_} =~ /(\d+)/) {
792           return "Removed remote link $jiraIssue (Bug ID $1)";
793         } # if
794       } # if
795       
796       $main::total{'Remote Links Removed'}++;
797     } else {
798       if ($remoteLinks->{$_} =~ /(\d+)/) {
799         return "Would have removed remote link $jiraIssue (Bug ID $1)";
800       } # if
801     } # if
802   } # for  
803 } # removeRemoteLink
804
805 sub getIssueLinks ($;$) {
806   my ($issue, $type) = @_;
807   
808   my @links = getIssue ($issue, ('issuelinks'));
809   
810   my @issueLinks;
811
812   for (@{$links[0]->{fields}{issuelinks}}) {
813      my %issueLink = %$_;
814      
815      next if ($type && $type ne $issueLink{type}{name});
816      
817      push @issueLinks, \%issueLink;  
818   }
819   
820   return @issueLinks;
821 } # getIssueLinks
822
823 sub getIssueWatchers ($) {
824   my ($issue) = @_;
825   
826   my $watchers;
827   
828   eval {$watchers = $jira->GET ("/issue/$issue/watchers")};
829   
830   return if $@;
831   
832   # The watcher information returned by the above is incomplete. Let's complete
833   # it.
834   my @watchers;
835   
836   for (@{$watchers->{watchers}}) {
837     my $user;
838     
839     eval {$user = $jira->GET ("/user?username=$_->{key}")};
840     
841     unless ($@) {
842       push @watchers, $user;
843     } else {
844       if ($main::log) {
845         $main::log->err ("Unable to find user record for $_->{name}")
846           unless $_->{name} eq 'jira-admin';
847       }# if
848     } # unless
849   } # for
850   
851   return @watchers;
852 } # getIssueWatchers
853
854 sub updateIssueWatchers ($%) {
855   my ($issue, %watchers) = @_;
856
857   my $existingWatchers;
858   
859   eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
860   
861   return "Unable to get issue $issue\n$@" if $@;
862   
863   for (@{$existingWatchers->{watchers}}) {
864     # Cleanup: Remove the current user from the watchers list.
865     # If he's on the list then remove him.
866     if ($_->{name} eq $jira->{username}) {
867       $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
868       
869       $main::total{"Admins destroyed"}++;
870     } # if
871     
872     # Delete any matching watchers
873     delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
874   } # for
875
876   return '' if keys %watchers == 0;
877   
878   my $issueUpdated;
879   
880   for (keys %watchers) {
881     if ($main::opts{exec}) {
882       eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
883     
884       if ($@) {
885         $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue") if $main::log;
886       
887         $main::total{'Watchers skipped'}++;
888       } else {
889         $issueUpdated = 1;
890         
891         $main::total{'Watchers added'}++;
892       } # if
893     } else {
894       $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue") if $main::log;
895       
896       $main::total{'Watchers that would have been added'}++;
897     } # if
898   } # for
899   
900   $main::total{'Issues updated'}++ if $issueUpdated;
901   
902   return '';
903 } # updateIssueWatchers
904
905 sub getUsersGroups ($) {
906   my ($username) = @_;
907   
908   my ($result, %query);
909   
910   %query = (
911     username => $username,
912     expand   => 'groups',
913   );
914   
915   eval {$result = $jira->GET ('/user/', \%query)};
916   
917   my @groups;
918   
919   for (@{$result->{groups}{items}}) {
920     push @groups, $_->{name};
921   } # for
922   
923   return @groups;
924 } # getusersGroups
925
926 sub updateUsersGroups ($@) {
927   my ($username, @groups) = @_;
928   
929   my ($result, @errors);
930   
931   my @oldgroups = getUsersGroups $username;
932   
933   # We can't always add groups to the new user due to either the group not being
934   # in the new LDAP directory or we are unable to see it. If we attempt to JIRA
935   # will try to add the group and we don't have write permission to the 
936   # directory. So we'll just return @errors and let the caller deal with it.  
937   for my $group (@groups) {
938     next if grep {$_ eq $group} @oldgroups;
939     
940     eval {$result = $jira->POST ('/group/user', {groupname => $group}, {name => $username})};
941   
942     push @errors, $@ if $@;  
943   } # for
944   
945   return @errors;
946 } # updateUsersGroups
947
948 sub copyGroupMembership ($$) {
949   my ($from_username, $to_username) = @_;
950   
951   return updateUsersGroups $to_username, getUsersGroups $from_username;
952 } # copyGroupMembership
953
954 sub updateColumn ($$$%) {
955   my ($table, $oldvalue, $newvalue, %info) = @_;
956   
957   # UGH! Sometimes values need to be quoted
958   $oldvalue = quotemeta $oldvalue;
959   $newvalue = quotemeta $newvalue;
960   
961   my $condition  =  "$info{column} = '$oldvalue'";
962      $condition .= " and $info{condition}" if $info{condition};
963   my $statement  = "update $table set $info{column} = '$newvalue' where $condition";
964
965   my $nbrRows = count $table, $condition;
966   
967   if ($nbrRows) {
968     if ($main::opts{exec}) {
969       $main::total{'Rows updated'}++;
970     
971       $jiradb->do ($statement);
972       
973       _checkDBError 'Unable to execute statement', $statement;
974     } else {
975       $main::total{'Rows would be updated'}++;
976
977       $main::log->msg ("Would have executed $statement") if $main::log;
978     } # if
979   } # if 
980   
981   return $nbrRows;
982 } # updateColumn
983
984 sub renameUsers (%) {
985   my (%users) = @_;
986
987   for my $olduser (sort keys %users) {
988     my $newuser = $users{$olduser};
989     
990     $main::log->msg ("Renaming $olduser -> $newuser") if $main::log;
991     display ("Renaming $olduser -> $newuser");
992     
993     if ($main::opts{exec}) {
994       $main::total{'Users renamed'}++;
995     } else {
996       $main::total{'Users would be updated'}++;
997     } # if
998     
999     for my $table (sort keys %tables) {
1000       $main::log->msg ("\tTable: $table Column: ", 1) if $main::log;
1001       
1002       my @columns = @{$tables{$table}};
1003       
1004       for my $column (@columns) {
1005         my %info = %$column;
1006         
1007         $main::log->msg ("$info{column} ", 1) if $main::log;
1008         
1009         my $rowsUpdated = updateColumn ($table, $olduser, $newuser, %info);
1010         
1011         if ($rowsUpdated) {
1012           my $msg  = " $rowsUpdated row";
1013              $msg .= 's' if $rowsUpdated > 1;
1014              $msg .= ' would have been' unless $main::opts{exec};
1015              $msg .= ' updated';
1016              
1017           $main::log->msg ($msg, 1) if $main::log;
1018         } # if
1019       } # for
1020       
1021       $main::log->msg ('') if $main::log;
1022     } # for
1023     
1024     if (my @result = copyGroupMembership ($olduser, $newuser)) {
1025       # Skip errors of the form 'Could not add user... group is read-only
1026       @result = grep {!/Could not add user.*group is read-only/} @result;
1027       
1028       if ($main::log) {
1029         $main::log->err ("Unable to copy group membership from $olduser -> $newuser\n@result", 1) if @result;
1030       } # if
1031     } # if
1032   } # for
1033   
1034   
1035   return $main::log ? $main::log->errors : 0;
1036 } # renameUsers
1037
1038 1;