--- /dev/null
+=pod
+
+=head1 NAME $RCSfile: JIRAUtils.pm,v $
+
+Some shared functions dealing with JIRA
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: 1.0 $
+
+=item Created
+
+Fri Mar 12 10:17:44 PST 2004
+
+=item Modified
+
+$Date: 2013/05/30 15:48:06 $
+
+=back
+
+=head1 ROUTINES
+
+The following routines are exported:
+
+=cut
+
+package 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;
--- /dev/null
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME importComments.pl
+
+This will import the comments from Bugzilla and update the corresponding JIRA
+Issues.
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+ $ importComments.pl [-bugzillaserver <bugshost>] [-login <login email>]
+ [-jiraserver <server>]
+ [-username <username>] [-password <password>]
+ [-bugids bugid,bugid,... | -file <filename>]
+ [-[no]exec]
+ [-verbose] [-help] [-usage]
+
+ Where:
+
+ -v|erbose: Display progress output
+ -he|lp: Display full help
+ -usa|ge: Display usage
+ -[no]e|xec: Whether or not to update JIRA. -noexec says only
+ tell me what you would have updated.
+ -use|rname: Username to log into JIRA with (Default: jira-admin)
+ -p|assword: Password to log into JIRA with (Default: jira-admin's
+ password)
+ -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+ -jiraserver: Machine where Jira lives (Default: jira-dev)
+ -bugi|ds: Comma separated list of BugIDs to process
+ -f|ile: File of BugIDs, one per line
+
+=head1 DESCRIPTION
+
+This will import the comments from Bugzilla and update the corresponding JIRA
+Issues.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+
+use Getopt::Long;
+use Pod::Usage;
+
+our %opts = (
+ exec => 0,
+ bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+ jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081',
+ username => $ENV{USERNAME},
+ password => $ENV{PASSWORD},
+ usage => sub { pod2usage },
+ help => sub { pod2usage (-verbose => 2)},
+ verbose => sub { set_verbose },
+ quiet => 0,
+);
+
+our ($log, %total);
+
+sub sanitize ($) {
+ my ($str) = @_;
+
+ my $p4web = 'http://p4web.audience.local:8080/@md=d&cd=//&c=vLW@/';
+ my $bugzilla = 'http://bugs.audience.com/show_bug.cgi?id=';
+
+ # 0x93 (147) and 0x94 (148) are "smart" quotes
+ $str =~ s/[\x93\x94]/"/gm;
+ # 0x91 (145) and 0x92 (146) are "smart" singlequotes
+ $str =~ s/[\x91\x92]/'/gm;
+ # 0x96 (150) and 0x97 (151) are emdashes
+ $str =~ s/[\x96\x97]/--/gm;
+ # 0x85 (133) is an ellipsis
+ $str =~ s/\x85/.../gm;
+ # 0x95 • replacement for unordered list
+ $str =~ s/\x95/*/gm;
+
+ # Make P4Web links for "CL (\d{3,6}+)"
+ $str =~ s/CL\s*(\d{3,6}+)/CL \[$1|${p4web}$1\?ac=10\]/igm;
+
+ # Make Bugzilla links for "Bug ID (\d{1,5}+)"
+ $str =~ s/Bug\s*ID\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+ # Make Bugzilla links for "Bug # (\d{1,5}+)"
+ $str =~ s/Bug\s*#\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+ # Make Bugzilla links for "Bug (\d{1,5}+)"
+ $str =~ s/Bug\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+ # Convert bug URLs to be more proper
+ $str =~ s/https\:\/\/bugs\.audience\.com\/show_bug\.cgi\?id=(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
+
+ return $str;
+} # sanitize
+
+sub addComments ($$) {
+ my ($jiraIssue, $bugid) = @_;
+
+ my @comments = @{getBugComments ($bugid)};
+
+ # Note: In Bugzilla the first comment is considered the description.
+ my $description = shift @comments;
+
+ my $result = addDescription $jiraIssue, sanitize $description;
+
+ $total{'Descriptions added'}++;
+
+ return $result if $result =~ /^Unable to add comment/;
+
+ # Process the remaining comments
+ for (@comments) {
+ $result = addJIRAComment $jiraIssue, sanitize $_;
+
+ if ($result =~ /Comment added/) {
+ $total{'Comments imported'}++;
+ } else {
+ return $result;
+ } # if
+ } # for
+
+ $result = '' unless $result;
+
+ return $result;
+} # addComments
+
+sub main () {
+ my $startTime = time;
+
+ GetOptions (
+ \%opts,
+ 'verbose',
+ 'usage',
+ 'help',
+ 'exec!',
+ 'quiet',
+ 'username=s',
+ 'password=s',
+ 'bugids=s@',
+ 'file=s',
+ 'jiraserver=s',
+ 'bugzillaserver=s',
+ 'linkbugzilla',
+ 'relinkbugzilla'
+ ) or pod2usage;
+
+ $log = Logger->new;
+
+ if ($opts{file}) {
+ open my $file, '<', $opts{file}
+ or $log->err ("Unable to open $opts{file} - $!", 1);
+
+ $opts{bugids} = [<$file>];
+
+ chomp @{$opts{bugids}};
+ } else {
+ my @bugids;
+
+ push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
+
+ $opts{bugids} = [@bugids];
+ } # if
+
+ pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+ unless $opts{bugids};
+
+ openBugzilla $opts{bugzillaserver}
+ or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+
+ Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+ or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+
+ $log->msg ("Processing comments");
+
+ for (@{$opts{bugids}}) {
+ my $jiraIssue = findIssue $_;
+
+ if ($jiraIssue =~ /^[A-Z]{1,5}-\d+$/) {
+ my $result = addComments $jiraIssue, $_;
+
+ if ($result =~ /^Unable/) {
+ $total{'Comment failures'}++;
+
+ $log->err ("Unable to add comments for $jiraIssue ($_)\n$result");
+ } elsif ($result =~ /^Comment added/) {
+ $log->msg ("Added comments for $jiraIssue ($_)");
+ } elsif ($result =~ /^Would have linked/) {
+ $total{'Comments would be added'}++;
+ } # if
+ } else {
+ $total{'Missing JIRA Issues'}++;
+
+ $log->err ("Unable to find JIRA Issue for Bug $_");
+ } # if
+ } # for
+
+ display_duration $startTime, $log;
+
+ Stats (\%total, $log) unless $opts{quiet};
+
+ return 0;
+} # main
+
+exit main;
--- /dev/null
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME jiradep.pl
+
+Update Bugzilla dependencies (Dependencies/Blockers/Duplicates and Related),
+transfering those relationships over to any matching JIRA issues.
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+ $ jiradep.pl [-bugzillaserver <bugshost>] [-login <login email>]
+ [-jiraserver <server>]
+ [-username <username>] [-password <password>]
+ [-bugids bugid,bugid,... | -file <filename>]
+ [-[no]exec] [-linkbugzilla] [-relinkbugzilla]
+ [-verbose] [-help] [-usage]
+
+ Where:
+
+ -v|erbose: Display progress output
+ -he|lp: Display full help
+ -usa|ge: Display usage
+ -[no]e|xec: Whether or not to update Bugilla. -noexec says only
+ tell me what you would have updated.
+ -use|rname: Username to log into JIRA with (Default: jira-admin)
+ -p|assword: Password to log into JIRA with (Default: jira-admin's
+ password)
+ -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+ -jiraserver: Machine where Jira lives (Default: jira-dev)
+ -bugi|ds: Comma separated list of BugIDs to process
+ -f|ile: File of BugIDs, one per line
+ -linkbugzilla: If specified and we find that we cannot translate
+ a Bugzilla Bud ID to a JIRA Issue then create a
+ remote link for the Bugzilla Bug. (Default:
+ do not create Bugzilla remote links).
+ -relinkbugzilla: Scan current Remote Bugzilla links and if there
+ exists a corresponding JIRA issue, remove the
+ Remote Bugzilla link and make it a JIRA Issue
+ link.
+ -jiradbhost: Host name of the machine where the MySQL jiradb
+ database is located (Default: cm-db-ldev01)
+
+=head1 DESCRIPTION
+
+This script will process all BugIDs translating them into JIRA Issues, if
+applicable. It will then determine the relationships of this BugID in Bugzilla -
+what it blocks, what it depends on, if it's a duplicate of another bug or if
+it has any related links. Those too will be translated to JIRA issues, again,
+if applicable. Then the JIRA issue will be updates to reflect these
+relationships.
+
+Note that it's not known at this time what to do for situations where BugIDs
+cannot be translated into JIRA issues if such Bugzilla bugs have not yet been
+migrated to JIRA. There's a though to simply make a Bugzilla Link but we will
+need to keep that in mind and when we import the next project to JIRA these
+old, no longer used Bugzilla Links should be converted to their corresponding
+JIRA issue. Perhaps this script can do that too.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+
+use Getopt::Long;
+use Pod::Usage;
+
+our %opts = (
+ exec => 0,
+ bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+ jiraserver => $ENV{JIRASERVER} || 'jira-dev',
+ jiradbhost => $ENV{JIRA_DB_HOST} || 'cm-db-ldev01',
+ username => 'jira-admin',
+ password => 'jira-admin',
+ usage => sub { pod2usage },
+ help => sub { pod2usage (-verbose => 2)},
+ verbose => sub { set_verbose },
+ quiet => 0,
+ usage => sub { pod2usage },
+ help => sub { pod2usage (-verbose => 2)},
+);
+
+our ($log, %total);
+
+my %relationshipMap = (
+ Blocks => 'Dependencies Linked',
+ Duplicate => 'Duplicates Linked',
+ Related => 'Related Linked',
+);
+
+sub callLink ($$$$) {
+ my ($from, $type, $to, $counter) = @_;
+
+ my $bugzillaType;
+
+ if ($from =~ /^\d+/) {
+ if ($type eq 'Blocks') {
+ $bugzillaType = 'is blocked by (Bugzilla)';
+ } elsif ($type eq 'Duplicate') {
+ $bugzillaType = 'duplicate (Bugzilla)';
+ } elsif ($type eq 'Related') {
+ $bugzillaType = 'related (Bugzilla)';
+ } # if
+ } elsif ($to =~ /^\d+/) {
+ if ($type eq 'Blocks') {
+ $bugzillaType = 'blocks (Bugzilla)';
+ } elsif ($type eq 'Duplicate') {
+ $bugzillaType = 'duplicate (Bugzilla)';
+ } elsif ($type eq 'Related') {
+ $bugzillaType = 'related (Bugzilla)';
+ } # if
+ } # if
+
+ $total{$counter}++;
+
+ if ($from =~ /^\d+/ && $to =~ /^\d+/) {
+ $total{'Skipped Bugzilla Links'}++;
+
+ return "Refusing to link because both from ($from) and to ($to) links are still a Bugzilla link";
+ } elsif ($from =~ /^\d+/) {
+ if ($opts{linkbugzilla}) {
+ my $result = addRemoteLink $from, $bugzillaType, $to;
+
+ $total{'Bugzilla Links'}++ unless $result;
+
+ if ($result eq '') {
+ return "Created remote $type link between Issue $to and Bug $from";
+ } else {
+ return $result;
+ } # if
+ } else {
+ $total{'Skipped Bugzilla Links'}++;
+
+ return "Refusing to link because from link ($from) is still a Bugzilla link";
+ } # if
+ } elsif ($to =~ /^\d+/) {
+ if ($opts{linkbugzilla}) {
+ my $result = addRemoteLink $to, $bugzillaType, $from;
+
+ $total{'Bugzilla Links'}++ unless $result;
+
+ if (!defined $result) {
+ print "huh?";
+ }
+ if ($result eq '') {
+ return "Created remote $type link between Issue $from and Bug $to";
+ } else {
+ return $result;
+ } # if
+ } else {
+ $total{'Skipped Bugzilla Links'}++;
+
+ return "Refusing to link because to link ($to) is still a Bugzilla link";
+ } # if
+ } # if
+
+ my $result = linkIssues $from, $type, $to;
+
+ $log->msg ($result);
+
+ if ($result =~ /^Unable/) {
+ $total{'Link Failures'}++;
+ } elsif ($result =~ /^Link made/) {
+ $total{'Links made'}++;
+ } elsif ($result =~ /^Would have linked/) {
+ $total{'Links would be made'}++;
+ } # if
+
+ return;
+} # callLink
+
+sub relinkBugzilla (@) {
+ my (@bugids) = @_;
+
+ my %mapRelationships = (
+ 'blocks (Bugzilla)' => 'Blocks',
+ 'is blocked by (Bugzilla)' => 'Blocks',
+ 'duplicates (Bugzilla)' => 'Duplicates',
+ 'is duplicated by (Bugzilla)' => 'Duplicates',
+ # old versions...
+ 'Bugzilla blocks' => 'Blocks',
+ 'Bugzilla is blocked by' => 'Blocks',
+ 'Bugzilla duplicates' => 'Duplicates',
+ 'Bugzilla is duplicated by' => 'Duplicates',
+ );
+
+ @bugids = getRemoteLinks unless @bugids;
+
+ for my $bugid (@bugids) {
+ $total{'Remote Links Scanned'}++;
+
+ my $links = findRemoteLinkByBugID $bugid;
+
+ my $jirafrom = findIssue ($bugid);
+
+ next if $jirafrom !~ /^[A-Z]{1,5}-\d+$/;
+
+ for (@$links) {
+ my %link = %$_;
+
+ # Found a link to JIRA. Remove remotelink and make an issuelink
+ if ($mapRelationships{$link{relationship}}) {
+ my ($fromIssue, $toIssue);
+
+ if ($link{relationship} =~ / by/) {
+ $fromIssue = $jirafrom;
+ $toIssue = $link{issue};
+ } else {
+ $fromIssue = $link{issue};
+ $toIssue = $jirafrom;
+ } # if
+
+ my $status = promoteBug2JIRAIssue $bugid, $fromIssue, $toIssue,
+ $mapRelationships{$link{relationship}};
+
+ $log->err ($status) if $status =~ /Unable to link/;
+ } else {
+ $log->err ("Unable to handle relationships of type $link{relationship}");
+ } # if
+ } # for
+ } # for
+
+ return;
+} # relinkBugzilla
+
+sub main () {
+ my $startTime = time;
+
+ GetOptions (
+ \%opts,
+ 'verbose',
+ 'usage',
+ 'help',
+ 'exec!',
+ 'quiet',
+ 'username=s',
+ 'password=s',
+ 'bugids=s@',
+ 'file=s',
+ 'jiraserver=s',
+ 'bugzillaserver=s',
+ 'linkbugzilla',
+ 'relinkbugzilla',
+ 'jiradbhost=s',
+ ) or pod2usage;
+
+ $log = Logger->new;
+
+ if ($opts{file}) {
+ open my $file, '<', $opts{file}
+ or $log->err ("Unable to open $opts{file} - $!", 1);
+
+ $opts{bugids} = [<$file>];
+
+ chomp @{$opts{bugids}};
+ } else {
+ my @bugids;
+
+ push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
+
+ $opts{bugids} = [@bugids];
+ } # if
+
+ pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+ unless ($opts{bugids} > 0 or $opts{relinkbugzilla});
+
+ openBugzilla $opts{bugzillaserver}
+ or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+
+ Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+ or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+
+ if ($opts{relinkbugzilla}) {
+ unless (@{$opts{bugids}}) {
+ relinkBugzilla;
+ } else {
+ relinkBugzilla $_ for @{$opts{bugids}}
+ } # unless
+
+ Stats (\%total, $log);
+
+ exit $log->errors;
+ } # if
+
+ my %relationships;
+
+ # The 'Blocks' IssueLinkType has two types of relationships in it - both
+ # blocks and dependson. Since JIRA has only one type - Blocks - we take
+ # the $dependson and flip the from and to.
+ my $blocks = getBlockers @{$opts{bugids}};
+ my $dependson = getDependencies @{$opts{bugids}};
+
+ # Now merge them - we did it backwards!
+ for my $fromLink (keys %$dependson) {
+ for my $toLink (@{$dependson->{$fromLink}}) {
+ push @{$relationships{Blocks}{$toLink}}, $fromLink;
+ } # for
+ } # for
+
+ #%{$relationships{Blocks}} = %$dependson;
+
+ for my $fromLink (keys %$blocks) {
+ # Check to see if we already have the reverse of this link
+ for my $toLink (@{$blocks->{$fromLink}}) {
+ unless (grep {$toLink eq $_} keys %{$relationships{Blocks}}) {
+ push @{$relationships{Blocks}{$fromLink}}, $toLink;
+ } # unless
+ } # for
+ } # for
+
+ $relationships{Duplicate} = getDuplicates @{$opts{bugids}};
+ $relationships{Relates} = getRelated @{$opts{bugids}};
+
+ # Process relationships (social programming... ;-)
+ $log->msg ("Processing relationships");
+
+ for my $type (keys %relationshipMap) {
+ for my $from (keys %{$relationships{$type}}) {
+ for my $to (@{$relationships{$type}{$from}}) {
+ $total{'Relationships processed'}++;
+
+ my $result = callLink $from, $type, $to, $relationshipMap{$type};
+
+ $log->msg ($result) if $result;
+ } # for
+ } # for
+ } # if
+
+ display_duration $startTime, $log;
+
+ Stats (\%total, $log) unless $opts{quiet};
+
+ return;
+} # main
+
+main;
+
+exit;
\ No newline at end of file
--- /dev/null
+=pod
+
+=head1 NAME $RCSfile: BugzillaUtils.pm,v $
+
+Some shared functions dealing with Bugzilla. Note this uses DBI to directly
+access Bugzilla's database. This requires that your userid was granted access.
+For this I setup adefaria with pretty much read only access.
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: 1.0 $
+
+=item Created
+
+Fri Mar 12 10:17:44 PST 2004
+
+=item Modified
+
+$Date: 2013/05/30 15:48:06 $
+
+=head1 ROUTINES
+
+The following routines are exported:
+
+=cut
+
+package BugzillaUtils;
+
+use strict;
+use warnings;
+
+use base 'Exporter';
+
+use FindBin;
+use Display;
+use Carp;
+use DBI;
+
+use lib 'lib';
+
+use JIRAUtils;
+
+our $bugzilla;
+
+our @EXPORT = qw (
+ openBugzilla
+ getRelationships
+ getDependencies
+ getBlockers
+ getDuplicates
+ getRelated
+ getBug
+ getBugComments
+ getWatchers
+);
+
+sub _checkDBError ($$) {
+ my ($msg, $statement) = @_;
+
+ my $dberr = $bugzilla->err;
+ my $dberrmsg = $bugzilla->errstr;
+
+ $dberr ||= 0;
+ $dberrmsg ||= 'Success';
+
+ my $message = '';
+
+ if ($dberr) {
+ my $function = (caller (1)) [3];
+
+ $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
+ . "SQL Statement: $statement";
+ } # if
+
+ $main::log->err ($message, $dberr) if $dberr;
+
+ return;
+} # _checkDBError
+
+sub openBugzilla (;$$$$) {
+ my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
+
+ $dbhost //= 'jira-dev';
+ $dbname //= 'bugzilla';
+ $dbuser //= 'adefaria';
+ $dbpass //= 'reader';
+
+ $main::log->msg ("Connecting to Bugzilla ($dbuser\@$dbhost)");
+
+ $bugzilla = DBI->connect (
+ "DBI:mysql:$dbname:$dbhost",
+ $dbuser,
+ $dbpass, {
+ PrintError => 0,
+ RaiseError => 1,
+ },
+ );
+
+ _checkDBError 'Unable to execute statement', 'Connect';
+
+ return $bugzilla;
+} # openBugzilla
+
+sub getBug ($;@) {
+ my ($bugid, @fields) = @_;
+
+ push @fields, 'short_desc' unless @fields;
+
+ my $statement = 'select ' . join (',', @fields) .
+ " from bugs where bug_id = $bugid";
+
+ my $sth = $bugzilla->prepare ($statement);
+
+ _checkDBError 'Unable to prepare statement', $statement;
+
+ _checkDBError 'Unable to execute statement', $statement;
+
+ $sth->execute;
+
+ return $sth->fetchrow_hashref;
+} # getBug
+
+sub getBugComments ($) {
+ my ($bugid) = @_;
+
+ my $statement = <<"END";
+select
+ bug_id,
+ bug_when,
+ substring_index(login_name,'\@',1) as username,
+ thetext
+from
+ longdescs,
+ profiles
+where
+ who = userid and
+ bug_id = $bugid
+END
+
+ my $sth = $bugzilla->prepare ($statement);
+
+ _checkDBError 'Unable to prepare statement', $statement;
+
+ $sth->execute;
+
+ _checkDBError 'Unable to execute statement', $statement;
+
+ my @comments;
+
+ while (my $comment = $sth->fetchrow_hashref) {
+ my $commentText = <<"END";
+The following comment was entered by [~$comment->{username}] on $comment->{bug_when}:
+
+$comment->{thetext}
+END
+
+ push @comments, $commentText;
+ } # while
+
+ return \@comments;
+} # getBugComments
+
+sub getRelationships ($$$$@) {
+ my ($table, $returnField, $testField, $relationshipType, @bugs) = @_;
+
+ $main::log->msg ("Getting $relationshipType");
+
+ my $statement = "select $returnField from $table where $table.$testField = ?";
+
+ my $sth = $bugzilla->prepare ($statement);
+
+ _checkDBError 'Unable to prepare statement', $statement;
+
+ my %relationships;
+
+ my %bugmap;
+
+ map {$bugmap{$_} = 1} @bugs unless %bugmap;
+
+ for my $bugid (@bugs) {
+ $sth->execute ($bugid);
+
+ _checkDBError 'Unable to exit statement', $statement;
+
+ my $result = JIRAUtils::findIssue ($bugid, %bugmap);
+
+ if ($result =~ /^Unable/) {
+ $main::log->warn ($result);
+
+ $main::total{'Missing JIRA Issues'}++;
+
+ undef $result;
+ } elsif ($result =~ /^Future/) {
+ $main::total{'Future JIRA Issues'}++;
+
+ undef $result;
+ } # if
+
+ my $jiraIssue = $result;
+ my $key = $jiraIssue || $bugid;
+
+ my @relationships;
+ my $relations = $sth->fetchall_arrayref;
+ my @relations;
+
+ map {push @relations, $_->[0]} @$relations;
+
+ for my $relation (@relations) {
+ $jiraIssue = JIRAUtils::findIssue ($relation);
+
+ if ($jiraIssue =~ /^Unable/ || $jiraIssue =~ /^Future/) {
+ $main::log->warn ($jiraIssue);
+
+ $main::total{'Missing JIRA Issues'}++ if $jiraIssue =~ /^Unable/;
+ $main::total{'Future JIRA Issues'}++ if $jiraIssue =~ /^Future/;
+
+ push @relationships, $relation;
+ } else {
+ push @relationships, $jiraIssue;
+ } # if
+ } # for
+
+ push @{$relationships{$key}}, @relationships if @relationships;
+ } # for
+
+ $main::total{$relationshipType} = keys %relationships;
+
+ return \%relationships;
+} # getRelationships
+
+sub getDependencies (@) {
+ my (@bugs) = @_;
+
+ return getRelationships (
+ 'dependencies', # table
+ 'dependson', # returned field
+ 'blocked', # test field
+ 'Depends on', # relationship
+ @bugs
+ );
+} # getDependencies
+
+sub getBlockers (@) {
+ my (@bugs) = @_;
+
+ return getRelationships (
+ 'dependencies',
+ 'blocked',
+ 'dependson',
+ 'Blocks',
+ @bugs
+ );
+} # getBlockers
+
+sub getDuplicates (@) {
+ my (@bugs) = @_;
+
+ return getRelationships (
+ 'duplicates',
+ 'dupe',
+ 'dupe_of',
+ 'Duplicates',
+ @bugs
+ );
+} # getDuplicates
+
+sub getRelated (@) {
+ my (@bugs) = @_;
+
+ return getRelationships (
+ 'bug_see_also',
+ 'value',
+ 'bug_id',
+ 'Relates',
+ @bugs
+ );
+} # getRelated
+
+sub getWatchers ($) {
+ my ($bugid) = @_;
+
+ my $statement = <<"END";
+select
+ profiles.login_name
+from
+ cc,
+ profiles
+where
+ cc.who = profiles.userid and
+ bug_id = ?
+END
+
+ my $sth = $bugzilla->prepare ($statement);
+
+ _checkDBError 'Unable to prepare statement', $statement;
+
+ $sth->execute ($bugid);
+
+ _checkDBError 'Unable to execute statement', $statement;
+
+ my @rows = @{$sth->fetchall_arrayref};
+
+ my %watchers;
+
+ for (@rows) {
+ if ($$_[0] =~ /(.*)\@/) {
+ $watchers{$1} = 1;
+ } # if
+
+ $main::total{'Watchers Processed'}++;
+ } # for
+
+ return %watchers;
+} # getWatchers
\ No newline at end of file
--- /dev/null
+=pod
+
+=head1 NAME $RCSfile: JIRAUtils.pm,v $
+
+Some shared functions dealing with JIRA
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: 1.0 $
+
+=item Created
+
+Fri Mar 12 10:17:44 PST 2004
+
+=item Modified
+
+$Date: 2013/05/30 15:48:06 $
+
+=back
+
+=head1 ROUTINES
+
+The following routines are exported:
+
+=cut
+
+package JIRAUtils;
+
+use strict;
+use warnings;
+
+use base 'Exporter';
+
+use FindBin;
+use Display;
+use Carp;
+use DBI;
+
+use JIRA::REST;
+use BugzillaUtils;
+
+our $jira;
+
+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;
--- /dev/null
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME updateWatchLists.pl
+
+Copy CC lists from Bugzilla -> JIRA
+
+=head1 VERSION
+
+=over
+
+=item Author
+
+Andrew DeFaria <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Thu Mar 20 10:11:53 PDT 2014
+
+=item Modified
+
+$Date: 2014/05/23 $
+
+=back
+
+=head1 SYNOPSIS
+
+ Updates JIRA watchlists by copying the CC list information from Bugzilla
+
+ $ updateWatchLists.pl [-login <login email>] [-products product1,
+ product2,...] [-[no]exec]
+ [-verbose] [-help] [-usage]
+
+ Where:
+
+ -v|erbose: Display progress output
+ -he|lp: Display full help
+ -usa|ge: Display usage
+ -[no]e|xec: Whether or not to update JIRA. -noexec says only
+ tell me what you would have updated.
+ -use|rname: Username to log into JIRA with (Default: jira-admin)
+ -p|assword: Password to log into JIRA with (Default: jira-admin's
+ password)
+ -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
+ -jiraserver: Machine where Jira lives (Default: jira-dev)
+ -bugi|ds: Comma separated list of BugIDs to process
+ -f|ile: File of BugIDs, one per line
+
+=head1 DESCRIPTION
+
+This script updates JIRA watchlists by copying the CC List information from
+Bugzilla to JIRA.
+
+=cut
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+
+$| = 1;
+
+use DBI;
+use Display;
+use Logger;
+use TimeUtils;
+use Utils;
+use JIRAUtils;
+use BugzillaUtils;
+use JIRA::REST;
+
+use Getopt::Long;
+use Pod::Usage;
+
+# Login should be the email address of the bugzilla account which has
+# priviledges to create products and components
+our %opts = (
+ exec => 0,
+ bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
+ jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081',
+ username => 'jira-admin',
+ password => 'jira-admin',
+ usage => sub { pod2usage },
+ help => sub { pod2usage (-verbose => 2)},
+ verbose => sub { set_verbose },
+);
+
+our ($log, %total);
+
+my ($bugzilla, $jira);
+
+sub main () {
+ my $startTime = time;
+
+ GetOptions (
+ \%opts,
+ 'verbose',
+ 'usage',
+ 'help',
+ 'exec!',
+ 'quiet',
+ 'username=s',
+ 'password=s',
+ 'bugids=s@',
+ 'file=s',
+ 'jiraserver=s',
+ 'bugzillaserver=s',
+ ) or pod2usage;
+
+ $log = Logger->new;
+
+ if ($opts{file}) {
+ open my $file, '<', $opts{file}
+ or die "Unable to open $opts{file} - $!";
+
+ $opts{bugids} = [<$file>];
+
+ chomp @{$opts{bugids}};
+ } else {
+ my @bugids;
+
+ push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
+
+ $opts{bugids} = [@bugids];
+ } # if
+
+ pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
+ unless $opts{bugids};
+
+ openBugzilla $opts{bugzillaserver}
+ or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
+
+ Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
+ or $log->err ("Unable to connect to $opts{jiraserver}", 1);
+
+ for (@{$opts{bugids}}) {
+ my $issue = findIssue $_;
+
+ if ($issue =~ /^Future JIRA Issue/ or $issue =~ /^Unable to find/) {
+ $log->msg ($issue);
+ } else {
+ my %watchers = getWatchers $_;
+
+ $log->msg ('Found ' . scalar (keys %watchers) . " watchers for JIRA Issue $issue");
+
+ my $result = updateIssueWatchers ($issue, %watchers);
+
+ if ($result =~ /^Unable to/) {
+ $total{'Missing JIRA Issues'}++;
+
+ $log->err ($result);
+ } else {
+ $total{'Issues updated'}++;
+ } # if
+ } # if
+ } # for
+
+ display_duration $startTime, $log;
+
+ Stats (\%total, $log) unless $opts{quiet};
+
+ return 0;
+} # main
+
+exit main;
--- /dev/null
+################################################################################
+#
+# File: getPicture.conf
+# Description: Configuration file for getPicture.pl
+# Author: Andrew DeFaria <Andrew@ClearSCM.com>
+# 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
--- /dev/null
+#!/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 <Andrew@ClearSCM.com>
+
+=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://<server>/cgi-bin/getPicture.pl?userid={user}'
+ 'https_url' => 'http://<server>/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=]<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
--- /dev/null
+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 <stdin> and truncate the newline - unless we already
+ # read in the password
+ unless ($password) {
+ if (-t STDIN) {
+ $password = GetPassword;
+ } else {
+ $password = <STDIN>;
+
+ 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;
--- /dev/null
+#!/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 <Andrew@ClearSCM.com>
+
+=item Revision
+
+$Revision: #1 $
+
+=item Created
+
+Fri Oct 30 12:16:39 PDT 2015
+
+=item Modified
+
+$Date: $
+
+=back
+
+=head1 SYNOPSIS
+
+ $ RenameUser.pl [-oldusername <username> -newusername <username> |
+ -file <filename>] [-p4port <p4port>]
+ [-username <user>] [-password <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
+ (<oldusername> <newusername>)
+ -p|4port: Perforce port (Default: perforce:1666)
+ -use|rname: Username to log in as (Default: root)
+ -p|assword: Password for -username (Defaul: <root's password>)
+ -[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 <oldusername> <newusername>
+
+=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
+++ /dev/null
-#!/usr/bin/perl
-use strict;
-use warnings;
-
-=pod
-
-=head1 NAME importComments.pl
-
-This will import the comments from Bugzilla and update the corresponding JIRA
-Issues.
-
-=head1 VERSION
-
-=over
-
-=item Author
-
-Andrew DeFaria <Andrew@ClearSCM.com>
-
-=item Revision
-
-$Revision: #1 $
-
-=item Created
-
-Thu Mar 20 10:11:53 PDT 2014
-
-=item Modified
-
-$Date: 2014/05/23 $
-
-=back
-
-=head1 SYNOPSIS
-
- $ importComments.pl [-bugzillaserver <bugshost>] [-login <login email>]
- [-jiraserver <server>]
- [-username <username>] [-password <password>]
- [-bugids bugid,bugid,... | -file <filename>]
- [-[no]exec]
- [-verbose] [-help] [-usage]
-
- Where:
-
- -v|erbose: Display progress output
- -he|lp: Display full help
- -usa|ge: Display usage
- -[no]e|xec: Whether or not to update JIRA. -noexec says only
- tell me what you would have updated.
- -use|rname: Username to log into JIRA with (Default: jira-admin)
- -p|assword: Password to log into JIRA with (Default: jira-admin's
- password)
- -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
- -jiraserver: Machine where Jira lives (Default: jira-dev)
- -bugi|ds: Comma separated list of BugIDs to process
- -f|ile: File of BugIDs, one per line
-
-=head1 DESCRIPTION
-
-This will import the comments from Bugzilla and update the corresponding JIRA
-Issues.
-
-=cut
-
-use FindBin;
-use lib "$FindBin::Bin/lib";
-
-$| = 1;
-
-use DBI;
-use Display;
-use Logger;
-use TimeUtils;
-use Utils;
-use JIRAUtils;
-use BugzillaUtils;
-
-use Getopt::Long;
-use Pod::Usage;
-
-our %opts = (
- exec => 0,
- bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
- jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081',
- username => $ENV{USERNAME},
- password => $ENV{PASSWORD},
- usage => sub { pod2usage },
- help => sub { pod2usage (-verbose => 2)},
- verbose => sub { set_verbose },
- quiet => 0,
-);
-
-our ($log, %total);
-
-sub sanitize ($) {
- my ($str) = @_;
-
- my $p4web = 'http://p4web.audience.local:8080/@md=d&cd=//&c=vLW@/';
- my $bugzilla = 'http://bugs.audience.com/show_bug.cgi?id=';
-
- # 0x93 (147) and 0x94 (148) are "smart" quotes
- $str =~ s/[\x93\x94]/"/gm;
- # 0x91 (145) and 0x92 (146) are "smart" singlequotes
- $str =~ s/[\x91\x92]/'/gm;
- # 0x96 (150) and 0x97 (151) are emdashes
- $str =~ s/[\x96\x97]/--/gm;
- # 0x85 (133) is an ellipsis
- $str =~ s/\x85/.../gm;
- # 0x95 • replacement for unordered list
- $str =~ s/\x95/*/gm;
-
- # Make P4Web links for "CL (\d{3,6}+)"
- $str =~ s/CL\s*(\d{3,6}+)/CL \[$1|${p4web}$1\?ac=10\]/igm;
-
- # Make Bugzilla links for "Bug ID (\d{1,5}+)"
- $str =~ s/Bug\s*ID\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
-
- # Make Bugzilla links for "Bug # (\d{1,5}+)"
- $str =~ s/Bug\s*#\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
-
- # Make Bugzilla links for "Bug (\d{1,5}+)"
- $str =~ s/Bug\s*(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
-
- # Convert bug URLs to be more proper
- $str =~ s/https\:\/\/bugs\.audience\.com\/show_bug\.cgi\?id=(\d{1,5}+)/Bug \[$1|${bugzilla}$1\]/igm;
-
- return $str;
-} # sanitize
-
-sub addComments ($$) {
- my ($jiraIssue, $bugid) = @_;
-
- my @comments = @{getBugComments ($bugid)};
-
- # Note: In Bugzilla the first comment is considered the description.
- my $description = shift @comments;
-
- my $result = addDescription $jiraIssue, sanitize $description;
-
- $total{'Descriptions added'}++;
-
- return $result if $result =~ /^Unable to add comment/;
-
- # Process the remaining comments
- for (@comments) {
- $result = addJIRAComment $jiraIssue, sanitize $_;
-
- if ($result =~ /Comment added/) {
- $total{'Comments imported'}++;
- } else {
- return $result;
- } # if
- } # for
-
- $result = '' unless $result;
-
- return $result;
-} # addComments
-
-sub main () {
- my $startTime = time;
-
- GetOptions (
- \%opts,
- 'verbose',
- 'usage',
- 'help',
- 'exec!',
- 'quiet',
- 'username=s',
- 'password=s',
- 'bugids=s@',
- 'file=s',
- 'jiraserver=s',
- 'bugzillaserver=s',
- 'linkbugzilla',
- 'relinkbugzilla'
- ) or pod2usage;
-
- $log = Logger->new;
-
- if ($opts{file}) {
- open my $file, '<', $opts{file}
- or $log->err ("Unable to open $opts{file} - $!", 1);
-
- $opts{bugids} = [<$file>];
-
- chomp @{$opts{bugids}};
- } else {
- my @bugids;
-
- push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
-
- $opts{bugids} = [@bugids];
- } # if
-
- pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
- unless $opts{bugids};
-
- openBugzilla $opts{bugzillaserver}
- or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
-
- Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
- or $log->err ("Unable to connect to $opts{jiraserver}", 1);
-
- $log->msg ("Processing comments");
-
- for (@{$opts{bugids}}) {
- my $jiraIssue = findIssue $_;
-
- if ($jiraIssue =~ /^[A-Z]{1,5}-\d+$/) {
- my $result = addComments $jiraIssue, $_;
-
- if ($result =~ /^Unable/) {
- $total{'Comment failures'}++;
-
- $log->err ("Unable to add comments for $jiraIssue ($_)\n$result");
- } elsif ($result =~ /^Comment added/) {
- $log->msg ("Added comments for $jiraIssue ($_)");
- } elsif ($result =~ /^Would have linked/) {
- $total{'Comments would be added'}++;
- } # if
- } else {
- $total{'Missing JIRA Issues'}++;
-
- $log->err ("Unable to find JIRA Issue for Bug $_");
- } # if
- } # for
-
- display_duration $startTime, $log;
-
- Stats (\%total, $log) unless $opts{quiet};
-
- return 0;
-} # main
-
-exit main;
+++ /dev/null
-#!/usr/bin/perl
-use strict;
-use warnings;
-
-=pod
-
-=head1 NAME jiradep.pl
-
-Update Bugzilla dependencies (Dependencies/Blockers/Duplicates and Related),
-transfering those relationships over to any matching JIRA issues.
-
-=head1 VERSION
-
-=over
-
-=item Author
-
-Andrew DeFaria <Andrew@ClearSCM.com>
-
-=item Revision
-
-$Revision: #1 $
-
-=item Created
-
-Thu Mar 20 10:11:53 PDT 2014
-
-=item Modified
-
-$Date: 2014/05/23 $
-
-=back
-
-=head1 SYNOPSIS
-
- $ jiradep.pl [-bugzillaserver <bugshost>] [-login <login email>]
- [-jiraserver <server>]
- [-username <username>] [-password <password>]
- [-bugids bugid,bugid,... | -file <filename>]
- [-[no]exec] [-linkbugzilla] [-relinkbugzilla]
- [-verbose] [-help] [-usage]
-
- Where:
-
- -v|erbose: Display progress output
- -he|lp: Display full help
- -usa|ge: Display usage
- -[no]e|xec: Whether or not to update Bugilla. -noexec says only
- tell me what you would have updated.
- -use|rname: Username to log into JIRA with (Default: jira-admin)
- -p|assword: Password to log into JIRA with (Default: jira-admin's
- password)
- -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
- -jiraserver: Machine where Jira lives (Default: jira-dev)
- -bugi|ds: Comma separated list of BugIDs to process
- -f|ile: File of BugIDs, one per line
- -linkbugzilla: If specified and we find that we cannot translate
- a Bugzilla Bud ID to a JIRA Issue then create a
- remote link for the Bugzilla Bug. (Default:
- do not create Bugzilla remote links).
- -relinkbugzilla: Scan current Remote Bugzilla links and if there
- exists a corresponding JIRA issue, remove the
- Remote Bugzilla link and make it a JIRA Issue
- link.
- -jiradbhost: Host name of the machine where the MySQL jiradb
- database is located (Default: cm-db-ldev01)
-
-=head1 DESCRIPTION
-
-This script will process all BugIDs translating them into JIRA Issues, if
-applicable. It will then determine the relationships of this BugID in Bugzilla -
-what it blocks, what it depends on, if it's a duplicate of another bug or if
-it has any related links. Those too will be translated to JIRA issues, again,
-if applicable. Then the JIRA issue will be updates to reflect these
-relationships.
-
-Note that it's not known at this time what to do for situations where BugIDs
-cannot be translated into JIRA issues if such Bugzilla bugs have not yet been
-migrated to JIRA. There's a though to simply make a Bugzilla Link but we will
-need to keep that in mind and when we import the next project to JIRA these
-old, no longer used Bugzilla Links should be converted to their corresponding
-JIRA issue. Perhaps this script can do that too.
-
-=cut
-
-use FindBin;
-use lib "$FindBin::Bin/lib";
-
-$| = 1;
-
-use DBI;
-use Display;
-use Logger;
-use TimeUtils;
-use Utils;
-use JIRAUtils;
-use BugzillaUtils;
-
-use Getopt::Long;
-use Pod::Usage;
-
-our %opts = (
- exec => 0,
- bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
- jiraserver => $ENV{JIRASERVER} || 'jira-dev',
- jiradbhost => $ENV{JIRA_DB_HOST} || 'cm-db-ldev01',
- username => 'jira-admin',
- password => 'jira-admin',
- usage => sub { pod2usage },
- help => sub { pod2usage (-verbose => 2)},
- verbose => sub { set_verbose },
- quiet => 0,
- usage => sub { pod2usage },
- help => sub { pod2usage (-verbose => 2)},
-);
-
-our ($log, %total);
-
-my %relationshipMap = (
- Blocks => 'Dependencies Linked',
- Duplicate => 'Duplicates Linked',
- Related => 'Related Linked',
-);
-
-sub callLink ($$$$) {
- my ($from, $type, $to, $counter) = @_;
-
- my $bugzillaType;
-
- if ($from =~ /^\d+/) {
- if ($type eq 'Blocks') {
- $bugzillaType = 'is blocked by (Bugzilla)';
- } elsif ($type eq 'Duplicate') {
- $bugzillaType = 'duplicate (Bugzilla)';
- } elsif ($type eq 'Related') {
- $bugzillaType = 'related (Bugzilla)';
- } # if
- } elsif ($to =~ /^\d+/) {
- if ($type eq 'Blocks') {
- $bugzillaType = 'blocks (Bugzilla)';
- } elsif ($type eq 'Duplicate') {
- $bugzillaType = 'duplicate (Bugzilla)';
- } elsif ($type eq 'Related') {
- $bugzillaType = 'related (Bugzilla)';
- } # if
- } # if
-
- $total{$counter}++;
-
- if ($from =~ /^\d+/ && $to =~ /^\d+/) {
- $total{'Skipped Bugzilla Links'}++;
-
- return "Refusing to link because both from ($from) and to ($to) links are still a Bugzilla link";
- } elsif ($from =~ /^\d+/) {
- if ($opts{linkbugzilla}) {
- my $result = addRemoteLink $from, $bugzillaType, $to;
-
- $total{'Bugzilla Links'}++ unless $result;
-
- if ($result eq '') {
- return "Created remote $type link between Issue $to and Bug $from";
- } else {
- return $result;
- } # if
- } else {
- $total{'Skipped Bugzilla Links'}++;
-
- return "Refusing to link because from link ($from) is still a Bugzilla link";
- } # if
- } elsif ($to =~ /^\d+/) {
- if ($opts{linkbugzilla}) {
- my $result = addRemoteLink $to, $bugzillaType, $from;
-
- $total{'Bugzilla Links'}++ unless $result;
-
- if (!defined $result) {
- print "huh?";
- }
- if ($result eq '') {
- return "Created remote $type link between Issue $from and Bug $to";
- } else {
- return $result;
- } # if
- } else {
- $total{'Skipped Bugzilla Links'}++;
-
- return "Refusing to link because to link ($to) is still a Bugzilla link";
- } # if
- } # if
-
- my $result = linkIssues $from, $type, $to;
-
- $log->msg ($result);
-
- if ($result =~ /^Unable/) {
- $total{'Link Failures'}++;
- } elsif ($result =~ /^Link made/) {
- $total{'Links made'}++;
- } elsif ($result =~ /^Would have linked/) {
- $total{'Links would be made'}++;
- } # if
-
- return;
-} # callLink
-
-sub relinkBugzilla (@) {
- my (@bugids) = @_;
-
- my %mapRelationships = (
- 'blocks (Bugzilla)' => 'Blocks',
- 'is blocked by (Bugzilla)' => 'Blocks',
- 'duplicates (Bugzilla)' => 'Duplicates',
- 'is duplicated by (Bugzilla)' => 'Duplicates',
- # old versions...
- 'Bugzilla blocks' => 'Blocks',
- 'Bugzilla is blocked by' => 'Blocks',
- 'Bugzilla duplicates' => 'Duplicates',
- 'Bugzilla is duplicated by' => 'Duplicates',
- );
-
- @bugids = getRemoteLinks unless @bugids;
-
- for my $bugid (@bugids) {
- $total{'Remote Links Scanned'}++;
-
- my $links = findRemoteLinkByBugID $bugid;
-
- my $jirafrom = findIssue ($bugid);
-
- next if $jirafrom !~ /^[A-Z]{1,5}-\d+$/;
-
- for (@$links) {
- my %link = %$_;
-
- # Found a link to JIRA. Remove remotelink and make an issuelink
- if ($mapRelationships{$link{relationship}}) {
- my ($fromIssue, $toIssue);
-
- if ($link{relationship} =~ / by/) {
- $fromIssue = $jirafrom;
- $toIssue = $link{issue};
- } else {
- $fromIssue = $link{issue};
- $toIssue = $jirafrom;
- } # if
-
- my $status = promoteBug2JIRAIssue $bugid, $fromIssue, $toIssue,
- $mapRelationships{$link{relationship}};
-
- $log->err ($status) if $status =~ /Unable to link/;
- } else {
- $log->err ("Unable to handle relationships of type $link{relationship}");
- } # if
- } # for
- } # for
-
- return;
-} # relinkBugzilla
-
-sub main () {
- my $startTime = time;
-
- GetOptions (
- \%opts,
- 'verbose',
- 'usage',
- 'help',
- 'exec!',
- 'quiet',
- 'username=s',
- 'password=s',
- 'bugids=s@',
- 'file=s',
- 'jiraserver=s',
- 'bugzillaserver=s',
- 'linkbugzilla',
- 'relinkbugzilla',
- 'jiradbhost=s',
- ) or pod2usage;
-
- $log = Logger->new;
-
- if ($opts{file}) {
- open my $file, '<', $opts{file}
- or $log->err ("Unable to open $opts{file} - $!", 1);
-
- $opts{bugids} = [<$file>];
-
- chomp @{$opts{bugids}};
- } else {
- my @bugids;
-
- push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
-
- $opts{bugids} = [@bugids];
- } # if
-
- pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
- unless ($opts{bugids} > 0 or $opts{relinkbugzilla});
-
- openBugzilla $opts{bugzillaserver}
- or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
-
- Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
- or $log->err ("Unable to connect to $opts{jiraserver}", 1);
-
- if ($opts{relinkbugzilla}) {
- unless (@{$opts{bugids}}) {
- relinkBugzilla;
- } else {
- relinkBugzilla $_ for @{$opts{bugids}}
- } # unless
-
- Stats (\%total, $log);
-
- exit $log->errors;
- } # if
-
- my %relationships;
-
- # The 'Blocks' IssueLinkType has two types of relationships in it - both
- # blocks and dependson. Since JIRA has only one type - Blocks - we take
- # the $dependson and flip the from and to.
- my $blocks = getBlockers @{$opts{bugids}};
- my $dependson = getDependencies @{$opts{bugids}};
-
- # Now merge them - we did it backwards!
- for my $fromLink (keys %$dependson) {
- for my $toLink (@{$dependson->{$fromLink}}) {
- push @{$relationships{Blocks}{$toLink}}, $fromLink;
- } # for
- } # for
-
- #%{$relationships{Blocks}} = %$dependson;
-
- for my $fromLink (keys %$blocks) {
- # Check to see if we already have the reverse of this link
- for my $toLink (@{$blocks->{$fromLink}}) {
- unless (grep {$toLink eq $_} keys %{$relationships{Blocks}}) {
- push @{$relationships{Blocks}{$fromLink}}, $toLink;
- } # unless
- } # for
- } # for
-
- $relationships{Duplicate} = getDuplicates @{$opts{bugids}};
- $relationships{Relates} = getRelated @{$opts{bugids}};
-
- # Process relationships (social programming... ;-)
- $log->msg ("Processing relationships");
-
- for my $type (keys %relationshipMap) {
- for my $from (keys %{$relationships{$type}}) {
- for my $to (@{$relationships{$type}{$from}}) {
- $total{'Relationships processed'}++;
-
- my $result = callLink $from, $type, $to, $relationshipMap{$type};
-
- $log->msg ($result) if $result;
- } # for
- } # for
- } # if
-
- display_duration $startTime, $log;
-
- Stats (\%total, $log) unless $opts{quiet};
-
- return;
-} # main
-
-main;
-
-exit;
\ No newline at end of file
+++ /dev/null
-=pod
-
-=head1 NAME $RCSfile: BugzillaUtils.pm,v $
-
-Some shared functions dealing with Bugzilla. Note this uses DBI to directly
-access Bugzilla's database. This requires that your userid was granted access.
-For this I setup adefaria with pretty much read only access.
-
-=head1 VERSION
-
-=over
-
-=item Author
-
-Andrew DeFaria <Andrew@ClearSCM.com>
-
-=item Revision
-
-$Revision: 1.0 $
-
-=item Created
-
-Fri Mar 12 10:17:44 PST 2004
-
-=item Modified
-
-$Date: 2013/05/30 15:48:06 $
-
-=head1 ROUTINES
-
-The following routines are exported:
-
-=cut
-
-package BugzillaUtils;
-
-use strict;
-use warnings;
-
-use base 'Exporter';
-
-use FindBin;
-use Display;
-use Carp;
-use DBI;
-
-use lib 'lib';
-
-use JIRAUtils;
-
-our $bugzilla;
-
-our @EXPORT = qw (
- openBugzilla
- getRelationships
- getDependencies
- getBlockers
- getDuplicates
- getRelated
- getBug
- getBugComments
- getWatchers
-);
-
-sub _checkDBError ($$) {
- my ($msg, $statement) = @_;
-
- my $dberr = $bugzilla->err;
- my $dberrmsg = $bugzilla->errstr;
-
- $dberr ||= 0;
- $dberrmsg ||= 'Success';
-
- my $message = '';
-
- if ($dberr) {
- my $function = (caller (1)) [3];
-
- $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
- . "SQL Statement: $statement";
- } # if
-
- $main::log->err ($message, $dberr) if $dberr;
-
- return;
-} # _checkDBError
-
-sub openBugzilla (;$$$$) {
- my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
-
- $dbhost //= 'jira-dev';
- $dbname //= 'bugzilla';
- $dbuser //= 'adefaria';
- $dbpass //= 'reader';
-
- $main::log->msg ("Connecting to Bugzilla ($dbuser\@$dbhost)");
-
- $bugzilla = DBI->connect (
- "DBI:mysql:$dbname:$dbhost",
- $dbuser,
- $dbpass, {
- PrintError => 0,
- RaiseError => 1,
- },
- );
-
- _checkDBError 'Unable to execute statement', 'Connect';
-
- return $bugzilla;
-} # openBugzilla
-
-sub getBug ($;@) {
- my ($bugid, @fields) = @_;
-
- push @fields, 'short_desc' unless @fields;
-
- my $statement = 'select ' . join (',', @fields) .
- " from bugs where bug_id = $bugid";
-
- my $sth = $bugzilla->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- _checkDBError 'Unable to execute statement', $statement;
-
- $sth->execute;
-
- return $sth->fetchrow_hashref;
-} # getBug
-
-sub getBugComments ($) {
- my ($bugid) = @_;
-
- my $statement = <<"END";
-select
- bug_id,
- bug_when,
- substring_index(login_name,'\@',1) as username,
- thetext
-from
- longdescs,
- profiles
-where
- who = userid and
- bug_id = $bugid
-END
-
- my $sth = $bugzilla->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- $sth->execute;
-
- _checkDBError 'Unable to execute statement', $statement;
-
- my @comments;
-
- while (my $comment = $sth->fetchrow_hashref) {
- my $commentText = <<"END";
-The following comment was entered by [~$comment->{username}] on $comment->{bug_when}:
-
-$comment->{thetext}
-END
-
- push @comments, $commentText;
- } # while
-
- return \@comments;
-} # getBugComments
-
-sub getRelationships ($$$$@) {
- my ($table, $returnField, $testField, $relationshipType, @bugs) = @_;
-
- $main::log->msg ("Getting $relationshipType");
-
- my $statement = "select $returnField from $table where $table.$testField = ?";
-
- my $sth = $bugzilla->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- my %relationships;
-
- my %bugmap;
-
- map {$bugmap{$_} = 1} @bugs unless %bugmap;
-
- for my $bugid (@bugs) {
- $sth->execute ($bugid);
-
- _checkDBError 'Unable to exit statement', $statement;
-
- my $result = JIRAUtils::findIssue ($bugid, %bugmap);
-
- if ($result =~ /^Unable/) {
- $main::log->warn ($result);
-
- $main::total{'Missing JIRA Issues'}++;
-
- undef $result;
- } elsif ($result =~ /^Future/) {
- $main::total{'Future JIRA Issues'}++;
-
- undef $result;
- } # if
-
- my $jiraIssue = $result;
- my $key = $jiraIssue || $bugid;
-
- my @relationships;
- my $relations = $sth->fetchall_arrayref;
- my @relations;
-
- map {push @relations, $_->[0]} @$relations;
-
- for my $relation (@relations) {
- $jiraIssue = JIRAUtils::findIssue ($relation);
-
- if ($jiraIssue =~ /^Unable/ || $jiraIssue =~ /^Future/) {
- $main::log->warn ($jiraIssue);
-
- $main::total{'Missing JIRA Issues'}++ if $jiraIssue =~ /^Unable/;
- $main::total{'Future JIRA Issues'}++ if $jiraIssue =~ /^Future/;
-
- push @relationships, $relation;
- } else {
- push @relationships, $jiraIssue;
- } # if
- } # for
-
- push @{$relationships{$key}}, @relationships if @relationships;
- } # for
-
- $main::total{$relationshipType} = keys %relationships;
-
- return \%relationships;
-} # getRelationships
-
-sub getDependencies (@) {
- my (@bugs) = @_;
-
- return getRelationships (
- 'dependencies', # table
- 'dependson', # returned field
- 'blocked', # test field
- 'Depends on', # relationship
- @bugs
- );
-} # getDependencies
-
-sub getBlockers (@) {
- my (@bugs) = @_;
-
- return getRelationships (
- 'dependencies',
- 'blocked',
- 'dependson',
- 'Blocks',
- @bugs
- );
-} # getBlockers
-
-sub getDuplicates (@) {
- my (@bugs) = @_;
-
- return getRelationships (
- 'duplicates',
- 'dupe',
- 'dupe_of',
- 'Duplicates',
- @bugs
- );
-} # getDuplicates
-
-sub getRelated (@) {
- my (@bugs) = @_;
-
- return getRelationships (
- 'bug_see_also',
- 'value',
- 'bug_id',
- 'Relates',
- @bugs
- );
-} # getRelated
-
-sub getWatchers ($) {
- my ($bugid) = @_;
-
- my $statement = <<"END";
-select
- profiles.login_name
-from
- cc,
- profiles
-where
- cc.who = profiles.userid and
- bug_id = ?
-END
-
- my $sth = $bugzilla->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- $sth->execute ($bugid);
-
- _checkDBError 'Unable to execute statement', $statement;
-
- my @rows = @{$sth->fetchall_arrayref};
-
- my %watchers;
-
- for (@rows) {
- if ($$_[0] =~ /(.*)\@/) {
- $watchers{$1} = 1;
- } # if
-
- $main::total{'Watchers Processed'}++;
- } # for
-
- return %watchers;
-} # getWatchers
\ No newline at end of file
+++ /dev/null
-=pod
-
-=head1 NAME $RCSfile: JIRAUtils.pm,v $
-
-Some shared functions dealing with JIRA
-
-=head1 VERSION
-
-=over
-
-=item Author
-
-Andrew DeFaria <Andrew@ClearSCM.com>
-
-=item Revision
-
-$Revision: 1.0 $
-
-=item Created
-
-Fri Mar 12 10:17:44 PST 2004
-
-=item Modified
-
-$Date: 2013/05/30 15:48:06 $
-
-=back
-
-=head1 ROUTINES
-
-The following routines are exported:
-
-=cut
-
-package JIRAUtils;
-
-use strict;
-use warnings;
-
-use base 'Exporter';
-
-use FindBin;
-use Display;
-use Carp;
-use DBI;
-
-use JIRA::REST;
-use BugzillaUtils;
-
-our ($jira, %opts);
-
-our @EXPORT = qw (
- addJIRAComment
- addDescription
- Connect2JIRA
- findIssue
- getIssue
- getIssueLinks
- getIssueLinkTypes
- getRemoteLinks
- updateIssueWatchers
- linkIssues
- addRemoteLink
- getRemoteLink
- removeRemoteLink
- getRemoteLinkByBugID
- promoteBug2JIRAIssue
- findRemoteLinkByBugID
-);
-
-my (@issueLinkTypes, %total, %cache, $jiradb);
-
-sub _checkDBError ($;$) {
- my ($msg, $statement) = @_;
-
- $statement //= 'Unknown';
-
- $main::log->err ('JIRA database not opened!', 1) unless $jiradb;
-
- my $dberr = $jiradb->err;
- my $dberrmsg = $jiradb->errstr;
-
- $dberr ||= 0;
- $dberrmsg ||= 'Success';
-
- my $message = '';
-
- if ($dberr) {
- my $function = (caller (1)) [3];
-
- $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
- . "SQL Statement: $statement";
- } # if
-
- $main::log->err ($message, 1) if $dberr;
-
- return;
-} # _checkDBError
-
-sub openJIRADB (;$$$$) {
- my ($dbhost, $dbname, $dbuser, $dbpass) = @_;
-
- $dbhost //= $main::opts{jiradbhost};
- $dbname //= 'jiradb';
- $dbuser //= 'adefaria';
- $dbpass //= 'reader';
-
- $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...");
-
- $jiradb = DBI->connect (
- "DBI:mysql:$dbname:$dbhost",
- $dbuser,
- $dbpass, {
- PrintError => 0,
- RaiseError => 1,
- },
- );
-
- _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)";
-
- return $jiradb;
-} # openJIRADB
-
-sub Connect2JIRA (;$$$) {
- my ($username, $password, $server) = @_;
-
- my %opts;
-
- $opts{username} = $username || 'jira-admin';
- $opts{password} = $password || $ENV{PASSWORD} || 'jira-admin';
- $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev:8081';
- $opts{URL} = "http://$opts{server}/rest/api/latest";
-
- $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})");
-
- $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password});
-
- # Store username as we might need it (see updateIssueWatchers)
- $jira->{username} = $opts{username};
-
- return $jira;
-} # Connect2JIRA
-
-sub addDescription ($$) {
- my ($issue, $description) = @_;
-
- if ($main::opts{exec}) {
- eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})};
-
- if ($@) {
- return "Unable to add description\n$@";
- } else {
- return 'Description added';
- } # if
- } # if
-} # addDescription
-
-sub addJIRAComment ($$) {
- my ($issue, $comment) = @_;
-
- if ($main::opts{exec}) {
- eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })};
-
- if ($@) {
- return "Unable to add comment\n$@";
- } else {
- return 'Comment added';
- } # if
- } else {
- return "Would have added comments to $issue";
- } # if
-} # addJIRAComment
-
-sub findIssue ($%) {
- my ($bugid, %bugmap) = @_;
-
-=pod
- # Check the cache...
- if ($cache{$bugid}) {
- if ($cache{$bugid} =~ /^\d+/) {
- # We have a cache hit but the contents here are a bugid. This means we had
- # searched for the corresponding JIRA issue for this bug before and came
- # up empty handed. In this situtaion we really have:
- return "Unable to find a JIRA issue for Bug $bugid";
- } else {
- return $cache{$bugid};
- } # if
- } # if
-=cut
- my $issue;
-
- my %query = (
- jql => "\"Bugzilla Bug Number\" ~ $bugid",
- fields => [ 'key' ],
- );
-
- eval {$issue = $jira->GET ("/search/", \%query)};
-
- my $issueID = $issue->{issues}[0]{key};
-
- if (@{$issue->{issues}} > 2) {
- $main::log->err ("Found more than 2 issues for Bug ID $bugid");
-
- return "Found more than 2 issues for Bug ID $bugid";
- } elsif (@{$issue->{issues}} == 2) {
- my ($issueNum0, $issueNum1);
-
- if ($issue->{issues}[0]{key} =~ /(\d+)/) {
- $issueNum0 = $1;
- } # if
-
- if ($issue->{issues}[1]{key} =~ /(\d+)/) {
- $issueNum1 = $1;
- } # if
-
- if ($issueNum0 < $issueNum1) {
- $issueID = $issue->{issues}[1]{key};
- } # if
-
- # Let's mark them as clones. See if this clone link already exists...
- my $alreadyCloned;
-
- for (getIssueLinks ($issueID, 'Cloners')) {
- my $inwardIssue = $_->{inwardIssue}{key} || '';
- my $outwardIssue = $_->{outwardIssue}{key} || '';
-
- if ("RDBNK-$issueNum0" eq $inwardIssue ||
- "RDBNK-$issueNum0" eq $outwardIssue ||
- "RDBNK-$issueNum1" eq $inwardIssue ||
- "RDBNK-$issueNum1" eq $outwardIssue) {
- $alreadyCloned = 1;
-
- last;
- } # if
- } # for
-
- unless ($alreadyCloned) {
- my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1");
-
- return $result if $result =~ /Unable to/;
-
- $main::log->msg ($result);
- } # unless
- } # if
-
- if ($issueID) {
- $main::log->msg ("Found JIRA issue $issueID for Bug $bugid");
-
- #$cache{$bugid} = $issueID;
-
- #return $cache{$bugid};
- return $issueID;
- } else {
- my $status = $bugmap{$bugid} ? 'Future JIRA Issue'
- : "Unable to find a JIRA issue for Bug $bugid";
-
- # Here we put this bugid into the cache but instead of a the JIRA issue
- # id we put the bugid. This will stop us from adding up multiple hits on
- # this bugid.
- #$cache{$bugid} = $bugid;
-
- return $status;
- } # if
-} # findJIRA
-
-sub getIssue ($;@) {
- my ($issue, @fields) = @_;
-
- my $fields = @fields ? "?fields=" . join ',', @fields : '';
-
- return $jira->GET ("/issue/$issue$fields");
-} # getIssue
-
-sub getIssueLinkTypes () {
- my $issueLinkTypes = $jira->GET ('/issueLinkType/');
-
- map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}};
-
- return @issueLinkTypes
-} # getIssueLinkTypes
-
-sub linkIssues ($$$) {
- my ($from, $type, $to) = @_;
-
- unless (@issueLinkTypes) {
- getIssueLinkTypes;
- } # unless
-
- unless (grep {$type eq $_} @issueLinkTypes) {
- $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n"
- . join "\n\t", @issueLinkTypes);
-
- return "Unable to $type link $from -> $to";
- } # unless
-
- my %link = (
- inwardIssue => {
- key => $from,
- },
- type => {
- name => $type,
- },
- outwardIssue => {
- key => $to,
- },
- comment => {
- body => "Link ported as part of the migration from Bugzilla: $from <-> $to",
- },
- );
-
- $main::total{'IssueLinks Added'}++;
-
- if ($main::opts{exec}) {
- eval {$jira->POST ("/issueLink", undef, \%link)};
-
- if ($@) {
- return "Unable to $type link $from -> $to\n$@";
- } else {
- return "Made $type link $from -> $to";
- } # if
- } else {
- return "Would have $type linked $from -> $to";
- } # if
-} # linkIssue
-
-sub getRemoteLink ($;$) {
- my ($jiraIssue, $id) = @_;
-
- $id //= '';
-
- my $result;
-
- eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")};
-
- return if $@;
-
- my %remoteLinks;
-
- if (ref $result eq 'ARRAY') {
- map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result;
- } else {
- $remoteLinks{$result->{id}} = $result->{object}{title};
- } # if
-
- return \%remoteLinks;
-} # getRemoteLink
-
-sub getRemoteLinks (;$) {
- my ($bugid) = @_;
-
- $jiradb = openJIRADB unless $jiradb;
-
- my $statement = 'select url from remotelink';
-
- $statement .= " where url like 'http://bugs%'";
- $statement .= " and url like '%$bugid'" if $bugid;
- $statement .= " group by issueid desc";
-
- my $sth = $jiradb->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- $sth->execute;
-
- _checkDBError 'Unable to execute statement', $statement;
-
- my %bugids;
-
- while (my $record = $sth->fetchrow_array) {
- if ($record =~ /(\d+)$/) {
- $bugids{$1} = 1;
- } # if
- } # while
-
- return keys %bugids;
-} # getRemoteLinks
-
-sub findRemoteLinkByBugID (;$) {
- my ($bugid) = @_;
-
- my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id';
-
- if ($bugid) {
- $condition .= " and remotelink.url like '%id=$bugid'";
- } # unless
-
- $jiradb = openJIRADB unless $jiradb;
-
- my $statement = <<"END";
-select
- remotelink.id,
- concat (project.pkey, '-', issuenum) as issue,
- relationship
-from
- remotelink,
- jiraissue,
- project
-$condition
-END
-
- my $sth = $jiradb->prepare ($statement);
-
- _checkDBError 'Unable to prepare statement', $statement;
-
- $sth->execute;
-
- _checkDBError 'Unable to execute statement', $statement;
-
- my @records;
-
- while (my $row = $sth->fetchrow_hashref) {
- $row->{bugid} = $bugid;
-
- push @records, $row;
- } # while
-
- return \@records;
-} # findRemoteLinkByBugID
-
-sub promoteBug2JIRAIssue ($$$$) {
- my ($bugid, $jirafrom, $jirato, $relationship) = @_;
-
- my $result = linkIssues $jirafrom, $relationship, $jirato;
-
- return $result if $result =~ /Unable to link/;
-
- $main::log->msg ($result . " (BugID $bugid)");
-
- for (@{findRemoteLinkByBugID $bugid}) {
- my %record = %$_;
-
- $result = removeRemoteLink ($record{issue}, $record{id});
-
- # We may not care if we couldn't remove this link because it may have been
- # removed by a prior pass.
- return $result if $result =~ /Unable to remove link/;
-
- $main::log->msg ($result) unless $result eq '';
- } # for
-
- return $result;
-} # promoteBug2JIRAIssue
-
-sub addRemoteLink ($$$) {
- my ($bugid, $relationship, $jiraIssue) = @_;
-
- my $bug = getBug $bugid;
-
- # Check to see if this Bug ID already exists on this JIRA Issue, otherwise
- # JIRA will duplicate it!
- my $remoteLinks = getRemoteLink $jiraIssue;
-
- for (keys %$remoteLinks) {
- if ($remoteLinks->{$_} =~ /Bug (\d+)/) {
- return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1;
- } # if
- } # for
-
- # Note this globalid thing is NOT working! ALl I see is null in the database
- my %remoteLink = (
-# globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid",
-# application => {
-# type => 'Bugzilla',
-# name => 'Bugzilla',
-# },
- relationship => $relationship,
- object => {
- url => "http://bugs.audience.com/show_bug.cgi?id=$bugid",
- title => "Bug $bugid",
- summary => $bug->{short_desc},
- icon => {
- url16x16 => 'http://bugs.audience.local/favicon.png',
- title => 'Bugzilla Bug',
- },
- },
- );
-
- $main::total{'RemoteLink Added'}++;
-
- if ($main::opts{exec}) {
- eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)};
-
- return $@;
- } else {
- return "Would have linked $bugid -> $jiraIssue";
- } # if
-} # addRemoteLink
-
-sub removeRemoteLink ($;$) {
- my ($jiraIssue, $id) = @_;
-
- $id //= '';
-
- my $remoteLinks = getRemoteLink ($jiraIssue, $id);
-
- for (keys %$remoteLinks) {
- my $result;
-
- $main::total{'RemoteLink Removed'}++;
-
- if ($main::opts{exec}) {
- eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")};
-
- if ($@) {
- return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@;
- } else {
- my $bugid;
-
- if ($remoteLinks->{$_} =~ /(\d+)/) {
- return "Removed remote link $jiraIssue (Bug ID $1)";
- } # if
- } # if
-
- $main::total{'Remote Links Removed'}++;
- } else {
- if ($remoteLinks->{$_} =~ /(\d+)/) {
- return "Would have removed remote link $jiraIssue (Bug ID $1)";
- } # if
- } # if
- } # for
-} # removeRemoteLink
-
-sub getIssueLinks ($;$) {
- my ($issue, $type) = @_;
-
- my @links = getIssue ($issue, ('issuelinks'));
-
- my @issueLinks;
-
- for (@{$links[0]->{fields}{issuelinks}}) {
- my %issueLink = %$_;
-
- next if ($type && $type ne $issueLink{type}{name});
-
- push @issueLinks, \%issueLink;
- }
-
- return @issueLinks;
-} # getIssueLinks
-
-sub updateIssueWatchers ($%) {
- my ($issue, %watchers) = @_;
-
- my $existingWatchers;
-
- eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
-
- return "Unable to get issue $issue\n$@" if $@;
-
- for (@{$existingWatchers->{watchers}}) {
- # Cleanup: Remove the current user from the watchers list.
- # If he's on the list then remove him.
- if ($_->{name} eq $jira->{username}) {
- $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
-
- $total{"Admins destroyed"}++;
- } # if
-
- # Delete any matching watchers
- delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})};
- } # for
-
- return '' if keys %watchers == 0;
-
- my $issueUpdated;
-
- for (keys %watchers) {
- if ($main::opts{exec}) {
- eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
-
- if ($@) {
- $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue");
-
- $main::total{'Watchers skipped'}++;
- } else {
- $issueUpdated = 1;
-
- $main::total{'Watchers added'}++;
- } # if
- } else {
- $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
-
- $main::total{'Watchers that would have been added'}++;
- } # if
- } # for
-
- $main::total{'Issues updated'}++ if $issueUpdated;
-
- return '';
-} # updateIssueWatchers
-
-=pod
-
-I'm pretty sure I'm not using this routine anymore and I don't think it works.
-If you wish to reserect this then please test.
-
-sub updateWatchers ($%) {
- my ($issue, %watchers) = @_;
-
- my $existingWatchers;
-
- eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")};
-
- if ($@) {
- error "Unable to get issue $issue";
-
- $main::total{'Missing JIRA Issues'}++;
-
- return;
- } # if
-
- for (@{$existingWatchers->{watchers}}) {
- # Cleanup: Mike Admin Cogan was added as a watcher for each issue imported.
- # If he's on the list then remove him.
- if ($_->{name} eq 'mcoganAdmin') {
- $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}");
-
- $main::total{"mcoganAdmin's destroyed"}++;
- } # if
-
- # Delete any matching watchers
- delete $watchers{$_->{name}} if $watchers{$_->{name}};
- } # for
-
- return if keys %watchers == 0;
-
- my $issueUpdated;
-
- for (keys %watchers) {
- if ($main::opts{exec}) {
- eval {$jira->POST ("/issue/$issue/watchers", undef, $_)};
-
- if ($@) {
- error "Unable to add user $_ as a watcher to JIRA Issue $issue";
-
- $main::total{'Watchers skipped'}++;
- } else {
- $main::total{'Watchers added'}++;
-
- $issueUpdated = 1;
- } # if
- } else {
- $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue");
-
- $main::total{'Watchers that would have been added'}++;
- } # if
- } # for
-
- $main::total{'Issues updated'}++ if $issueUpdated;
-
- return;
-} # updateWatchers
-=cut
-
-1;
+++ /dev/null
-#!/usr/bin/perl
-use strict;
-use warnings;
-
-=pod
-
-=head1 NAME updateWatchLists.pl
-
-Copy CC lists from Bugzilla -> JIRA
-
-=head1 VERSION
-
-=over
-
-=item Author
-
-Andrew DeFaria <Andrew@ClearSCM.com>
-
-=item Revision
-
-$Revision: #1 $
-
-=item Created
-
-Thu Mar 20 10:11:53 PDT 2014
-
-=item Modified
-
-$Date: 2014/05/23 $
-
-=back
-
-=head1 SYNOPSIS
-
- Updates JIRA watchlists by copying the CC list information from Bugzilla
-
- $ updateWatchLists.pl [-login <login email>] [-products product1,
- product2,...] [-[no]exec]
- [-verbose] [-help] [-usage]
-
- Where:
-
- -v|erbose: Display progress output
- -he|lp: Display full help
- -usa|ge: Display usage
- -[no]e|xec: Whether or not to update JIRA. -noexec says only
- tell me what you would have updated.
- -use|rname: Username to log into JIRA with (Default: jira-admin)
- -p|assword: Password to log into JIRA with (Default: jira-admin's
- password)
- -bugzillaserver: Machine where Bugzilla lives (Default: bugs-dev)
- -jiraserver: Machine where Jira lives (Default: jira-dev)
- -bugi|ds: Comma separated list of BugIDs to process
- -f|ile: File of BugIDs, one per line
-
-=head1 DESCRIPTION
-
-This script updates JIRA watchlists by copying the CC List information from
-Bugzilla to JIRA.
-
-=cut
-
-use FindBin;
-use lib "$FindBin::Bin/lib";
-
-$| = 1;
-
-use DBI;
-use Display;
-use Logger;
-use TimeUtils;
-use Utils;
-use JIRAUtils;
-use BugzillaUtils;
-use JIRA::REST;
-
-use Getopt::Long;
-use Pod::Usage;
-
-# Login should be the email address of the bugzilla account which has
-# priviledges to create products and components
-our %opts = (
- exec => 0,
- bugzillaserver => $ENV{BUGZILLASERVER} || 'bugs-dev',
- jiraserver => $ENV{JIRASERVER} || 'jira-dev:8081',
- username => 'jira-admin',
- password => 'jira-admin',
- usage => sub { pod2usage },
- help => sub { pod2usage (-verbose => 2)},
- verbose => sub { set_verbose },
-);
-
-our ($log, %total);
-
-my ($bugzilla, $jira);
-
-sub main () {
- my $startTime = time;
-
- GetOptions (
- \%opts,
- 'verbose',
- 'usage',
- 'help',
- 'exec!',
- 'quiet',
- 'username=s',
- 'password=s',
- 'bugids=s@',
- 'file=s',
- 'jiraserver=s',
- 'bugzillaserver=s',
- ) or pod2usage;
-
- $log = Logger->new;
-
- if ($opts{file}) {
- open my $file, '<', $opts{file}
- or die "Unable to open $opts{file} - $!";
-
- $opts{bugids} = [<$file>];
-
- chomp @{$opts{bugids}};
- } else {
- my @bugids;
-
- push @bugids, (split /,/, join (',', $_)) for (@{$opts{bugids}});
-
- $opts{bugids} = [@bugids];
- } # if
-
- pod2usage 'Must specify -bugids <bugid>[,<bugid>,...] or -file <filename>'
- unless $opts{bugids};
-
- openBugzilla $opts{bugzillaserver}
- or $log->err ("Unable to connect to $opts{bugzillaserver}", 1);
-
- Connect2JIRA ($opts{username}, $opts{password}, $opts{jiraserver})
- or $log->err ("Unable to connect to $opts{jiraserver}", 1);
-
- for (@{$opts{bugids}}) {
- my $issue = findIssue $_;
-
- if ($issue =~ /^Future JIRA Issue/ or $issue =~ /^Unable to find/) {
- $log->msg ($issue);
- } else {
- my %watchers = getWatchers $_;
-
- $log->msg ('Found ' . scalar (keys %watchers) . " watchers for JIRA Issue $issue");
-
- my $result = updateIssueWatchers ($issue, %watchers);
-
- if ($result =~ /^Unable to/) {
- $total{'Missing JIRA Issues'}++;
-
- $log->err ($result);
- } else {
- $total{'Issues updated'}++;
- } # if
- } # if
- } # for
-
- display_duration $startTime, $log;
-
- Stats (\%total, $log) unless $opts{quiet};
-
- return 0;
-} # main
-
-exit main;
--- /dev/null
+################################################################################
+#
+# File: LDAP.conf
+# Description: Configuration file for LDAP
+# Author: Andrew DeFaria <Andrew@ClearSCM.com>
+# 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)
--- /dev/null
+################################################################################
+#
+# 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>/$$SERVER/" rmc.conf > $(TEMPFILE); \
+ $(SED) "s/<PORT>/$$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
--- /dev/null
+#!/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 <Andrew@ClearSCM.com>
+
+=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 <username>] [-password <password>]
+ [-client client] [-port] [-[no]html] [-csv]
+ [-comments] [-files] [-description]
+
+ -from <revRange> [-to <revRange>]
+ [-branchpath <path>]
+
+ [-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 '<center>', a {
+ href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}",
+ }, 'Export CSV</center>' 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'}, '<br>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'}, '<br>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} ? '<br>' : "\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 //= '<today>';
+ $to //= '<today>';
+
+ 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 <a href="..." target="bugzilla"> and other times I
+ # get <a target="bugzilla" href="...">! 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
+ ? "<a href=\"$bugsweb$bugid\" target=\"bugzilla\">$bugid</a>"
+ : font {color => '#aaa'}, 'N/A';
+ my $jiralink = $jiraIssue
+ ? "<a href=\"$jiraWeb$jiraIssue\" target=\"jira\">$jiraIssue</a>"
+ : font {color => '#aaa'}, 'N/A';
+ my $cllink = "<a href=\"$p4web/$_->{change}?ac=133\" target=\"p4web\">$_->{change}</a>";
+ 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 '<br>', @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<br>";
+ } # 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 <Andrew@ClearSCM.com>',
+ script => [{
+ language => 'JavaScript',
+ src => 'rmc.js',
+ }, {
+ language => 'JavaScript',
+ src => 'rmctable.js',
+ }],
+ style => ['rmc.css'],
+ onload => 'setOptions();',
+ }, $title;
+
+ print h1 {class => 'title'}, "<center><img src=\"Audience.png\"> $title</center>";
+ print h3 "<center><font color='#838'>$subtitle</font></center>" 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 '<today>' or $opts{to} eq '');
+ $opts{from} = $midnight if ($opts{from} eq '<today>' 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;
+
--- /dev/null
+################################################################################
+#
+# 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 <PORT>
+
+<VirtualHost *:<PORT>>
+ ServerName <SERVER>.audience.local:<PORT>
+ ServerAlias <SERVER>
+ ErrorLog "/var/log/httpd/rmc.error.log"
+ CustomLog "/var/log/httpd/rmc.access.log" common
+ DocumentRoot "/opt/audience/Web/rmc"
+
+ <Directory "/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
+ </Directory>
+</VirtualHost>
--- /dev/null
+/*******************************************************************************
+*
+* 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
--- /dev/null
+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
--- /dev/null
+#!/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 <Andrew@ClearSCM.com>
+
+=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 <username>] [-password <password>]
+ [-client client] [-port] [-[no]html] [-csv]
+ [-comments] [-files] [-description]
+
+ -from <revRange> [-to <revRange>]
+ [-branchpath <path>]
+
+ [-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 '<center>', a {
+ href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}",
+ }, 'Export CSV</center>' 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'}, '<br>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'}, '<br>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} ? '<br>' : "\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 //= '<today>';
+ $to //= '<today>';
+
+ 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 <a href="..." target="bugzilla"> and other times I
+ # get <a target="bugzilla" href="...">! 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
+ ? "<a href=\"$bugsweb$bugid\" target=\"bugzilla\">$bugid</a>"
+ : font {color => '#aaa'}, 'N/A';
+ my $jiralink = $jiraIssue
+ ? "<a href=\"$jiraWeb$jiraIssue\" target=\"jira\">$jiraIssue</a>"
+ : font {color => '#aaa'}, 'N/A';
+ my $cllink = "<a href=\"$p4web/$_->{change}?ac=133\" target=\"p4web\">$_->{change}</a>";
+ 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 '<br>', @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<br>";
+ } # 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 <Andrew@ClearSCM.com>',
+ script => [{
+ language => 'JavaScript',
+ src => 'rmc.js',
+ }, {
+ language => 'JavaScript',
+ src => 'rmctable.js',
+ }],
+ style => ['rmc.css'],
+ onload => 'setOptions();',
+ }, $title;
+
+ print h1 {class => 'title'}, "<center><img src=\"Audience.png\"> $title</center>";
+ print h3 "<center><font color='#838'>$subtitle</font></center>" 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 '<today>' or $opts{to} eq '');
+ $opts{from} = $midnight if ($opts{from} eq '<today>' 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;
+
--- /dev/null
+/**
+ * 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:(a<b)?-1:1;
+ };
+ sort['default'] = sort.alphanumeric; // IE chokes on sort.default
+
+ // This conversion is generalized to work for either a decimal separator of , or .
+ sort.numeric_converter = function(separator) {
+ return function(val) {
+ if (typeof(val)=="string") {
+ val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0;
+ }
+ return val || 0;
+ };
+ };
+
+ // Numeric Sort
+ // ------------
+ sort.numeric = function(a,b) {
+ return sort.numeric.convert(a)-sort.numeric.convert(b);
+ };
+ sort.numeric.convert = sort.numeric_converter(".");
+
+ // Numeric Sort - comma decimal separator
+ // --------------------------------------
+ sort.numeric_comma = function(a,b) {
+ return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b);
+ };
+ sort.numeric_comma.convert = sort.numeric_converter(",");
+
+ // Case-insensitive Sort
+ // ---------------------
+ sort.ignorecase = function(a,b) {
+ return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));
+ };
+ sort.ignorecase.convert = function(val) {
+ if (val==null) { return ""; }
+ return (""+val).toLowerCase();
+ };
+
+ // Currency Sort
+ // -------------
+ sort.currency = sort.numeric; // Just treat it as numeric!
+ sort.currency_comma = sort.numeric_comma;
+
+ // Date sort
+ // ---------
+ sort.date = function(a,b) {
+ return sort.numeric(sort.date.convert(a),sort.date.convert(b));
+ };
+ // Convert 2-digit years to 4
+ sort.date.fixYear=function(yr) {
+ yr = +yr;
+ if (yr<50) { yr += 2000; }
+ else if (yr<100) { yr += 1900; }
+ return yr;
+ };
+ sort.date.formats = [
+ // YY[YY]-MM-DD
+ { re:/(\d{2,4})-(\d{1,2})-(\d{1,2})/ , f:function(x){ return (new Date(sort.date.fixYear(x[1]),+x[2],+x[3])).getTime(); } }
+ // MM/DD/YY[YY] or MM-DD-YY[YY]
+ ,{ re:/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/ , f:function(x){ return (new Date(sort.date.fixYear(x[3]),+x[1],+x[2])).getTime(); } }
+ // Any catch-all format that new Date() can handle. This is not reliable except for long formats, for example: 31 Jan 2000 01:23:45 GMT
+ ,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } }
+ ];
+ sort.date.convert = function(val) {
+ var m,v, f = sort.date.formats;
+ for (var i=0,L=f.length; i<L; i++) {
+ if (m=val.match(f[i].re)) {
+ v=f[i].f(m);
+ if (typeof(v)!="undefined") { return v; }
+ }
+ }
+ return 9999999999999; // So non-parsed dates will be last, not first
+ };
+
+ return sort;
+})();
+
+/**
+ * The main Table namespace
+ */
+var Table = (function(){
+
+ /**
+ * Determine if a reference is defined
+ */
+ function def(o) {return (typeof o!="undefined");};
+
+ /**
+ * Determine if an object or class string contains a given class.
+ */
+ function hasClass(o,name) {
+ return new RegExp("(^|\\s)"+name+"(\\s|$)").test(o.className);
+ };
+
+ /**
+ * Add a class to an object
+ */
+ function addClass(o,name) {
+ var c = o.className || "";
+ if (def(c) && !hasClass(o,name)) {
+ o.className += (c?" ":"") + name;
+ }
+ };
+
+ /**
+ * Remove a class from an object
+ */
+ function removeClass(o,name) {
+ var c = o.className || "";
+ o.className = c.replace(new RegExp("(^|\\s)"+name+"(\\s|$)"),"$1");
+ };
+
+ /**
+ * For classes that match a given substring, return the rest
+ */
+ function classValue(o,prefix) {
+ var c = o.className;
+ if (c.match(new RegExp("(^|\\s)"+prefix+"([^ ]+)"))) {
+ return RegExp.$2;
+ }
+ return null;
+ };
+
+ /**
+ * Return true if an object is hidden.
+ * This uses the "russian doll" technique to unwrap itself to the most efficient
+ * function after the first pass. This avoids repeated feature detection that
+ * would always fall into the same block of code.
+ */
+ function isHidden(o) {
+ if (window.getComputedStyle) {
+ var cs = window.getComputedStyle;
+ return (isHidden = function(o) {
+ return 'none'==cs(o,null).getPropertyValue('display');
+ })(o);
+ }
+ else if (window.currentStyle) {
+ return(isHidden = function(o) {
+ return 'none'==o.currentStyle['display'];
+ })(o);
+ }
+ return (isHidden = function(o) {
+ return 'none'==o.style['display'];
+ })(o);
+ };
+
+ /**
+ * Get a parent element by tag name, or the original element if it is of the tag type
+ */
+ function getParent(o,a,b) {
+ if (o!=null && o.nodeName) {
+ if (o.nodeName==a || (b && o.nodeName==b)) {
+ return o;
+ }
+ while (o=o.parentNode) {
+ if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {
+ return o;
+ }
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Utility function to copy properties from one object to another
+ */
+ function copy(o1,o2) {
+ for (var i=2;i<arguments.length; i++) {
+ var a = arguments[i];
+ if (def(o1[a])) {
+ o2[a] = o1[a];
+ }
+ }
+ }
+
+ // The table object itself
+ var table = {
+ //Class names used in the code
+ AutoStripeClassName:"table-autostripe",
+ StripeClassNamePrefix:"table-stripeclass:",
+
+ AutoSortClassName:"table-autosort",
+ AutoSortColumnPrefix:"table-autosort:",
+ AutoSortTitle:"Click to sort",
+ SortedAscendingClassName:"table-sorted-asc",
+ SortedDescendingClassName:"table-sorted-desc",
+ SortableClassName:"table-sortable",
+ SortableColumnPrefix:"table-sortable:",
+ NoSortClassName:"table-nosort",
+
+ AutoFilterClassName:"table-autofilter",
+ FilteredClassName:"table-filtered",
+ FilterableClassName:"table-filterable",
+ FilteredRowcountPrefix:"table-filtered-rowcount:",
+ RowcountPrefix:"table-rowcount:",
+ FilterAllLabel:"Filter: All",
+
+ AutoPageSizePrefix:"table-autopage:",
+ AutoPageJumpPrefix:"table-page:",
+ PageNumberPrefix:"table-page-number:",
+ PageCountPrefix:"table-page-count:"
+ };
+
+ /**
+ * A place to store misc table information, rather than in the table objects themselves
+ */
+ table.tabledata = {};
+
+ /**
+ * Resolve a table given an element reference, and make sure it has a unique ID
+ */
+ table.uniqueId=1;
+ table.resolve = function(o,args) {
+ if (o!=null && o.nodeName && o.nodeName!="TABLE") {
+ o = getParent(o,"TABLE");
+ }
+ if (o==null) { return null; }
+ if (!o.id) {
+ var id = null;
+ do { var id = "TABLE_"+(table.uniqueId++); }
+ while (document.getElementById(id)!=null);
+ o.id = id;
+ }
+ this.tabledata[o.id] = this.tabledata[o.id] || {};
+ if (args) {
+ copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");
+ }
+ return o;
+ };
+
+
+ /**
+ * Run a function against each cell in a table header or footer, usually
+ * to add or remove css classes based on sorting, filtering, etc.
+ */
+ table.processTableCells = function(t, type, func, arg) {
+ t = this.resolve(t);
+ if (t==null) { return; }
+ if (type!="TFOOT") {
+ this.processCells(t.tHead, func, arg);
+ }
+ if (type!="THEAD") {
+ this.processCells(t.tFoot, func, arg);
+ }
+ };
+
+ /**
+ * Internal method used to process an arbitrary collection of cells.
+ * Referenced by processTableCells.
+ * It's done this way to avoid getElementsByTagName() which would also return nested table cells.
+ */
+ table.processCells = function(section,func,arg) {
+ if (section!=null) {
+ if (section.rows && section.rows.length && section.rows.length>0) {
+ var rows = section.rows;
+ for (var j=0,L2=rows.length; j<L2; j++) {
+ var row = rows[j];
+ if (row.cells && row.cells.length && row.cells.length>0) {
+ var cells = row.cells;
+ for (var k=0,L3=cells.length; k<L3; k++) {
+ var cellsK = cells[k];
+ func.call(this,cellsK,arg);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Get the cellIndex value for a cell. This is only needed because of a Safari
+ * bug that causes cellIndex to exist but always be 0.
+ * Rather than feature-detecting each time it is called, the function will
+ * re-write itself the first time it is called.
+ */
+ table.getCellIndex = function(td) {
+ var tr = td.parentNode;
+ var cells = tr.cells;
+ if (cells && cells.length) {
+ if (cells.length>1 && 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<L; i++) {
+ if (tr.cells[i]==td) {
+ return i;
+ }
+ }
+ }
+ return 0;
+ };
+
+ /**
+ * A map of node names and how to convert them into their "value" for sorting, filtering, etc.
+ * These are put here so it is extensible.
+ */
+ table.nodeValue = {
+ 'INPUT':function(node) {
+ if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {
+ return node.value;
+ }
+ return "";
+ },
+ 'SELECT':function(node) {
+ if (node.selectedIndex>=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<L; i++) {
+ var node = childNodes[i];
+ var type = node.nodeType;
+ // In order to get realistic sort results, we need to treat some elements in a special way.
+ // These behaviors are defined in the nodeValue() object, keyed by node name
+ if (type==1) {
+ var nname = node.nodeName;
+ if (this.nodeValue[nname]) {
+ ret += this.nodeValue[nname](node);
+ }
+ else {
+ ret += this.getCellValue(node);
+ }
+ }
+ else if (type==3) {
+ if (def(node.innerText)) {
+ ret += node.innerText;
+ }
+ else if (def(node.nodeValue)) {
+ ret += node.nodeValue;
+ }
+ }
+ }
+ return ret;
+ };
+
+ /**
+ * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex
+ * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2,
+ * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really
+ * starts in the second column rather than the first.
+ * See: http://www.javascripttoolbox.com/temp/table_cellindex.html
+ */
+ table.tableHeaderIndexes = {};
+ table.getActualCellIndex = function(tableCellObj) {
+ if (!def(tableCellObj.cellIndex)) { return null; }
+ var tableObj = getParent(tableCellObj,"TABLE");
+ var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);
+
+ // If it has already been computed, return the answer from the lookup table
+ if (def(this.tableHeaderIndexes[tableObj.id])) {
+ return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
+ }
+
+ var matrix = [];
+ this.tableHeaderIndexes[tableObj.id] = {};
+ var thead = getParent(tableCellObj,"THEAD");
+ var trs = thead.getElementsByTagName('TR');
+
+ // Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets
+ // populated with an "x" for each space that a cell takes up. If the first cell is colspan
+ // 2, it will fill in values [0] and [1] in the first array, so that the second cell will
+ // find the first empty cell in the first row (which will be [2]) and know that this is
+ // where it sits, rather than its internal .cellIndex value of [1].
+ for (var i=0; i<trs.length; i++) {
+ var cells = trs[i].cells;
+ for (var j=0; j<cells.length; j++) {
+ var c = cells[j];
+ var rowIndex = c.parentNode.rowIndex;
+ var cellId = rowIndex+"-"+this.getCellIndex(c);
+ var rowSpan = c.rowSpan || 1;
+ var colSpan = c.colSpan || 1;
+ var firstAvailCol;
+ if(!def(matrix[rowIndex])) {
+ matrix[rowIndex] = [];
+ }
+ var m = matrix[rowIndex];
+ // Find first available column in the first row
+ for (var k=0; k<m.length+1; k++) {
+ if (!def(m[k])) {
+ firstAvailCol = k;
+ break;
+ }
+ }
+ this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
+ for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
+ if(!def(matrix[k])) {
+ matrix[k] = [];
+ }
+ var matrixrow = matrix[k];
+ for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
+ matrixrow[l] = "x";
+ }
+ }
+ }
+ }
+ // Store the map so future lookups are fast.
+ return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
+ };
+
+ /**
+ * Sort all rows in each TBODY (tbodies are sorted independent of each other)
+ */
+ table.sort = function(o,args) {
+ var t, tdata, sortconvert=null;
+ // Allow for a simple passing of sort type as second parameter
+ if (typeof(args)=="function") {
+ args={sorttype:args};
+ }
+ args = args || {};
+
+ // If no col is specified, deduce it from the object sent in
+ if (!def(args.col)) {
+ args.col = this.getActualCellIndex(o) || 0;
+ }
+ // If no sort type is specified, default to the default sort
+ args.sorttype = args.sorttype || Sort['default'];
+
+ // Resolve the table
+ t = this.resolve(o,args);
+ tdata = this.tabledata[t.id];
+
+ // If we are sorting on the same column as last time, flip the sort direction
+ if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) {
+ tdata.desc = !tdata.lastdesc;
+ }
+ else {
+ tdata.desc = !!args.desc;
+ }
+
+ // Store the last sorted column so clicking again will reverse the sort order
+ tdata.lastcol=tdata.col;
+ tdata.lastdesc=!!tdata.desc;
+
+ // If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort
+ var sorttype = tdata.sorttype;
+ if (typeof(sorttype.convert)=="function") {
+ sortconvert=tdata.sorttype.convert;
+ sorttype=Sort.alphanumeric;
+ }
+
+ // Loop through all THEADs and remove sorted class names, then re-add them for the col
+ // that is being sorted
+ this.processTableCells(t,"THEAD",
+ function(cell) {
+ if (hasClass(cell,this.SortableClassName)) {
+ removeClass(cell,this.SortedAscendingClassName);
+ removeClass(cell,this.SortedDescendingClassName);
+ // If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted
+ if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) {
+ addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName);
+ }
+ }
+ }
+ );
+
+ // Sort each tbody independently
+ var bodies = t.tBodies;
+ if (bodies==null || bodies.length==0) { return; }
+
+ // Define a new sort function to be called to consider descending or not
+ var newSortFunc = (tdata.desc)?
+ function(a,b){return sorttype(b[0],a[0]);}
+ :function(a,b){return sorttype(a[0],b[0]);};
+
+ var useinnertext=!!tdata.useinnertext;
+ var col = tdata.col;
+
+ for (var i=0,L=bodies.length; i<L; i++) {
+ var tb = bodies[i], tbrows = tb.rows, rows = [];
+
+ // Allow tbodies to request that they not be sorted
+ if(!hasClass(tb,table.NoSortClassName)) {
+ // Create a separate array which will store the converted values and refs to the
+ // actual rows. This is the array that will be sorted.
+ var cRow, cRowIndex=0;
+ if (cRow=tbrows[cRowIndex]){
+ // Funky loop style because it's considerably faster in IE
+ do {
+ if (rowCells = cRow.cells) {
+ var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null;
+ if (sortconvert) cellValue = sortconvert(cellValue);
+ rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];
+ }
+ } while (cRow=tbrows[++cRowIndex])
+ }
+
+ // Do the actual sorting
+ rows.sort(newSortFunc);
+
+ // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!
+ cRowIndex=0;
+ var displayedCount=0;
+ var f=[removeClass,addClass];
+ if (cRow=rows[cRowIndex]){
+ do {
+ tb.appendChild(cRow[1]);
+ } while (cRow=rows[++cRowIndex])
+ }
+ }
+ }
+
+ // If paging is enabled on the table, then we need to re-page because the order of rows has changed!
+ if (tdata.pagesize) {
+ this.page(t); // This will internally do the striping
+ }
+ else {
+ // Re-stripe if a class name was supplied
+ if (tdata.stripeclass) {
+ this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);
+ }
+ }
+ };
+
+ /**
+ * Apply a filter to rows in a table and hide those that do not match.
+ */
+ table.filter = function(o,filters,args) {
+ var cell;
+ args = args || {};
+
+ var t = this.resolve(o,args);
+ var tdata = this.tabledata[t.id];
+
+ // If new filters were passed in, apply them to the table's list of filters
+ if (!filters) {
+ // If a null or blank value was sent in for 'filters' then that means reset the table to no filters
+ tdata.filters = null;
+ }
+ else {
+ // Allow for passing a select list in as the filter, since this is common design
+ if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-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; i<L; i++) {
+ var filter = filters[i];
+ if (typeof(filter.filter)=="string") {
+ // If a filter string is like "/expr/" then turn it into a Regex
+ if (filter.filter.match(/^\/(.*)\/$/)) {
+ filter.filter = new RegExp(RegExp.$1);
+ filter.filter.regex=true;
+ }
+ // If filter string is like "function (x) { ... }" then turn it into a function
+ else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {
+ filter.filter = Function(RegExp.$1,RegExp.$2);
+ }
+ }
+ // If some non-table object was passed in rather than a 'col' value, resolve it
+ // and assign it's column index to the filter if it doesn't have one. This way,
+ // passing in a cell reference or a select object etc instead of a table object
+ // will automatically set the correct column to filter.
+ if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) {
+ filter.col = this.getCellIndex(cell);
+ }
+
+ // Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or ""
+ if ((!filter || !filter.filter) && tdata.filters) {
+ delete tdata.filters[filter.col];
+ }
+ else {
+ tdata.filters = tdata.filters || {};
+ tdata.filters[filter.col] = filter.filter;
+ }
+ }
+ // If no more filters are left, then make sure to empty out the filters object
+ for (var j in tdata.filters) { var keep = true; }
+ if (!keep) {
+ tdata.filters = null;
+ }
+ }
+ // Everything's been setup, so now scrape the table rows
+ return table.scrape(o);
+ };
+
+ /**
+ * "Page" a table by showing only a subset of the rows
+ */
+ table.page = function(t,page,args) {
+ args = args || {};
+ if (def(page)) { args.page = page; }
+ return table.scrape(t,args);
+ };
+
+ /**
+ * Jump forward or back any number of pages
+ */
+ table.pageJump = function(t,count,args) {
+ t = this.resolve(t,args);
+ return this.page(t,(table.tabledata[t.id].page||0)+count,args);
+ };
+
+ /**
+ * Go to the next page of a paged table
+ */
+ table.pageNext = function(t,args) {
+ return this.pageJump(t,1,args);
+ };
+
+ /**
+ * Go to the previous page of a paged table
+ */
+ table.pagePrevious = function(t,args) {
+ return this.pageJump(t,-1,args);
+ };
+
+ /**
+ * Scrape a table to either hide or show each row based on filters and paging
+ */
+ table.scrape = function(o,args) {
+ var col,cell,filterList,filterReset=false,filter;
+ var page,pagesize,pagestart,pageend;
+ var unfilteredrows=[],unfilteredrowcount=0,totalrows=0;
+ var t,tdata,row,hideRow;
+ args = args || {};
+
+ // Resolve the table object
+ t = this.resolve(o,args);
+ tdata = this.tabledata[t.id];
+
+ // Setup for Paging
+ var page = tdata.page;
+ if (def(page)) {
+ // Don't let the page go before the beginning
+ if (page<0) { tdata.page=page=0; }
+ pagesize = tdata.pagesize || 25; // 25=arbitrary default
+ pagestart = page*pagesize+1;
+ pageend = pagestart + pagesize - 1;
+ }
+
+ // Scrape each row of each tbody
+ var bodies = t.tBodies;
+ if (bodies==null || bodies.length==0) { return; }
+ for (var i=0,L=bodies.length; i<L; i++) {
+ var tb = bodies[i];
+ for (var j=0,L2=tb.rows.length; j<L2; j++) {
+ row = tb.rows[j];
+ hideRow = false;
+
+ // Test if filters will hide the row
+ if (tdata.filters && row.cells) {
+ var cells = row.cells;
+ var cellsLength = cells.length;
+ // Test each filter
+ for (col in tdata.filters) {
+ if (!hideRow) {
+ filter = tdata.filters[col];
+ if (filter && col<cellsLength) {
+ var val = this.getCellValue(cells[col]);
+ if (filter.regex && val.search) {
+ hideRow=(val.search(filter)<0);
+ }
+ else if (typeof(filter)=="function") {
+ hideRow=!filter(val,cells[col]);
+ }
+ else {
+ hideRow = (val!=filter);
+ }
+ }
+ }
+ }
+ }
+
+ // Keep track of the total rows scanned and the total runs _not_ filtered out
+ totalrows++;
+ if (!hideRow) {
+ unfilteredrowcount++;
+ if (def(page)) {
+ // Temporarily keep an array of unfiltered rows in case the page we're on goes past
+ // the last page and we need to back up. Don't want to filter again!
+ unfilteredrows.push(row);
+ if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) {
+ 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; i<L; i++) {
+ unfilteredrows[i].style.display="";
+ }
+ }
+ }
+
+ // Loop through all THEADs and add/remove filtered class names
+ this.processTableCells(t,"THEAD",
+ function(c) {
+ ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);
+ }
+ );
+
+ // Stripe the table if necessary
+ if (tdata.stripeclass) {
+ this.stripe(t);
+ }
+
+ // Calculate some values to be returned for info and updating purposes
+ var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;
+ if (def(page)) {
+ // Update the page number/total containers if they exist
+ if (tdata.container_number) {
+ tdata.container_number.innerHTML = page+1;
+ }
+ if (tdata.container_count) {
+ tdata.container_count.innerHTML = pagecount;
+ }
+ }
+
+ // Update the row count containers if they exist
+ if (tdata.container_filtered_count) {
+ tdata.container_filtered_count.innerHTML = unfilteredrowcount;
+ }
+ if (tdata.container_all_count) {
+ tdata.container_all_count.innerHTML = totalrows;
+ }
+ return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };
+ };
+
+ /**
+ * Shade alternate rows, aka Stripe the table.
+ */
+ table.stripe = function(t,className,args) {
+ args = args || {};
+ args.stripeclass = className;
+
+ t = this.resolve(t,args);
+ var tdata = this.tabledata[t.id];
+
+ var bodies = t.tBodies;
+ if (bodies==null || bodies.length==0) {
+ return;
+ }
+
+ className = tdata.stripeclass;
+ // Cache a shorter, quicker reference to either the remove or add class methods
+ var f=[removeClass,addClass];
+ for (var i=0,L=bodies.length; i<L; i++) {
+ var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0;
+ if (cRow=tbrows[cRowIndex]){
+ // The ignorehiddenrows test is pulled out of the loop for a slight speed increase.
+ // Makes a bigger difference in FF than in IE.
+ // In this case, speed always wins over brevity!
+ if (tdata.ignoreHiddenRows) {
+ do {
+ f[displayedCount++%2](cRow,className);
+ } while (cRow=tbrows[++cRowIndex])
+ }
+ else {
+ do {
+ if (!isHidden(cRow)) {
+ f[displayedCount++%2](cRow,className);
+ }
+ } while (cRow=tbrows[++cRowIndex])
+ }
+ }
+ }
+ };
+
+ /**
+ * Build up a list of unique values in a table column
+ */
+ table.getUniqueColValues = function(t,col) {
+ var values={}, bodies = this.resolve(t).tBodies;
+ for (var i=0,L=bodies.length; i<L; i++) {
+ var tbody = bodies[i];
+ for (var r=0,L2=tbody.rows.length; r<L2; r++) {
+ values[this.getCellValue(tbody.rows[r].cells[col])] = true;
+ }
+ }
+ var valArray = [];
+ for (var val in values) {
+ valArray.push(val);
+ }
+ return valArray.sort();
+ };
+
+ /**
+ * Scan the document on load and add sorting, filtering, paging etc ability automatically
+ * based on existence of class names on the table and cells.
+ */
+ table.auto = function(args) {
+ var cells = [], tables = document.getElementsByTagName("TABLE");
+ var val,tdata;
+ if (tables!=null) {
+ for (var i=0,L=tables.length; i<L; i++) {
+ var t = table.resolve(tables[i]);
+ tdata = table.tabledata[t.id];
+ if (val=classValue(t,table.StripeClassNamePrefix)) {
+ tdata.stripeclass=val;
+ }
+ // Do auto-filter if necessary
+ if (hasClass(t,table.AutoFilterClassName)) {
+ table.autofilter(t);
+ }
+ // Do auto-page if necessary
+ if (val = classValue(t,table.AutoPageSizePrefix)) {
+ table.autopage(t,{'pagesize':+val});
+ }
+ // Do auto-sort if necessary
+ if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) {
+ table.autosort(t,{'col':(val==null)?null:+val});
+ }
+ // Do auto-stripe if necessary
+ if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {
+ table.stripe(t);
+ }
+ }
+ }
+ };
+
+ /**
+ * Add sorting functionality to a table header cell
+ */
+ table.autosort = function(t,args) {
+ t = this.resolve(t,args);
+ var tdata = this.tabledata[t.id];
+ this.processTableCells(t, "THEAD", function(c) {
+ var type = classValue(c,table.SortableColumnPrefix);
+ if (type!=null) {
+ type = type || "default";
+ c.title =c.title || table.AutoSortTitle;
+ addClass(c,table.SortableClassName);
+ c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})");
+ // If we are going to auto sort on a column, we need to keep track of what kind of sort it will be
+ if (args.col!=null) {
+ if (args.col==table.getActualCellIndex(c)) {
+ tdata.sorttype=Sort['"+type+"'];
+ }
+ }
+ }
+ } );
+ if (args.col!=null) {
+ table.sort(t,args);
+ }
+ };
+
+ /**
+ * Add paging functionality to a table
+ */
+ table.autopage = function(t,args) {
+ t = this.resolve(t,args);
+ var tdata = this.tabledata[t.id];
+ if (tdata.pagesize) {
+ this.processTableCells(t, "THEAD,TFOOT", function(c) {
+ var type = classValue(c,table.AutoPageJumpPrefix);
+ if (type=="next") { type = 1; }
+ else if (type=="previous") { type = -1; }
+ if (type!=null) {
+ c.onclick = Function("","Table.pageJump(this,"+type+")");
+ }
+ } );
+ if (val = classValue(t,table.PageNumberPrefix)) {
+ tdata.container_number = document.getElementById(val);
+ }
+ if (val = classValue(t,table.PageCountPrefix)) {
+ tdata.container_count = document.getElementById(val);
+ }
+ return table.page(t,0,args);
+ }
+ };
+
+ /**
+ * A util function to cancel bubbling of clicks on filter dropdowns
+ */
+ table.cancelBubble = function(e) {
+ e = e || window.event;
+ if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); }
+ if (def(e.cancelBubble)) { e.cancelBubble = true; }
+ };
+
+ /**
+ * Auto-filter a table
+ */
+ table.autofilter = function(t,args) {
+ args = args || {};
+ t = this.resolve(t,args);
+ var tdata = this.tabledata[t.id],val;
+ table.processTableCells(t, "THEAD", function(cell) {
+ if (hasClass(cell,table.FilterableClassName)) {
+ var cellIndex = table.getCellIndex(cell);
+ var colValues = table.getUniqueColValues(t,cellIndex);
+ if (colValues.length>0) {
+ if (typeof(args.insert)=="function") {
+ func.insert(cell,colValues);
+ }
+ else {
+ var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>';
+ for (var i=0; i<colValues.length; i++) {
+ sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>';
+ }
+ sel += '</select>';
+ cell.innerHTML += "<br>"+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;
+})();
--- /dev/null
+#/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',
+);
+
--- /dev/null
+#!/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