From: Andrew DeFaria Date: Mon, 21 Dec 2015 18:11:32 +0000 (-0800) Subject: Adding some files of recent work. X-Git-Url: https://defaria.com/gitweb/?a=commitdiff_plain;h=1729e51b9b8288680eb46aed0d34808508930f52;p=clearscm.git Adding some files of recent work. --- diff --git a/Confluence/lib/Confluence.pm b/Confluence/lib/Confluence.pm new file mode 100644 index 0000000..2ad6d18 --- /dev/null +++ b/Confluence/lib/Confluence.pm @@ -0,0 +1,118 @@ +=pod + +=head1 NAME $RCSfile: JIRAUtils.pm,v $ + +Some shared functions dealing with JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=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 Confluence; + +use strict; +use warnings; + +use File::Basename; +use MIME::Base64; + +use Display; +use GetConfig; +use Carp; + +use REST::Client; + +our $VERSION = '$Revision: 1.0 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $confluenceConf = $ENV{CONFLUENCE_CONF} || dirname (__FILE__) . '../etc/confluence.conf'; + +my %OPTS = GetConfig $confluenceConf if -r $confluenceConf; + +sub _get () { + my ($self, $url) = @_; + + unless ($self->{headers}) { + $self->{headers} = { + Accept => 'application/json', + Authorization => 'Basic ' + . encode_base64 ($self->{username} . ':' . $self->{password}), + }; + } # unless + + return $self->{REST}->GET ($url, $self->{headers}); +} # _get + +sub new (;%) { + my ($class, %parms) = @_; + + my $self = bless {}, $class; + + $self->{username} = $parms{username} || $OPTS{username} || $ENV{CONFLUENCE_USERNAME}; + $self->{password} = $parms{password} || $OPTS{password} || $ENV{CONFLUENCE_PASSWORD}; + $self->{server} = $parms{server} || $OPTS{server} || $ENV{CONFLUENCE_SERVER}; + $self->{port} = $parms{port} || $OPTS{port} || $ENV{CONFLUENCE_PORT}; + $self->{URL} = "http://$self->{server}:$self->{port}/rest/api"; + + return $self->connect; +} # new + +sub connect () { + my ($self) = @_; + + $self->{REST} = REST::Client->new ( + host => "http://$self->{server}:$self->{port}", + ); + + $self->{REST}->getUseragent()->ssl_opts (verify_hostname => 0); + $self->{REST}->setFollow (1); + + return $self; +} # connect + +sub getContent (%) { + my ($self, %parms) = @_; + + my $url = 'content?'; + + my @parms; + + push @parms, "type=$parms{type}" if $parms{type}; + push @parms, "spacekey=$parms{spaceKey}" if $parms{spaceKey}; + push @parms, "title=$parms{title}" if $parms{title}; + push @parms, "status=$parms{status}" if $parms{status}; + push @parms, "postingDay=$parms{postingDay}" if $parms{postingDay}; + push @parms, "expand=$parms{expand}" if $parms{expand}; + push @parms, "start=$parms{start}" if $parms{start}; + push @parms, "limit==$parms{limit}" if $parms{limit}; + + my $content = $self->_get ('/content/', join ',', @parms); + + return $content; +} # getContent + +1; diff --git a/JIRA/importComments.pl b/JIRA/importComments.pl new file mode 100644 index 0000000..6a1712c --- /dev/null +++ b/JIRA/importComments.pl @@ -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 + +=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 ] [-login ] + [-jiraserver ] + [-username ] [-password ] + [-bugids bugid,bugid,... | -file ] + [-[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 • 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 [,,...] or -file ' + 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/JIRA/jiradep.pl b/JIRA/jiradep.pl new file mode 100644 index 0000000..a8ce4c4 --- /dev/null +++ b/JIRA/jiradep.pl @@ -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 + +=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 ] [-login ] + [-jiraserver ] + [-username ] [-password ] + [-bugids bugid,bugid,... | -file ] + [-[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 [,,...] or -file ' + 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/JIRA/lib/BugzillaUtils.pm b/JIRA/lib/BugzillaUtils.pm new file mode 100644 index 0000000..899f506 --- /dev/null +++ b/JIRA/lib/BugzillaUtils.pm @@ -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 + +=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/JIRA/lib/JIRAUtils.pm b/JIRA/lib/JIRAUtils.pm new file mode 100644 index 0000000..b57219d --- /dev/null +++ b/JIRA/lib/JIRAUtils.pm @@ -0,0 +1,1038 @@ +=pod + +=head1 NAME $RCSfile: JIRAUtils.pm,v $ + +Some shared functions dealing with JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=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; + +our @EXPORT = qw ( + Connect2JIRA + addDescription + addJIRAComment + addRemoteLink + attachFiles2Issue + attachmentExists + blankBugzillaNbr + copyGroupMembership + count + findIssue + findIssues + findRemoteLinkByBugID + getIssue + getIssueFromBugID + getIssueLinkTypes + getIssueLinks + getIssueWatchers + getIssues + getNextIssue + getRemoteLink + getRemoteLinkByBugID + getRemoteLinks + getUsersGroups + linkIssues + promoteBug2JIRAIssue + removeRemoteLink + renameUsers + updateColumn + updateIssueWatchers + updateUsersGroups +); + +my (@issueLinkTypes, %cache, $jiradb, %findQuery); + +my %tables = ( + ao_08d66b_filter_display_conf => [ + {column => 'user_name'} + ], + ao_0c0737_vote_info => [ + {column => 'user_name'} + ], + ao_3a112f_audit_log_entry => [ + {column => 'user'} + ], + ao_563aee_activity_entity => [ + {column => 'username'} + ], + ao_60db71_auditentry => [ + {column => 'user'} + ], + ao_60db71_boardadmins => [ + {column => "'key'"} + ], + ao_60db71_rapidview => [ + {column => 'owner_user_name'} + ], + ao_caff30_favourite_issue => [ + {column => 'user_key'} + ], +# app_user => [ +# {column => 'user_key'}, +# {column => 'lower_user_name'}, +# ], + audit_log => [ + {column => 'author_key'} + ], + avatar => [ + {column => 'owner'} + ], + changegroup => [ + {column => 'author'} + ], + changeitem => [ + {column => 'oldvalue', + condition => 'field = "assignee"'}, + {column => 'newvalue', + condition => 'field = "assignee"'}, + ], + columnlayout => [ + {column => 'username'}, + ], + component => [ + {column => 'lead'}, + ], + customfieldvalue => [ + {column => 'stringvalue'}, + ], + favouriteassociations => [ + {column => 'username'}, + ], +# cwd_membership => [ +# {column => 'child_name'}, +# {column => 'lower_child_name'}, +# ], + fileattachment => [ + {column => 'author'}, + ], + filtersubscription => [ + {column => 'username'}, + ], + jiraaction => [ + {column => 'author'}, + ], + jiraissue => [ + {column => 'reporter'}, + {column => 'assignee'}, + ], + jiraworkflows => [ + {column => 'creatorname'}, + ], + membershipbase => [ + {column => 'user_name'}, + ], + os_currentstep => [ + {column => 'owner'}, + {column => 'caller'}, + ], + os_historystep => [ + {column => 'owner'}, + {column => 'caller'}, + ], + project => [ + {column => 'lead'}, + ], + portalpage => [ + {column => 'username'}, + ], + schemepermissions => [ + {column => 'perm_parameter', + condition => 'perm_type = "user"'}, + ], + searchrequest => [ + {column => 'authorname'}, + {column => 'username'}, + ], + userassociation => [ + {column => 'source_name'}, + ], + userbase => [ + {column => 'username'}, + ], + userhistoryitem => [ + {column => 'username'} + ], + worklog => [ + {column => 'author'}, + ], +); + +sub _checkDBError ($;$) { + my ($msg, $statement) = @_; + + $statement //= 'Unknown'; + + if ($main::log) { + $main::log->err ('JIRA database not opened!', 1) unless $jiradb; + } # if + + 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 + + if ($main::log) { + $main::log->err ($message, 1) if $dberr; + } # if + + return; +} # _checkDBError + +sub openJIRADB (;$$$$) { + my ($dbhost, $dbname, $dbuser, $dbpass) = @_; + + $dbhost //= $main::opts{jiradbhost}; + $dbname //= 'jiradb'; + $dbuser //= 'root'; + $dbpass //= 'r00t'; + + $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...") if $main::log; + + $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'; + $opts{URL} = "http://$opts{server}/rest/api/latest"; + + $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})") if $main::log; + + $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 count ($$) { + my ($table, $condition) = @_; + + my $statement; + + $jiradb = openJIRADB unless $jiradb; + + if ($condition) { + $statement = "select count(*) from $table where $condition"; + } else { + $statement = "select count(*) from $table"; + } # if + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'count: Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'count: Unable to execute statement', $statement; + + # Get return value, which should be how many message there are + my @row = $sth->fetchrow_array; + + # Done with $sth + $sth->finish; + + my $count; + + # Retrieve returned value + unless ($row[0]) { + $count = 0 + } else { + $count = $row[0]; + } # unless + + return $count +} # count + +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 blankBugzillaNbr ($) { + my ($issue) = @_; + + eval {$jira->PUT ("/issue/$issue", undef, {fields => {'Bugzilla Bug Origin' => ''}})}; + #eval {$jira->PUT ("/issue/$issue", undef, {fields => {'customfield_10132' => ''}})}; + + if ($@) { + return "Unable to blank Bugzilla number$@\n" + } else { + return 'Corrected' + } # if +} # blankBugzillaNbr + +sub attachmentExists ($$) { + my ($issue, $filename) = @_; + + my $attachments = getIssue ($issue, qw(attachment)); + + for (@{$attachments->{fields}{attachment}}) { + return 1 if $filename eq $_->{filename}; + } # for + + return 0; +} # attachmentExists + +sub attachFiles2Issue ($@) { + my ($issue, @files) = @_; + + my $status = $jira->attach_files ($issue, @files); + + return $status; +} # attachFiles2Issue + +sub getIssueFromBugID ($) { + my ($bugid) = @_; + + my $issue; + + my %query = ( + jql => "\"Bugzilla Bug Origin\" ~ $bugid", + fields => [ 'key' ], + ); + + eval {$issue = $jira->GET ("/search/", \%query)}; + + my $issueID = $issue->{issues}[0]{key}; + + return $issue->{issues} if @{$issue->{issues}} > 1; + return $issueID; +} # getIssueFromBugID + +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 Origin\" ~ $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") if $main::log; + + return "Found more than 2 issues for Bug ID $bugid"; + } elsif (@{$issue->{issues}} == 2) { + my ($issueNum0, $issueNum1, $projectName0, $projectName1); + + if ($issue->{issues}[0]{key} =~ /(.*)-(\d+)/) { + $projectName0 = $1; + $issueNum0 = $2; + } # if + + if ($issue->{issues}[1]{key} =~ /(.*)-(\d+)/) { + $projectName1 = $1; + $issueNum1 = $2; + } # 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 ("$projectName0-$issueNum0" eq $inwardIssue || + "$projectName0-$issueNum0" eq $outwardIssue || + "$projectName1-$issueNum1" eq $inwardIssue || + "$projectName1-$issueNum1" eq $outwardIssue) { + $alreadyCloned = 1; + + last; + } # if + } # for + + unless ($alreadyCloned) { + my $result = linkIssues ("$projectName0-$issueNum0", 'Cloners', "$projectName1-$issueNum1"); + + return $result if $result =~ /Unable to/; + + $main::log->msg ($result) if $main::log; + } # unless + } # if + + if ($issueID) { + $main::log->msg ("Found JIRA issue $issueID for Bug $bugid") if $main::log; + + #$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 findIssues (;$@) { + my ($condition, @fields) = @_; + + push @fields, '*all' unless @fields; + + $findQuery{jql} = $condition || ''; + $findQuery{startAt} = 0; + $findQuery{maxResults} = 1; + $findQuery{fields} = join ',', @fields; + + return; +} # findIssues + +sub getNextIssue () { + my $result; + + eval {$result = $jira->GET ('/search/', \%findQuery)}; + + $findQuery{startAt}++; + + # Move id and key into fields + return unless @{$result->{issues}}; + + $result->{issues}[0]{fields}{id} = $result->{issues}[0]{id}; + $result->{issues}[0]{fields}{key} = $result->{issues}[0]{key}; + + return %{$result->{issues}[0]{fields}}; +} # getNextIssue + +sub getIssues (;$$$@) { + my ($condition, $start, $max, @fields) = @_; + + push @fields, '*all' unless @fields; + + my ($result, %query); + + $query{jql} = $condition || ''; + $query{startAt} = $start || 0; + $query{maxResults} = $max || 50; + $query{fields} = join ',', @fields; + + eval {$result = $jira->GET ('/search/', \%query)}; + + # We sometimes get an error here when $result->{issues} is undef. + # I suspect this is when the number of issues just happens to be + # an even number like on a $query{maxResults} boundry. So when + # $result->{issues} is undef we assume it's the last of the issues. + # (I should really verify this). + if ($result->{issues}) { + return @{$result->{issues}}; + } else { + return; + } # if +} # getIssues + +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\t" + . join "\n\t", @issueLinkTypes) if $main::log; + + 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)") if $main::log; + + 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/; + + if ($main::log) { + $main::log->msg ($result) unless $result eq ''; + } # if + } # 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 getIssueWatchers ($) { + my ($issue) = @_; + + my $watchers; + + eval {$watchers = $jira->GET ("/issue/$issue/watchers")}; + + return if $@; + + # The watcher information returned by the above is incomplete. Let's complete + # it. + my @watchers; + + for (@{$watchers->{watchers}}) { + my $user; + + eval {$user = $jira->GET ("/user?username=$_->{key}")}; + + unless ($@) { + push @watchers, $user; + } else { + if ($main::log) { + $main::log->err ("Unable to find user record for $_->{name}") + unless $_->{name} eq 'jira-admin'; + }# if + } # unless + } # for + + return @watchers; +} # getIssueWatchers + +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}"); + + $main::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") if $main::log; + + $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") if $main::log; + + $main::total{'Watchers that would have been added'}++; + } # if + } # for + + $main::total{'Issues updated'}++ if $issueUpdated; + + return ''; +} # updateIssueWatchers + +sub getUsersGroups ($) { + my ($username) = @_; + + my ($result, %query); + + %query = ( + username => $username, + expand => 'groups', + ); + + eval {$result = $jira->GET ('/user/', \%query)}; + + my @groups; + + for (@{$result->{groups}{items}}) { + push @groups, $_->{name}; + } # for + + return @groups; +} # getusersGroups + +sub updateUsersGroups ($@) { + my ($username, @groups) = @_; + + my ($result, @errors); + + my @oldgroups = getUsersGroups $username; + + # We can't always add groups to the new user due to either the group not being + # in the new LDAP directory or we are unable to see it. If we attempt to JIRA + # will try to add the group and we don't have write permission to the + # directory. So we'll just return @errors and let the caller deal with it. + for my $group (@groups) { + next if grep {$_ eq $group} @oldgroups; + + eval {$result = $jira->POST ('/group/user', {groupname => $group}, {name => $username})}; + + push @errors, $@ if $@; + } # for + + return @errors; +} # updateUsersGroups + +sub copyGroupMembership ($$) { + my ($from_username, $to_username) = @_; + + return updateUsersGroups $to_username, getUsersGroups $from_username; +} # copyGroupMembership + +sub updateColumn ($$$%) { + my ($table, $oldvalue, $newvalue, %info) = @_; + + # UGH! Sometimes values need to be quoted + $oldvalue = quotemeta $oldvalue; + $newvalue = quotemeta $newvalue; + + my $condition = "$info{column} = '$oldvalue'"; + $condition .= " and $info{condition}" if $info{condition}; + my $statement = "update $table set $info{column} = '$newvalue' where $condition"; + + my $nbrRows = count $table, $condition; + + if ($nbrRows) { + if ($main::opts{exec}) { + $main::total{'Rows updated'}++; + + $jiradb->do ($statement); + + _checkDBError 'Unable to execute statement', $statement; + } else { + $main::total{'Rows would be updated'}++; + + $main::log->msg ("Would have executed $statement") if $main::log; + } # if + } # if + + return $nbrRows; +} # updateColumn + +sub renameUsers (%) { + my (%users) = @_; + + for my $olduser (sort keys %users) { + my $newuser = $users{$olduser}; + + $main::log->msg ("Renaming $olduser -> $newuser") if $main::log; + display ("Renaming $olduser -> $newuser"); + + if ($main::opts{exec}) { + $main::total{'Users renamed'}++; + } else { + $main::total{'Users would be updated'}++; + } # if + + for my $table (sort keys %tables) { + $main::log->msg ("\tTable: $table Column: ", 1) if $main::log; + + my @columns = @{$tables{$table}}; + + for my $column (@columns) { + my %info = %$column; + + $main::log->msg ("$info{column} ", 1) if $main::log; + + my $rowsUpdated = updateColumn ($table, $olduser, $newuser, %info); + + if ($rowsUpdated) { + my $msg = " $rowsUpdated row"; + $msg .= 's' if $rowsUpdated > 1; + $msg .= ' would have been' unless $main::opts{exec}; + $msg .= ' updated'; + + $main::log->msg ($msg, 1) if $main::log; + } # if + } # for + + $main::log->msg ('') if $main::log; + } # for + + if (my @result = copyGroupMembership ($olduser, $newuser)) { + # Skip errors of the form 'Could not add user... group is read-only + @result = grep {!/Could not add user.*group is read-only/} @result; + + if ($main::log) { + $main::log->err ("Unable to copy group membership from $olduser -> $newuser\n@result", 1) if @result; + } # if + } # if + } # for + + + return $main::log ? $main::log->errors : 0; +} # renameUsers + +1; diff --git a/JIRA/updateWatchLists.pl b/JIRA/updateWatchLists.pl new file mode 100644 index 0000000..5a5c4a0 --- /dev/null +++ b/JIRA/updateWatchLists.pl @@ -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 + +=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 ] [-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 [,,...] or -file ' + 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; diff --git a/Perforce/getPicture.conf b/Perforce/getPicture.conf new file mode 100644 index 0000000..51ea679 --- /dev/null +++ b/Perforce/getPicture.conf @@ -0,0 +1,21 @@ +################################################################################ +# +# File: getPicture.conf +# Description: Configuration file for getPicture.pl +# Author: Andrew DeFaria +# Version: 1.0 +# Created: Fri Oct 3 18:16:26 PDT 2014 +# Modified: $Date: 2014/10/03 18:17:20 $ +# Language: Conf +# +# This file contains configurable parameters that are unique to your site that +# identify the LDAP parms needed to retrieve users pictures from Active +# Directory. It is assumed that thumbnailPhoto is the attribute name and that +# the user can be found by their unique uid. +# +################################################################################ +AD_HOST: ad-vip.audience.local +AD_PORT: 389 +AD_BINDDN: CN=jenkins cm,OU=Service Accounts,DC=audience,DC=local +AD_BINDPW: j55ln8Y6 +AD_BASEDN: DC=audience,DC=local \ No newline at end of file diff --git a/Perforce/getPicture.pl b/Perforce/getPicture.pl new file mode 100755 index 0000000..5a36c8c --- /dev/null +++ b/Perforce/getPicture.pl @@ -0,0 +1,203 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +=pod + +=head1 NAME $File: //AudEngr/Import/VSS/ReleaseEng/Dev/Perforce/getPicture.pl $ + +Retrieve thumbnailPhoto for the userid from Active Directory + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Fri Oct 3 18:16:26 PDT 2014 + +=item Modified + +$Date: 2015/03/03 $ + +=back + +=head1 DESCRIPTION + +This script will take a userid and search the Active Directory for the user and +return an image file if the user has an image associated with his +thumbnailPhoto attribute. + +This can be configured into Perforce Swarn as documented: + +http://www.perforce.com/perforce/doc.current/manuals/swarm/admin.avatars.html + +One would use something like + + // this block shoudl be a peer of 'p4' + 'avatars' => array( + 'http_url' => 'http:///cgi-bin/getPicture.pl?userid={user}' + 'https_url' => 'http:///cgi-bin/getPicture.pl?userid={user}', + ), + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; +use Net::LDAP; +use CGI qw (:standard); + +# Interpolate variable in str (if any) from %opts +sub interpolate ($%) { + my ($str, %opts) = @_; + + # Since we wish to leave undefined $var references in tact the following while + # loop would loop indefinitely if we don't change the variable. So we work + # with a copy of $str changing it always, but only changing the original $str + # for proper interpolations. + my $copyStr = $str; + + while ($copyStr =~ /\$(\w+)/) { + my $var = $1; + + if (exists $opts{$var}) { + $str =~ s/\$$var/$opts{$var}/; + $copyStr =~ s/\$$var/$opts{$var}/; + } elsif (exists $ENV{$var}) { + $str =~ s/\$$var/$ENV{$var}/; + $copyStr =~ s/\$$var/$ENV{$var}/; + } else { + $copyStr =~ s/\$$var//; + } # if + } # while + + return $str; +} # interpolate + +sub _processFile ($%) { + my ($configFile, %opts) = @_; + + while (<$configFile>) { + chomp; + + next if /^\s*[\#|\!]/; # Skip comments + + if (/\s*(.*?)\s*[:=]\s*(.*)\s*/) { + my $key = $1; + my $value = $2; + + # Strip trailing spaces + $value =~ s/\s+$//; + + # Interpolate + $value = interpolate $value, %opts; + + if ($opts{$key}) { + # If the key exists already then we have a case of multiple values for + # the same key. Since we support this we need to replace the scalar + # value with an array of values... + if (ref $opts{$key} eq "ARRAY") { + # It's already an array, just add to it! + push @{$opts{$key}}, $value; + } else { + # It's not an array so make it one + my @a; + + push @a, $opts{$key}; + push @a, $value; + $opts{$key} = \@a; + } # if + } else { + # It's a simple value + $opts{$key} = $value; + } # if + } # if + } # while + + return %opts; +} # _processFile + +sub GetConfig ($) { + my ($filename) = @_; + + my %opts; + + open my $configFile, '<', $filename + or die "Unable to open config file $filename"; + + %opts = _processFile $configFile; + + close $configFile; + + return %opts; +} # GetConfig + +sub checkLDAPError ($$) { + my ($msg, $result) = @_; + + my $code = $result->code; + + die "$msg (Error $code)\n" . $result->error if $code; +} # checkLDAPError + +my ($confFile) = ($FindBin::Script =~ /(.*)\.pl$/); + $confFile = "$FindBin::Bin/$confFile.conf"; + +my %opts = GetConfig ($confFile); + +## Main +$| = 1; + +GetOptions ( + \%opts, + 'AD_HOST=s', + 'AD_PORT=s', + 'AD_BINDDN=s', + 'AD_BINDPW=s', + 'AD_BASEDN=s', + 'userid=s', +) or pod2usage; + +$opts{userid} = param 'userid' unless $opts{userid}; + +pod2usage "Usage getPicture.pl [userid=]\n" unless $opts{userid}; + +my $ldap = Net::LDAP->new ( + $opts{AD_HOST}, ( + host => $opts{AD_HOST}, + port => $opts{AD_PORT}, + basedn => $opts{AD_BASEDN}, + binddn => $opts{AD_BINDDN}, + bindpw => $opts{AD_BINDPW}, + ), +) or die $@; + +my $result = $ldap->bind ( + dn => $opts{AD_BINDDN}, + password => $opts{AD_BINDPW}, +) or die "Unable to bind\n$@"; + +checkLDAPError ('Unable to bind', $result); + +$result = $ldap->search ( + base => $opts{AD_BASEDN}, + filter => "sAMAccountName=$opts{userid}", +); + +checkLDAPError ('Unable to search', $result); + +my @entries = ($result->entries); + +if ($entries[0]) { + print header 'image/jpeg'; + print $entries[0]->get_value ('thumbnailPhoto'); +} # if diff --git a/Perforce/lib/Perforce.pm b/Perforce/lib/Perforce.pm new file mode 100644 index 0000000..a2d5897 --- /dev/null +++ b/Perforce/lib/Perforce.pm @@ -0,0 +1,394 @@ +package Perforce; + +use strict; +use warnings; + +use Carp; +use File::Basename; +use File::Temp; + +use P4; +use Authen::Simple::LDAP; + +use Display; +use GetConfig; +use Utils; + +our $VERSION = '$Revision: 2.23 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4config = $ENV{P4_CONF} || dirname (__FILE__) . '/../etc/p4.conf'; +my $ldapconfig = $ENV{LDAP_CONF} || dirname (__FILE__) . '/../etc/LDAP.conf'; + +my %P4OPTS = GetConfig $p4config if -r $p4config; +my %LDAPOPTS = GetConfig $ldapconfig if -r $ldapconfig; + +my $serviceUser = 'shared'; +my ($domain, $password); +my $defaultPort = 'perforce:1666'; +my $p4tickets = $^O =~ /win/i ? 'C:/Program Files/Devops/Perforce/p4tickets' + : '/opt/audience/perforce/p4tickets'; + +my $keys; + +# If USERDOMAIN is set and equal to audience then set $domain to ''. This will +# use the Audience domain settings in LDAP.conf. +if ($ENV{USERDOMAIN}) { + if (lc $ENV{USERDOMAIN} eq 'audience') { + $domain = ''; + } else { + $domain = $ENV{USERDOMAIN} + } # if +} # if + +sub new (;%) { + my ($class, %parms) = @_; + + my $self = bless {}, $class; + + $self->{P4USER} = $parms{username} || $P4OPTS{P4USER} || $ENV{P4USER} || $serviceUser; + $self->{P4PASSWD} = $parms{password} || $P4OPTS{P4PASSWD} || $ENV{P4PASSWD} || undef; + $self->{P4CLIENT} = $parms{p4client} || $P4OPTS{P4CLIENT} || $ENV{P4CLIENT} || undef; + $self->{P4PORT} = $parms{p4port} || $ENV{P4PORT} || $defaultPort; + + $self->{P4} = $self->connect (%parms); + + return $self; +} # new + +sub errors ($;$) { + my ($self, $cmd, $exit) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + my $errors = $self->{P4}->ErrorCount; + + error "$msg\n" . $self->{P4}->Errors, $exit if $errors; + + return $errors; +} # errors + +sub connect () { + my ($self) = @_; + + $self->{P4} = P4->new; + + $self->{P4}->SetUser ($self->{P4USER}); + $self->{P4}->SetClient ($self->{P4CLIENT}) if $self->{P4CLIENT}; + $self->{P4}->SetPort ($self->{P4PORT}); + $self->{P4}->SetPassword ($self->{P4PASSWD}) unless $self->{P4USER} eq $serviceUser; + + verbose_nolf "Connecting to Perforce server $self->{P4PORT}..."; + $self->{P4}->Connect or croak "Unable to connect to Perforce Server\n"; + verbose 'done'; + + verbose_nolf "Logging in as $self->{P4USER}\@$self->{P4PORT}..."; + + unless ($self->{P4USER} eq $serviceUser) { + $self->{P4}->RunLogin; + + $self->errors ('login', $self->{P4}->ErrorCount); + } else { + $ENV{P4TICKETS} = $p4tickets if $self->{P4USER} eq $serviceUser; + } # unless + + verbose 'done'; + + return $self->{P4}; +} # connect + +sub _authenticateUser ($$$$) { + my ($self, $domain, $username, $p4client) = @_; + + $domain .= '_' unless $domain eq ''; + + # Connect to LDAP + my $ad = Authen::Simple::LDAP->new ( + host => $LDAPOPTS{"${domain}AD_HOST"}, + basedn => $LDAPOPTS{"${domain}AD_BASEDN"}, + port => $LDAPOPTS{"${domain}AD_PORT"}, + filter => $LDAPOPTS{"${domain}AD_FILTER"}, + ) or croak $@; + + # Read the password from and truncate the newline - unless we already + # read in the password + unless ($password) { + if (-t STDIN) { + $password = GetPassword; + } else { + $password = ; + + chomp $password; + } # if + } # unless + + # Special handling of "shared" user + if ($username eq 'shared') { + my $sharedAcl = "$FindBin::Bin/sharedAcl.txt"; + + croak "Unable to find file $sharedAcl" unless -f $sharedAcl; + + open my $sharedAcls, '<', $sharedAcl + or croak "Unable to open $sharedAcl - $!"; + + chomp (my @acls = <$sharedAcls>); + + close $sharedAcls; + + for (@acls) { + if (/\*$/) { + chop; + + exit if $p4client =~ /$_/; + } else { + exit if $_ eq $p4client; + } # if + } # for + } # if + + # Connect to Perforce + $self->connect unless $self->{P4}; + + # Must be a valid Perforce user + return unless $self->getUser ($username); + + # And supply a valid username/password + return $ad->authenticate ($username, $password); +} # _authenticateUser + +sub authenticateUser ($;$) { + my ($self, $username, $p4client) = @_; + +=pod + # If $domain is set to '' then we'll check Audience's LDAP. + # If $domain is not set (undef) then we'll try Knowles first, then Audience + # otherwise we will take $DOMAIN and look for those settings... + unless ($domain) { + unless ($self->_authenticateUser ('KNOWLES', $username, $p4client)) { + unless ($self->_authenticateUser ('', $username, $p4client)) { + return; + } # unless + } # unless + } else { + if ($domain eq '') { + unless ($self->_authenticateUser ('', $username, $p4client)) { + return; + } # unless + } else { + unless ($self->_authenticateUser ($domain, $username, $p4client)) { + return; + } # unless + } # if + } # unless +=cut + + return $self->_authenticateUser ('KNOWLES', $username, $p4client); + +# return 1; +} # authenticateUser + +sub changes (;$%) { + my ($self, $args, %opts) = @_; + + my $cmd = 'changes'; + + for (keys %opts) { + if (/from/i and $opts{to}) { + $args .= " $opts{$_},$opts{to}"; + + delete $opts{to}; + } else { + $args .= " $opts{$_}"; + } # if + } # for + + my $changes = $self->{P4}->Run ($cmd, $args); + + return $self->errors ("$cmd $args") || $changes; +} # changes + +sub job ($) { + my ($self, $job) = @_; + + my $jobs = $self->{P4}->IterateJobs ("-e $job"); + + return $self->errors ("jobs -e $job") || $job; +} # job + +sub comments ($) { + my ($self, $changelist) = @_; + + my $change = $self->{P4}->FetchChange ($changelist); + + return $self->errors ("change $changelist") || $change; +} # comments + +sub files ($) { + my ($self, $changelist) = @_; + + my $files = $self->{P4}->Run ('files', "\@=$changelist"); + + return $self->errors ("files \@=$changelist") || $files; +} # files + +sub filelog ($;%) { + my ($self, $fileSpec, %opts) = @_; + + return $self->{P4}->RunFilelog ($fileSpec, %opts); +} # filelog + +sub getRevision ($;$) { + my ($self, $filename, $revision) = @_; + + unless ($revision) { + if ($filename =~ /#/) { + ($filename, $revision) = split $filename, '#'; + } else { + error "No revision specified in $filename"; + + return; + } # if + } # unlessf + + my @contents = $self->{P4}->RunPrint ("$filename#$revision"); + + if ($self->{P4}->ErrorCount) { + $self->errors ("Print $filename#$revision"); + + return; + } else { + return @contents; + } # if +} # getRevision + +sub getUser (;$) { + my ($self, $user) = @_; + + $user //= $ENV{P4USER} || $ENV{USER}; + + my $cmd = 'user'; + my @args = ('-o', $user); + + my $userRecs = $self->{P4}->Run ($cmd, @args); + + # Perforce returns an array of qualifying users. We only care about the first + # one. However if the username is invalid, Perforce still returns something + # that looks like a user. We look to see if there is a Type field here which + # indicates that it's a valid user + if ($userRecs->[0]{Type}) { + return %{$userRecs->[0]}; + } else { + return; + } # if +} # getUser + +sub renameSwarmUser ($$) { + my ($self, $oldusername, $newusername) = @_; + + # We are turning this off because Perforce support says that just modifying + # the keys we do not update the indexing done in the Perforce Server/Database. + # So instead we have a PHP script (renameUser.php) which goes through the + # official, but still unsupported, "Swarm Record API" to change the usernames + # and call the object's method "save" which should perform the necessary + # reindexing... Stay tuned! :-) + # + # BTW One needs to run renameUser.php by hand as we do not do that here. + return; + + $keys = $self->getKeys ('swarm-*') unless $keys; + + for (@$keys) { + my %key = %$_; + + if ($key{value} =~ /$oldusername/) { + $key{value} =~ s/\"$oldusername\"/\"$newusername\"/g; + $key{value} =~ s/\@$oldusername /\@$newusername /g; + $key{value} =~ s/\@$oldusername\./\@$newusername\./g; + $key{value} =~ s/\@$oldusername,/\@$newusername,/g; + $key{value} =~ s/ $oldusername / $newusername /g; + $key{value} =~ s/ $oldusername,/ $newusername,/g; + $key{value} =~ s/ $oldusername\./ $newusername\./g; + $key{value} =~ s/-$oldusername\"/-$newusername\"/g; + + my $cmd = 'key'; + + display "Correcting key $key{key}"; + + my @result = $self->{P4}->Run ($cmd, $key{key}, $key{value}); + + $self->errors ($cmd, $result[0]->{key} || 1); + } # if + } # for + + return; +} # renameSwarmUser + +sub renameUser ($$) { + my ($self, $old, $new) = @_; + + my $cmd = 'renameuser'; + my @args = ("--from=$old", "--to=$new"); + + $self->{P4}->Run ($cmd, @args); + + my $status = $self->errors (join ' ', $cmd, @args); + + return $status if $status; + +# return $self->renameSwarmUser ($old, $new); +} # renameUser + +sub updateUser (%) { + my ($self, %user) = @_; + + # Trying to do this with P4Perl is difficult. First off the structure needs + # to be AOH and secondly you need to call SetUser to be the other user. That + # said you need to also specify -f to force the update (which means you must + # a admin (or superuser?) and I found no way to specify -f so I've reverted + # back to using p4 from the command line. I also don't like having to use + # a file here... + my $tmpfile = File::Temp->new; + my $tmpfilename = $tmpfile->filename; + + print $tmpfile "User: $user{User}\n"; + print $tmpfile "Email: $user{Email}\n"; + print $tmpfile "Update: $user{Update}\n"; + print $tmpfile "FullName: $user{FullName}\n"; + + close $tmpfile; + + my @lines = `p4 -p $self->{P4PORT} user -f -i < $tmpfilename`; + my $status = $?; + + return wantarray ? @lines : join '', @lines; +} # updateUser + +sub getKeys (;$) { + my ($self, $filter) = @_; + + my $cmd = 'keys'; + my @args; + + if ($filter) { + push @args, '-e'; + push @args, $filter; + } # if + + my $keys = $self->{P4}->Run ($cmd, @args); + + $self->errors ($cmd . join (' ', @args), 1); + + return $keys; +} # getKeys + +sub key ($$) { + my ($self, $name, $value) = @_; + + my $cmd = 'key'; + my @args = ($name, $value); + + $self->{P4}->Run ($cmd, @args); + + return $self->errors (join ' ', $cmd, @args); +} # key + +1; diff --git a/Perforce/renameUser.pl b/Perforce/renameUser.pl new file mode 100755 index 0000000..f542372 --- /dev/null +++ b/Perforce/renameUser.pl @@ -0,0 +1,224 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +=pod + +=head1 NAME RenameUser.pl + +Renames a Perforce user in Perforce + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Fri Oct 30 12:16:39 PDT 2015 + +=item Modified + +$Date: $ + +=back + +=head1 SYNOPSIS + + $ RenameUser.pl [-oldusername -newusername | + -file ] [-p4port ] + [-username ] [-password ] + [-[no]exec] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -o|ldusername: Old username to rename + -ne|wusername: New username + -f|ile: File of usernames to process + ( ) + -p|4port: Perforce port (Default: perforce:1666) + -use|rname: Username to log in as (Default: root) + -p|assword: Password for -username (Defaul: ) + -[no]e|xec: Whether or not to update the database + + -v|erbose: Display verbose info about actions being taken + -d|ebug: Display debug info + -h|elp: Display full documentation + -usa|ge: Display usage + +Note that -file is a list of whitespace separated usernames, one per line listed +as + +=head1 DESCRIPTION + +This script will rename a Perforce user from -oldusername to -newusername. It +will also update the users email address. + +=cut + +use Getopt::Long; +use Pod::Usage; +use FindBin; + +use lib "$FindBin::Bin/../lib"; + +use CMUtils; +use Display; +use Logger; +use Perforce; +use Utils; +use TimeUtils; + +my ($p4, $log, $keys); + +my %total; + +my %opts = ( + p4port => $ENV{P4PORT}, + username => $ENV{P4USER}, + password => $ENV{P4PASSWD}, + verbose => $ENV{VERBOSE} || sub { set_verbose }, + debug => $ENV{DEBUG} || sub { set_debug }, + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, +); + +sub check4Dups (%) { + my (%users) = @_; + + my %newusers; + + for my $key (keys %users) { + my $value = $users{$key}; + + if ($users{$value}) { + $log->warn ("$value exists as both a key and a value"); + } else { + $newusers{$key} = $users{$key}; + } # if + } # for + + return %newusers; +} # check4Dups + +sub renameUsers (%) { + my (%users) = @_; + + for my $olduser (keys %users) { + my $newuser = $users{$olduser}; + + if ($opts{exec}) { + if ($p4->getUser ($olduser)) { + my $status = $p4->renameUser ($olduser, $newuser); + + unless ($status) { + $log->msg ("Renamed $olduser -> $newuser"); + + $total{'Users renamed'}++; + } else { + $log->err ("Unable to rename $olduser -> $newuser"); + + return 1; + } # unless + } else { + $total{'Non Perforce users'}++; + + $log->msg ("$olduser is not a Perforce user"); + + next; + } # if + } else { + $total{'Users would be renamed'}++; + + next; + } # if + + my %user = $p4->getUser ($newuser); + + $log->err ("Unable to retrieve user info for $newuser", 1) unless %user; + + my $email = getUserEmail ($newuser); + + if ($user{Email} ne $email) { + $user{Email} = $email; + + my $result = $p4->updateUser (%user); + + $log->err ("Unable to update user $newuser", 1) unless $result; + + $log->msg ("Updated ${newuser}'s email to $email"); + + $total{'User email updated'}++; + } # if + } # for + + return $log->errors; +} # renameUsers + +sub main () { + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'usage', + 'help', + 'jiradbserver=s', + 'username=s', + 'password=s', + 'file=s', + 'oldusername=s', + 'newusername=s', + 'exec!', + ) or pod2usage; + + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + $log = Logger->new; + + if ($opts{username} && !$opts{password}) { + $opts{password} = GetPassword; + } # if + + $p4 = Perforce->new (%opts); + + my %users; + + my $startTime = time; + + if ($opts{oldusername} and $opts{newusername}) { + $opts{oldusername} = lc $opts{oldusername}; + $opts{newusername} = lc $opts{newusername}; + + $users{$opts{oldusername}} = $opts{newusername}; + } elsif ($opts{file}) { + for (ReadFile $opts{file}) { + my ($olduser, $newuser) = split; + + $users{lc $olduser} = lc $newuser; + } # while + } else { + pod2usage "You must specify either -file or -oldname/-newname"; + } # if + + %users = check4Dups %users; + + my $status = renameUsers (%users); + + display_duration $startTime, $log; + + Stats \%total, $log; + + return $status; +} # main + +exit main; \ No newline at end of file diff --git a/audience/JIRA/importComments.pl b/audience/JIRA/importComments.pl deleted file mode 100644 index 6a1712c..0000000 --- a/audience/JIRA/importComments.pl +++ /dev/null @@ -1,237 +0,0 @@ -#!/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 - -=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 ] [-login ] - [-jiraserver ] - [-username ] [-password ] - [-bugids bugid,bugid,... | -file ] - [-[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 • 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 [,,...] or -file ' - 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 deleted file mode 100644 index a8ce4c4..0000000 --- a/audience/JIRA/jiradep.pl +++ /dev/null @@ -1,372 +0,0 @@ -#!/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 - -=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 ] [-login ] - [-jiraserver ] - [-username ] [-password ] - [-bugids bugid,bugid,... | -file ] - [-[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 [,,...] or -file ' - 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 deleted file mode 100644 index 899f506..0000000 --- a/audience/JIRA/lib/BugzillaUtils.pm +++ /dev/null @@ -1,322 +0,0 @@ -=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 - -=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 deleted file mode 100644 index a60e01c..0000000 --- a/audience/JIRA/lib/JIRAUtils.pm +++ /dev/null @@ -1,655 +0,0 @@ -=pod - -=head1 NAME $RCSfile: JIRAUtils.pm,v $ - -Some shared functions dealing with JIRA - -=head1 VERSION - -=over - -=item Author - -Andrew DeFaria - -=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 deleted file mode 100644 index 5a5c4a0..0000000 --- a/audience/JIRA/updateWatchLists.pl +++ /dev/null @@ -1,170 +0,0 @@ -#!/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 - -=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 ] [-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 [,,...] or -file ' - 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; diff --git a/etc/LDAP.conf b/etc/LDAP.conf new file mode 100644 index 0000000..f5315da --- /dev/null +++ b/etc/LDAP.conf @@ -0,0 +1,30 @@ +################################################################################ +# +# File: LDAP.conf +# Description: Configuration file for LDAP +# Author: Andrew DeFaria +# Version: 1.0 +# Created: Fri Oct 3 18:16:26 PDT 2014 +# Modified: $Date: 2014/10/03 18:17:20 $ +# Language: Conf +# +# This file contains configurable parameters that are unique to your site that +# identify the LDAP parms needed to retrieve user information from Active +# Directory. +# +################################################################################ +AD_HOST: ad-vip.audience.local +AD_PORT: 389 +AD_BINDDN: CN=jenkins cm,OU=Service Accounts,DC=audience,DC=local +AD_BINDPW: j55ln8Y6 +AD_BASEDN: DC=audience,DC=local +AD_PRINCIPAL: audience.local + +#KNOWLES_AD_HOST: kmvdcgc01.knowles.com +KNOWLES_AD_HOST: 10.252.2.28 +KNOWLES_AD_PORT: 389 +KNOWLES_AD_BINDDN: CN=AD Reader,OU=Users,OU=KMV,OU=Knowles,DC=knowles,DC=com +KNOWLES_AD_BINDPW: @Dre@D2015 +KNOWLES_AD_BASEDN: DC=knowles,DC=com +KNOWLES_AD_PRINCIPAL: knowles.com +KNOWLES_AD_FILTER: (sAMAccountName=%s) diff --git a/rmc/Audience.png b/rmc/Audience.png new file mode 100644 index 0000000..5420ea1 Binary files /dev/null and b/rmc/Audience.png differ diff --git a/rmc/Makefile b/rmc/Makefile new file mode 100644 index 0000000..45defae --- /dev/null +++ b/rmc/Makefile @@ -0,0 +1,56 @@ +################################################################################ +# +# File: Makefile +# Revision: $Revision: 1 $ +# Description: Makefile for Devops/Web/rmc +# Author: Andrew@Clearscm.com +# Created: Mon, Jun 01, 2015 12:19:02 PM +# Modified: $Date: 2012/09/20 06:52:37 $ +# Language: Makefile +# +# (c) Copyright 2015, Audience, Inc., all rights reserved. +# +# Aside from the standard make targets, the following additional targets exist: +# +# setup: Set up rmc web app +# +################################################################################ +include ../../make.inc + +WEBAPPS := rmc +HTTPCONF := /etc/httpd/conf.d +SERVER := $(shell hostname -s) +PORT := 8000 +TEMPFILE := $(shell mktemp --tmpdir $(TMP) -u rmc.conf.XXXX) + +define helpText +Aside from the standard make targets, the following additional targets exist:\n\\n\ +install: Set up rmc web app\n\ +uninstall: Remove rmc web app\n +endef + +all: + install + +help: + @echo -e "$(helpText)" + +test: + @read -p "Enter SERVER:" SERVER;\ + echo "SERVER = $$SERVER";\ + exit 1; + +install: + @read -p "Enter server name for this instance (Default: $(SERVER)):" SERVER; \ + read -p "Enter port number for this instance (Default: $(PORT)):" PORT; \ + $(SUDO) $(RMF) $(HTTPCONF)/rmc.conf; \ + $(SED) "s//$$SERVER/" rmc.conf > $(TEMPFILE); \ + $(SED) "s//$$PORT/" $(TEMPFILE) > /tmp/rmc.conf; \ + $(SUDO) $(RMF) $(TEMPFILE); \ + $(SUDO) chown root.root /tmp/rmc.conf; \ + $(SUDO) $(MV) /tmp/rmc.conf $(HTTPCONF)/rmc.conf; \ + $(SUDO) $(SERVICE) httpd reload + +uninstall: + $(SUDO) $(RMF) $(HTTPCONF)/rmc.conf + $(SUDO) $(SERVICE) httpd reload diff --git a/rmc/index.pl b/rmc/index.pl new file mode 100644 index 0000000..be8403e --- /dev/null +++ b/rmc/index.pl @@ -0,0 +1,1043 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use CGI qw ( + :standard + :cgi-lib + start_div end_div + *table + start_Tr end_Tr + start_td end_td + start_pre end_pre + start_thead end_thead +); + +=pod + +=head1 NAME rmc.pl + +Release Mission Control: Customized Release Notes + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #7 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2015/07/22 $ + +=back + +=head1 SYNOPSIS + + $ rmc.pl [-username ] [-password ] + [-client client] [-port] [-[no]html] [-csv] + [-comments] [-files] [-description] + + -from [-to ] + [-branchpath ] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -deb|ug: Display debugging information + -he|lp: Display full help + -usa|ge: Display usage + -p|ort: Perforce server and port (Default: Env P4PORT). + -use|rname: Name of the user to connect to Perforce with with + (Default:Env P4USER). + -p|assword: Password for the user to connect to Perforce with + (Default: Env P4PASSWD). + -cl|ient: Perforce Client (Default: Env P4CLIENT) + -co|mments: Include comments in output + -fi|les: Include files in output + -cs|v: Produce a csv file + -des|cription: Include description from Bugzilla + -fr|om: From revSpec + -l|ong: Shorthand for -comments & -files + -t|o: To revSpec (Default: @now) + -b|ranchpath: Path to limit changes to + -[no]ht|ml: Whether or not to produce html output + +Note that revSpecs are Perforce's way of handling changelist/label/dates. For +more info see p4 help revisions. For your reference: + + #rev - A revision number or one of the following keywords: + #none - A nonexistent revision (also #0). + #head - The current head revision (also @now). + #have - The revision on the current client. + @change - A change number: the revision as of that change. + @client - A client name: the revision on the client. + @label - A label name: the revision in the label. + @date - A date or date/time: the revision as of that time. + Either yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss + Note that yyyy/mm/dd means yyyy/mm/dd:00:00:00. + To include all events on a day, specify the next day. + +=head1 DESCRIPTION + +This script produces release notes on the web or in a .csv file. You can also +produce an .html file by using -html and redirecting stdout. + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; + +use P4; + +use lib "$FindBin::Bin/../../Web/common/lib"; +use lib "$FindBin::Bin/../../common/lib"; +use lib "$FindBin::Bin/../../lib"; +use lib "$FindBin::Bin/../lib"; + +use Display; +use DateUtils; +use JIRAUtils; +use Utils; + +#use webutils; + +# Globals +my $VERSION = '$Revision: #7 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4; +my @labels; +my $headerPrinted; +my $p4ticketsFile = '/opt/audience/perforce/p4tickets'; + +my $bugsweb = 'http://bugs.audience.local/show_bug.cgi?id='; +my $p4web = 'http://p4web.audience.local:8080'; +my $jiraWeb = 'http://jira.audience.local/browse/'; +my %opts; + +my $changesCommand = ''; + +local $| = 1; + +my $title = 'Release Mission Control'; +my $subtitle = 'Select from and to revspecs to see the bugs changes between them'; +my $helpStr = 'Both From and To are Perforce ' + . i ('revSpecs') + . '. You can use changelists, labels, dates or clients. For more' + . ' see p4 help revisions or ' + . a { + href => 'http://www.perforce.com/perforce/r12.2/manuals/cmdref/o.fspecs.html#1047453', + target => 'rmcHelp', + }, + 'Perforce File Specifications', + . '.' + . br + . b ('revSpec examples') + . ': <change>, <client>, <label>, ' + . '<date> - yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss' + . br + . b ('Note:') + . ' To show all changes after label1 but before label2 use >label1 for From and @label2 for To. Or specify To as now'; +my @columnHeadings = ( + '#', + 'Changelist', + 'Bug ID', + 'Issue', + 'Type', + 'Status', + 'Fix Versions', + 'User ID', + 'Date', + '# of Files', + 'Summary', + 'Checkin Comments', + 'Files', +); + +sub displayForm (;$$$); + +sub footing (;$) { + my ($startTime) = @_; + + print '
', a { + href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}", + }, 'Export CSV
' if $opts{from} or $opts{to}; + + print end_form; + + my $script = $FindBin::Script =~ /index.pl/ + ? 'rmc.pl' + : $FindBin::Script; + + my ($sec, $min, $hour, $mday, $mon, $year) = + localtime ((stat ($script))[9]); + + $year += 1900; + $mon++; + + my $dateModified = "$mon/$mday/$year @ $hour:$min"; + my $secondsElapsed = $startTime ? time () - $startTime . ' secs' : ''; + + print end_div; + + print start_div {-class => 'copyright'}; + print "$script version $VERSION: Last modified: $dateModified"; + print " ($secondsElapsed)" if $secondsElapsed; + print br "Copyright © $year, Audience - All rights reserved - Design by ClearSCM"; + print end_div; + + print end_html; + + return; +} # footing + +sub errorMsg ($;$) { + my ($msg, $exit) = @_; + + unless ($opts{html}) { + error ($msg, $exit); + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted =1; + } # unless + + print font ({class => 'error'}, '
ERROR: ') . $msg; + + if ($exit) { + footing; + exit $exit; + } # if +} # errorMsg + +sub debugMsg ($) { + my ($msg) = @_; + + return unless $opts{debug}; + + unless ($opts{html}) { + debug $msg; + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted = 1; + } # unless + + print font ({class => 'error'}, '
DEBUG: ') . $msg; +} # debugMsg + +sub formatTimestamp (;$) { + my ($time) = @_; + + my $date = YMDHMS ($time); + my $formattedDate = substr ($date, 0, 4) . '-' + . substr ($date, 4, 2) . '-' + . substr ($date, 6, 2) . ' ' + . substr ($date, 9); + + return $formattedDate; +} # formatTimestamp + +sub p4errors ($) { + my ($cmd) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + $msg .= $opts{html} ? '
' : "\n"; + + if ($p4->ErrorCount) { + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + errorMsg $msg . $p4->Errors, $p4->ErrorCount; + } # if + + return; +} # p4errors + +sub p4connect () { + $p4 = P4->new; + + $p4->SetUser ($opts{username}); + $p4->SetClient ($opts{client}) if $opts{client}; + $p4->SetPort ($opts{port}); + + if ($opts{username} eq 'shared') { + $p4->SetTicketFile ($p4ticketsFile); + } else { + $p4->SetPassword ($opts{password}); + } # if + + verbose_nolf "Connecting to Perforce server $opts{port}..."; + $p4->Connect or die "Unable to connect to Perforce Server\n"; + verbose 'done'; + + unless ($opts{username} eq 'shared') { + verbose_nolf "Logging in as $opts{username}\@$opts{port}..."; + + $p4->RunLogin; + + p4errors 'login'; + + verbose 'done'; + } # unless + + return $p4; +} # p4connect + +sub getChanges ($;$$) { + my ($from, $to, $branchpath) = @_; + + $from = "\@$from" unless $from =~ /^@/; + $to = "\@$to" unless $to =~ /^@/; + + my $args; + #$args = '-s shelved '; + $args .= $branchpath if $branchpath; + $args .= $from; + $args .= ",$to" if $to; + + my $cmd = 'changes'; + my $changes = $p4->Run ($cmd, $args); + + $changesCommand = "p4 $cmd $args" if $opts{debug}; + + p4errors "$cmd $args"; + + unless (@$changes) { + if ($to =~ /\@now/i) { + verbose "No changes since $from"; + } else { + verbose "No changes between $from - $to"; + } # if + + return; + } else { + return @$changes; + } # unless +} # getChanges + +sub getJobInfo ($) { + my ($job) = @_; + + my $jobs = $p4->IterateJobs ("-e $job"); + + p4errors "jobs -e $job"; + + $job = $jobs->next if $jobs->hasNext; + + return $job; +} # getJobInfo + +sub getComments ($) { + my ($changelist) = @_; + + my $change = $p4->FetchChange ($changelist); + + p4errors "change $changelist"; + + return $change->{Description}; +} # getComments + +sub getFiles ($) { + my ($changelist) = @_; + + my $files = $p4->Run ('files', "\@=$changelist"); + + p4errors "files \@=$changelist"; + + my @files; + + push @files, $_->{depotFile} . '#' . $_->{rev} for @$files; + + return @files; +} # getFiles + +sub displayForm (;$$$) { + my ($from, $to, $branchpath) = @_; + + $from //= ''; + $to //= ''; + + print p {align => 'center', class => 'dim'}, $helpStr; + + print start_form { + method => 'get', + actions => $FindBin::Script, + }; + + print start_table { + class => 'table', + align => 'center', + cellspacing => 1, + width => '95%', + }; + + print Tr [th ['From', 'To']]; + print start_Tr; + print td {align => 'center'}, textfield ( + -name => 'from', + value => $from, + size => 60, + ); + print td {align => 'center'}, textfield ( + -name => 'to', + value => $to, + size => 60, + ); + print end_Tr; + + print Tr [th {colspan => 2}, 'Branch/Path']; + print start_Tr; + print td { + colspan => 2, + align => 'center', + }, textfield ( + -name => 'branchpath', + value => $branchpath, + size => 136, + ); + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, b ('Options:'), checkbox ( + -name => 'comments', + id => 'comments', + onclick => 'toggleOption ("comments");', + label => 'Comments', + checked => $opts{comments} ? 'checked' : '', + value => $opts{comments}, + ), checkbox ( + -name => 'files', + id => 'files', + onclick => 'toggleOption ("files");', + label => 'Files', + checked => $opts{files} ? 'checked' : '', + value => $opts{files}, +# ), checkbox ( +# -name => 'group', +# id => 'group', +# onclick => 'groupIndicate();', +# label => 'Group Indicate', +# checked => 'checked', +# value => $opts{group}, + ); + + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, input { + type => 'Submit', + value => 'Submit', + }; + print end_Tr; + print end_table; + print p; + + return; +} # displayForm + +sub displayChangesHTML (@) { + my (@changes) = @_; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $displayComments = $opts{comments} ? '' : 'none'; + my $displayFiles = $opts{files} ? '' : 'none'; + + debugMsg "Changes command used: $changesCommand"; + + print start_table { + class => 'table-autosort', + align => 'center', + width => '95%', + }; + + print start_thead; + print start_Tr; + print th '#'; + print + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, 'Changelist', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Bug ID', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Issue', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Type', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Status', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Fix Version', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'User ID', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Date', + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, '# of Files', + th 'Summary', + th { + id => 'comments0', + style => "display: $displayComments", + }, 'Checkin Comments', + th { + id => 'files0', + style => "display: $displayFiles", + }, 'Files'; + print end_Tr; + print end_thead; + + my $i = 0; + + for (sort {$b->{change} <=> $a->{change}} @changes) { + my %change = %$_; + + my @files = getFiles $change{change}; + + my $job; + + for ($p4->Run ('fixes', "-c$change{change}")) { # Why am I uses fixes here? + $job = getJobInfo $_->{Job}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job->{Description} = font {color => '#aaa'}, 'N/A' unless $job; + + my ($bugid, $jiraIssue); + + if ($job->{Job}) { + if ($job->{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job->{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + # Using the following does not guarantee the ordering of the elements + # emitted! + # + # my $buglink = a { + # href => "$bugsweb$bugid", + # target => 'bugzilla', + # }, $bugid; + # + # IOW sometimes I get and other times I + # get ! Not cool because the JavaScript + # later on is comparing innerHTML and this messes that up. So we write this + # out by hand instead. + my $buglink = $bugid + ? "$bugid" + : font {color => '#aaa'}, 'N/A'; + my $jiralink = $jiraIssue + ? "$jiraIssue" + : font {color => '#aaa'}, 'N/A'; + my $cllink = "{change}?ac=133\" target=\"p4web\">$_->{change}"; + my $userid = $_->{user}; + my $description = $job->{Description}; + my $jiraStatus = font {color => '#aaa'}, 'N/A'; + my $issueType = font {color => '#aaa'}, 'N/A'; + my $fixVersions = font {color => '#aaa'}, 'N/A'; + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join '
', @fixVersions; + } # unless + } # if + + print start_Tr {id => ++$i}; + + # Attempting to "right size" the columns... + print + td {width => '10px', align => 'center'}, $i, + td {width => '15px', align => 'center', id => "changelist$i"}, $cllink, + td {width => '60px', align => 'center', id => "bugzilla$i"}, $buglink, + td {width => '80px', align => 'center', id => "jira$i"}, $jiralink, + td {width => '50px', align => 'center', id => "type$i"}, $issueType, + td {width => '50px', align => 'center', id => "jirastatus$i"}, $jiraStatus, + td {width => '50px', align => 'center', id => "fixVersion$i"}, $fixVersions, + td {width => '30px', align => 'center', id => "userid$i"}, a {href => "mailto:$userid\@audience.com" }, $userid, + td {width => '130px', align => 'center'}, formatTimestamp ($_->{time}), + td {width => '10px', align => 'center'}, scalar @files; + + if ($description =~ /N\/A/) { + print td {id => "description$i", align => 'center'}, $description; + } else { + print td {id => "description$i"}, $description; + } # if + + print + td {id => "comments$i", + valign => 'top', + style => "display: $displayComments", + }, pre {class => 'code'}, getComments ($_->{change}); + + print start_td { + id => "files$i", + valign => 'top', + style => "display: $displayFiles" + }; + + print start_pre {class => 'code'}; + + for my $file (@files) { + my ($filelink) = ($file =~ /(.*)\#/); + my ($revision) = ($file =~ /\#(\d+)/); + + # Note: For a Perforce "Add to Source Control" operation, revision is + # actually "none". Let's fix this. + $revision = 1 unless $revision; + + if ($revision == 1) { + print a { + href => '#', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "There's nothing to diff since this is the first revision", + }; + } else { + print a { + href => "$p4web$filelink?ac=19&rev1=$revision&rev2=" . ($revision - 1), + target => 'p4web', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "Diff rev #$revision vs. rev #" . ($revision -1), + }; + } # if + + print a { + href => "$p4web$filelink?ac=22", + target => 'p4web', + }, "$file
"; + } # for + + print end_pre; + print end_td; + + print end_Tr; + } # for + + print end_table; + + return; +} # displayChangesHTML + +sub displayChange (\%;$) { + my ($change, $nbr) = @_; + + $nbr //= 1; + + # Note: $change must about -c! + my $args = "-c$change->{change}"; + my $cmd = 'fixes'; + my $fix = $p4->Run ($cmd, $args); + + p4errors "$cmd $args"; + + errorMsg "Change $change->{change} is associated with multiple jobs. This case is not handled yet" if @$fix > 1; + + $fix = $$fix[0]; + + # If there was no fix associated with this change we will use the change data. + unless ($fix) { + $fix->{Change} = $change->{change}; + $fix->{User} = $change->{user}; + $fix->{Date} = $change->{time}; + } # unless + + my $job; + $job = getJobInfo ($fix->{Job}) if $fix->{Job}; + + unless ($job) { + chomp $change->{desc}; + + $job = { + Description => $change->{desc}, + Job => 'Unknown', + }; + } # unless + + my ($bugid) = ($job->{Job} =~ /^(\d+)/); + + chomp $job->{Description}; + + my $description = "$change->{change}"; + $description .= "/$bugid" if $bugid; + $description .= ": $job->{Description} ($fix->{User} "; + $description .= ymdhms ($fix->{Date}) . ')'; + + display $nbr++ . ") $description"; + + if ($opts{comments}) { + print "\n"; + print "Comments:\n" . '-'x80 . "\n" . getComments ($fix->{Change}) . "\n"; + } # if + + if ($opts{description}) { + display ''; + display "Description:\n" . '-'x80 . "\n" . $job->{Description}; + } # if + + if ($opts{files}) { + display "Files:\n" . '-'x80; + + for (getFiles $fix->{Change}) { + display "\t$_"; + } # for + + display ''; + } # if + + return $nbr; +} # displayChangesHTML + +sub displayChanges (@) { + my (@changes) = @_; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $i; + + debugMsg "Changes command used: $changesCommand"; + + $i = displayChange %$_, $i for @changes; + + return; +} # displayChanges + +sub heading ($;$) { + my ($title, $subtitle) = @_; + + print header unless $headerPrinted; + + $headerPrinted = 1; + + print start_html { + title => $title, + head => Link ({ + rel => 'shortcut icon', + href => 'http://p4web.audience.local:8080/favicon.ico', + type => 'image/x-icon', + }), + author => 'Andrew DeFaria ', + script => [{ + language => 'JavaScript', + src => 'rmc.js', + }, { + language => 'JavaScript', + src => 'rmctable.js', + }], + style => ['rmc.css'], + onload => 'setOptions();', + }, $title; + + print h1 {class => 'title'}, "
$title
"; + print h3 "
$subtitle
" if $subtitle; + + return; +} # heading + +sub exportCSV ($@) { + my ($filename, @data) = @_; + + print header ( + -type => 'application/octect-stream', + -attachment => $filename, + ); + + # Print heading line + my $columns; + + # Note that we do not include the '#' column so start at 1 + for (my $i = 1; $i < @columnHeadings; $i++) { + $columns .= "\"$columnHeadings[$i]\""; + + $columns .= ',' unless $i == @columnHeadings; + } # for + + print "$columns\n"; + + for (sort {$b->{change} <=> $a->{change}} @data) { + my %change = %$_; + + ## TODO: This code is duplicated (See displayChange). Consider refactoring. + # Note: $change must be right next to the -c! + my (%job, $jiraStatus, $issueType, $fixVersions); + + for ($p4->Run ('fixes', "-c$change{change}")) { + %job = %{getJobInfo $_->{Job}}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job{Description} = '' unless %job; + + my ($bugid, $jiraIssue); + + if ($job{Job}) { + if ($job{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join "\n", @fixVersions; + } # unless + } # if + + my $job; + + unless ($job) { + chomp $change{desc}; + + $job = { + Description => $change{desc}, + Job => 'Unknown', + }; + } # unless + ## End of refactor code + + $job{Description} = join ("\r\n", split "\n", $job{Description}); + + my @files = getFiles $change{change}; + my $comments = join ("\r\n", split "\n", getComments ($change{change})); + + # Fix up double quotes in description and comments + $job{Description} =~ s/"/""/g; + $comments =~ s/"/""/g; + + print "$change{change},"; + print $bugid ? "$bugid," : ','; + print $jiraIssue ? "$jiraIssue," : ','; + print $issueType ? "$issueType," : ','; + print $jiraStatus ? "$jiraStatus," : ','; + print $fixVersions ? "$fixVersions," : ','; + print "$change{user},"; + print '"' . formatTimestamp ($change{time}) . '",'; + print scalar @files . ','; + print "\"$job{Description}\","; + print "\"$comments\","; + print '"' . join ("\n", @files) . "\""; + print "\n"; + } # for + + return; +} # exportCSV + +sub main { + # Standard opts + $opts{usage} = sub { pod2usage }; + $opts{help} = sub { pod2usage (-verbose => 2)}; + + # Opts for this script + $opts{username} //= $ENV{P4USER} || $ENV{USERNAME} || $ENV{USER}; + $opts{client} //= $ENV{P4CLIENT}; + $opts{port} //= $ENV{P4PORT} || 'perforce:1666'; + $opts{password} //= $ENV{P4PASSWD}; + $opts{html} //= $ENV{HTML} || param ('html') || 1; + $opts{html} = (-t) ? 0 : 1; + $opts{debug} //= $ENV{DEBUG} || param ('debug') || sub { set_debug }; + $opts{verbose} //= $ENV{VERBOSE} || param ('verbose') || sub { set_verbose }; + $opts{jiraserver} //= 'jira'; + $opts{from} = param 'from'; + $opts{to} = param 'to'; + $opts{branchpath} = param ('branchpath') || '//AudEngr/Import/VSS/...'; + $opts{group} = param 'group'; + $opts{comments} //= $ENV{COMMENTS} || param 'comments'; + $opts{files} //= $ENV{FILES} || param 'files'; + $opts{long} //= $ENV{LONG} || param 'long'; + $opts{csv} //= $ENV{CSV} || param 'csv'; + + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'help', + 'usage', + 'port=s', + 'username=s', + 'password=s', + 'client=s', + 'comments', + 'files', + 'description', + 'long', + 'from=s', + 'to=s', + 'branchpath=s', + 'html!', + 'csv', + ) || pod2usage; + + $opts{comments} = $opts{files} = 1 if $opts{long}; + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + # Are we doing HTML? + if ($opts{html}) { + require CGI::Carp; + + CGI::Carp->import ('fatalsToBrowser'); + + $opts{username} ||= 'shared'; + } # if + + # Needed if using the shared user + if ($opts{username} eq 'shared') { + unless (-f $p4ticketsFile) { + errorMsg "Using 'shared' user but there is no P4TICKETS file ($p4ticketsFile)", 1; + } # unless + } else { + if ($opts{username} and not $opts{password}) { + $opts{password} = GetPassword "I need the Perforce password for $opts{username}"; + } # if + } # if + + p4connect; + + my $jira = Connect2JIRA (undef, undef, $opts{jiraserver}); + + unless ($opts{from} or $opts{to}) { + if ($opts{html}) { + heading $title, $subtitle; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + footing; + + exit; + } # if + } # unless + + my $ymd = YMD; + my $midnight = substr ($ymd, 0, 4) . '/' + . substr ($ymd, 4, 2) . '/' + . substr ($ymd, 6, 2) . ':00:00:00'; + + $opts{to} //= 'now'; + $opts{from} //= $midnight; + + $opts{to} = 'now' if ($opts{to} eq '' or $opts{to} eq ''); + $opts{from} = $midnight if ($opts{from} eq '' or $opts{from} eq ''); + + my $msg = 'Changes made '; + + if ($opts{to} =~ /^now$/i and $opts{from} eq $midnight) { + $msg .= 'today'; + } elsif ($opts{to} =~ /^now$/i) { + $msg .= "since $opts{from}"; + } else { + $msg .= "between $opts{from} and $opts{to}"; + } # if + + if ($opts{csv}) { + my $filename = "$opts{from}_$opts{to}.csv"; + + $filename =~ s/\//-/g; + $filename =~ s/\@//g; + + debug "branchpath = $opts{branchpath}"; + + exportCSV $filename, getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + return; + } # if + + if ($opts{html}) { + heading 'Release Mission Control', $msg; + } else { + display "$msg\n"; + } # if + + if ($opts{html}) { + my $startTime = time; + displayChangesHTML getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + footing $startTime; + } else { + displayChanges getChanges $opts{from}, $opts{to}, $opts{branchpath}; + } # if + + return; +} # main + +main; + +exit; + diff --git a/rmc/rmc.conf b/rmc/rmc.conf new file mode 100644 index 0000000..e35ff64 --- /dev/null +++ b/rmc/rmc.conf @@ -0,0 +1,34 @@ +################################################################################ +# +# File: rmc.conf +# Revision: $Revision: 1 $ +# Description: Apache conf file for RMC +# Author: Andrew@Clearscm.com +# Created: Mon, Jun 01, 2015 12:19:02 PM +# Modified: $Date: 2012/09/20 06:52:37 $ +# Language: Apache +# +# (c) Copyright 2015, Audience, Inc., all rights reserved. +# +# This file defines the RMC web app for Apache. Generally it is symlinked into +# /etc/httpd/conf.d +# +################################################################################ +Listen + +> + ServerName .audience.local: + ServerAlias + ErrorLog "/var/log/httpd/rmc.error.log" + CustomLog "/var/log/httpd/rmc.access.log" common + DocumentRoot "/opt/audience/Web/rmc" + + + Options Indexes MultiViews FollowSymLinks ExecCGI + DirectoryIndex index.html index.pl + AllowOverride None + Order allow,deny + Allow from all + AddHandler cgi-script .pl + + diff --git a/rmc/rmc.css b/rmc/rmc.css new file mode 100644 index 0000000..0ee6b8b --- /dev/null +++ b/rmc/rmc.css @@ -0,0 +1,355 @@ +/******************************************************************************* +* +* File: $RCSfile: rmc.css,v $ +* Revision: $Revision: 1 $ +* Description: Cascading Style Sheet definitions for rmc +* Author: Andrew@ClearSCM.com +* Created: Tue May 27 20:48:21 PDT 2014 +* Modified: $Date: $ +* Language: Cascading Style Sheet +* +* (c) Copyright 2014, Audience, all rights reserved. +* +*******************************************************************************/ +body { + font-size: 11px; + color: black; + /*background-color:#f1f1ed;*/ + margin: 0px; + /*padding: 0; */ + /*overflow:scroll;*/ +} + +body,p,h1,h2,h3,table,td,th,ul,ol,textarea,input { + font-family: verdana, helvetica, arial, sans-serif; +} + +h1.title { + font-size: 3em; + color: #838; + text-shadow: 3px 2px 5px #666; +} +.nobr { + white-space: nowrap; + wrap: off; +} + +table.noborder { + border: none; +} + +table.noborder td { + border: none; +} + +td.noboder { + border-top: none; +} + +table.invisible1 { + border: none; + border-collapse: collapse; +} + +.indent { + display: block; + margin-left: 50px; +} + +table { + border: 1px solid #c3c3c3; + padding: 3px; + border-collapse: collapse; +} + +table.framework { + /*border:none;*/ + border-collapse: collapse; + /*width:100%;*/ +} + +table.frameworktop { + border: none; + border-collapse: collapse; + width: 100%; +} + +tr.highlighthover:hover { + background-color: #FFF8F0; + color: #000; +} + +td.highlighthover:hover { + background-color: #F8F0F0; + color: #000; +} + +caption { + font-size: 130%; + font-weight: bold; + text-align: left; + padding-bottom: 2px; +} + +.envbanner { + color: RED; + font-weight: bold; + font-style: Italic; +} + +.statuserrorfont { + color: RED; + /*font-weight:Bold;*/ +} + +.statusinprogressfont { + color: Blue; + font-style: Italic +} + +.statusgoodfont { + color: #10B618; +} + +span.rightalign { + /* display:block; */ + display: block; + text-align: right; + padding: 0px; + margin: 0px; +} + +table th { + vertical-align: top; + text-align: center; + border: 1px solid #c3c3c3; + padding: 3px; + background-color: #f0f0f0; +} + +table td { + vertical-align: top; + /*text-align: left;*/ + border: 1px solid #c3c3c3; + padding: 3px; +} + +/* Sorting */ +th.table-sortable { + cursor:pointer; + /*background-image:url("/icons/sortable.gif");*/ + background-position:center left; + background-repeat:no-repeat; + padding-left:12px; +} +th.table-sorted-asc { + background-image:url("/icons/sorted_up.gif"); + background-position:center left; + background-repeat:no-repeat; +} +th.table-sorted-desc { + background-image:url("/icons/sorted_down.gif"); + background-position:center left; + background-repeat:no-repeat; +} + +table.framework td.frameworkleft /*td.frameworkleft */ { + /*background: #ffff00;*/ + width: 15em; + min-width: 15em; + max-width: 15em; + vertical-align: top; + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 1px; + border-top-width: 0px; + border-bottom-width: 0px; + /*width:15%;*/ + /*max-width:80px;*/ +} + +table.framework td.frameworkcenter { + /*border:3px red;*/ + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; + border-bottom-width: 0px; + /*width:75%;*/ +} + +table.frameworktop td.frameworktop { + border-collapse: collapse; + border: none; +} + +table.framework td.frameworkright { + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; + border-bottom-width: 0px; + width: 5%; /* modify if there is content */ +} +smallfiltericon { + width: 8px; + min-width: 8px; + max-width: 8px; + width: 8px; + visibility: hidden; +} + +h1,h2,h3 { + background-color: transparent; + color: #000000; + margin-bottom: 3px; +} + +pre { + font-family: "Courier New", monospace; + margin-left: 0; + margin-bottom: 0; + white-space: pre-wrap; +} + +span.show { + display: block; +} + +span.hide { + visibility: hidden; +} + +a:link { + text-decoration: none; + color: #5150F7; +} + +a:visited { + color: #8E0094; +} + +a:hover,a:active { + text-decoration: none; + border-bottom-width: 1px; + border-bottom-style: dotted; +} + +a.downloadgood { + color: #10B618; +} + + +hr { + background-color: #d4d4d4; + color: #d4d4d4; + height: 1px; + border: 0px; +} + + +span.insert { + color: #e80000; + background-color: transparent; +} + +span.highlight_code { + color: #e80000; + background-color: transparent; +} + +a.m_item:link { + text-decoration: none; + color: white; + background-color: transparent +} + +a.m_item:visited { + text-decoration: none; + color: white; + background-color: transparent +} + +a.m_item:active { + text-decoration: underline; + color: white; + background-color: transparent +} + +a.m_item:hover { + text-decoration: underline; + color: white; + background-color: transparent +} + +a.bold { + font-weight: bold; +} + +table.noborder td { + border: none; +} + +table.dashboardtable th { + border: none; + background-color: none; + border-bottom: 1px solid #000000; + border-top: 1px solid #000000; +} + +div.showdiv { + display: block; +} + +div.hidediv { + display: none; +} + +div.inline { + display: inline; +} + +greyfont { + font-color: red; +} + +img { + border: none; +} + +/* Copyright block */ +.copyright { + border-bottom:1px dotted #ccc; + border-top:1px dotted #ccc; + color:#999; + font-family:verdana, arial, sans-serif; + font-size:10px; + margin-top:5px; + text-align:center; + width:auto; +} + +.copyright a:link, a:visited { + color:#666; + font-weight:bold; + text-decoration:none; +} + +.copyright a:hover { + color:#333; +} + +.error { + color:red; + font-weight:bold; +} + +.dim { + color: #aaa; +} + +pre.code { + display: inline; + white-space: pre-line; + font-family: monospace; +} \ No newline at end of file diff --git a/rmc/rmc.js b/rmc/rmc.js new file mode 100644 index 0000000..49d5343 --- /dev/null +++ b/rmc/rmc.js @@ -0,0 +1,107 @@ +var comments; +var files; + +function setOptions () { + comments = document.getElementById ("comments").checked; + files = document.getElementById ("files").checked; + //group = document.getElementById ("group").checked; +} // setOptions + +function colorLines () { + return; // not used + if (comments || files) { + color = '#ffc'; + } else { + color = 'white'; + } // if + + i = 1; + + while (element = document.getElementById (i++)) { + element.style.backgroundColor = color; + } // while +} // colorLines + +function hideElement (elementName) { + var i = 0; + + while ((element = document.getElementById (elementName + i++)) != null) { + element.style.display = "none"; + } // while +} // hideElement + +function showElement (elementName) { + var i = 0; + + while ((element = document.getElementById (elementName + i++)) != null) { + element.colSpan = 7; + element.style.display = ""; + } // while +} // showElement + +function toggleOption (option) { + if (option == "comments") { + if (comments) { + hideElement (option); + + comments = false; + } else { + showElement (option); + + comments = true; + } // if + } else if (option == "files") { + if (files) { + hideElement (option); + + files = false; + } else { + showElement (option); + + files = true; + } // if + } // if + + //colorLines (); +} // toggleOption + +function groupIndicate () { + var fields = ['bugzilla', 'changelist', 'userid', 'summary']; + var values = []; + + // Seed values + for (var i = 0; i < fields.length; i++) { + values[fields[i]] = document.getElementById (fields[i] + 1).innerHTML; + } // for + + i = 1; + + while (document.getElementById (i) != null) { + i++; + + for (var j = 0; j < fields.length; j++) { + var element = document.getElementById (fields[j] + i); + + if (element == null) break; + + if (group) { + if (element.innerHTML == values[fields[j]]) { + element.innerHTML = ''; + } else { + values[fields[j]] = element.innerHTML; + } // if + } else { + if (element.innerHTML == '') { + element.innerHTML = values[fields[j]]; + } // if + } // if + } // for + } // while + + // Toggle group + if (group) { + group = false; + } else { + group = true; + } // if +} // groupIndicate \ No newline at end of file diff --git a/rmc/rmc.pl b/rmc/rmc.pl new file mode 100644 index 0000000..be8403e --- /dev/null +++ b/rmc/rmc.pl @@ -0,0 +1,1043 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use CGI qw ( + :standard + :cgi-lib + start_div end_div + *table + start_Tr end_Tr + start_td end_td + start_pre end_pre + start_thead end_thead +); + +=pod + +=head1 NAME rmc.pl + +Release Mission Control: Customized Release Notes + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #7 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2015/07/22 $ + +=back + +=head1 SYNOPSIS + + $ rmc.pl [-username ] [-password ] + [-client client] [-port] [-[no]html] [-csv] + [-comments] [-files] [-description] + + -from [-to ] + [-branchpath ] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -deb|ug: Display debugging information + -he|lp: Display full help + -usa|ge: Display usage + -p|ort: Perforce server and port (Default: Env P4PORT). + -use|rname: Name of the user to connect to Perforce with with + (Default:Env P4USER). + -p|assword: Password for the user to connect to Perforce with + (Default: Env P4PASSWD). + -cl|ient: Perforce Client (Default: Env P4CLIENT) + -co|mments: Include comments in output + -fi|les: Include files in output + -cs|v: Produce a csv file + -des|cription: Include description from Bugzilla + -fr|om: From revSpec + -l|ong: Shorthand for -comments & -files + -t|o: To revSpec (Default: @now) + -b|ranchpath: Path to limit changes to + -[no]ht|ml: Whether or not to produce html output + +Note that revSpecs are Perforce's way of handling changelist/label/dates. For +more info see p4 help revisions. For your reference: + + #rev - A revision number or one of the following keywords: + #none - A nonexistent revision (also #0). + #head - The current head revision (also @now). + #have - The revision on the current client. + @change - A change number: the revision as of that change. + @client - A client name: the revision on the client. + @label - A label name: the revision in the label. + @date - A date or date/time: the revision as of that time. + Either yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss + Note that yyyy/mm/dd means yyyy/mm/dd:00:00:00. + To include all events on a day, specify the next day. + +=head1 DESCRIPTION + +This script produces release notes on the web or in a .csv file. You can also +produce an .html file by using -html and redirecting stdout. + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; + +use P4; + +use lib "$FindBin::Bin/../../Web/common/lib"; +use lib "$FindBin::Bin/../../common/lib"; +use lib "$FindBin::Bin/../../lib"; +use lib "$FindBin::Bin/../lib"; + +use Display; +use DateUtils; +use JIRAUtils; +use Utils; + +#use webutils; + +# Globals +my $VERSION = '$Revision: #7 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4; +my @labels; +my $headerPrinted; +my $p4ticketsFile = '/opt/audience/perforce/p4tickets'; + +my $bugsweb = 'http://bugs.audience.local/show_bug.cgi?id='; +my $p4web = 'http://p4web.audience.local:8080'; +my $jiraWeb = 'http://jira.audience.local/browse/'; +my %opts; + +my $changesCommand = ''; + +local $| = 1; + +my $title = 'Release Mission Control'; +my $subtitle = 'Select from and to revspecs to see the bugs changes between them'; +my $helpStr = 'Both From and To are Perforce ' + . i ('revSpecs') + . '. You can use changelists, labels, dates or clients. For more' + . ' see p4 help revisions or ' + . a { + href => 'http://www.perforce.com/perforce/r12.2/manuals/cmdref/o.fspecs.html#1047453', + target => 'rmcHelp', + }, + 'Perforce File Specifications', + . '.' + . br + . b ('revSpec examples') + . ': <change>, <client>, <label>, ' + . '<date> - yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss' + . br + . b ('Note:') + . ' To show all changes after label1 but before label2 use >label1 for From and @label2 for To. Or specify To as now'; +my @columnHeadings = ( + '#', + 'Changelist', + 'Bug ID', + 'Issue', + 'Type', + 'Status', + 'Fix Versions', + 'User ID', + 'Date', + '# of Files', + 'Summary', + 'Checkin Comments', + 'Files', +); + +sub displayForm (;$$$); + +sub footing (;$) { + my ($startTime) = @_; + + print '
', a { + href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}", + }, 'Export CSV
' if $opts{from} or $opts{to}; + + print end_form; + + my $script = $FindBin::Script =~ /index.pl/ + ? 'rmc.pl' + : $FindBin::Script; + + my ($sec, $min, $hour, $mday, $mon, $year) = + localtime ((stat ($script))[9]); + + $year += 1900; + $mon++; + + my $dateModified = "$mon/$mday/$year @ $hour:$min"; + my $secondsElapsed = $startTime ? time () - $startTime . ' secs' : ''; + + print end_div; + + print start_div {-class => 'copyright'}; + print "$script version $VERSION: Last modified: $dateModified"; + print " ($secondsElapsed)" if $secondsElapsed; + print br "Copyright © $year, Audience - All rights reserved - Design by ClearSCM"; + print end_div; + + print end_html; + + return; +} # footing + +sub errorMsg ($;$) { + my ($msg, $exit) = @_; + + unless ($opts{html}) { + error ($msg, $exit); + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted =1; + } # unless + + print font ({class => 'error'}, '
ERROR: ') . $msg; + + if ($exit) { + footing; + exit $exit; + } # if +} # errorMsg + +sub debugMsg ($) { + my ($msg) = @_; + + return unless $opts{debug}; + + unless ($opts{html}) { + debug $msg; + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted = 1; + } # unless + + print font ({class => 'error'}, '
DEBUG: ') . $msg; +} # debugMsg + +sub formatTimestamp (;$) { + my ($time) = @_; + + my $date = YMDHMS ($time); + my $formattedDate = substr ($date, 0, 4) . '-' + . substr ($date, 4, 2) . '-' + . substr ($date, 6, 2) . ' ' + . substr ($date, 9); + + return $formattedDate; +} # formatTimestamp + +sub p4errors ($) { + my ($cmd) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + $msg .= $opts{html} ? '
' : "\n"; + + if ($p4->ErrorCount) { + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + errorMsg $msg . $p4->Errors, $p4->ErrorCount; + } # if + + return; +} # p4errors + +sub p4connect () { + $p4 = P4->new; + + $p4->SetUser ($opts{username}); + $p4->SetClient ($opts{client}) if $opts{client}; + $p4->SetPort ($opts{port}); + + if ($opts{username} eq 'shared') { + $p4->SetTicketFile ($p4ticketsFile); + } else { + $p4->SetPassword ($opts{password}); + } # if + + verbose_nolf "Connecting to Perforce server $opts{port}..."; + $p4->Connect or die "Unable to connect to Perforce Server\n"; + verbose 'done'; + + unless ($opts{username} eq 'shared') { + verbose_nolf "Logging in as $opts{username}\@$opts{port}..."; + + $p4->RunLogin; + + p4errors 'login'; + + verbose 'done'; + } # unless + + return $p4; +} # p4connect + +sub getChanges ($;$$) { + my ($from, $to, $branchpath) = @_; + + $from = "\@$from" unless $from =~ /^@/; + $to = "\@$to" unless $to =~ /^@/; + + my $args; + #$args = '-s shelved '; + $args .= $branchpath if $branchpath; + $args .= $from; + $args .= ",$to" if $to; + + my $cmd = 'changes'; + my $changes = $p4->Run ($cmd, $args); + + $changesCommand = "p4 $cmd $args" if $opts{debug}; + + p4errors "$cmd $args"; + + unless (@$changes) { + if ($to =~ /\@now/i) { + verbose "No changes since $from"; + } else { + verbose "No changes between $from - $to"; + } # if + + return; + } else { + return @$changes; + } # unless +} # getChanges + +sub getJobInfo ($) { + my ($job) = @_; + + my $jobs = $p4->IterateJobs ("-e $job"); + + p4errors "jobs -e $job"; + + $job = $jobs->next if $jobs->hasNext; + + return $job; +} # getJobInfo + +sub getComments ($) { + my ($changelist) = @_; + + my $change = $p4->FetchChange ($changelist); + + p4errors "change $changelist"; + + return $change->{Description}; +} # getComments + +sub getFiles ($) { + my ($changelist) = @_; + + my $files = $p4->Run ('files', "\@=$changelist"); + + p4errors "files \@=$changelist"; + + my @files; + + push @files, $_->{depotFile} . '#' . $_->{rev} for @$files; + + return @files; +} # getFiles + +sub displayForm (;$$$) { + my ($from, $to, $branchpath) = @_; + + $from //= ''; + $to //= ''; + + print p {align => 'center', class => 'dim'}, $helpStr; + + print start_form { + method => 'get', + actions => $FindBin::Script, + }; + + print start_table { + class => 'table', + align => 'center', + cellspacing => 1, + width => '95%', + }; + + print Tr [th ['From', 'To']]; + print start_Tr; + print td {align => 'center'}, textfield ( + -name => 'from', + value => $from, + size => 60, + ); + print td {align => 'center'}, textfield ( + -name => 'to', + value => $to, + size => 60, + ); + print end_Tr; + + print Tr [th {colspan => 2}, 'Branch/Path']; + print start_Tr; + print td { + colspan => 2, + align => 'center', + }, textfield ( + -name => 'branchpath', + value => $branchpath, + size => 136, + ); + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, b ('Options:'), checkbox ( + -name => 'comments', + id => 'comments', + onclick => 'toggleOption ("comments");', + label => 'Comments', + checked => $opts{comments} ? 'checked' : '', + value => $opts{comments}, + ), checkbox ( + -name => 'files', + id => 'files', + onclick => 'toggleOption ("files");', + label => 'Files', + checked => $opts{files} ? 'checked' : '', + value => $opts{files}, +# ), checkbox ( +# -name => 'group', +# id => 'group', +# onclick => 'groupIndicate();', +# label => 'Group Indicate', +# checked => 'checked', +# value => $opts{group}, + ); + + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, input { + type => 'Submit', + value => 'Submit', + }; + print end_Tr; + print end_table; + print p; + + return; +} # displayForm + +sub displayChangesHTML (@) { + my (@changes) = @_; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $displayComments = $opts{comments} ? '' : 'none'; + my $displayFiles = $opts{files} ? '' : 'none'; + + debugMsg "Changes command used: $changesCommand"; + + print start_table { + class => 'table-autosort', + align => 'center', + width => '95%', + }; + + print start_thead; + print start_Tr; + print th '#'; + print + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, 'Changelist', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Bug ID', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Issue', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Type', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Status', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Fix Version', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'User ID', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Date', + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, '# of Files', + th 'Summary', + th { + id => 'comments0', + style => "display: $displayComments", + }, 'Checkin Comments', + th { + id => 'files0', + style => "display: $displayFiles", + }, 'Files'; + print end_Tr; + print end_thead; + + my $i = 0; + + for (sort {$b->{change} <=> $a->{change}} @changes) { + my %change = %$_; + + my @files = getFiles $change{change}; + + my $job; + + for ($p4->Run ('fixes', "-c$change{change}")) { # Why am I uses fixes here? + $job = getJobInfo $_->{Job}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job->{Description} = font {color => '#aaa'}, 'N/A' unless $job; + + my ($bugid, $jiraIssue); + + if ($job->{Job}) { + if ($job->{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job->{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + # Using the following does not guarantee the ordering of the elements + # emitted! + # + # my $buglink = a { + # href => "$bugsweb$bugid", + # target => 'bugzilla', + # }, $bugid; + # + # IOW sometimes I get and other times I + # get ! Not cool because the JavaScript + # later on is comparing innerHTML and this messes that up. So we write this + # out by hand instead. + my $buglink = $bugid + ? "$bugid" + : font {color => '#aaa'}, 'N/A'; + my $jiralink = $jiraIssue + ? "$jiraIssue" + : font {color => '#aaa'}, 'N/A'; + my $cllink = "{change}?ac=133\" target=\"p4web\">$_->{change}"; + my $userid = $_->{user}; + my $description = $job->{Description}; + my $jiraStatus = font {color => '#aaa'}, 'N/A'; + my $issueType = font {color => '#aaa'}, 'N/A'; + my $fixVersions = font {color => '#aaa'}, 'N/A'; + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join '
', @fixVersions; + } # unless + } # if + + print start_Tr {id => ++$i}; + + # Attempting to "right size" the columns... + print + td {width => '10px', align => 'center'}, $i, + td {width => '15px', align => 'center', id => "changelist$i"}, $cllink, + td {width => '60px', align => 'center', id => "bugzilla$i"}, $buglink, + td {width => '80px', align => 'center', id => "jira$i"}, $jiralink, + td {width => '50px', align => 'center', id => "type$i"}, $issueType, + td {width => '50px', align => 'center', id => "jirastatus$i"}, $jiraStatus, + td {width => '50px', align => 'center', id => "fixVersion$i"}, $fixVersions, + td {width => '30px', align => 'center', id => "userid$i"}, a {href => "mailto:$userid\@audience.com" }, $userid, + td {width => '130px', align => 'center'}, formatTimestamp ($_->{time}), + td {width => '10px', align => 'center'}, scalar @files; + + if ($description =~ /N\/A/) { + print td {id => "description$i", align => 'center'}, $description; + } else { + print td {id => "description$i"}, $description; + } # if + + print + td {id => "comments$i", + valign => 'top', + style => "display: $displayComments", + }, pre {class => 'code'}, getComments ($_->{change}); + + print start_td { + id => "files$i", + valign => 'top', + style => "display: $displayFiles" + }; + + print start_pre {class => 'code'}; + + for my $file (@files) { + my ($filelink) = ($file =~ /(.*)\#/); + my ($revision) = ($file =~ /\#(\d+)/); + + # Note: For a Perforce "Add to Source Control" operation, revision is + # actually "none". Let's fix this. + $revision = 1 unless $revision; + + if ($revision == 1) { + print a { + href => '#', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "There's nothing to diff since this is the first revision", + }; + } else { + print a { + href => "$p4web$filelink?ac=19&rev1=$revision&rev2=" . ($revision - 1), + target => 'p4web', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "Diff rev #$revision vs. rev #" . ($revision -1), + }; + } # if + + print a { + href => "$p4web$filelink?ac=22", + target => 'p4web', + }, "$file
"; + } # for + + print end_pre; + print end_td; + + print end_Tr; + } # for + + print end_table; + + return; +} # displayChangesHTML + +sub displayChange (\%;$) { + my ($change, $nbr) = @_; + + $nbr //= 1; + + # Note: $change must about -c! + my $args = "-c$change->{change}"; + my $cmd = 'fixes'; + my $fix = $p4->Run ($cmd, $args); + + p4errors "$cmd $args"; + + errorMsg "Change $change->{change} is associated with multiple jobs. This case is not handled yet" if @$fix > 1; + + $fix = $$fix[0]; + + # If there was no fix associated with this change we will use the change data. + unless ($fix) { + $fix->{Change} = $change->{change}; + $fix->{User} = $change->{user}; + $fix->{Date} = $change->{time}; + } # unless + + my $job; + $job = getJobInfo ($fix->{Job}) if $fix->{Job}; + + unless ($job) { + chomp $change->{desc}; + + $job = { + Description => $change->{desc}, + Job => 'Unknown', + }; + } # unless + + my ($bugid) = ($job->{Job} =~ /^(\d+)/); + + chomp $job->{Description}; + + my $description = "$change->{change}"; + $description .= "/$bugid" if $bugid; + $description .= ": $job->{Description} ($fix->{User} "; + $description .= ymdhms ($fix->{Date}) . ')'; + + display $nbr++ . ") $description"; + + if ($opts{comments}) { + print "\n"; + print "Comments:\n" . '-'x80 . "\n" . getComments ($fix->{Change}) . "\n"; + } # if + + if ($opts{description}) { + display ''; + display "Description:\n" . '-'x80 . "\n" . $job->{Description}; + } # if + + if ($opts{files}) { + display "Files:\n" . '-'x80; + + for (getFiles $fix->{Change}) { + display "\t$_"; + } # for + + display ''; + } # if + + return $nbr; +} # displayChangesHTML + +sub displayChanges (@) { + my (@changes) = @_; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $i; + + debugMsg "Changes command used: $changesCommand"; + + $i = displayChange %$_, $i for @changes; + + return; +} # displayChanges + +sub heading ($;$) { + my ($title, $subtitle) = @_; + + print header unless $headerPrinted; + + $headerPrinted = 1; + + print start_html { + title => $title, + head => Link ({ + rel => 'shortcut icon', + href => 'http://p4web.audience.local:8080/favicon.ico', + type => 'image/x-icon', + }), + author => 'Andrew DeFaria ', + script => [{ + language => 'JavaScript', + src => 'rmc.js', + }, { + language => 'JavaScript', + src => 'rmctable.js', + }], + style => ['rmc.css'], + onload => 'setOptions();', + }, $title; + + print h1 {class => 'title'}, "
$title
"; + print h3 "
$subtitle
" if $subtitle; + + return; +} # heading + +sub exportCSV ($@) { + my ($filename, @data) = @_; + + print header ( + -type => 'application/octect-stream', + -attachment => $filename, + ); + + # Print heading line + my $columns; + + # Note that we do not include the '#' column so start at 1 + for (my $i = 1; $i < @columnHeadings; $i++) { + $columns .= "\"$columnHeadings[$i]\""; + + $columns .= ',' unless $i == @columnHeadings; + } # for + + print "$columns\n"; + + for (sort {$b->{change} <=> $a->{change}} @data) { + my %change = %$_; + + ## TODO: This code is duplicated (See displayChange). Consider refactoring. + # Note: $change must be right next to the -c! + my (%job, $jiraStatus, $issueType, $fixVersions); + + for ($p4->Run ('fixes', "-c$change{change}")) { + %job = %{getJobInfo $_->{Job}}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job{Description} = '' unless %job; + + my ($bugid, $jiraIssue); + + if ($job{Job}) { + if ($job{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join "\n", @fixVersions; + } # unless + } # if + + my $job; + + unless ($job) { + chomp $change{desc}; + + $job = { + Description => $change{desc}, + Job => 'Unknown', + }; + } # unless + ## End of refactor code + + $job{Description} = join ("\r\n", split "\n", $job{Description}); + + my @files = getFiles $change{change}; + my $comments = join ("\r\n", split "\n", getComments ($change{change})); + + # Fix up double quotes in description and comments + $job{Description} =~ s/"/""/g; + $comments =~ s/"/""/g; + + print "$change{change},"; + print $bugid ? "$bugid," : ','; + print $jiraIssue ? "$jiraIssue," : ','; + print $issueType ? "$issueType," : ','; + print $jiraStatus ? "$jiraStatus," : ','; + print $fixVersions ? "$fixVersions," : ','; + print "$change{user},"; + print '"' . formatTimestamp ($change{time}) . '",'; + print scalar @files . ','; + print "\"$job{Description}\","; + print "\"$comments\","; + print '"' . join ("\n", @files) . "\""; + print "\n"; + } # for + + return; +} # exportCSV + +sub main { + # Standard opts + $opts{usage} = sub { pod2usage }; + $opts{help} = sub { pod2usage (-verbose => 2)}; + + # Opts for this script + $opts{username} //= $ENV{P4USER} || $ENV{USERNAME} || $ENV{USER}; + $opts{client} //= $ENV{P4CLIENT}; + $opts{port} //= $ENV{P4PORT} || 'perforce:1666'; + $opts{password} //= $ENV{P4PASSWD}; + $opts{html} //= $ENV{HTML} || param ('html') || 1; + $opts{html} = (-t) ? 0 : 1; + $opts{debug} //= $ENV{DEBUG} || param ('debug') || sub { set_debug }; + $opts{verbose} //= $ENV{VERBOSE} || param ('verbose') || sub { set_verbose }; + $opts{jiraserver} //= 'jira'; + $opts{from} = param 'from'; + $opts{to} = param 'to'; + $opts{branchpath} = param ('branchpath') || '//AudEngr/Import/VSS/...'; + $opts{group} = param 'group'; + $opts{comments} //= $ENV{COMMENTS} || param 'comments'; + $opts{files} //= $ENV{FILES} || param 'files'; + $opts{long} //= $ENV{LONG} || param 'long'; + $opts{csv} //= $ENV{CSV} || param 'csv'; + + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'help', + 'usage', + 'port=s', + 'username=s', + 'password=s', + 'client=s', + 'comments', + 'files', + 'description', + 'long', + 'from=s', + 'to=s', + 'branchpath=s', + 'html!', + 'csv', + ) || pod2usage; + + $opts{comments} = $opts{files} = 1 if $opts{long}; + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + # Are we doing HTML? + if ($opts{html}) { + require CGI::Carp; + + CGI::Carp->import ('fatalsToBrowser'); + + $opts{username} ||= 'shared'; + } # if + + # Needed if using the shared user + if ($opts{username} eq 'shared') { + unless (-f $p4ticketsFile) { + errorMsg "Using 'shared' user but there is no P4TICKETS file ($p4ticketsFile)", 1; + } # unless + } else { + if ($opts{username} and not $opts{password}) { + $opts{password} = GetPassword "I need the Perforce password for $opts{username}"; + } # if + } # if + + p4connect; + + my $jira = Connect2JIRA (undef, undef, $opts{jiraserver}); + + unless ($opts{from} or $opts{to}) { + if ($opts{html}) { + heading $title, $subtitle; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + footing; + + exit; + } # if + } # unless + + my $ymd = YMD; + my $midnight = substr ($ymd, 0, 4) . '/' + . substr ($ymd, 4, 2) . '/' + . substr ($ymd, 6, 2) . ':00:00:00'; + + $opts{to} //= 'now'; + $opts{from} //= $midnight; + + $opts{to} = 'now' if ($opts{to} eq '' or $opts{to} eq ''); + $opts{from} = $midnight if ($opts{from} eq '' or $opts{from} eq ''); + + my $msg = 'Changes made '; + + if ($opts{to} =~ /^now$/i and $opts{from} eq $midnight) { + $msg .= 'today'; + } elsif ($opts{to} =~ /^now$/i) { + $msg .= "since $opts{from}"; + } else { + $msg .= "between $opts{from} and $opts{to}"; + } # if + + if ($opts{csv}) { + my $filename = "$opts{from}_$opts{to}.csv"; + + $filename =~ s/\//-/g; + $filename =~ s/\@//g; + + debug "branchpath = $opts{branchpath}"; + + exportCSV $filename, getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + return; + } # if + + if ($opts{html}) { + heading 'Release Mission Control', $msg; + } else { + display "$msg\n"; + } # if + + if ($opts{html}) { + my $startTime = time; + displayChangesHTML getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + footing $startTime; + } else { + displayChanges getChanges $opts{from}, $opts{to}, $opts{branchpath}; + } # if + + return; +} # main + +main; + +exit; + diff --git a/rmc/rmctable.js b/rmc/rmctable.js new file mode 100644 index 0000000..778ccb1 --- /dev/null +++ b/rmc/rmctable.js @@ -0,0 +1,1002 @@ +/** + * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com) + * + * Dual licensed under the MIT and GPL licenses. + * This basically means you can use this code however you want for + * free, but don't claim to have written it yourself! + * Donations always accepted: http://www.JavascriptToolbox.com/donate/ + * + * Please do not link to the .js files on javascripttoolbox.com from + * your site. Copy the files locally to your server instead. + * + */ +/** + * Table.js + * Functions for interactive Tables + * + * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com) + * Dual licensed under the MIT and GPL licenses. + * + * @version 0.981 + * + * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats + * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin. + * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes + * @history 0.958 2007-02-28 Added auto functionality based on class names + * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality + * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality. + * @history 0.950 2006-11-15 First BETA release. + * + * @todo Add more date format parsers + * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column + * @todo Correct for colspans in data rows (this may slow it down) + * @todo Fix for IE losing form control values after sort? + */ + +/** + * Sort Functions + */ +var Sort = (function(){ + var sort = {}; + // Default alpha-numeric sort + // -------------------------- + sort.alphanumeric = function(a,b) { + return (a==b)?0:(a0) { + var rows = section.rows; + for (var j=0,L2=rows.length; j0) { + var cells = row.cells; + for (var k=0,L3=cells.length; k1 && cells[cells.length-1].cellIndex>0) { + // Define the new function, overwrite the one we're running now, and then run the new one + (this.getCellIndex = function(td) { + return td.cellIndex; + })(td); + } + // Safari will always go through this slower block every time. Oh well. + for (var i=0,L=cells.length; i=0 && node.options) { + // Sort select elements by the visible text + return node.options[node.selectedIndex].text; + } + return ""; + }, + 'IMG':function(node) { + return node.name || ""; + } + }; + + /** + * Get the text value of a cell. Only use innerText if explicitly told to, because + * otherwise we want to be able to handle sorting on inputs and other types + */ + table.getCellValue = function(td,useInnerText) { + if (useInnerText && def(td.innerText)) { + return td.innerText; + } + if (!td.childNodes) { + return ""; + } + var childNodes=td.childNodes; + var ret = ""; + for (var i=0,L=childNodes.length; i-1) { + filters={ 'filter':filters.options[filters.selectedIndex].value }; + } + // Also allow for a regular input + if (filters.nodeName=="INPUT" && filters.type=="text") { + filters={ 'filter':"/^"+filters.value+"/" }; + } + // Force filters to be an array + if (typeof(filters)=="object" && !filters.length) { + filters = [filters]; + } + + // Convert regular expression strings to RegExp objects and function strings to function objects + for (var i=0,L=filters.length; ipageend) { + hideRow = true; + } + } + } + + row.style.display = hideRow?"none":""; + } + } + + if (def(page)) { + // Check to see if filtering has put us past the requested page index. If it has, + // then go back to the last page and show it. + if (pagestart>=unfilteredrowcount) { + pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize); + tdata.page = page = pagestart/pagesize; + for (var i=pagestart,L=unfilteredrows.length; i0) { + if (typeof(args.insert)=="function") { + func.insert(cell,colValues); + } + else { + var sel = ''; + cell.innerHTML += "
"+sel; + } + } + } + }); + if (val = classValue(t,table.FilteredRowcountPrefix)) { + tdata.container_filtered_count = document.getElementById(val); + } + if (val = classValue(t,table.RowcountPrefix)) { + tdata.container_all_count = document.getElementById(val); + } + }; + + /** + * Attach the auto event so it happens on load. + * use jQuery's ready() function if available + */ + if (typeof(jQuery)!="undefined") { + jQuery(table.auto); + } + else if (window.addEventListener) { + window.addEventListener( "load", table.auto, false ); + } + else if (window.attachEvent) { + window.attachEvent( "onload", table.auto ); + } + + return table; +})(); diff --git a/test/testConfluence.pl b/test/testConfluence.pl new file mode 100755 index 0000000..6ec842e --- /dev/null +++ b/test/testConfluence.pl @@ -0,0 +1,20 @@ +#/usr/bin/env perl +use strict; +use warnings; + +use FindBin; + +use lib "$FindBin::Bin/../lib"; + +use Confluence; + +my $confluence = Confluence->new ( + username => 'adefaria', + server => 'confluence', + port => 8080, +); + +my $content = $confluence->getContent ( + title => 'Knowles Migration', +); + diff --git a/test/testldap.pl b/test/testldap.pl new file mode 100755 index 0000000..a5c7602 --- /dev/null +++ b/test/testldap.pl @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Net::LDAP; +use Carp; + +sub getUserEmail ($) { + my ($userid) = @_; + + my (@entries, $result); + + my %opts = ( + KNOWLES_AD_HOST => '10.252.2.28', + KNOWLES_AD_PORT => 389, + KNOWLES_AD_BASEDN => 'DC=knowles,DC=com', + KNOWLES_AD_BINDDN => 'CN=AD Reader,OU=Users,OU=KMV,OU=Knowles,DC=knowles,DC=com', + KNOWLES_AD_BINDPW => '@Dre@D2015', + ); + + my $mailAttribute = 'mail'; + + print "Creating new LDAP object for Knowles\n"; + my $knowlesLDAP = Net::LDAP->new ( + $opts{KNOWLES_AD_HOST}, ( + host => $opts{KNOWLES_AD_HOST}, + port => $opts{KNOWLES_AD_PORT}, + basedn => $opts{KNOWLES_AD_BASEDN}, + #binddn => $opts{KNOWLES_AD_BINDDN}, + #bindpw => $opts{KNOWLES_AD_BINDPW}, + ) + ) or croak $@; + + print "Binding anonymously\n"; +# if ($opts{KNOWLES_AD_BINDDN}) { + $result = $knowlesLDAP->bind ( +# dn => $opts{KNOWLES_AD_BINDDN}, +# password => $opts{KNOWLES_AD_BINDPW}, + ) or croak "Unable to bind\n$@"; + + croak "Unable to bind (Error " . $result->code . "\n" . $result->error + if $result->code; + + print "Searching for $userid\n"; + $result = $knowlesLDAP->search ( + base => $opts{KNOWLES_AD_BASEDN}, + filter => "sAMAccountName=$userid", + ); + + print "Getting entries\n"; + @entries = ($result->entries); + + if ($entries[0]) { + return $entries[0]->get_value ($mailAttribute); + } else { + return 'Unknown'; + } # if +} # getUserEmail + +print getUserEmail ('adefari'); +print "\n"; \ No newline at end of file