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:
85 my (@issueLinkTypes, %cache, $jiradb, %findQuery);
88 ao_08d66b_filter_display_conf => [
89 {column => 'user_name'}
91 ao_0c0737_vote_info => [
92 {column => 'user_name'}
94 ao_3a112f_audit_log_entry => [
97 ao_563aee_activity_entity => [
98 {column => 'username'}
100 ao_60db71_auditentry => [
103 ao_60db71_boardadmins => [
106 ao_60db71_rapidview => [
107 {column => 'owner_user_name'}
109 ao_caff30_favourite_issue => [
110 {column => 'user_key'}
113 # {column => 'user_key'},
114 # {column => 'lower_user_name'},
117 {column => 'author_key'}
126 {column => 'oldvalue',
127 condition => 'field = "assignee"'},
128 {column => 'newvalue',
129 condition => 'field = "assignee"'},
132 {column => 'username'},
137 customfieldvalue => [
138 {column => 'stringvalue'},
140 favouriteassociations => [
141 {column => 'username'},
143 # cwd_membership => [
144 # {column => 'child_name'},
145 # {column => 'lower_child_name'},
148 {column => 'author'},
150 filtersubscription => [
151 {column => 'username'},
154 {column => 'author'},
157 {column => 'reporter'},
158 {column => 'assignee'},
161 {column => 'creatorname'},
164 {column => 'user_name'},
168 {column => 'caller'},
172 {column => 'caller'},
178 {column => 'username'},
180 schemepermissions => [
181 {column => 'perm_parameter',
182 condition => 'perm_type = "user"'},
185 {column => 'authorname'},
186 {column => 'username'},
189 {column => 'source_name'},
192 {column => 'username'},
195 {column => 'username'}
198 {column => 'author'},
202 sub _checkDBError ($;$) {
203 my ($msg, $statement) = @_;
205 $statement //= 'Unknown';
208 $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
211 my $dberr = $jiradb->err;
212 my $dberrmsg = $jiradb->errstr;
215 $dberrmsg ||= 'Success';
220 my $function = (caller (1)) [3];
222 $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
223 . "SQL Statement: $statement";
227 $main::log->err ($message, 1) if $dberr;
233 sub openJIRADB (;$$$$) {
234 my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
240 Opens the JIRA database directly using MySQL. This is only for certain
241 operations for which there is no corresponding REST interface
245 =for html <blockquote>
251 Name of the database host
263 Database user's password
267 =for html </blockquote>
271 =for html <blockquote>
281 =for html </blockquote>
285 $dbhost //= $main::opts{jiradbhost};
286 $dbname //= 'jiradb';
288 $dbpass //= '*********';
290 $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...") if $main::log;
292 $jiradb = DBI->connect (
293 "DBI:mysql:$dbname:$dbhost",
301 _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
306 sub Connect2JIRA (;$$$) {
307 my ($username, $password, $server) = @_;
311 =head2 Connect2JIRA ()
313 Establishes a connection to the JIRA instance using the REST API
317 =for html <blockquote>
323 Username to authenticate with
327 Password to authenticate with
331 JIRA server to connect to
335 =for html </blockquote>
339 =for html <blockquote>
349 =for html </blockquote>
355 $opts{username} = $username || 'jira-admin';
356 $opts{password} = $password || $ENV{PASSWORD} || '********';
357 $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev';
358 $opts{URL} = "http://$opts{server}/rest/api/latest";
360 $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})") if $main::log;
362 $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
364 # Store username as we might need it (see updateIssueWatchers)
365 $jira->{username} = $opts{username};
371 my ($table, $condition) = @_;
377 Return the count of a table in the JIRA database given a condition
381 =for html <blockquote>
387 Name of table to perform count of
391 MySQL condition to apply
395 =for html </blockquote>
399 =for html <blockquote>
405 Count of qualifying entries
409 =for html </blockquote>
415 $jiradb = openJIRADB unless $jiradb;
418 $statement = "select count(*) from $table where $condition";
420 $statement = "select count(*) from $table";
423 my $sth = $jiradb->prepare ($statement);
425 _checkDBError 'count: Unable to prepare statement', $statement;
429 _checkDBError 'count: Unable to execute statement', $statement;
431 # Get return value, which should be how many message there are
432 my @row = $sth->fetchrow_array;
439 # Retrieve returned value
449 sub addDescription ($$) {
450 my ($issue, $description) = @_;
454 =head2 addDescription ()
456 Add a description to a JIRA issue
460 =for html <blockquote>
474 =for html </blockquote>
478 =for html <blockquote>
486 =for html </blockquote>
490 if ($main::opts{exec}) {
491 eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
494 return "Unable to add description\n$@";
496 return 'Description added';
501 sub addJIRAComment ($$) {
502 my ($issue, $comment) = @_;
506 =head2 addJIRAComment ()
508 Add a comment to a JIRA issue
512 =for html <blockquote>
526 =for html </blockquote>
530 =for html <blockquote>
538 =for html </blockquote>
542 if ($main::opts{exec}) {
543 eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
546 return "Unable to add comment\n$@";
548 return 'Comment added';
551 return "Would have added comments to $issue";
555 sub blankBugzillaNbr ($) {
558 eval {$jira->PUT ("/issue/$issue", undef, {fields => {'Bugzilla Bug Origin' => ''}})};
559 #eval {$jira->PUT ("/issue/$issue", undef, {fields => {'customfield_10132' => ''}})};
562 return "Unable to blank Bugzilla number$@\n"
568 sub attachmentExists ($$) {
569 my ($issue, $filename) = @_;
573 =head2 attachmentExists ()
575 Determine if an attachment to a JIRA issue exists
579 =for html <blockquote>
589 Filename of attachment
593 =for html </blockquote>
597 =for html <blockquote>
605 =for html </blockquote>
609 my $attachments = getIssue ($issue, qw(attachment));
611 for (@{$attachments->{fields}{attachment}}) {
612 return 1 if $filename eq $_->{filename};
618 sub attachFiles2Issue ($@) {
619 my ($issue, @files) = @_;
623 =head2 attachFiles2Issue ()
625 Attach a list of files to a JIRA issue
629 =for html <blockquote>
643 =for html </blockquote>
647 =for html <blockquote>
655 =for html </blockquote>
659 my $status = $jira->attach_files ($issue, @files);
662 } # attachFiles2Issue
664 sub getIssueFromBugID ($) {
670 jql => "\"Bugzilla Bug Origin\" ~ $bugid",
674 eval {$issue = $jira->GET ("/search/", \%query)};
676 my $issueID = $issue->{issues}[0]{key};
678 return $issue->{issues} if @{$issue->{issues}} > 1;
680 } # getIssueFromBugID
683 my ($bugid, %bugmap) = @_;
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";
695 return $cache{$bugid};
704 jql => "\"Bugzilla Bug Origin\" ~ $bugid",
708 eval {$issue = $jira->GET ("/search/", \%query)};
710 my $issueID = $issue->{issues}[0]{key};
712 if (@{$issue->{issues}} > 2) {
713 $main::log->err ("Found more than 2 issues for Bug ID $bugid") if $main::log;
715 return "Found more than 2 issues for Bug ID $bugid";
716 } elsif (@{$issue->{issues}} == 2) {
717 my ($issueNum0, $issueNum1, $projectName0, $projectName1);
719 if ($issue->{issues}[0]{key} =~ /(.*)-(\d+)/) {
724 if ($issue->{issues}[1]{key} =~ /(.*)-(\d+)/) {
729 if ($issueNum0 < $issueNum1) {
730 $issueID = $issue->{issues}[1]{key};
733 # Let's mark them as clones. See if this clone link already exists...
736 for (getIssueLinks ($issueID, 'Cloners')) {
737 my $inwardIssue = $_->{inwardIssue}{key} || '';
738 my $outwardIssue = $_->{outwardIssue}{key} || '';
740 if ("$projectName0-$issueNum0" eq $inwardIssue ||
741 "$projectName0-$issueNum0" eq $outwardIssue ||
742 "$projectName1-$issueNum1" eq $inwardIssue ||
743 "$projectName1-$issueNum1" eq $outwardIssue) {
750 unless ($alreadyCloned) {
751 my $result = linkIssues ("$projectName0-$issueNum0", 'Cloners', "$projectName1-$issueNum1");
753 return $result if $result =~ /Unable to/;
755 $main::log->msg ($result) if $main::log;
760 $main::log->msg ("Found JIRA issue $issueID for Bug $bugid") if $main::log;
762 #$cache{$bugid} = $issueID;
764 #return $cache{$bugid};
767 my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
768 : "Unable to find a JIRA issue for Bug $bugid";
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
773 #$cache{$bugid} = $bugid;
779 sub findIssues (;$@) {
780 my ($condition, @fields) = @_;
786 Set up a find for JIRA issues based on a condition
790 =for html <blockquote>
796 Condition to use. JQL is supported
800 List of fields to retrieve data for
804 =for html </blockquote>
808 =for html <blockquote>
816 =for html </blockquote>
820 push @fields, '*all' unless @fields;
822 $findQuery{jql} = $condition || '';
823 $findQuery{startAt} = 0;
824 $findQuery{maxResults} = 1;
825 $findQuery{fields} = join ',', @fields;
830 sub getNextIssue () {
835 =head2 getNextIssue ()
837 Get next qualifying issue. Call findIssues first
841 =for html <blockquote>
849 =for html </blockquote>
853 =for html <blockquote>
859 Perl hash of the fields in the next JIRA issue
863 =for html </blockquote>
867 eval {$result = $jira->GET ('/search/', \%findQuery)};
869 $findQuery{startAt}++;
871 # Move id and key into fields
872 return unless @{$result->{issues}};
874 $result->{issues}[0]{fields}{id} = $result->{issues}[0]{id};
875 $result->{issues}[0]{fields}{key} = $result->{issues}[0]{key};
877 return %{$result->{issues}[0]{fields}};
880 sub getIssues (;$$$@) {
881 my ($condition, $start, $max, @fields) = @_;
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.
893 =for html <blockquote>
899 JQL condition to apply
903 Starting point to get issues from
907 Max number of entrist to get
911 List of fields to retrieve
915 =for html </blockquote>
919 =for html <blockquote>
925 Perl array of hashes of JIRA issue records
929 =for html </blockquote>
933 push @fields, '*all' unless @fields;
935 my ($result, %query);
937 $query{jql} = $condition || '';
938 $query{startAt} = $start || 0;
939 $query{maxResults} = $max || 50;
940 $query{fields} = join ',', @fields;
942 eval {$result = $jira->GET ('/search/', \%query)};
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}};
957 my ($issue, @fields) = @_;
963 Get individual JIRA issue
967 =for html <blockquote>
977 List of fields to retrieve
981 =for html </blockquote>
985 =for html <blockquote>
991 Perl hash of JIRA issue
995 =for html </blockquote>
999 my $fields = @fields ? "?fields=" . join ',', @fields : '';
1001 return $jira->GET ("/issue/$issue$fields");
1004 sub getIssueLinkTypes () {
1005 my $issueLinkTypes = $jira->GET ('/issueLinkType/');
1007 map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
1009 return @issueLinkTypes
1010 } # getIssueLinkTypes
1012 sub linkIssues ($$$) {
1013 my ($from, $type, $to) = @_;
1015 unless (@issueLinkTypes) {
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;
1023 return "Unable to $type link $from -> $to";
1037 body => "Link ported as part of the migration from Bugzilla: $from <-> $to",
1041 $main::total{'IssueLinks Added'}++;
1043 if ($main::opts{exec}) {
1044 eval {$jira->POST ("/issueLink", undef, \%link)};
1047 return "Unable to $type link $from -> $to\n$@";
1049 return "Made $type link $from -> $to";
1052 return "Would have $type linked $from -> $to";
1056 sub getRemoteLink ($;$) {
1057 my ($jiraIssue, $id) = @_;
1061 =head2 getRemoteLink ()
1063 Retrieve a remote link
1067 =for html <blockquote>
1077 Which ID to retrieve
1081 =for html </blockquote>
1085 =for html <blockquote>
1091 Perl hash of remote links
1095 =for html </blockquote>
1103 eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
1109 if (ref $result eq 'ARRAY') {
1110 map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;
1112 $remoteLinks{$result->{id}} = $result->{object}{title};
1115 return \%remoteLinks;
1118 sub getRemoteLinks (;$) {
1121 $jiradb = openJIRADB unless $jiradb;
1123 my $statement = 'select url from remotelink';
1125 $statement .= " where url like 'http://bugs%'";
1126 $statement .= " and url like '%$bugid'" if $bugid;
1127 $statement .= " group by issueid desc";
1129 my $sth = $jiradb->prepare ($statement);
1131 _checkDBError 'Unable to prepare statement', $statement;
1135 _checkDBError 'Unable to execute statement', $statement;
1139 while (my $record = $sth->fetchrow_array) {
1140 if ($record =~ /(\d+)$/) {
1145 return keys %bugids;
1148 sub findRemoteLinkByBugID (;$) {
1151 my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
1154 $condition .= " and remotelink.url like '%id=$bugid'";
1157 $jiradb = openJIRADB unless $jiradb;
1159 my $statement = <<"END";
1162 concat (project.pkey, '-', issuenum) as issue,
1171 my $sth = $jiradb->prepare ($statement);
1173 _checkDBError 'Unable to prepare statement', $statement;
1177 _checkDBError 'Unable to execute statement', $statement;
1181 while (my $row = $sth->fetchrow_hashref) {
1182 $row->{bugid} = $bugid;
1184 push @records, $row;
1188 } # findRemoteLinkByBugID
1190 sub promoteBug2JIRAIssue ($$$$) {
1191 my ($bugid, $jirafrom, $jirato, $relationship) = @_;
1193 my $result = linkIssues $jirafrom, $relationship, $jirato;
1195 return $result if $result =~ /Unable to link/;
1197 $main::log->msg ($result . " (BugID $bugid)") if $main::log;
1199 for (@{findRemoteLinkByBugID $bugid}) {
1202 $result = removeRemoteLink ($record{issue}, $record{id});
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/;
1209 $main::log->msg ($result) unless $result eq '';
1214 } # promoteBug2JIRAIssue
1216 sub addRemoteLink ($$$) {
1217 my ($bugid, $relationship, $jiraIssue) = @_;
1219 my $bug = getBug $bugid;
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;
1225 for (keys %$remoteLinks) {
1226 if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
1227 return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
1231 # Note this globalid thing is NOT working! ALl I see is null in the database
1233 # globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
1235 # type => 'Bugzilla',
1236 # name => 'Bugzilla',
1238 relationship => $relationship,
1240 url => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
1241 title => "Bug $bugid",
1242 summary => $bug->{short_desc},
1244 url16x16 => 'http://bugs.audience.local/favicon.png',
1245 title => 'Bugzilla Bug',
1250 $main::total{'RemoteLink Added'}++;
1252 if ($main::opts{exec}) {
1253 eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
1257 return "Would have linked $bugid -> $jiraIssue";
1261 sub removeRemoteLink ($;$) {
1262 my ($jiraIssue, $id) = @_;
1266 my $remoteLinks = getRemoteLink ($jiraIssue, $id);
1268 for (keys %$remoteLinks) {
1271 $main::total{'RemoteLink Removed'}++;
1273 if ($main::opts{exec}) {
1274 eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
1277 return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
1281 if ($remoteLinks->{$_} =~ /(\d+)/) {
1282 return "Removed remote link $jiraIssue (Bug ID $1)";
1286 $main::total{'Remote Links Removed'}++;
1288 if ($remoteLinks->{$_} =~ /(\d+)/) {
1289 return "Would have removed remote link $jiraIssue (Bug ID $1)";
1293 } # removeRemoteLink
1295 sub getIssueLinks ($;$) {
1296 my ($issue, $type) = @_;
1298 my @links = getIssue ($issue, ('issuelinks'));
1302 for (@{$links[0]->{fields}{issuelinks}}) {
1303 my %issueLink = %$_;
1305 next if ($type && $type ne $issueLink{type}{name});
1307 push @issueLinks, \%issueLink;
1313 sub getIssueWatchers ($) {
1318 eval {$watchers = $jira->GET ("/issue/$issue/watchers")};
1322 # The watcher information returned by the above is incomplete. Let's complete
1326 for (@{$watchers->{watchers}}) {
1329 eval {$user = $jira->GET ("/user?username=$_->{key}")};
1332 push @watchers, $user;
1335 $main::log->err ("Unable to find user record for $_->{name}")
1336 unless $_->{name} eq 'jira-admin';
1342 } # getIssueWatchers
1344 sub updateIssueWatchers ($%) {
1345 my ($issue, %watchers) = @_;
1349 =head2 updateIssueWatchers ()
1351 Updates the issue watchers list
1355 =for html <blockquote>
1365 List of watchers to add
1369 =for html </blockquote>
1373 =for html <blockquote>
1379 Error message or '' to indicate no error
1383 =for html </blockquote>
1387 my $existingWatchers;
1389 eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
1391 return "Unable to get issue $issue\n$@" if $@;
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}");
1399 $main::total{"Admins destroyed"}++;
1402 # Delete any matching watchers
1403 delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
1406 return '' if keys %watchers == 0;
1410 for (keys %watchers) {
1411 if ($main::opts{exec}) {
1412 eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
1415 $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue") if $main::log;
1417 $main::total{'Watchers skipped'}++;
1421 $main::total{'Watchers added'}++;
1424 $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue") if $main::log;
1426 $main::total{'Watchers that would have been added'}++;
1430 $main::total{'Issues updated'}++ if $issueUpdated;
1433 } # updateIssueWatchers
1435 sub getUsersGroups ($) {
1436 my ($username) = @_;
1440 =head2 getUsersGroups ()
1442 Returns the groups that the user is a member of
1446 =for html <blockquote>
1456 =for html </blockquote>
1460 =for html <blockquote>
1470 =for html </blockquote>
1474 my ($result, %query);
1477 username => $username,
1481 eval {$result = $jira->GET ('/user/', \%query)};
1485 for (@{$result->{groups}{items}}) {
1486 push @groups, $_->{name};
1492 sub updateUsersGroups ($@) {
1493 my ($username, @groups) = @_;
1497 =head2 updateUsersGroups ()
1499 Updates the users group membership
1503 =for html <blockquote>
1509 Username to operate on
1513 List of groups the user should be a member of
1517 =for html </blockquote>
1521 =for html <blockquote>
1527 List of errors (if any)
1531 =for html </blockquote>
1535 my ($result, @errors);
1537 my @oldgroups = getUsersGroups $username;
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;
1546 eval {$result = $jira->POST ('/group/user', {groupname => $group}, {name => $username})};
1548 push @errors, $@ if $@;
1552 } # updateUsersGroups
1554 sub copyGroupMembership ($$) {
1555 my ($from_username, $to_username) = @_;
1557 return updateUsersGroups $to_username, getUsersGroups $from_username;
1558 } # copyGroupMembership
1560 sub updateColumn ($$$%) {
1561 my ($table, $oldvalue, $newvalue, %info) = @_;
1565 =head2 updateColumn ()
1567 Updates a column in the MySQL JIRA database (SQL surgery)
1571 =for html <blockquote>
1589 Hash of column names and optional conditions
1593 =for html </blockquote>
1597 =for html <blockquote>
1603 Number of rows updated
1607 =for html </blockquote>
1611 # UGH! Sometimes values need to be quoted
1612 $oldvalue = quotemeta $oldvalue;
1613 $newvalue = quotemeta $newvalue;
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";
1619 my $nbrRows = count $table, $condition;
1622 if ($main::opts{exec}) {
1623 $main::total{'Rows updated'}++;
1625 $jiradb->do ($statement);
1627 _checkDBError 'Unable to execute statement', $statement;
1629 $main::total{'Rows would be updated'}++;
1631 $main::log->msg ("Would have executed $statement") if $main::log;
1638 sub renameUsers (%) {
1643 =head2 renameUsers ()
1649 =for html <blockquote>
1655 Hash of old -> new usernames
1659 =for html </blockquote>
1663 =for html <blockquote>
1673 =for html </blockquote>
1677 for my $olduser (sort keys %users) {
1678 my $newuser = $users{$olduser};
1680 $main::log->msg ("Renaming $olduser -> $newuser") if $main::log;
1681 display ("Renaming $olduser -> $newuser");
1683 if ($main::opts{exec}) {
1684 $main::total{'Users renamed'}++;
1686 $main::total{'Users would be updated'}++;
1689 for my $table (sort keys %tables) {
1690 $main::log->msg ("\tTable: $table Column: ", 1) if $main::log;
1692 my @columns = @{$tables{$table}};
1694 for my $column (@columns) {
1695 my %info = %$column;
1697 $main::log->msg ("$info{column} ", 1) if $main::log;
1699 my $rowsUpdated = updateColumn ($table, $olduser, $newuser, %info);
1702 my $msg = " $rowsUpdated row";
1703 $msg .= 's' if $rowsUpdated > 1;
1704 $msg .= ' would have been' unless $main::opts{exec};
1707 $main::log->msg ($msg, 1) if $main::log;
1711 $main::log->msg ('') if $main::log;
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;
1719 $main::log->err ("Unable to copy group membership from $olduser -> $newuser\n@result", 1) if @result;
1724 return $main::log ? $main::log->errors : 0;
1731 =head1 CONFIGURATION AND ENVIRONMENT
1733 DEBUG: If set then $debug is set to this level.
1735 VERBOSE: If set then $verbose is set to this level.
1737 TRACE: If set then $trace is set to this level.
1749 =head3 ClearSCM Perl Modules
1751 =for html <p><a href="/php/scm_man.php?file=lib/Display.pm">Display</a></p>
1753 =for html <p><a href="/php/scm_man.php?file=JIRA/lib/BugzillaUtils.pm">BugzillaUtils</a></p>
1755 =head1 INCOMPATABILITIES
1759 =head1 BUGS AND LIMITATIONS
1761 There are no known bugs in this module.
1763 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
1765 =head1 LICENSE AND COPYRIGHT
1767 This Perl Module is freely available; you can redistribute it and/or
1768 modify it under the terms of the GNU General Public License as
1769 published by the Free Software Foundation; either version 2 of the
1770 License, or (at your option) any later version.
1772 This Perl Module is distributed in the hope that it will be useful,
1773 but WITHOUT ANY WARRANTY; without even the implied warranty of
1774 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1775 General Public License (L<http://www.gnu.org/copyleft/gpl.html>) for more
1778 You should have received a copy of the GNU General Public License
1779 along with this Perl Module; if not, write to the Free Software Foundation,
1780 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.