Some JIRA scripts
authoradefaria <adefaria@adefaria-lt.audience.local>
Sat, 1 Nov 2014 00:41:27 +0000 (17:41 -0700)
committeradefaria <adefaria@adefaria-lt.audience.local>
Sat, 1 Nov 2014 00:41:27 +0000 (17:41 -0700)
audience/JIRA/importComments.pl [new file with mode: 0644]
audience/JIRA/jiradep.pl [new file with mode: 0644]
audience/JIRA/lib/BugzillaUtils.pm [new file with mode: 0644]
audience/JIRA/lib/JIRAUtils.pm [new file with mode: 0644]
audience/JIRA/updateWatchLists.pl [new file with mode: 0644]

diff --git a/audience/JIRA/importComments.pl b/audience/JIRA/importComments.pl
new file mode 100644 (file)
index 0000000..6a1712c
--- /dev/null
@@ -0,0 +1,237 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME importComments.pl
+
+This will import the comments from Bugzilla and update the corresponding JIRA
+Issues.
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+  $ importComments.pl [-bugzillaserver <bugshost>] [-login <login email>]
+                      [-jiraserver <server>]
+                      [-username <username>] [-password <password>] 
+                      [-bugids bugid,bugid,... | -file <filename>] 
+                      [-[no]exec]
+                      [-verbose] [-help] [-usage]
+
+  Where:
+
+    -v|erbose:       Display progress output
+    -he|lp:          Display full help
+    -usa|ge:         Display usage
+    -[no]e|xec:      Whether or not to update JIRA. -noexec says only 
+                     tell me what you would have updated.
+    -use|rname:      Username to log into JIRA with (Default: jira-admin)
+    -p|assword:      Password to log into JIRA with (Default: jira-admin's 
+                     password)
+    -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+    -jiraserver:     Machine where Jira lives (Default: jira-dev)
+    -bugi|ds:        Comma separated list of BugIDs to process
+    -f|ile:          File of BugIDs, one per line
+
+=head1 DESCRIPTION
+
+This will import the comments from Bugzilla and update the corresponding JIRA
+Issues.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+
+use Getopt::Long; 
+use Pod::Usage;
+
+our %opts = (
+  exec           => 0,
+  bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+  jiraserver     => $ENV{JIRASERVER}     || 'jira-dev:8081',
+  username       => $ENV{USERNAME},
+  password       => $ENV{PASSWORD},
+  usage          => sub { pod2usage },
+  help           => sub { pod2usage (-verbose => 2)},
+  verbose        => sub { set_verbose },
+  quiet          => 0,
+);
+
+our ($log, %total);
+
+sub sanitize ($) {
+  my ($str) = @_;
+  
+  my $p4web    = 'http://p4web.audience.local:8080/@md=d&cd=//&c=vLW@/';
+  my $bugzilla = 'http://bugs.audience.com/show_bug.cgi?id=';
+
+  # 0x93 (147) and 0x94 (148) are "smart" quotes
+  $str =~ s/[\x93\x94]/"/gm;
+  # 0x91 (145) and 0x92 (146) are "smart" singlequotes
+  $str =~ s/[\x91\x92]/'/gm;
+  # 0x96 (150) and 0x97 (151) are emdashes
+  $str =~ s/[\x96\x97]/--/gm;
+  # 0x85 (133) is an ellipsis
+  $str =~ s/\x85/.../gm;
+  # 0x95 &bull; replacement for unordered list
+  $str =~ s/\x95/*/gm;
+
+  # Make P4Web links for "CL (\d{3,6}+)"
+  $str =~ s/CL\s*(\d{3,6}+)/CL \[$1|${p4web}$1\?ac=10\]/igm;
+
+  # Make Bugzilla links for "Bug ID (\d{1,5}+)"
+  $str =~ s/Bug\s*ID\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+  # Make Bugzilla links for "Bug # (\d{1,5}+)"
+  $str =~ s/Bug\s*#\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+  # Make Bugzilla links for "Bug (\d{1,5}+)"
+  $str =~ s/Bug\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+  # Convert bug URLs to be more proper
+  $str =~ s/https\:\/\/bugs\.audience\.com\/show_bug\.cgi\?id=(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+  return $str;
+} # sanitize
+
+sub addComments ($$) {
+  my ($jiraIssue, $bugid) = @_;
+  
+  my @comments = @{getBugComments ($bugid)};
+  
+  # Note: In Bugzilla the first comment is considered the description.
+  my $description = shift @comments;
+  
+  my $result = addDescription $jiraIssue, sanitize $description;
+  
+  $total{'Descriptions added'}++;
+  
+  return $result if $result =~ /^Unable to add comment/;
+   
+  # Process the remaining comments  
+  for (@comments) {
+    $result = addJIRAComment $jiraIssue, sanitize $_;
+    
+    if ($result =~ /Comment added/) {
+      $total{'Comments imported'}++;
+    } else {
+      return $result;
+    } # if
+  } # for
+  
+  $result = '' unless $result;
+  
+  return $result;
+} # addComments
+
+sub main () {
+  my $startTime = time;
+  
+  GetOptions (
+    \%opts,
+    'verbose',
+    'usage',
+    'help',
+    'exec!',
+    'quiet',
+    'username=s',
+    'password=s',
+    'bugids=s@',
+    'file=s',
+    'jiraserver=s',
+    'bugzillaserver=s',
+    'linkbugzilla',
+    'relinkbugzilla'
+  ) or pod2usage;
+  
+  $log = Logger->new;
+
+  if ($opts{file}) {
+    open my $file, '<', $opts{file} 
+      or $log->err ("Unable to open $opts{file} - $!", 1);
+      
+    $opts{bugids} = [<$file>];
+    
+    chomp @{$opts{bugids}};
+  } else {
+    my @bugids;
+    
+    push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); 
+  
+    $opts{bugids} = [@bugids];
+  } # if
+  
+  pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+    unless $opts{bugids};
+  
+  openBugzilla $opts{bugzillaserver}
+    or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+  
+  Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+    or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+
+  $log->msg ("Processing comments");
+
+  for (@{$opts{bugids}}) {
+    my $jiraIssue = findIssue $_;
+    
+    if ($jiraIssue =~ /^[A-Z]{1,5}-\d+$/) {
+      my $result = addComments $jiraIssue, $_;
+      
+      if ($result =~ /^Unable/) {
+        $total{'Comment failures'}++;
+
+        $log->err ("Unable to add comments for $jiraIssue ($_)\n$result");
+      } elsif ($result =~ /^Comment added/) {
+        $log->msg ("Added comments for $jiraIssue ($_)");
+      } elsif ($result =~ /^Would have linked/) {
+        $total{'Comments would be added'}++;
+      } # if
+    } else {
+      $total{'Missing JIRA Issues'}++;
+      
+      $log->err ("Unable to find JIRA Issue for Bug $_");
+    } # if
+  } # for
+
+  display_duration $startTime, $log;
+  
+  Stats (\%total, $log) unless $opts{quiet};
+
+  return 0;
+} # main
+
+exit main;
diff --git a/audience/JIRA/jiradep.pl b/audience/JIRA/jiradep.pl
new file mode 100644 (file)
index 0000000..a8ce4c4
--- /dev/null
@@ -0,0 +1,372 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME jiradep.pl
+
+Update Bugzilla dependencies (Dependencies/Blockers/Duplicates and Related),
+transfering those relationships over to any matching JIRA issues. 
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+  $ jiradep.pl [-bugzillaserver <bugshost>] [-login <login email>]
+               [-jiraserver <server>]
+               [-username <username>] [-password <password>] 
+               [-bugids bugid,bugid,... | -file <filename>] 
+               [-[no]exec] [-linkbugzilla] [-relinkbugzilla]
+               [-verbose] [-help] [-usage]
+
+  Where:
+
+    -v|erbose:       Display progress output
+    -he|lp:          Display full help
+    -usa|ge:         Display usage
+    -[no]e|xec:      Whether or not to update Bugilla. -noexec says only 
+                     tell me what you would have updated.
+    -use|rname:      Username to log into JIRA with (Default: jira-admin)
+    -p|assword:      Password to log into JIRA with (Default: jira-admin's 
+                     password)
+    -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+    -jiraserver:     Machine where Jira lives (Default: jira-dev)
+    -bugi|ds:        Comma separated list of BugIDs to process
+    -f|ile:          File of BugIDs, one per line
+    -linkbugzilla:   If specified and we find that we cannot translate
+                     a Bugzilla Bud ID to a JIRA Issue then create a 
+                     remote link for the Bugzilla Bug. (Default:
+                     do not create Bugzilla remote links).
+    -relinkbugzilla: Scan current Remote Bugzilla links and if there
+                     exists a corresponding JIRA issue, remove the 
+                     Remote Bugzilla link and make it a JIRA Issue
+                     link.
+    -jiradbhost:     Host name of the machine where the MySQL jiradb
+                     database is located (Default: cm-db-ldev01)
+
+=head1 DESCRIPTION
+
+This script will process all BugIDs translating them into JIRA Issues, if
+applicable. It will then determine the relationships of this BugID in Bugzilla -
+what it blocks, what it depends on, if it's a duplicate of another bug or if
+it has any related links. Those too will be translated to JIRA issues, again,
+if applicable. Then the JIRA issue will be updates to reflect these 
+relationships. 
+
+Note that it's not known at this time what to do for situations where BugIDs
+cannot be translated into JIRA issues if such Bugzilla bugs have not yet been
+migrated to JIRA. There's a though to simply make a Bugzilla Link but we will
+need to keep that in mind and when we import the next project to JIRA these
+old, no longer used Bugzilla Links should be converted to their corresponding 
+JIRA issue. Perhaps this script can do that too.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+
+use Getopt::Long; 
+use Pod::Usage;
+
+our %opts = (
+  exec           => 0,
+  bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+  jiraserver     => $ENV{JIRASERVER}     || 'jira-dev',
+  jiradbhost     => $ENV{JIRA_DB_HOST}   || 'cm-db-ldev01',
+  username       => 'jira-admin',
+  password       => 'jira-admin',
+  usage          => sub { pod2usage },
+  help           => sub { pod2usage (-verbose => 2)},
+  verbose        => sub { set_verbose },
+  quiet          => 0,
+  usage          => sub { pod2usage },
+  help           => sub { pod2usage (-verbose => 2)},
+);
+
+our ($log, %total);
+
+my %relationshipMap = (
+  Blocks    => 'Dependencies Linked',
+  Duplicate => 'Duplicates Linked',
+  Related   => 'Related Linked',
+);
+
+sub callLink ($$$$) {
+  my ($from, $type, $to, $counter) = @_;
+
+  my $bugzillaType;
+  
+  if ($from =~ /^\d+/) {
+    if ($type eq 'Blocks') {
+      $bugzillaType = 'is blocked by (Bugzilla)';
+    } elsif ($type eq 'Duplicate') {
+      $bugzillaType = 'duplicate (Bugzilla)';
+    } elsif ($type eq 'Related') {
+      $bugzillaType = 'related (Bugzilla)';
+    } # if
+  } elsif ($to =~ /^\d+/) {
+    if ($type eq 'Blocks') {
+      $bugzillaType = 'blocks (Bugzilla)';
+    } elsif ($type eq 'Duplicate') {
+      $bugzillaType = 'duplicate (Bugzilla)';
+    } elsif ($type eq 'Related') {
+      $bugzillaType = 'related (Bugzilla)';
+    } # if
+  } # if
+    
+  $total{$counter}++;
+
+  if ($from =~ /^\d+/ && $to =~ /^\d+/) {
+    $total{'Skipped Bugzilla Links'}++;
+    
+    return "Refusing to link because both from ($from) and to ($to) links are still a Bugzilla link";
+  } elsif ($from =~ /^\d+/) {
+    if ($opts{linkbugzilla}) {
+      my $result = addRemoteLink $from, $bugzillaType, $to;
+
+      $total{'Bugzilla Links'}++ unless $result;
+    
+      if ($result eq '') {
+        return "Created remote $type link between Issue $to and Bug $from";
+      } else {
+        return $result;
+      } # if 
+    } else {
+      $total{'Skipped Bugzilla Links'}++;
+    
+      return "Refusing to link because from link ($from) is still a Bugzilla link";
+    } # if
+  } elsif ($to =~ /^\d+/) {
+    if ($opts{linkbugzilla}) {
+      my $result = addRemoteLink $to, $bugzillaType, $from;
+    
+      $total{'Bugzilla Links'}++ unless $result;
+
+      if (!defined $result) {
+        print "huh?";
+      }
+      if ($result eq '') {
+        return "Created remote $type link between Issue $from and Bug $to";
+      } else {
+        return $result;
+      } # if 
+    } else {
+      $total{'Skipped Bugzilla Links'}++;
+    
+      return "Refusing to link because to link ($to) is still a Bugzilla link";
+    } # if
+  } # if
+   
+  my $result = linkIssues $from, $type, $to;
+  
+  $log->msg ($result);
+    
+  if ($result =~ /^Unable/) {
+    $total{'Link Failures'}++;
+  } elsif ($result =~ /^Link made/) {
+    $total{'Links made'}++;
+  } elsif ($result =~ /^Would have linked/) {
+    $total{'Links would be made'}++;
+  } # if
+      
+  return;
+} # callLink
+
+sub relinkBugzilla (@) {
+  my (@bugids) = @_;
+  
+  my %mapRelationships = (
+    'blocks (Bugzilla)'           => 'Blocks',
+    'is blocked by (Bugzilla)'    => 'Blocks',
+    'duplicates (Bugzilla)'       => 'Duplicates',
+    'is duplicated by (Bugzilla)' => 'Duplicates',
+    # old versions...
+    'Bugzilla blocks'             => 'Blocks',
+    'Bugzilla is blocked by'      => 'Blocks',
+    'Bugzilla duplicates'         => 'Duplicates',
+    'Bugzilla is duplicated by'   => 'Duplicates',
+  );
+  
+  @bugids = getRemoteLinks unless @bugids;
+  
+  for my $bugid (@bugids) {
+    $total{'Remote Links Scanned'}++;
+    
+    my $links = findRemoteLinkByBugID $bugid;
+
+    my $jirafrom = findIssue ($bugid);
+    
+    next if $jirafrom !~ /^[A-Z]{1,5}-\d+$/;
+        
+    for (@$links) {
+      my %link = %$_;
+      
+      # Found a link to JIRA. Remove remotelink and make an issuelink
+      if ($mapRelationships{$link{relationship}}) {
+        my ($fromIssue, $toIssue);
+        
+        if ($link{relationship} =~ / by/) {
+          $fromIssue = $jirafrom;
+          $toIssue   = $link{issue};
+        } else {
+          $fromIssue = $link{issue};
+          $toIssue   = $jirafrom;
+        } # if
+        
+        my $status = promoteBug2JIRAIssue $bugid, $fromIssue, $toIssue,
+                                          $mapRelationships{$link{relationship}};
+
+        $log->err ($status) if $status =~ /Unable to link/;
+      } else {
+        $log->err ("Unable to handle relationships of type $link{relationship}");
+      } # if
+    } # for
+  } # for
+    
+  return;
+} # relinkBugzilla
+
+sub main () {
+  my $startTime = time;
+  
+  GetOptions (
+    \%opts,
+    'verbose',
+    'usage',
+    'help',
+    'exec!',
+    'quiet',
+    'username=s',
+    'password=s',
+    'bugids=s@',
+    'file=s',
+    'jiraserver=s',
+    'bugzillaserver=s',
+    'linkbugzilla',
+    'relinkbugzilla',
+    'jiradbhost=s',
+  ) or pod2usage;
+  
+  $log = Logger->new;
+
+  if ($opts{file}) {
+    open my $file, '<', $opts{file} 
+      or $log->err ("Unable to open $opts{file} - $!", 1);
+      
+    $opts{bugids} = [<$file>];
+    
+    chomp @{$opts{bugids}};
+  } else {
+    my @bugids;
+    
+    push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); 
+  
+    $opts{bugids} = [@bugids];
+  } # if
+  
+  pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+    unless ($opts{bugids} > 0 or $opts{relinkbugzilla});
+  
+  openBugzilla $opts{bugzillaserver}
+    or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+  
+  Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+    or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+
+  if ($opts{relinkbugzilla}) {
+    unless (@{$opts{bugids}}) {
+      relinkBugzilla;
+    } else {
+      relinkBugzilla $_ for @{$opts{bugids}}
+    } # unless
+        
+    Stats (\%total, $log);
+    
+    exit $log->errors;
+  } # if
+  
+  my %relationships;
+  
+  # The 'Blocks' IssueLinkType has two types of relationships in it - both
+  # blocks and dependson. Since JIRA has only one type - Blocks - we take
+  # the $dependson and flip the from and to.
+  my $blocks    = getBlockers @{$opts{bugids}};
+  my $dependson = getDependencies @{$opts{bugids}};
+  
+  # Now merge them - we did it backwards!
+  for my $fromLink (keys %$dependson) {
+    for my $toLink (@{$dependson->{$fromLink}}) {
+      push @{$relationships{Blocks}{$toLink}}, $fromLink;
+    } # for
+  } # for
+
+  #%{$relationships{Blocks}} = %$dependson;
+  
+  for my $fromLink (keys %$blocks) {
+    # Check to see if we already have the reverse of this link
+    for my $toLink (@{$blocks->{$fromLink}}) {
+      unless (grep {$toLink eq $_} keys %{$relationships{Blocks}}) {
+        push @{$relationships{Blocks}{$fromLink}}, $toLink;
+      } # unless
+    } # for
+  } # for
+  
+  $relationships{Duplicate} = getDuplicates   @{$opts{bugids}};
+  $relationships{Relates}   = getRelated      @{$opts{bugids}};
+  
+  # Process relationships (social programming... ;-)
+  $log->msg ("Processing relationships");
+  
+  for my $type (keys %relationshipMap) {
+    for my $from (keys %{$relationships{$type}}) {
+      for my $to (@{$relationships{$type}{$from}}) {
+        $total{'Relationships processed'}++;
+        
+        my $result = callLink $from, $type, $to, $relationshipMap{$type};
+        
+        $log->msg ($result) if $result;
+      } # for
+    } # for
+  } # if
+
+  display_duration $startTime, $log;
+  
+  Stats (\%total, $log) unless $opts{quiet};
+
+  return;
+} # main
+
+main;
+
+exit;
\ No newline at end of file
diff --git a/audience/JIRA/lib/BugzillaUtils.pm b/audience/JIRA/lib/BugzillaUtils.pm
new file mode 100644 (file)
index 0000000..899f506
--- /dev/null
@@ -0,0 +1,322 @@
+=pod
+
+=head1 NAME $RCSfile: BugzillaUtils.pm,v $
+
+Some shared functions dealing with Bugzilla. Note this uses DBI to directly
+access Bugzilla's database. This requires that your userid was granted access.
+For this I setup adefaria with pretty much read only access.
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: 1.0 $
+
+=item Created
+
+Fri Mar 12 10:17:44 PST 2004
+
+=item Modified
+
+$Date: 2013/05/30 15:48:06 $
+
+=head1 ROUTINES
+
+The following routines are exported:
+
+=cut
+
+package BugzillaUtils;
+
+use strict;
+use warnings;
+
+use base 'Exporter';
+
+use FindBin;
+use Display;
+use Carp;
+use DBI;
+
+use lib 'lib';
+
+use JIRAUtils;
+
+our $bugzilla;
+
+our @EXPORT = qw (
+  openBugzilla
+  getRelationships
+  getDependencies
+  getBlockers
+  getDuplicates
+  getRelated
+  getBug
+  getBugComments
+  getWatchers
+);
+
+sub _checkDBError ($$) {
+  my ($msg, $statement) = @_;
+
+  my $dberr    = $bugzilla->err;
+  my $dberrmsg = $bugzilla->errstr;
+
+  $dberr    ||= 0;
+  $dberrmsg ||= 'Success';
+
+  my $message = '';
+
+  if ($dberr) {
+    my $function = (caller (1)) [3];
+
+    $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
+             . "SQL Statement: $statement";
+  } # if
+
+  $main::log->err ($message, $dberr) if $dberr;
+
+  return;
+} # _checkDBError
+
+sub openBugzilla (;$$$$) {
+  my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
+
+  $dbhost //= 'jira-dev';
+  $dbname //= 'bugzilla';
+  $dbuser //= 'adefaria';
+  $dbpass //= 'reader';
+  
+  $main::log->msg ("Connecting to Bugzilla ($dbuser\@$dbhost)");
+  
+  $bugzilla = DBI->connect (
+    "DBI:mysql:$dbname:$dbhost",
+    $dbuser,
+    $dbpass, {
+      PrintError => 0,
+      RaiseError => 1,
+    },
+  );
+  
+  _checkDBError 'Unable to execute statement', 'Connect';
+
+  return $bugzilla;
+} # openBugzilla
+
+sub getBug ($;@) {
+  my ($bugid, @fields) = @_;
+  
+  push @fields, 'short_desc' unless @fields;
+  
+  my $statement = 'select ' . join (',', @fields) .
+                  " from bugs where bug_id = $bugid";
+                  
+  my $sth = $bugzilla->prepare ($statement);
+
+  _checkDBError 'Unable to prepare statement', $statement;
+  
+  _checkDBError 'Unable to execute statement', $statement;
+
+  $sth->execute;
+
+  return $sth->fetchrow_hashref;
+} # getBug
+
+sub getBugComments ($) {
+  my ($bugid) = @_;
+  
+  my $statement = <<"END";
+select 
+  bug_id, 
+  bug_when, 
+  substring_index(login_name,'\@',1) as username,
+  thetext
+from
+  longdescs,
+  profiles
+where
+  who    = userid and
+  bug_id = $bugid 
+END
+  
+  my $sth = $bugzilla->prepare ($statement);
+  
+  _checkDBError 'Unable to prepare statement', $statement;
+
+  $sth->execute;
+  
+  _checkDBError 'Unable to execute statement', $statement;
+  
+  my @comments;
+  
+  while (my $comment = $sth->fetchrow_hashref) {
+    my $commentText = <<"END";
+The following comment was entered by [~$comment->{username}] on $comment->{bug_when}:
+
+$comment->{thetext}
+END
+
+    push @comments, $commentText;
+  } # while
+  
+  return \@comments;
+} # getBugComments
+
+sub getRelationships ($$$$@) {
+  my ($table, $returnField, $testField, $relationshipType, @bugs) = @_;
+  
+  $main::log->msg ("Getting $relationshipType");
+  
+  my $statement = "select $returnField from $table where $table.$testField = ?";
+
+  my $sth = $bugzilla->prepare ($statement);
+
+  _checkDBError 'Unable to prepare statement', $statement;
+  
+  my %relationships;
+
+  my %bugmap;
+  
+  map {$bugmap{$_} = 1} @bugs unless %bugmap;
+      
+  for my $bugid (@bugs) {
+    $sth->execute ($bugid);
+    
+    _checkDBError 'Unable to exit statement', $statement;
+    
+    my $result = JIRAUtils::findIssue ($bugid, %bugmap);
+    
+    if ($result =~ /^Unable/) {
+      $main::log->warn ($result);
+      
+      $main::total{'Missing JIRA Issues'}++;
+      
+      undef $result;
+    } elsif ($result =~ /^Future/) {
+      $main::total{'Future JIRA Issues'}++;
+      
+      undef $result;
+    } # if
+    
+    my $jiraIssue = $result;
+    my $key       = $jiraIssue || $bugid;
+
+    my @relationships;
+    my $relations = $sth->fetchall_arrayref;
+    my @relations;
+    
+    map {push @relations, $_->[0]} @$relations;
+    
+    for my $relation (@relations) {
+      $jiraIssue = JIRAUtils::findIssue ($relation);
+      
+      if ($jiraIssue =~ /^Unable/ || $jiraIssue =~ /^Future/) {
+        $main::log->warn ($jiraIssue);
+
+        $main::total{'Missing JIRA Issues'}++ if $jiraIssue =~ /^Unable/;
+        $main::total{'Future JIRA Issues'}++  if $jiraIssue =~ /^Future/;
+        
+        push @relationships, $relation;
+      } else {
+        push @relationships, $jiraIssue;
+      } # if
+    } # for
+      
+    push @{$relationships{$key}}, @relationships if @relationships;
+  } # for
+  
+  $main::total{$relationshipType} = keys %relationships;
+  
+  return \%relationships;
+} # getRelationships
+
+sub getDependencies (@) {
+  my (@bugs) = @_;
+
+  return getRelationships (
+    'dependencies', # table 
+    'dependson',    # returned field
+    'blocked',      # test field
+    'Depends on',   # relationship
+    @bugs
+  );
+} # getDependencies
+
+sub getBlockers (@) {
+  my (@bugs) = @_;
+  
+  return getRelationships (
+    'dependencies', 
+    'blocked', 
+    'dependson', 
+    'Blocks',
+    @bugs
+  );
+} # getBlockers
+
+sub getDuplicates (@) {
+  my (@bugs) = @_;
+  
+  return getRelationships (
+    'duplicates', 
+    'dupe', 
+    'dupe_of', 
+    'Duplicates',
+    @bugs
+  );
+} # getDuplicates
+
+sub getRelated (@) {
+  my (@bugs) = @_;
+  
+  return getRelationships (
+    'bug_see_also', 
+    'value', 
+    'bug_id', 
+    'Relates',
+    @bugs
+  );
+} # getRelated
+
+sub getWatchers ($) {
+  my ($bugid) = @_;
+  
+  my $statement = <<"END";
+select 
+  profiles.login_name
+from 
+  cc,
+  profiles
+where 
+  cc.who = profiles.userid and
+  bug_id = ?
+END
+
+  my $sth = $bugzilla->prepare ($statement);
+
+  _checkDBError 'Unable to prepare statement', $statement;
+
+  $sth->execute ($bugid);
+
+  _checkDBError 'Unable to execute statement', $statement;
+
+  my @rows = @{$sth->fetchall_arrayref};
+
+  my %watchers;
+
+  for (@rows) {
+    if ($$_[0] =~ /(.*)\@/) {
+      $watchers{$1} = 1;
+    } # if
+
+    $main::total{'Watchers Processed'}++;  
+  } # for
+
+  return %watchers;
+} # getWatchers
\ No newline at end of file
diff --git a/audience/JIRA/lib/JIRAUtils.pm b/audience/JIRA/lib/JIRAUtils.pm
new file mode 100644 (file)
index 0000000..a60e01c
--- /dev/null
@@ -0,0 +1,655 @@
+=pod
+
+=head1 NAME $RCSfile: JIRAUtils.pm,v $
+
+Some shared functions dealing with JIRA
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: 1.0 $
+
+=item Created
+
+Fri Mar 12 10:17:44 PST 2004
+
+=item Modified
+
+$Date: 2013/05/30 15:48:06 $
+
+=back
+
+=head1 ROUTINES
+
+The following routines are exported:
+
+=cut
+
+package JIRAUtils;
+
+use strict;
+use warnings;
+
+use base 'Exporter';
+
+use FindBin;
+use Display;
+use Carp;
+use DBI;
+
+use JIRA::REST;
+use BugzillaUtils;
+
+our ($jira, %opts);
+
+our @EXPORT = qw (
+  addJIRAComment
+  addDescription
+  Connect2JIRA
+  findIssue
+  getIssue
+  getIssueLinks
+  getIssueLinkTypes
+  getRemoteLinks
+  updateIssueWatchers
+  linkIssues
+  addRemoteLink
+  getRemoteLink
+  removeRemoteLink
+  getRemoteLinkByBugID
+  promoteBug2JIRAIssue
+  findRemoteLinkByBugID
+);
+
+my (@issueLinkTypes, %total, %cache, $jiradb);
+
+sub _checkDBError ($;$) {
+  my ($msg, $statement) = @_;
+
+  $statement //= 'Unknown';
+  
+  $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
+  
+  my $dberr    = $jiradb->err;
+  my $dberrmsg = $jiradb->errstr;
+  
+  $dberr    ||= 0;
+  $dberrmsg ||= 'Success';
+
+  my $message = '';
+  
+  if ($dberr) {
+    my $function = (caller (1)) [3];
+
+    $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
+             . "SQL Statement: $statement";
+  } # if
+
+  $main::log->err ($message, 1) if $dberr;
+
+  return;
+} # _checkDBError
+
+sub openJIRADB (;$$$$) {
+  my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
+
+  $dbhost //= $main::opts{jiradbhost};
+  $dbname //= 'jiradb';
+  $dbuser //= 'adefaria';
+  $dbpass //= 'reader';
+  
+  $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...");
+  
+  $jiradb = DBI->connect (
+    "DBI:mysql:$dbname:$dbhost",
+    $dbuser,
+    $dbpass, {
+      PrintError => 0,
+      RaiseError => 1,
+    },
+  );
+
+  _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
+  
+  return $jiradb;
+} # openJIRADB
+
+sub Connect2JIRA (;$$$) {
+  my ($username, $password, $server) = @_;
+
+  my %opts;
+  
+  $opts{username} = $username || 'jira-admin';
+  $opts{password} = $password || $ENV{PASSWORD}    || 'jira-admin';
+  $opts{server}   = $server   || $ENV{JIRA_SERVER} || 'jira-dev:8081';
+  $opts{URL}      = "http://$opts{server}/rest/api/latest";
+  
+  $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})");
+  
+  $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
+
+  # Store username as we might need it (see updateIssueWatchers)
+  $jira->{username} = $opts{username};
+  
+  return $jira;  
+} # Connect2JIRA
+
+sub addDescription ($$) {
+  my ($issue, $description) = @_;
+  
+  if ($main::opts{exec}) {
+    eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
+  
+    if ($@) {
+      return "Unable to add description\n$@";
+    } else {
+      return 'Description added';
+    } # if
+  } # if
+} # addDescription
+
+sub addJIRAComment ($$) {
+  my ($issue, $comment) = @_;
+  
+  if ($main::opts{exec}) {
+    eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
+  
+    if ($@) {
+      return "Unable to add comment\n$@";
+    } else {
+      return 'Comment added';
+    } # if
+  } else {
+    return "Would have added comments to $issue";
+  } # if
+} # addJIRAComment
+
+sub findIssue ($%) {
+  my ($bugid, %bugmap) = @_;
+  
+=pod
+  # Check the cache...
+  if ($cache{$bugid}) {
+    if ($cache{$bugid} =~ /^\d+/) {
+      # We have a cache hit but the contents here are a bugid. This means we had
+      # searched for the corresponding JIRA issue for this bug before and came
+      # up empty handed. In this situtaion we really have:
+      return "Unable to find a JIRA issue for Bug $bugid"; 
+    } else {
+      return $cache{$bugid};
+    } # if
+  } # if
+=cut  
+  my $issue;
+
+  my %query = (
+    jql    => "\"Bugzilla Bug Number\" ~ $bugid",
+    fields => [ 'key' ],
+  );
+  
+  eval {$issue = $jira->GET ("/search/", \%query)};
+
+  my $issueID = $issue->{issues}[0]{key};
+  
+  if (@{$issue->{issues}} > 2) {
+    $main::log->err ("Found more than 2 issues for Bug ID $bugid");
+    
+    return "Found more than 2 issues for Bug ID $bugid";
+  } elsif (@{$issue->{issues}} == 2) {
+    my ($issueNum0, $issueNum1);
+    
+    if ($issue->{issues}[0]{key} =~ /(\d+)/) {
+      $issueNum0 = $1;
+    } # if
+    
+    if ($issue->{issues}[1]{key} =~ /(\d+)/) {
+      $issueNum1 = $1;
+    } # if
+    
+    if ($issueNum0 < $issueNum1) {
+      $issueID = $issue->{issues}[1]{key};
+    } # if
+    
+    # Let's mark them as clones. See if this clone link already exists...
+    my $alreadyCloned;
+    
+    for (getIssueLinks ($issueID, 'Cloners')) {
+      my $inwardIssue  = $_->{inwardIssue}{key}  || '';
+      my $outwardIssue = $_->{outwardIssue}{key} || '';
+      
+      if ("RDBNK-$issueNum0" eq $inwardIssue  ||
+          "RDBNK-$issueNum0" eq $outwardIssue ||
+          "RDBNK-$issueNum1" eq $inwardIssue  ||
+          "RDBNK-$issueNum1" eq $outwardIssue) {
+         $alreadyCloned = 1;
+         
+         last;
+      } # if
+    } # for
+
+    unless ($alreadyCloned) {
+      my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1");
+    
+      return $result if $result =~ /Unable to/;
+    
+      $main::log->msg ($result);
+    } # unless
+  } # if
+
+  if ($issueID) {
+    $main::log->msg ("Found JIRA issue $issueID for Bug $bugid");
+  
+    #$cache{$bugid} = $issueID;
+      
+    #return $cache{$bugid};
+    return $issueID;
+  } else {
+    my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
+                                 : "Unable to find a JIRA issue for Bug $bugid";
+    
+    # Here we put this bugid into the cache but instead of a the JIRA issue
+    # id we put the bugid. This will stop us from adding up multiple hits on
+    # this bugid.
+    #$cache{$bugid} = $bugid;
+
+    return $status;
+  } # if
+} # findJIRA
+
+sub getIssue ($;@) {
+  my ($issue, @fields) = @_;
+  
+  my $fields = @fields ? "?fields=" . join ',', @fields : '';
+
+  return $jira->GET ("/issue/$issue$fields");
+} # getIssue
+
+sub getIssueLinkTypes () {
+  my $issueLinkTypes = $jira->GET ('/issueLinkType/');
+  
+  map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
+  
+  return @issueLinkTypes
+} # getIssueLinkTypes
+
+sub linkIssues ($$$) {
+  my ($from, $type, $to) = @_;
+  
+  unless (@issueLinkTypes) {
+    getIssueLinkTypes;
+  } # unless
+  
+  unless (grep {$type eq $_} @issueLinkTypes) {
+    $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n" 
+               . join "\n\t", @issueLinkTypes);
+               
+    return "Unable to $type link $from -> $to";           
+  } # unless  
+  
+  my %link = (
+    inwardIssue  => {
+      key        => $from,
+    },
+    type         => {
+      name       => $type,
+    },
+    outwardIssue => {
+      key        => $to,
+    },
+    comment      => {
+      body       => "Link ported as part of the migration from Bugzilla: $from <-> $to",
+    },
+  );
+  
+  $main::total{'IssueLinks Added'}++;
+  
+  if ($main::opts{exec}) {
+    eval {$jira->POST ("/issueLink", undef, \%link)};
+    
+    if ($@) {
+      return "Unable to $type link $from -> $to\n$@";
+    } else {
+      return "Made $type link $from -> $to";
+    } # if
+  } else {
+    return "Would have $type linked $from -> $to";
+  } # if
+} # linkIssue
+
+sub getRemoteLink ($;$) {
+  my ($jiraIssue, $id) = @_;
+  
+  $id //= '';
+  
+  my $result;
+  
+  eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
+  
+  return if $@;
+  
+  my %remoteLinks;
+
+  if (ref $result eq 'ARRAY') {
+    map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;  
+  } else {
+    $remoteLinks{$result->{id}} = $result->{object}{title};
+  } # if
+    
+  return \%remoteLinks;
+} # getRemoteLink
+
+sub getRemoteLinks (;$) {
+  my ($bugid) = @_;
+  
+  $jiradb = openJIRADB unless $jiradb;
+  
+  my $statement = 'select url from remotelink';
+
+  $statement .= " where url like 'http://bugs%'";  
+  $statement .= " and url like '%$bugid'" if $bugid; 
+  $statement .= " group by issueid desc";
+  
+  my $sth = $jiradb->prepare ($statement);
+  
+  _checkDBError 'Unable to prepare statement', $statement;
+  
+  $sth->execute;
+  
+  _checkDBError 'Unable to execute statement', $statement;
+
+  my %bugids;
+  
+  while (my $record = $sth->fetchrow_array) {
+    if ($record =~ /(\d+)$/) {
+      $bugids{$1} = 1;
+    } # if 
+  } # while
+  
+  return keys %bugids;
+} # getRemoteLinks
+
+sub findRemoteLinkByBugID (;$) {
+  my ($bugid) = @_;
+  
+  my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
+  
+  if ($bugid) {
+    $condition .= " and remotelink.url like '%id=$bugid'";
+  } # unless
+  
+  $jiradb = openJIRADB unless $jiradb;
+
+  my $statement = <<"END";
+select 
+  remotelink.id, 
+  concat (project.pkey, '-', issuenum) as issue,
+  relationship
+from
+  remotelink,
+  jiraissue,
+  project
+$condition
+END
+
+  my $sth = $jiradb->prepare ($statement);
+  
+  _checkDBError 'Unable to prepare statement', $statement;
+  
+  $sth->execute;
+  
+  _checkDBError 'Unable to execute statement', $statement;
+  
+  my @records;
+  
+  while (my $row = $sth->fetchrow_hashref) {
+    $row->{bugid} = $bugid;
+        
+    push @records, $row;
+  } # while
+  
+  return \@records;
+} # findRemoteLinkByBugID
+
+sub promoteBug2JIRAIssue ($$$$) {
+  my ($bugid, $jirafrom, $jirato, $relationship) = @_;
+
+  my $result = linkIssues $jirafrom, $relationship, $jirato;
+        
+  return $result if $result =~ /Unable to link/;
+  
+  $main::log->msg ($result . " (BugID $bugid)");
+  
+  for (@{findRemoteLinkByBugID $bugid}) {
+    my %record = %$_;
+    
+    $result = removeRemoteLink ($record{issue}, $record{id});
+    
+    # We may not care if we couldn't remove this link because it may have been
+    # removed by a prior pass.
+    return $result if $result =~ /Unable to remove link/;
+    
+    $main::log->msg ($result) unless $result eq '';
+  } # for
+  
+  return $result;
+} # promoteBug2JIRAIssue
+
+sub addRemoteLink ($$$) {
+  my ($bugid, $relationship, $jiraIssue) = @_;
+  
+  my $bug = getBug $bugid;
+  
+  # Check to see if this Bug ID already exists on this JIRA Issue, otherwise
+  # JIRA will duplicate it! 
+  my $remoteLinks = getRemoteLink $jiraIssue;
+  
+  for (keys %$remoteLinks) {
+    if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
+      return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
+    } # if
+  } # for
+  
+  # Note this globalid thing is NOT working! ALl I see is null in the database
+  my %remoteLink = (
+#    globalid     => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
+#    application  => {
+#      type       => 'Bugzilla',
+#      name       => 'Bugzilla',
+#    },
+    relationship => $relationship, 
+    object       => {
+      url        => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
+      title      => "Bug $bugid",
+      summary    => $bug->{short_desc},
+      icon       => {
+        url16x16 => 'http://bugs.audience.local/favicon.png',
+        title    => 'Bugzilla Bug',
+      },
+    },
+  );
+  
+  $main::total{'RemoteLink Added'}++;
+  
+  if ($main::opts{exec}) {
+    eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
+  
+    return $@;
+  } else {
+    return "Would have linked $bugid -> $jiraIssue";
+  } # if
+} # addRemoteLink
+
+sub removeRemoteLink ($;$) {
+  my ($jiraIssue, $id) = @_;
+  
+  $id //= '';
+  
+  my $remoteLinks = getRemoteLink ($jiraIssue, $id);
+  
+  for (keys %$remoteLinks) {
+    my $result;
+    
+    $main::total{'RemoteLink Removed'}++;
+  
+    if ($main::opts{exec}) {
+      eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
+
+      if ($@) {  
+        return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
+      } else {
+        my $bugid;
+        
+        if ($remoteLinks->{$_} =~ /(\d+)/) {
+          return "Removed remote link $jiraIssue (Bug ID $1)";
+        } # if
+      } # if
+      
+      $main::total{'Remote Links Removed'}++;
+    } else {
+      if ($remoteLinks->{$_} =~ /(\d+)/) {
+        return "Would have removed remote link $jiraIssue (Bug ID $1)";
+      } # if
+    } # if
+  } # for  
+} # removeRemoteLink
+
+sub getIssueLinks ($;$) {
+  my ($issue, $type) = @_;
+  
+  my @links = getIssue ($issue, ('issuelinks'));
+  
+  my @issueLinks;
+
+  for (@{$links[0]->{fields}{issuelinks}}) {
+     my %issueLink = %$_;
+     
+     next if ($type && $type ne $issueLink{type}{name});
+     
+     push @issueLinks, \%issueLink;  
+  }
+  
+  return @issueLinks;
+} # getIssueLinks
+
+sub updateIssueWatchers ($%) {
+  my ($issue, %watchers) = @_;
+
+  my $existingWatchers;
+  
+  eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
+  
+  return "Unable to get issue $issue\n$@" if $@;
+  
+  for (@{$existingWatchers->{watchers}}) {
+    # Cleanup: Remove the current user from the watchers list.
+    # If he's on the list then remove him.
+    if ($_->{name} eq $jira->{username}) {
+      $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
+      
+      $total{"Admins destroyed"}++;
+    } # if
+    
+    # Delete any matching watchers
+    delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
+  } # for
+
+  return '' if keys %watchers == 0;
+  
+  my $issueUpdated;
+  
+  for (keys %watchers) {
+    if ($main::opts{exec}) {
+      eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
+    
+      if ($@) {
+        $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue");
+      
+        $main::total{'Watchers skipped'}++;
+      } else {
+        $issueUpdated = 1;
+        
+        $main::total{'Watchers added'}++;
+      } # if
+    } else {
+      $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
+      
+      $main::total{'Watchers that would have been added'}++;
+    } # if
+  } # for
+  
+  $main::total{'Issues updated'}++ if $issueUpdated;
+  
+  return '';
+} # updateIssueWatchers
+
+=pod
+
+I'm pretty sure I'm not using this routine anymore and I don't think it works.
+If you wish to reserect this then please test.
+
+sub updateWatchers ($%) {
+  my ($issue, %watchers) = @_;
+
+  my $existingWatchers;
+  
+  eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
+  
+  if ($@) {
+    error "Unable to get issue $issue";
+    
+    $main::total{'Missing JIRA Issues'}++;
+    
+    return;
+  } # if
+  
+  for (@{$existingWatchers->{watchers}}) {
+    # Cleanup: Mike Admin Cogan was added as a watcher for each issue imported.
+    # If he's on the list then remove him.
+    if ($_->{name} eq 'mcoganAdmin') {
+      $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
+      
+      $main::total{"mcoganAdmin's destroyed"}++;
+    } # if
+    
+    # Delete any matching watchers
+    delete $watchers{$_->{name}} if $watchers{$_->{name}};
+  } # for
+
+  return if keys %watchers == 0;
+  
+  my $issueUpdated;
+  
+  for (keys %watchers) {
+    if ($main::opts{exec}) {
+      eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
+    
+      if ($@) {
+        error "Unable to add user $_ as a watcher to JIRA Issue $issue";
+      
+        $main::total{'Watchers skipped'}++;
+      } else {
+        $main::total{'Watchers added'}++;
+        
+        $issueUpdated = 1;
+      } # if
+    } else {
+      $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
+      
+      $main::total{'Watchers that would have been added'}++;
+    } # if
+  } # for
+  
+  $main::total{'Issues updated'}++ if $issueUpdated;
+  
+  return;
+} # updateWatchers
+=cut
+
+1;
diff --git a/audience/JIRA/updateWatchLists.pl b/audience/JIRA/updateWatchLists.pl
new file mode 100644 (file)
index 0000000..5a5c4a0
--- /dev/null
@@ -0,0 +1,170 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME updateWatchLists.pl
+
+Copy CC lists from Bugzilla -> JIRA
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+ Updates JIRA watchlists by copying the CC list information from Bugzilla
+  $ updateWatchLists.pl [-login <login email>] [-products product1,
+                        product2,...] [-[no]exec]
+                        [-verbose] [-help] [-usage]
+
+  Where:
+
+    -v|erbose:       Display progress output
+    -he|lp:          Display full help
+    -usa|ge:         Display usage
+    -[no]e|xec:      Whether or not to update JIRA. -noexec says only 
+                     tell me what you would have updated.
+    -use|rname:      Username to log into JIRA with (Default: jira-admin)
+    -p|assword:      Password to log into JIRA with (Default: jira-admin's 
+                     password)
+    -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+    -jiraserver:     Machine where Jira lives (Default: jira-dev)
+    -bugi|ds:        Comma separated list of BugIDs to process
+    -f|ile:          File of BugIDs, one per line
+
+=head1 DESCRIPTION
+
+This script updates JIRA watchlists by copying the CC List information from
+Bugzilla to JIRA.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+use JIRA::REST;
+
+use Getopt::Long; 
+use Pod::Usage;
+
+# Login should be the email address of the bugzilla account which has 
+# priviledges to create products and components
+our %opts = (
+  exec           => 0,
+  bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+  jiraserver     => $ENV{JIRASERVER}     || 'jira-dev:8081',
+  username       => 'jira-admin',
+  password       => 'jira-admin',
+  usage          => sub { pod2usage },
+  help           => sub { pod2usage (-verbose => 2)},
+  verbose        => sub { set_verbose },
+);
+
+our ($log, %total);
+
+my ($bugzilla, $jira);
+
+sub main () {
+  my $startTime = time;
+  
+  GetOptions (
+    \%opts,
+    'verbose',
+    'usage',
+    'help',
+    'exec!',
+    'quiet',
+    'username=s',
+    'password=s',
+    'bugids=s@',
+    'file=s',
+    'jiraserver=s',
+    'bugzillaserver=s',
+  ) or pod2usage;
+  
+  $log = Logger->new;
+  
+  if ($opts{file}) {
+    open my $file, '<', $opts{file} 
+      or die "Unable to open $opts{file} - $!";
+      
+    $opts{bugids} = [<$file>];
+    
+    chomp @{$opts{bugids}};
+  } else {
+    my @bugids;
+    
+    push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}}); 
+  
+    $opts{bugids} = [@bugids];
+  } # if
+  
+  pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+    unless $opts{bugids};
+
+  openBugzilla $opts{bugzillaserver}
+    or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+    
+  Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+    or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+  
+  for (@{$opts{bugids}}) {
+    my $issue = findIssue $_;
+    
+    if ($issue =~ /^Future JIRA Issue/ or $issue =~ /^Unable to find/) {
+      $log->msg ($issue);
+    } else {
+      my %watchers = getWatchers $_;
+      
+      $log->msg ('Found ' . scalar (keys %watchers) . " watchers for JIRA Issue $issue");
+      
+      my $result = updateIssueWatchers ($issue, %watchers);
+      
+      if ($result =~ /^Unable to/) {
+        $total{'Missing JIRA Issues'}++;
+        
+        $log->err ($result);
+      } else {
+        $total{'Issues updated'}++;
+      } # if
+    } # if
+  } # for
+
+  display_duration $startTime, $log;
+  
+  Stats (\%total, $log) unless $opts{quiet};
+  
+  return  0;
+} # main
+
+exit main;