From 1729e51b9b8288680eb46aed0d34808508930f52 Mon Sep 17 00:00:00 2001 From: Andrew DeFaria Date: Mon, 21 Dec 2015 10:11:32 -0800 Subject: [PATCH] Adding some files of recent work. --- Confluence/lib/Confluence.pm | 118 ++ {audience/JIRA => JIRA}/importComments.pl | 0 {audience/JIRA => JIRA}/jiradep.pl | 0 {audience/JIRA => JIRA}/lib/BugzillaUtils.pm | 0 JIRA/lib/JIRAUtils.pm | 1038 +++++++++++++++++ {audience/JIRA => JIRA}/updateWatchLists.pl | 0 Perforce/getPicture.conf | 21 + Perforce/getPicture.pl | 203 ++++ Perforce/lib/Perforce.pm | 394 +++++++ Perforce/renameUser.pl | 224 ++++ audience/JIRA/lib/JIRAUtils.pm | 655 ----------- etc/LDAP.conf | 30 + rmc/Audience.png | Bin 0 -> 4231 bytes rmc/Makefile | 56 + rmc/index.pl | 1043 ++++++++++++++++++ rmc/rmc.conf | 34 + rmc/rmc.css | 355 ++++++ rmc/rmc.js | 107 ++ rmc/rmc.pl | 1043 ++++++++++++++++++ rmc/rmctable.js | 1002 +++++++++++++++++ test/testConfluence.pl | 20 + test/testldap.pl | 61 + 22 files changed, 5749 insertions(+), 655 deletions(-) create mode 100644 Confluence/lib/Confluence.pm rename {audience/JIRA => JIRA}/importComments.pl (100%) rename {audience/JIRA => JIRA}/jiradep.pl (100%) rename {audience/JIRA => JIRA}/lib/BugzillaUtils.pm (100%) create mode 100644 JIRA/lib/JIRAUtils.pm rename {audience/JIRA => JIRA}/updateWatchLists.pl (100%) create mode 100644 Perforce/getPicture.conf create mode 100755 Perforce/getPicture.pl create mode 100644 Perforce/lib/Perforce.pm create mode 100755 Perforce/renameUser.pl delete mode 100644 audience/JIRA/lib/JIRAUtils.pm create mode 100644 etc/LDAP.conf create mode 100644 rmc/Audience.png create mode 100644 rmc/Makefile create mode 100644 rmc/index.pl create mode 100644 rmc/rmc.conf create mode 100644 rmc/rmc.css create mode 100644 rmc/rmc.js create mode 100644 rmc/rmc.pl create mode 100644 rmc/rmctable.js create mode 100755 test/testConfluence.pl create mode 100755 test/testldap.pl diff --git a/Confluence/lib/Confluence.pm b/Confluence/lib/Confluence.pm new file mode 100644 index 0000000..2ad6d18 --- /dev/null +++ b/Confluence/lib/Confluence.pm @@ -0,0 +1,118 @@ +=pod + +=head1 NAME $RCSfile: JIRAUtils.pm,v $ + +Some shared functions dealing with JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: 1.0 $ + +=item Created + +Fri Mar 12 10:17:44 PST 2004 + +=item Modified + +$Date: 2013/05/30 15:48:06 $ + +=back + +=head1 ROUTINES + +The following routines are exported: + +=cut + +package Confluence; + +use strict; +use warnings; + +use File::Basename; +use MIME::Base64; + +use Display; +use GetConfig; +use Carp; + +use REST::Client; + +our $VERSION = '$Revision: 1.0 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $confluenceConf = $ENV{CONFLUENCE_CONF} || dirname (__FILE__) . '../etc/confluence.conf'; + +my %OPTS = GetConfig $confluenceConf if -r $confluenceConf; + +sub _get () { + my ($self, $url) = @_; + + unless ($self->{headers}) { + $self->{headers} = { + Accept => 'application/json', + Authorization => 'Basic ' + . encode_base64 ($self->{username} . ':' . $self->{password}), + }; + } # unless + + return $self->{REST}->GET ($url, $self->{headers}); +} # _get + +sub new (;%) { + my ($class, %parms) = @_; + + my $self = bless {}, $class; + + $self->{username} = $parms{username} || $OPTS{username} || $ENV{CONFLUENCE_USERNAME}; + $self->{password} = $parms{password} || $OPTS{password} || $ENV{CONFLUENCE_PASSWORD}; + $self->{server} = $parms{server} || $OPTS{server} || $ENV{CONFLUENCE_SERVER}; + $self->{port} = $parms{port} || $OPTS{port} || $ENV{CONFLUENCE_PORT}; + $self->{URL} = "http://$self->{server}:$self->{port}/rest/api"; + + return $self->connect; +} # new + +sub connect () { + my ($self) = @_; + + $self->{REST} = REST::Client->new ( + host => "http://$self->{server}:$self->{port}", + ); + + $self->{REST}->getUseragent()->ssl_opts (verify_hostname => 0); + $self->{REST}->setFollow (1); + + return $self; +} # connect + +sub getContent (%) { + my ($self, %parms) = @_; + + my $url = 'content?'; + + my @parms; + + push @parms, "type=$parms{type}" if $parms{type}; + push @parms, "spacekey=$parms{spaceKey}" if $parms{spaceKey}; + push @parms, "title=$parms{title}" if $parms{title}; + push @parms, "status=$parms{status}" if $parms{status}; + push @parms, "postingDay=$parms{postingDay}" if $parms{postingDay}; + push @parms, "expand=$parms{expand}" if $parms{expand}; + push @parms, "start=$parms{start}" if $parms{start}; + push @parms, "limit==$parms{limit}" if $parms{limit}; + + my $content = $self->_get ('/content/', join ',', @parms); + + return $content; +} # getContent + +1; diff --git a/audience/JIRA/importComments.pl b/JIRA/importComments.pl similarity index 100% rename from audience/JIRA/importComments.pl rename to JIRA/importComments.pl diff --git a/audience/JIRA/jiradep.pl b/JIRA/jiradep.pl similarity index 100% rename from audience/JIRA/jiradep.pl rename to JIRA/jiradep.pl diff --git a/audience/JIRA/lib/BugzillaUtils.pm b/JIRA/lib/BugzillaUtils.pm similarity index 100% rename from audience/JIRA/lib/BugzillaUtils.pm rename to JIRA/lib/BugzillaUtils.pm diff --git a/JIRA/lib/JIRAUtils.pm b/JIRA/lib/JIRAUtils.pm new file mode 100644 index 0000000..b57219d --- /dev/null +++ b/JIRA/lib/JIRAUtils.pm @@ -0,0 +1,1038 @@ +=pod + +=head1 NAME $RCSfile: JIRAUtils.pm,v $ + +Some shared functions dealing with JIRA + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: 1.0 $ + +=item Created + +Fri Mar 12 10:17:44 PST 2004 + +=item Modified + +$Date: 2013/05/30 15:48:06 $ + +=back + +=head1 ROUTINES + +The following routines are exported: + +=cut + +package JIRAUtils; + +use strict; +use warnings; + +use base 'Exporter'; + +use FindBin; +use Display; +use Carp; +use DBI; + +use JIRA::REST; +use BugzillaUtils; + +our $jira; + +our @EXPORT = qw ( + Connect2JIRA + addDescription + addJIRAComment + addRemoteLink + attachFiles2Issue + attachmentExists + blankBugzillaNbr + copyGroupMembership + count + findIssue + findIssues + findRemoteLinkByBugID + getIssue + getIssueFromBugID + getIssueLinkTypes + getIssueLinks + getIssueWatchers + getIssues + getNextIssue + getRemoteLink + getRemoteLinkByBugID + getRemoteLinks + getUsersGroups + linkIssues + promoteBug2JIRAIssue + removeRemoteLink + renameUsers + updateColumn + updateIssueWatchers + updateUsersGroups +); + +my (@issueLinkTypes, %cache, $jiradb, %findQuery); + +my %tables = ( + ao_08d66b_filter_display_conf => [ + {column => 'user_name'} + ], + ao_0c0737_vote_info => [ + {column => 'user_name'} + ], + ao_3a112f_audit_log_entry => [ + {column => 'user'} + ], + ao_563aee_activity_entity => [ + {column => 'username'} + ], + ao_60db71_auditentry => [ + {column => 'user'} + ], + ao_60db71_boardadmins => [ + {column => "'key'"} + ], + ao_60db71_rapidview => [ + {column => 'owner_user_name'} + ], + ao_caff30_favourite_issue => [ + {column => 'user_key'} + ], +# app_user => [ +# {column => 'user_key'}, +# {column => 'lower_user_name'}, +# ], + audit_log => [ + {column => 'author_key'} + ], + avatar => [ + {column => 'owner'} + ], + changegroup => [ + {column => 'author'} + ], + changeitem => [ + {column => 'oldvalue', + condition => 'field = "assignee"'}, + {column => 'newvalue', + condition => 'field = "assignee"'}, + ], + columnlayout => [ + {column => 'username'}, + ], + component => [ + {column => 'lead'}, + ], + customfieldvalue => [ + {column => 'stringvalue'}, + ], + favouriteassociations => [ + {column => 'username'}, + ], +# cwd_membership => [ +# {column => 'child_name'}, +# {column => 'lower_child_name'}, +# ], + fileattachment => [ + {column => 'author'}, + ], + filtersubscription => [ + {column => 'username'}, + ], + jiraaction => [ + {column => 'author'}, + ], + jiraissue => [ + {column => 'reporter'}, + {column => 'assignee'}, + ], + jiraworkflows => [ + {column => 'creatorname'}, + ], + membershipbase => [ + {column => 'user_name'}, + ], + os_currentstep => [ + {column => 'owner'}, + {column => 'caller'}, + ], + os_historystep => [ + {column => 'owner'}, + {column => 'caller'}, + ], + project => [ + {column => 'lead'}, + ], + portalpage => [ + {column => 'username'}, + ], + schemepermissions => [ + {column => 'perm_parameter', + condition => 'perm_type = "user"'}, + ], + searchrequest => [ + {column => 'authorname'}, + {column => 'username'}, + ], + userassociation => [ + {column => 'source_name'}, + ], + userbase => [ + {column => 'username'}, + ], + userhistoryitem => [ + {column => 'username'} + ], + worklog => [ + {column => 'author'}, + ], +); + +sub _checkDBError ($;$) { + my ($msg, $statement) = @_; + + $statement //= 'Unknown'; + + if ($main::log) { + $main::log->err ('JIRA database not opened!', 1) unless $jiradb; + } # if + + my $dberr = $jiradb->err; + my $dberrmsg = $jiradb->errstr; + + $dberr ||= 0; + $dberrmsg ||= 'Success'; + + my $message = ''; + + if ($dberr) { + my $function = (caller (1)) [3]; + + $message = "$function: $msg\nError #$dberr: $dberrmsg\n" + . "SQL Statement: $statement"; + } # if + + if ($main::log) { + $main::log->err ($message, 1) if $dberr; + } # if + + return; +} # _checkDBError + +sub openJIRADB (;$$$$) { + my ($dbhost, $dbname, $dbuser, $dbpass) = @_; + + $dbhost //= $main::opts{jiradbhost}; + $dbname //= 'jiradb'; + $dbuser //= 'root'; + $dbpass //= 'r00t'; + + $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)...") if $main::log; + + $jiradb = DBI->connect ( + "DBI:mysql:$dbname:$dbhost", + $dbuser, + $dbpass, { + PrintError => 0, + RaiseError => 1, + }, + ); + + _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)"; + + return $jiradb; +} # openJIRADB + +sub Connect2JIRA (;$$$) { + my ($username, $password, $server) = @_; + + my %opts; + + $opts{username} = $username || 'jira-admin'; + $opts{password} = $password || $ENV{PASSWORD} || 'jira-admin'; + $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev'; + $opts{URL} = "http://$opts{server}/rest/api/latest"; + + $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})") if $main::log; + + $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password}); + + # Store username as we might need it (see updateIssueWatchers) + $jira->{username} = $opts{username}; + + return $jira; +} # Connect2JIRA + +sub count ($$) { + my ($table, $condition) = @_; + + my $statement; + + $jiradb = openJIRADB unless $jiradb; + + if ($condition) { + $statement = "select count(*) from $table where $condition"; + } else { + $statement = "select count(*) from $table"; + } # if + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'count: Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'count: Unable to execute statement', $statement; + + # Get return value, which should be how many message there are + my @row = $sth->fetchrow_array; + + # Done with $sth + $sth->finish; + + my $count; + + # Retrieve returned value + unless ($row[0]) { + $count = 0 + } else { + $count = $row[0]; + } # unless + + return $count +} # count + +sub addDescription ($$) { + my ($issue, $description) = @_; + + if ($main::opts{exec}) { + eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})}; + + if ($@) { + return "Unable to add description\n$@"; + } else { + return 'Description added'; + } # if + } # if +} # addDescription + +sub addJIRAComment ($$) { + my ($issue, $comment) = @_; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })}; + + if ($@) { + return "Unable to add comment\n$@"; + } else { + return 'Comment added'; + } # if + } else { + return "Would have added comments to $issue"; + } # if +} # addJIRAComment + +sub blankBugzillaNbr ($) { + my ($issue) = @_; + + eval {$jira->PUT ("/issue/$issue", undef, {fields => {'Bugzilla Bug Origin' => ''}})}; + #eval {$jira->PUT ("/issue/$issue", undef, {fields => {'customfield_10132' => ''}})}; + + if ($@) { + return "Unable to blank Bugzilla number$@\n" + } else { + return 'Corrected' + } # if +} # blankBugzillaNbr + +sub attachmentExists ($$) { + my ($issue, $filename) = @_; + + my $attachments = getIssue ($issue, qw(attachment)); + + for (@{$attachments->{fields}{attachment}}) { + return 1 if $filename eq $_->{filename}; + } # for + + return 0; +} # attachmentExists + +sub attachFiles2Issue ($@) { + my ($issue, @files) = @_; + + my $status = $jira->attach_files ($issue, @files); + + return $status; +} # attachFiles2Issue + +sub getIssueFromBugID ($) { + my ($bugid) = @_; + + my $issue; + + my %query = ( + jql => "\"Bugzilla Bug Origin\" ~ $bugid", + fields => [ 'key' ], + ); + + eval {$issue = $jira->GET ("/search/", \%query)}; + + my $issueID = $issue->{issues}[0]{key}; + + return $issue->{issues} if @{$issue->{issues}} > 1; + return $issueID; +} # getIssueFromBugID + +sub findIssue ($%) { + my ($bugid, %bugmap) = @_; + +=pod + # Check the cache... + if ($cache{$bugid}) { + if ($cache{$bugid} =~ /^\d+/) { + # We have a cache hit but the contents here are a bugid. This means we had + # searched for the corresponding JIRA issue for this bug before and came + # up empty handed. In this situtaion we really have: + return "Unable to find a JIRA issue for Bug $bugid"; + } else { + return $cache{$bugid}; + } # if + } # if +=cut + my $issue; + + my %query = ( + jql => "\"Bugzilla Bug Origin\" ~ $bugid", + fields => [ 'key' ], + ); + + eval {$issue = $jira->GET ("/search/", \%query)}; + + my $issueID = $issue->{issues}[0]{key}; + + if (@{$issue->{issues}} > 2) { + $main::log->err ("Found more than 2 issues for Bug ID $bugid") if $main::log; + + return "Found more than 2 issues for Bug ID $bugid"; + } elsif (@{$issue->{issues}} == 2) { + my ($issueNum0, $issueNum1, $projectName0, $projectName1); + + if ($issue->{issues}[0]{key} =~ /(.*)-(\d+)/) { + $projectName0 = $1; + $issueNum0 = $2; + } # if + + if ($issue->{issues}[1]{key} =~ /(.*)-(\d+)/) { + $projectName1 = $1; + $issueNum1 = $2; + } # if + + if ($issueNum0 < $issueNum1) { + $issueID = $issue->{issues}[1]{key}; + } # if + + # Let's mark them as clones. See if this clone link already exists... + my $alreadyCloned; + + for (getIssueLinks ($issueID, 'Cloners')) { + my $inwardIssue = $_->{inwardIssue}{key} || ''; + my $outwardIssue = $_->{outwardIssue}{key} || ''; + + if ("$projectName0-$issueNum0" eq $inwardIssue || + "$projectName0-$issueNum0" eq $outwardIssue || + "$projectName1-$issueNum1" eq $inwardIssue || + "$projectName1-$issueNum1" eq $outwardIssue) { + $alreadyCloned = 1; + + last; + } # if + } # for + + unless ($alreadyCloned) { + my $result = linkIssues ("$projectName0-$issueNum0", 'Cloners', "$projectName1-$issueNum1"); + + return $result if $result =~ /Unable to/; + + $main::log->msg ($result) if $main::log; + } # unless + } # if + + if ($issueID) { + $main::log->msg ("Found JIRA issue $issueID for Bug $bugid") if $main::log; + + #$cache{$bugid} = $issueID; + + #return $cache{$bugid}; + return $issueID; + } else { + my $status = $bugmap{$bugid} ? 'Future JIRA Issue' + : "Unable to find a JIRA issue for Bug $bugid"; + + # Here we put this bugid into the cache but instead of a the JIRA issue + # id we put the bugid. This will stop us from adding up multiple hits on + # this bugid. + #$cache{$bugid} = $bugid; + + return $status; + } # if +} # findJIRA + +sub findIssues (;$@) { + my ($condition, @fields) = @_; + + push @fields, '*all' unless @fields; + + $findQuery{jql} = $condition || ''; + $findQuery{startAt} = 0; + $findQuery{maxResults} = 1; + $findQuery{fields} = join ',', @fields; + + return; +} # findIssues + +sub getNextIssue () { + my $result; + + eval {$result = $jira->GET ('/search/', \%findQuery)}; + + $findQuery{startAt}++; + + # Move id and key into fields + return unless @{$result->{issues}}; + + $result->{issues}[0]{fields}{id} = $result->{issues}[0]{id}; + $result->{issues}[0]{fields}{key} = $result->{issues}[0]{key}; + + return %{$result->{issues}[0]{fields}}; +} # getNextIssue + +sub getIssues (;$$$@) { + my ($condition, $start, $max, @fields) = @_; + + push @fields, '*all' unless @fields; + + my ($result, %query); + + $query{jql} = $condition || ''; + $query{startAt} = $start || 0; + $query{maxResults} = $max || 50; + $query{fields} = join ',', @fields; + + eval {$result = $jira->GET ('/search/', \%query)}; + + # We sometimes get an error here when $result->{issues} is undef. + # I suspect this is when the number of issues just happens to be + # an even number like on a $query{maxResults} boundry. So when + # $result->{issues} is undef we assume it's the last of the issues. + # (I should really verify this). + if ($result->{issues}) { + return @{$result->{issues}}; + } else { + return; + } # if +} # getIssues + +sub getIssue ($;@) { + my ($issue, @fields) = @_; + + my $fields = @fields ? "?fields=" . join ',', @fields : ''; + + return $jira->GET ("/issue/$issue$fields"); +} # getIssue + +sub getIssueLinkTypes () { + my $issueLinkTypes = $jira->GET ('/issueLinkType/'); + + map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}}; + + return @issueLinkTypes +} # getIssueLinkTypes + +sub linkIssues ($$$) { + my ($from, $type, $to) = @_; + + unless (@issueLinkTypes) { + getIssueLinkTypes; + } # unless + + unless (grep {$type eq $_} @issueLinkTypes) { + $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n\t" + . join "\n\t", @issueLinkTypes) if $main::log; + + return "Unable to $type link $from -> $to"; + } # unless + + my %link = ( + inwardIssue => { + key => $from, + }, + type => { + name => $type, + }, + outwardIssue => { + key => $to, + }, + comment => { + body => "Link ported as part of the migration from Bugzilla: $from <-> $to", + }, + ); + + $main::total{'IssueLinks Added'}++; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issueLink", undef, \%link)}; + + if ($@) { + return "Unable to $type link $from -> $to\n$@"; + } else { + return "Made $type link $from -> $to"; + } # if + } else { + return "Would have $type linked $from -> $to"; + } # if +} # linkIssue + +sub getRemoteLink ($;$) { + my ($jiraIssue, $id) = @_; + + $id //= ''; + + my $result; + + eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")}; + + return if $@; + + my %remoteLinks; + + if (ref $result eq 'ARRAY') { + map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result; + } else { + $remoteLinks{$result->{id}} = $result->{object}{title}; + } # if + + return \%remoteLinks; +} # getRemoteLink + +sub getRemoteLinks (;$) { + my ($bugid) = @_; + + $jiradb = openJIRADB unless $jiradb; + + my $statement = 'select url from remotelink'; + + $statement .= " where url like 'http://bugs%'"; + $statement .= " and url like '%$bugid'" if $bugid; + $statement .= " group by issueid desc"; + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'Unable to execute statement', $statement; + + my %bugids; + + while (my $record = $sth->fetchrow_array) { + if ($record =~ /(\d+)$/) { + $bugids{$1} = 1; + } # if + } # while + + return keys %bugids; +} # getRemoteLinks + +sub findRemoteLinkByBugID (;$) { + my ($bugid) = @_; + + my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id'; + + if ($bugid) { + $condition .= " and remotelink.url like '%id=$bugid'"; + } # unless + + $jiradb = openJIRADB unless $jiradb; + + my $statement = <<"END"; +select + remotelink.id, + concat (project.pkey, '-', issuenum) as issue, + relationship +from + remotelink, + jiraissue, + project +$condition +END + + my $sth = $jiradb->prepare ($statement); + + _checkDBError 'Unable to prepare statement', $statement; + + $sth->execute; + + _checkDBError 'Unable to execute statement', $statement; + + my @records; + + while (my $row = $sth->fetchrow_hashref) { + $row->{bugid} = $bugid; + + push @records, $row; + } # while + + return \@records; +} # findRemoteLinkByBugID + +sub promoteBug2JIRAIssue ($$$$) { + my ($bugid, $jirafrom, $jirato, $relationship) = @_; + + my $result = linkIssues $jirafrom, $relationship, $jirato; + + return $result if $result =~ /Unable to link/; + + $main::log->msg ($result . " (BugID $bugid)") if $main::log; + + for (@{findRemoteLinkByBugID $bugid}) { + my %record = %$_; + + $result = removeRemoteLink ($record{issue}, $record{id}); + + # We may not care if we couldn't remove this link because it may have been + # removed by a prior pass. + return $result if $result =~ /Unable to remove link/; + + if ($main::log) { + $main::log->msg ($result) unless $result eq ''; + } # if + } # for + + return $result; +} # promoteBug2JIRAIssue + +sub addRemoteLink ($$$) { + my ($bugid, $relationship, $jiraIssue) = @_; + + my $bug = getBug $bugid; + + # Check to see if this Bug ID already exists on this JIRA Issue, otherwise + # JIRA will duplicate it! + my $remoteLinks = getRemoteLink $jiraIssue; + + for (keys %$remoteLinks) { + if ($remoteLinks->{$_} =~ /Bug (\d+)/) { + return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1; + } # if + } # for + + # Note this globalid thing is NOT working! ALl I see is null in the database + my %remoteLink = ( +# globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid", +# application => { +# type => 'Bugzilla', +# name => 'Bugzilla', +# }, + relationship => $relationship, + object => { + url => "http://bugs.audience.com/show_bug.cgi?id=$bugid", + title => "Bug $bugid", + summary => $bug->{short_desc}, + icon => { + url16x16 => 'http://bugs.audience.local/favicon.png', + title => 'Bugzilla Bug', + }, + }, + ); + + $main::total{'RemoteLink Added'}++; + + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)}; + + return $@; + } else { + return "Would have linked $bugid -> $jiraIssue"; + } # if +} # addRemoteLink + +sub removeRemoteLink ($;$) { + my ($jiraIssue, $id) = @_; + + $id //= ''; + + my $remoteLinks = getRemoteLink ($jiraIssue, $id); + + for (keys %$remoteLinks) { + my $result; + + $main::total{'RemoteLink Removed'}++; + + if ($main::opts{exec}) { + eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")}; + + if ($@) { + return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@; + } else { + my $bugid; + + if ($remoteLinks->{$_} =~ /(\d+)/) { + return "Removed remote link $jiraIssue (Bug ID $1)"; + } # if + } # if + + $main::total{'Remote Links Removed'}++; + } else { + if ($remoteLinks->{$_} =~ /(\d+)/) { + return "Would have removed remote link $jiraIssue (Bug ID $1)"; + } # if + } # if + } # for +} # removeRemoteLink + +sub getIssueLinks ($;$) { + my ($issue, $type) = @_; + + my @links = getIssue ($issue, ('issuelinks')); + + my @issueLinks; + + for (@{$links[0]->{fields}{issuelinks}}) { + my %issueLink = %$_; + + next if ($type && $type ne $issueLink{type}{name}); + + push @issueLinks, \%issueLink; + } + + return @issueLinks; +} # getIssueLinks + +sub getIssueWatchers ($) { + my ($issue) = @_; + + my $watchers; + + eval {$watchers = $jira->GET ("/issue/$issue/watchers")}; + + return if $@; + + # The watcher information returned by the above is incomplete. Let's complete + # it. + my @watchers; + + for (@{$watchers->{watchers}}) { + my $user; + + eval {$user = $jira->GET ("/user?username=$_->{key}")}; + + unless ($@) { + push @watchers, $user; + } else { + if ($main::log) { + $main::log->err ("Unable to find user record for $_->{name}") + unless $_->{name} eq 'jira-admin'; + }# if + } # unless + } # for + + return @watchers; +} # getIssueWatchers + +sub updateIssueWatchers ($%) { + my ($issue, %watchers) = @_; + + my $existingWatchers; + + eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")}; + + return "Unable to get issue $issue\n$@" if $@; + + for (@{$existingWatchers->{watchers}}) { + # Cleanup: Remove the current user from the watchers list. + # If he's on the list then remove him. + if ($_->{name} eq $jira->{username}) { + $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}"); + + $main::total{"Admins destroyed"}++; + } # if + + # Delete any matching watchers + delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})}; + } # for + + return '' if keys %watchers == 0; + + my $issueUpdated; + + for (keys %watchers) { + if ($main::opts{exec}) { + eval {$jira->POST ("/issue/$issue/watchers", undef, $_)}; + + if ($@) { + $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue") if $main::log; + + $main::total{'Watchers skipped'}++; + } else { + $issueUpdated = 1; + + $main::total{'Watchers added'}++; + } # if + } else { + $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue") if $main::log; + + $main::total{'Watchers that would have been added'}++; + } # if + } # for + + $main::total{'Issues updated'}++ if $issueUpdated; + + return ''; +} # updateIssueWatchers + +sub getUsersGroups ($) { + my ($username) = @_; + + my ($result, %query); + + %query = ( + username => $username, + expand => 'groups', + ); + + eval {$result = $jira->GET ('/user/', \%query)}; + + my @groups; + + for (@{$result->{groups}{items}}) { + push @groups, $_->{name}; + } # for + + return @groups; +} # getusersGroups + +sub updateUsersGroups ($@) { + my ($username, @groups) = @_; + + my ($result, @errors); + + my @oldgroups = getUsersGroups $username; + + # We can't always add groups to the new user due to either the group not being + # in the new LDAP directory or we are unable to see it. If we attempt to JIRA + # will try to add the group and we don't have write permission to the + # directory. So we'll just return @errors and let the caller deal with it. + for my $group (@groups) { + next if grep {$_ eq $group} @oldgroups; + + eval {$result = $jira->POST ('/group/user', {groupname => $group}, {name => $username})}; + + push @errors, $@ if $@; + } # for + + return @errors; +} # updateUsersGroups + +sub copyGroupMembership ($$) { + my ($from_username, $to_username) = @_; + + return updateUsersGroups $to_username, getUsersGroups $from_username; +} # copyGroupMembership + +sub updateColumn ($$$%) { + my ($table, $oldvalue, $newvalue, %info) = @_; + + # UGH! Sometimes values need to be quoted + $oldvalue = quotemeta $oldvalue; + $newvalue = quotemeta $newvalue; + + my $condition = "$info{column} = '$oldvalue'"; + $condition .= " and $info{condition}" if $info{condition}; + my $statement = "update $table set $info{column} = '$newvalue' where $condition"; + + my $nbrRows = count $table, $condition; + + if ($nbrRows) { + if ($main::opts{exec}) { + $main::total{'Rows updated'}++; + + $jiradb->do ($statement); + + _checkDBError 'Unable to execute statement', $statement; + } else { + $main::total{'Rows would be updated'}++; + + $main::log->msg ("Would have executed $statement") if $main::log; + } # if + } # if + + return $nbrRows; +} # updateColumn + +sub renameUsers (%) { + my (%users) = @_; + + for my $olduser (sort keys %users) { + my $newuser = $users{$olduser}; + + $main::log->msg ("Renaming $olduser -> $newuser") if $main::log; + display ("Renaming $olduser -> $newuser"); + + if ($main::opts{exec}) { + $main::total{'Users renamed'}++; + } else { + $main::total{'Users would be updated'}++; + } # if + + for my $table (sort keys %tables) { + $main::log->msg ("\tTable: $table Column: ", 1) if $main::log; + + my @columns = @{$tables{$table}}; + + for my $column (@columns) { + my %info = %$column; + + $main::log->msg ("$info{column} ", 1) if $main::log; + + my $rowsUpdated = updateColumn ($table, $olduser, $newuser, %info); + + if ($rowsUpdated) { + my $msg = " $rowsUpdated row"; + $msg .= 's' if $rowsUpdated > 1; + $msg .= ' would have been' unless $main::opts{exec}; + $msg .= ' updated'; + + $main::log->msg ($msg, 1) if $main::log; + } # if + } # for + + $main::log->msg ('') if $main::log; + } # for + + if (my @result = copyGroupMembership ($olduser, $newuser)) { + # Skip errors of the form 'Could not add user... group is read-only + @result = grep {!/Could not add user.*group is read-only/} @result; + + if ($main::log) { + $main::log->err ("Unable to copy group membership from $olduser -> $newuser\n@result", 1) if @result; + } # if + } # if + } # for + + + return $main::log ? $main::log->errors : 0; +} # renameUsers + +1; diff --git a/audience/JIRA/updateWatchLists.pl b/JIRA/updateWatchLists.pl similarity index 100% rename from audience/JIRA/updateWatchLists.pl rename to JIRA/updateWatchLists.pl diff --git a/Perforce/getPicture.conf b/Perforce/getPicture.conf new file mode 100644 index 0000000..51ea679 --- /dev/null +++ b/Perforce/getPicture.conf @@ -0,0 +1,21 @@ +################################################################################ +# +# File: getPicture.conf +# Description: Configuration file for getPicture.pl +# Author: Andrew DeFaria +# Version: 1.0 +# Created: Fri Oct 3 18:16:26 PDT 2014 +# Modified: $Date: 2014/10/03 18:17:20 $ +# Language: Conf +# +# This file contains configurable parameters that are unique to your site that +# identify the LDAP parms needed to retrieve users pictures from Active +# Directory. It is assumed that thumbnailPhoto is the attribute name and that +# the user can be found by their unique uid. +# +################################################################################ +AD_HOST: ad-vip.audience.local +AD_PORT: 389 +AD_BINDDN: CN=jenkins cm,OU=Service Accounts,DC=audience,DC=local +AD_BINDPW: j55ln8Y6 +AD_BASEDN: DC=audience,DC=local \ No newline at end of file diff --git a/Perforce/getPicture.pl b/Perforce/getPicture.pl new file mode 100755 index 0000000..5a36c8c --- /dev/null +++ b/Perforce/getPicture.pl @@ -0,0 +1,203 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +=pod + +=head1 NAME $File: //AudEngr/Import/VSS/ReleaseEng/Dev/Perforce/getPicture.pl $ + +Retrieve thumbnailPhoto for the userid from Active Directory + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Fri Oct 3 18:16:26 PDT 2014 + +=item Modified + +$Date: 2015/03/03 $ + +=back + +=head1 DESCRIPTION + +This script will take a userid and search the Active Directory for the user and +return an image file if the user has an image associated with his +thumbnailPhoto attribute. + +This can be configured into Perforce Swarn as documented: + +http://www.perforce.com/perforce/doc.current/manuals/swarm/admin.avatars.html + +One would use something like + + // this block shoudl be a peer of 'p4' + 'avatars' => array( + 'http_url' => 'http:///cgi-bin/getPicture.pl?userid={user}' + 'https_url' => 'http:///cgi-bin/getPicture.pl?userid={user}', + ), + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; +use Net::LDAP; +use CGI qw (:standard); + +# Interpolate variable in str (if any) from %opts +sub interpolate ($%) { + my ($str, %opts) = @_; + + # Since we wish to leave undefined $var references in tact the following while + # loop would loop indefinitely if we don't change the variable. So we work + # with a copy of $str changing it always, but only changing the original $str + # for proper interpolations. + my $copyStr = $str; + + while ($copyStr =~ /\$(\w+)/) { + my $var = $1; + + if (exists $opts{$var}) { + $str =~ s/\$$var/$opts{$var}/; + $copyStr =~ s/\$$var/$opts{$var}/; + } elsif (exists $ENV{$var}) { + $str =~ s/\$$var/$ENV{$var}/; + $copyStr =~ s/\$$var/$ENV{$var}/; + } else { + $copyStr =~ s/\$$var//; + } # if + } # while + + return $str; +} # interpolate + +sub _processFile ($%) { + my ($configFile, %opts) = @_; + + while (<$configFile>) { + chomp; + + next if /^\s*[\#|\!]/; # Skip comments + + if (/\s*(.*?)\s*[:=]\s*(.*)\s*/) { + my $key = $1; + my $value = $2; + + # Strip trailing spaces + $value =~ s/\s+$//; + + # Interpolate + $value = interpolate $value, %opts; + + if ($opts{$key}) { + # If the key exists already then we have a case of multiple values for + # the same key. Since we support this we need to replace the scalar + # value with an array of values... + if (ref $opts{$key} eq "ARRAY") { + # It's already an array, just add to it! + push @{$opts{$key}}, $value; + } else { + # It's not an array so make it one + my @a; + + push @a, $opts{$key}; + push @a, $value; + $opts{$key} = \@a; + } # if + } else { + # It's a simple value + $opts{$key} = $value; + } # if + } # if + } # while + + return %opts; +} # _processFile + +sub GetConfig ($) { + my ($filename) = @_; + + my %opts; + + open my $configFile, '<', $filename + or die "Unable to open config file $filename"; + + %opts = _processFile $configFile; + + close $configFile; + + return %opts; +} # GetConfig + +sub checkLDAPError ($$) { + my ($msg, $result) = @_; + + my $code = $result->code; + + die "$msg (Error $code)\n" . $result->error if $code; +} # checkLDAPError + +my ($confFile) = ($FindBin::Script =~ /(.*)\.pl$/); + $confFile = "$FindBin::Bin/$confFile.conf"; + +my %opts = GetConfig ($confFile); + +## Main +$| = 1; + +GetOptions ( + \%opts, + 'AD_HOST=s', + 'AD_PORT=s', + 'AD_BINDDN=s', + 'AD_BINDPW=s', + 'AD_BASEDN=s', + 'userid=s', +) or pod2usage; + +$opts{userid} = param 'userid' unless $opts{userid}; + +pod2usage "Usage getPicture.pl [userid=]\n" unless $opts{userid}; + +my $ldap = Net::LDAP->new ( + $opts{AD_HOST}, ( + host => $opts{AD_HOST}, + port => $opts{AD_PORT}, + basedn => $opts{AD_BASEDN}, + binddn => $opts{AD_BINDDN}, + bindpw => $opts{AD_BINDPW}, + ), +) or die $@; + +my $result = $ldap->bind ( + dn => $opts{AD_BINDDN}, + password => $opts{AD_BINDPW}, +) or die "Unable to bind\n$@"; + +checkLDAPError ('Unable to bind', $result); + +$result = $ldap->search ( + base => $opts{AD_BASEDN}, + filter => "sAMAccountName=$opts{userid}", +); + +checkLDAPError ('Unable to search', $result); + +my @entries = ($result->entries); + +if ($entries[0]) { + print header 'image/jpeg'; + print $entries[0]->get_value ('thumbnailPhoto'); +} # if diff --git a/Perforce/lib/Perforce.pm b/Perforce/lib/Perforce.pm new file mode 100644 index 0000000..a2d5897 --- /dev/null +++ b/Perforce/lib/Perforce.pm @@ -0,0 +1,394 @@ +package Perforce; + +use strict; +use warnings; + +use Carp; +use File::Basename; +use File::Temp; + +use P4; +use Authen::Simple::LDAP; + +use Display; +use GetConfig; +use Utils; + +our $VERSION = '$Revision: 2.23 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4config = $ENV{P4_CONF} || dirname (__FILE__) . '/../etc/p4.conf'; +my $ldapconfig = $ENV{LDAP_CONF} || dirname (__FILE__) . '/../etc/LDAP.conf'; + +my %P4OPTS = GetConfig $p4config if -r $p4config; +my %LDAPOPTS = GetConfig $ldapconfig if -r $ldapconfig; + +my $serviceUser = 'shared'; +my ($domain, $password); +my $defaultPort = 'perforce:1666'; +my $p4tickets = $^O =~ /win/i ? 'C:/Program Files/Devops/Perforce/p4tickets' + : '/opt/audience/perforce/p4tickets'; + +my $keys; + +# If USERDOMAIN is set and equal to audience then set $domain to ''. This will +# use the Audience domain settings in LDAP.conf. +if ($ENV{USERDOMAIN}) { + if (lc $ENV{USERDOMAIN} eq 'audience') { + $domain = ''; + } else { + $domain = $ENV{USERDOMAIN} + } # if +} # if + +sub new (;%) { + my ($class, %parms) = @_; + + my $self = bless {}, $class; + + $self->{P4USER} = $parms{username} || $P4OPTS{P4USER} || $ENV{P4USER} || $serviceUser; + $self->{P4PASSWD} = $parms{password} || $P4OPTS{P4PASSWD} || $ENV{P4PASSWD} || undef; + $self->{P4CLIENT} = $parms{p4client} || $P4OPTS{P4CLIENT} || $ENV{P4CLIENT} || undef; + $self->{P4PORT} = $parms{p4port} || $ENV{P4PORT} || $defaultPort; + + $self->{P4} = $self->connect (%parms); + + return $self; +} # new + +sub errors ($;$) { + my ($self, $cmd, $exit) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + my $errors = $self->{P4}->ErrorCount; + + error "$msg\n" . $self->{P4}->Errors, $exit if $errors; + + return $errors; +} # errors + +sub connect () { + my ($self) = @_; + + $self->{P4} = P4->new; + + $self->{P4}->SetUser ($self->{P4USER}); + $self->{P4}->SetClient ($self->{P4CLIENT}) if $self->{P4CLIENT}; + $self->{P4}->SetPort ($self->{P4PORT}); + $self->{P4}->SetPassword ($self->{P4PASSWD}) unless $self->{P4USER} eq $serviceUser; + + verbose_nolf "Connecting to Perforce server $self->{P4PORT}..."; + $self->{P4}->Connect or croak "Unable to connect to Perforce Server\n"; + verbose 'done'; + + verbose_nolf "Logging in as $self->{P4USER}\@$self->{P4PORT}..."; + + unless ($self->{P4USER} eq $serviceUser) { + $self->{P4}->RunLogin; + + $self->errors ('login', $self->{P4}->ErrorCount); + } else { + $ENV{P4TICKETS} = $p4tickets if $self->{P4USER} eq $serviceUser; + } # unless + + verbose 'done'; + + return $self->{P4}; +} # connect + +sub _authenticateUser ($$$$) { + my ($self, $domain, $username, $p4client) = @_; + + $domain .= '_' unless $domain eq ''; + + # Connect to LDAP + my $ad = Authen::Simple::LDAP->new ( + host => $LDAPOPTS{"${domain}AD_HOST"}, + basedn => $LDAPOPTS{"${domain}AD_BASEDN"}, + port => $LDAPOPTS{"${domain}AD_PORT"}, + filter => $LDAPOPTS{"${domain}AD_FILTER"}, + ) or croak $@; + + # Read the password from and truncate the newline - unless we already + # read in the password + unless ($password) { + if (-t STDIN) { + $password = GetPassword; + } else { + $password = ; + + chomp $password; + } # if + } # unless + + # Special handling of "shared" user + if ($username eq 'shared') { + my $sharedAcl = "$FindBin::Bin/sharedAcl.txt"; + + croak "Unable to find file $sharedAcl" unless -f $sharedAcl; + + open my $sharedAcls, '<', $sharedAcl + or croak "Unable to open $sharedAcl - $!"; + + chomp (my @acls = <$sharedAcls>); + + close $sharedAcls; + + for (@acls) { + if (/\*$/) { + chop; + + exit if $p4client =~ /$_/; + } else { + exit if $_ eq $p4client; + } # if + } # for + } # if + + # Connect to Perforce + $self->connect unless $self->{P4}; + + # Must be a valid Perforce user + return unless $self->getUser ($username); + + # And supply a valid username/password + return $ad->authenticate ($username, $password); +} # _authenticateUser + +sub authenticateUser ($;$) { + my ($self, $username, $p4client) = @_; + +=pod + # If $domain is set to '' then we'll check Audience's LDAP. + # If $domain is not set (undef) then we'll try Knowles first, then Audience + # otherwise we will take $DOMAIN and look for those settings... + unless ($domain) { + unless ($self->_authenticateUser ('KNOWLES', $username, $p4client)) { + unless ($self->_authenticateUser ('', $username, $p4client)) { + return; + } # unless + } # unless + } else { + if ($domain eq '') { + unless ($self->_authenticateUser ('', $username, $p4client)) { + return; + } # unless + } else { + unless ($self->_authenticateUser ($domain, $username, $p4client)) { + return; + } # unless + } # if + } # unless +=cut + + return $self->_authenticateUser ('KNOWLES', $username, $p4client); + +# return 1; +} # authenticateUser + +sub changes (;$%) { + my ($self, $args, %opts) = @_; + + my $cmd = 'changes'; + + for (keys %opts) { + if (/from/i and $opts{to}) { + $args .= " $opts{$_},$opts{to}"; + + delete $opts{to}; + } else { + $args .= " $opts{$_}"; + } # if + } # for + + my $changes = $self->{P4}->Run ($cmd, $args); + + return $self->errors ("$cmd $args") || $changes; +} # changes + +sub job ($) { + my ($self, $job) = @_; + + my $jobs = $self->{P4}->IterateJobs ("-e $job"); + + return $self->errors ("jobs -e $job") || $job; +} # job + +sub comments ($) { + my ($self, $changelist) = @_; + + my $change = $self->{P4}->FetchChange ($changelist); + + return $self->errors ("change $changelist") || $change; +} # comments + +sub files ($) { + my ($self, $changelist) = @_; + + my $files = $self->{P4}->Run ('files', "\@=$changelist"); + + return $self->errors ("files \@=$changelist") || $files; +} # files + +sub filelog ($;%) { + my ($self, $fileSpec, %opts) = @_; + + return $self->{P4}->RunFilelog ($fileSpec, %opts); +} # filelog + +sub getRevision ($;$) { + my ($self, $filename, $revision) = @_; + + unless ($revision) { + if ($filename =~ /#/) { + ($filename, $revision) = split $filename, '#'; + } else { + error "No revision specified in $filename"; + + return; + } # if + } # unlessf + + my @contents = $self->{P4}->RunPrint ("$filename#$revision"); + + if ($self->{P4}->ErrorCount) { + $self->errors ("Print $filename#$revision"); + + return; + } else { + return @contents; + } # if +} # getRevision + +sub getUser (;$) { + my ($self, $user) = @_; + + $user //= $ENV{P4USER} || $ENV{USER}; + + my $cmd = 'user'; + my @args = ('-o', $user); + + my $userRecs = $self->{P4}->Run ($cmd, @args); + + # Perforce returns an array of qualifying users. We only care about the first + # one. However if the username is invalid, Perforce still returns something + # that looks like a user. We look to see if there is a Type field here which + # indicates that it's a valid user + if ($userRecs->[0]{Type}) { + return %{$userRecs->[0]}; + } else { + return; + } # if +} # getUser + +sub renameSwarmUser ($$) { + my ($self, $oldusername, $newusername) = @_; + + # We are turning this off because Perforce support says that just modifying + # the keys we do not update the indexing done in the Perforce Server/Database. + # So instead we have a PHP script (renameUser.php) which goes through the + # official, but still unsupported, "Swarm Record API" to change the usernames + # and call the object's method "save" which should perform the necessary + # reindexing... Stay tuned! :-) + # + # BTW One needs to run renameUser.php by hand as we do not do that here. + return; + + $keys = $self->getKeys ('swarm-*') unless $keys; + + for (@$keys) { + my %key = %$_; + + if ($key{value} =~ /$oldusername/) { + $key{value} =~ s/\"$oldusername\"/\"$newusername\"/g; + $key{value} =~ s/\@$oldusername /\@$newusername /g; + $key{value} =~ s/\@$oldusername\./\@$newusername\./g; + $key{value} =~ s/\@$oldusername,/\@$newusername,/g; + $key{value} =~ s/ $oldusername / $newusername /g; + $key{value} =~ s/ $oldusername,/ $newusername,/g; + $key{value} =~ s/ $oldusername\./ $newusername\./g; + $key{value} =~ s/-$oldusername\"/-$newusername\"/g; + + my $cmd = 'key'; + + display "Correcting key $key{key}"; + + my @result = $self->{P4}->Run ($cmd, $key{key}, $key{value}); + + $self->errors ($cmd, $result[0]->{key} || 1); + } # if + } # for + + return; +} # renameSwarmUser + +sub renameUser ($$) { + my ($self, $old, $new) = @_; + + my $cmd = 'renameuser'; + my @args = ("--from=$old", "--to=$new"); + + $self->{P4}->Run ($cmd, @args); + + my $status = $self->errors (join ' ', $cmd, @args); + + return $status if $status; + +# return $self->renameSwarmUser ($old, $new); +} # renameUser + +sub updateUser (%) { + my ($self, %user) = @_; + + # Trying to do this with P4Perl is difficult. First off the structure needs + # to be AOH and secondly you need to call SetUser to be the other user. That + # said you need to also specify -f to force the update (which means you must + # a admin (or superuser?) and I found no way to specify -f so I've reverted + # back to using p4 from the command line. I also don't like having to use + # a file here... + my $tmpfile = File::Temp->new; + my $tmpfilename = $tmpfile->filename; + + print $tmpfile "User: $user{User}\n"; + print $tmpfile "Email: $user{Email}\n"; + print $tmpfile "Update: $user{Update}\n"; + print $tmpfile "FullName: $user{FullName}\n"; + + close $tmpfile; + + my @lines = `p4 -p $self->{P4PORT} user -f -i < $tmpfilename`; + my $status = $?; + + return wantarray ? @lines : join '', @lines; +} # updateUser + +sub getKeys (;$) { + my ($self, $filter) = @_; + + my $cmd = 'keys'; + my @args; + + if ($filter) { + push @args, '-e'; + push @args, $filter; + } # if + + my $keys = $self->{P4}->Run ($cmd, @args); + + $self->errors ($cmd . join (' ', @args), 1); + + return $keys; +} # getKeys + +sub key ($$) { + my ($self, $name, $value) = @_; + + my $cmd = 'key'; + my @args = ($name, $value); + + $self->{P4}->Run ($cmd, @args); + + return $self->errors (join ' ', $cmd, @args); +} # key + +1; diff --git a/Perforce/renameUser.pl b/Perforce/renameUser.pl new file mode 100755 index 0000000..f542372 --- /dev/null +++ b/Perforce/renameUser.pl @@ -0,0 +1,224 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +=pod + +=head1 NAME RenameUser.pl + +Renames a Perforce user in Perforce + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #1 $ + +=item Created + +Fri Oct 30 12:16:39 PDT 2015 + +=item Modified + +$Date: $ + +=back + +=head1 SYNOPSIS + + $ RenameUser.pl [-oldusername -newusername | + -file ] [-p4port ] + [-username ] [-password ] + [-[no]exec] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -o|ldusername: Old username to rename + -ne|wusername: New username + -f|ile: File of usernames to process + ( ) + -p|4port: Perforce port (Default: perforce:1666) + -use|rname: Username to log in as (Default: root) + -p|assword: Password for -username (Defaul: ) + -[no]e|xec: Whether or not to update the database + + -v|erbose: Display verbose info about actions being taken + -d|ebug: Display debug info + -h|elp: Display full documentation + -usa|ge: Display usage + +Note that -file is a list of whitespace separated usernames, one per line listed +as + +=head1 DESCRIPTION + +This script will rename a Perforce user from -oldusername to -newusername. It +will also update the users email address. + +=cut + +use Getopt::Long; +use Pod::Usage; +use FindBin; + +use lib "$FindBin::Bin/../lib"; + +use CMUtils; +use Display; +use Logger; +use Perforce; +use Utils; +use TimeUtils; + +my ($p4, $log, $keys); + +my %total; + +my %opts = ( + p4port => $ENV{P4PORT}, + username => $ENV{P4USER}, + password => $ENV{P4PASSWD}, + verbose => $ENV{VERBOSE} || sub { set_verbose }, + debug => $ENV{DEBUG} || sub { set_debug }, + usage => sub { pod2usage }, + help => sub { pod2usage (-verbose => 2)}, +); + +sub check4Dups (%) { + my (%users) = @_; + + my %newusers; + + for my $key (keys %users) { + my $value = $users{$key}; + + if ($users{$value}) { + $log->warn ("$value exists as both a key and a value"); + } else { + $newusers{$key} = $users{$key}; + } # if + } # for + + return %newusers; +} # check4Dups + +sub renameUsers (%) { + my (%users) = @_; + + for my $olduser (keys %users) { + my $newuser = $users{$olduser}; + + if ($opts{exec}) { + if ($p4->getUser ($olduser)) { + my $status = $p4->renameUser ($olduser, $newuser); + + unless ($status) { + $log->msg ("Renamed $olduser -> $newuser"); + + $total{'Users renamed'}++; + } else { + $log->err ("Unable to rename $olduser -> $newuser"); + + return 1; + } # unless + } else { + $total{'Non Perforce users'}++; + + $log->msg ("$olduser is not a Perforce user"); + + next; + } # if + } else { + $total{'Users would be renamed'}++; + + next; + } # if + + my %user = $p4->getUser ($newuser); + + $log->err ("Unable to retrieve user info for $newuser", 1) unless %user; + + my $email = getUserEmail ($newuser); + + if ($user{Email} ne $email) { + $user{Email} = $email; + + my $result = $p4->updateUser (%user); + + $log->err ("Unable to update user $newuser", 1) unless $result; + + $log->msg ("Updated ${newuser}'s email to $email"); + + $total{'User email updated'}++; + } # if + } # for + + return $log->errors; +} # renameUsers + +sub main () { + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'usage', + 'help', + 'jiradbserver=s', + 'username=s', + 'password=s', + 'file=s', + 'oldusername=s', + 'newusername=s', + 'exec!', + ) or pod2usage; + + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + $log = Logger->new; + + if ($opts{username} && !$opts{password}) { + $opts{password} = GetPassword; + } # if + + $p4 = Perforce->new (%opts); + + my %users; + + my $startTime = time; + + if ($opts{oldusername} and $opts{newusername}) { + $opts{oldusername} = lc $opts{oldusername}; + $opts{newusername} = lc $opts{newusername}; + + $users{$opts{oldusername}} = $opts{newusername}; + } elsif ($opts{file}) { + for (ReadFile $opts{file}) { + my ($olduser, $newuser) = split; + + $users{lc $olduser} = lc $newuser; + } # while + } else { + pod2usage "You must specify either -file or -oldname/-newname"; + } # if + + %users = check4Dups %users; + + my $status = renameUsers (%users); + + display_duration $startTime, $log; + + Stats \%total, $log; + + return $status; +} # main + +exit main; \ No newline at end of file diff --git a/audience/JIRA/lib/JIRAUtils.pm b/audience/JIRA/lib/JIRAUtils.pm deleted file mode 100644 index a60e01c..0000000 --- a/audience/JIRA/lib/JIRAUtils.pm +++ /dev/null @@ -1,655 +0,0 @@ -=pod - -=head1 NAME $RCSfile: JIRAUtils.pm,v $ - -Some shared functions dealing with JIRA - -=head1 VERSION - -=over - -=item Author - -Andrew DeFaria - -=item Revision - -$Revision: 1.0 $ - -=item Created - -Fri Mar 12 10:17:44 PST 2004 - -=item Modified - -$Date: 2013/05/30 15:48:06 $ - -=back - -=head1 ROUTINES - -The following routines are exported: - -=cut - -package JIRAUtils; - -use strict; -use warnings; - -use base 'Exporter'; - -use FindBin; -use Display; -use Carp; -use DBI; - -use JIRA::REST; -use BugzillaUtils; - -our ($jira, %opts); - -our @EXPORT = qw ( - addJIRAComment - addDescription - Connect2JIRA - findIssue - getIssue - getIssueLinks - getIssueLinkTypes - getRemoteLinks - updateIssueWatchers - linkIssues - addRemoteLink - getRemoteLink - removeRemoteLink - getRemoteLinkByBugID - promoteBug2JIRAIssue - findRemoteLinkByBugID -); - -my (@issueLinkTypes, %total, %cache, $jiradb); - -sub _checkDBError ($;$) { - my ($msg, $statement) = @_; - - $statement //= 'Unknown'; - - $main::log->err ('JIRA database not opened!', 1) unless $jiradb; - - my $dberr = $jiradb->err; - my $dberrmsg = $jiradb->errstr; - - $dberr ||= 0; - $dberrmsg ||= 'Success'; - - my $message = ''; - - if ($dberr) { - my $function = (caller (1)) [3]; - - $message = "$function: $msg\nError #$dberr: $dberrmsg\n" - . "SQL Statement: $statement"; - } # if - - $main::log->err ($message, 1) if $dberr; - - return; -} # _checkDBError - -sub openJIRADB (;$$$$) { - my ($dbhost, $dbname, $dbuser, $dbpass) = @_; - - $dbhost //= $main::opts{jiradbhost}; - $dbname //= 'jiradb'; - $dbuser //= 'adefaria'; - $dbpass //= 'reader'; - - $main::log->msg ("Connecting to JIRA ($dbuser\@$dbhost)..."); - - $jiradb = DBI->connect ( - "DBI:mysql:$dbname:$dbhost", - $dbuser, - $dbpass, { - PrintError => 0, - RaiseError => 1, - }, - ); - - _checkDBError "Unable to open $dbname ($dbuser\@$dbhost)"; - - return $jiradb; -} # openJIRADB - -sub Connect2JIRA (;$$$) { - my ($username, $password, $server) = @_; - - my %opts; - - $opts{username} = $username || 'jira-admin'; - $opts{password} = $password || $ENV{PASSWORD} || 'jira-admin'; - $opts{server} = $server || $ENV{JIRA_SERVER} || 'jira-dev:8081'; - $opts{URL} = "http://$opts{server}/rest/api/latest"; - - $main::log->msg ("Connecting to JIRA ($opts{username}\@$opts{server})"); - - $jira = JIRA::REST->new ($opts{URL}, $opts{username}, $opts{password}); - - # Store username as we might need it (see updateIssueWatchers) - $jira->{username} = $opts{username}; - - return $jira; -} # Connect2JIRA - -sub addDescription ($$) { - my ($issue, $description) = @_; - - if ($main::opts{exec}) { - eval {$jira->PUT ("/issue/$issue", undef, {fields => {description => $description}})}; - - if ($@) { - return "Unable to add description\n$@"; - } else { - return 'Description added'; - } # if - } # if -} # addDescription - -sub addJIRAComment ($$) { - my ($issue, $comment) = @_; - - if ($main::opts{exec}) { - eval {$jira->POST ("/issue/$issue/comment", undef, { body => $comment })}; - - if ($@) { - return "Unable to add comment\n$@"; - } else { - return 'Comment added'; - } # if - } else { - return "Would have added comments to $issue"; - } # if -} # addJIRAComment - -sub findIssue ($%) { - my ($bugid, %bugmap) = @_; - -=pod - # Check the cache... - if ($cache{$bugid}) { - if ($cache{$bugid} =~ /^\d+/) { - # We have a cache hit but the contents here are a bugid. This means we had - # searched for the corresponding JIRA issue for this bug before and came - # up empty handed. In this situtaion we really have: - return "Unable to find a JIRA issue for Bug $bugid"; - } else { - return $cache{$bugid}; - } # if - } # if -=cut - my $issue; - - my %query = ( - jql => "\"Bugzilla Bug Number\" ~ $bugid", - fields => [ 'key' ], - ); - - eval {$issue = $jira->GET ("/search/", \%query)}; - - my $issueID = $issue->{issues}[0]{key}; - - if (@{$issue->{issues}} > 2) { - $main::log->err ("Found more than 2 issues for Bug ID $bugid"); - - return "Found more than 2 issues for Bug ID $bugid"; - } elsif (@{$issue->{issues}} == 2) { - my ($issueNum0, $issueNum1); - - if ($issue->{issues}[0]{key} =~ /(\d+)/) { - $issueNum0 = $1; - } # if - - if ($issue->{issues}[1]{key} =~ /(\d+)/) { - $issueNum1 = $1; - } # if - - if ($issueNum0 < $issueNum1) { - $issueID = $issue->{issues}[1]{key}; - } # if - - # Let's mark them as clones. See if this clone link already exists... - my $alreadyCloned; - - for (getIssueLinks ($issueID, 'Cloners')) { - my $inwardIssue = $_->{inwardIssue}{key} || ''; - my $outwardIssue = $_->{outwardIssue}{key} || ''; - - if ("RDBNK-$issueNum0" eq $inwardIssue || - "RDBNK-$issueNum0" eq $outwardIssue || - "RDBNK-$issueNum1" eq $inwardIssue || - "RDBNK-$issueNum1" eq $outwardIssue) { - $alreadyCloned = 1; - - last; - } # if - } # for - - unless ($alreadyCloned) { - my $result = linkIssues ("RDBNK-$issueNum0", 'Cloners', "RDBNK-$issueNum1"); - - return $result if $result =~ /Unable to/; - - $main::log->msg ($result); - } # unless - } # if - - if ($issueID) { - $main::log->msg ("Found JIRA issue $issueID for Bug $bugid"); - - #$cache{$bugid} = $issueID; - - #return $cache{$bugid}; - return $issueID; - } else { - my $status = $bugmap{$bugid} ? 'Future JIRA Issue' - : "Unable to find a JIRA issue for Bug $bugid"; - - # Here we put this bugid into the cache but instead of a the JIRA issue - # id we put the bugid. This will stop us from adding up multiple hits on - # this bugid. - #$cache{$bugid} = $bugid; - - return $status; - } # if -} # findJIRA - -sub getIssue ($;@) { - my ($issue, @fields) = @_; - - my $fields = @fields ? "?fields=" . join ',', @fields : ''; - - return $jira->GET ("/issue/$issue$fields"); -} # getIssue - -sub getIssueLinkTypes () { - my $issueLinkTypes = $jira->GET ('/issueLinkType/'); - - map {push @issueLinkTypes, $_->{name}} @{$issueLinkTypes->{issueLinkTypes}}; - - return @issueLinkTypes -} # getIssueLinkTypes - -sub linkIssues ($$$) { - my ($from, $type, $to) = @_; - - unless (@issueLinkTypes) { - getIssueLinkTypes; - } # unless - - unless (grep {$type eq $_} @issueLinkTypes) { - $main::log->err ("Type $type is not a valid issue link type\nValid types include:\n" - . join "\n\t", @issueLinkTypes); - - return "Unable to $type link $from -> $to"; - } # unless - - my %link = ( - inwardIssue => { - key => $from, - }, - type => { - name => $type, - }, - outwardIssue => { - key => $to, - }, - comment => { - body => "Link ported as part of the migration from Bugzilla: $from <-> $to", - }, - ); - - $main::total{'IssueLinks Added'}++; - - if ($main::opts{exec}) { - eval {$jira->POST ("/issueLink", undef, \%link)}; - - if ($@) { - return "Unable to $type link $from -> $to\n$@"; - } else { - return "Made $type link $from -> $to"; - } # if - } else { - return "Would have $type linked $from -> $to"; - } # if -} # linkIssue - -sub getRemoteLink ($;$) { - my ($jiraIssue, $id) = @_; - - $id //= ''; - - my $result; - - eval {$result = $jira->GET ("/issue/$jiraIssue/remotelink/$id")}; - - return if $@; - - my %remoteLinks; - - if (ref $result eq 'ARRAY') { - map {$remoteLinks{$_->{id}} = $_->{object}{title}} @$result; - } else { - $remoteLinks{$result->{id}} = $result->{object}{title}; - } # if - - return \%remoteLinks; -} # getRemoteLink - -sub getRemoteLinks (;$) { - my ($bugid) = @_; - - $jiradb = openJIRADB unless $jiradb; - - my $statement = 'select url from remotelink'; - - $statement .= " where url like 'http://bugs%'"; - $statement .= " and url like '%$bugid'" if $bugid; - $statement .= " group by issueid desc"; - - my $sth = $jiradb->prepare ($statement); - - _checkDBError 'Unable to prepare statement', $statement; - - $sth->execute; - - _checkDBError 'Unable to execute statement', $statement; - - my %bugids; - - while (my $record = $sth->fetchrow_array) { - if ($record =~ /(\d+)$/) { - $bugids{$1} = 1; - } # if - } # while - - return keys %bugids; -} # getRemoteLinks - -sub findRemoteLinkByBugID (;$) { - my ($bugid) = @_; - - my $condition = 'where issueid = jiraissue.id and jiraissue.project = project.id'; - - if ($bugid) { - $condition .= " and remotelink.url like '%id=$bugid'"; - } # unless - - $jiradb = openJIRADB unless $jiradb; - - my $statement = <<"END"; -select - remotelink.id, - concat (project.pkey, '-', issuenum) as issue, - relationship -from - remotelink, - jiraissue, - project -$condition -END - - my $sth = $jiradb->prepare ($statement); - - _checkDBError 'Unable to prepare statement', $statement; - - $sth->execute; - - _checkDBError 'Unable to execute statement', $statement; - - my @records; - - while (my $row = $sth->fetchrow_hashref) { - $row->{bugid} = $bugid; - - push @records, $row; - } # while - - return \@records; -} # findRemoteLinkByBugID - -sub promoteBug2JIRAIssue ($$$$) { - my ($bugid, $jirafrom, $jirato, $relationship) = @_; - - my $result = linkIssues $jirafrom, $relationship, $jirato; - - return $result if $result =~ /Unable to link/; - - $main::log->msg ($result . " (BugID $bugid)"); - - for (@{findRemoteLinkByBugID $bugid}) { - my %record = %$_; - - $result = removeRemoteLink ($record{issue}, $record{id}); - - # We may not care if we couldn't remove this link because it may have been - # removed by a prior pass. - return $result if $result =~ /Unable to remove link/; - - $main::log->msg ($result) unless $result eq ''; - } # for - - return $result; -} # promoteBug2JIRAIssue - -sub addRemoteLink ($$$) { - my ($bugid, $relationship, $jiraIssue) = @_; - - my $bug = getBug $bugid; - - # Check to see if this Bug ID already exists on this JIRA Issue, otherwise - # JIRA will duplicate it! - my $remoteLinks = getRemoteLink $jiraIssue; - - for (keys %$remoteLinks) { - if ($remoteLinks->{$_} =~ /Bug (\d+)/) { - return "Bug $bugid is already linked to $jiraIssue" if $bugid == $1; - } # if - } # for - - # Note this globalid thing is NOT working! ALl I see is null in the database - my %remoteLink = ( -# globalid => "system=http://bugs.audience.com/show_bug.cgi?id=$bugid", -# application => { -# type => 'Bugzilla', -# name => 'Bugzilla', -# }, - relationship => $relationship, - object => { - url => "http://bugs.audience.com/show_bug.cgi?id=$bugid", - title => "Bug $bugid", - summary => $bug->{short_desc}, - icon => { - url16x16 => 'http://bugs.audience.local/favicon.png', - title => 'Bugzilla Bug', - }, - }, - ); - - $main::total{'RemoteLink Added'}++; - - if ($main::opts{exec}) { - eval {$jira->POST ("/issue/$jiraIssue/remotelink", undef, \%remoteLink)}; - - return $@; - } else { - return "Would have linked $bugid -> $jiraIssue"; - } # if -} # addRemoteLink - -sub removeRemoteLink ($;$) { - my ($jiraIssue, $id) = @_; - - $id //= ''; - - my $remoteLinks = getRemoteLink ($jiraIssue, $id); - - for (keys %$remoteLinks) { - my $result; - - $main::total{'RemoteLink Removed'}++; - - if ($main::opts{exec}) { - eval {$result = $jira->DELETE ("/issue/$jiraIssue/remotelink/$_")}; - - if ($@) { - return "Unable to remove remotelink $jiraIssue ($id)\n$@" if $@; - } else { - my $bugid; - - if ($remoteLinks->{$_} =~ /(\d+)/) { - return "Removed remote link $jiraIssue (Bug ID $1)"; - } # if - } # if - - $main::total{'Remote Links Removed'}++; - } else { - if ($remoteLinks->{$_} =~ /(\d+)/) { - return "Would have removed remote link $jiraIssue (Bug ID $1)"; - } # if - } # if - } # for -} # removeRemoteLink - -sub getIssueLinks ($;$) { - my ($issue, $type) = @_; - - my @links = getIssue ($issue, ('issuelinks')); - - my @issueLinks; - - for (@{$links[0]->{fields}{issuelinks}}) { - my %issueLink = %$_; - - next if ($type && $type ne $issueLink{type}{name}); - - push @issueLinks, \%issueLink; - } - - return @issueLinks; -} # getIssueLinks - -sub updateIssueWatchers ($%) { - my ($issue, %watchers) = @_; - - my $existingWatchers; - - eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")}; - - return "Unable to get issue $issue\n$@" if $@; - - for (@{$existingWatchers->{watchers}}) { - # Cleanup: Remove the current user from the watchers list. - # If he's on the list then remove him. - if ($_->{name} eq $jira->{username}) { - $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}"); - - $total{"Admins destroyed"}++; - } # if - - # Delete any matching watchers - delete $watchers{lc ($_->{name})} if $watchers{lc ($_->{name})}; - } # for - - return '' if keys %watchers == 0; - - my $issueUpdated; - - for (keys %watchers) { - if ($main::opts{exec}) { - eval {$jira->POST ("/issue/$issue/watchers", undef, $_)}; - - if ($@) { - $main::log->warn ("Unable to add user $_ as a watcher to JIRA Issue $issue"); - - $main::total{'Watchers skipped'}++; - } else { - $issueUpdated = 1; - - $main::total{'Watchers added'}++; - } # if - } else { - $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue"); - - $main::total{'Watchers that would have been added'}++; - } # if - } # for - - $main::total{'Issues updated'}++ if $issueUpdated; - - return ''; -} # updateIssueWatchers - -=pod - -I'm pretty sure I'm not using this routine anymore and I don't think it works. -If you wish to reserect this then please test. - -sub updateWatchers ($%) { - my ($issue, %watchers) = @_; - - my $existingWatchers; - - eval {$existingWatchers = $jira->GET ("/issue/$issue/watchers")}; - - if ($@) { - error "Unable to get issue $issue"; - - $main::total{'Missing JIRA Issues'}++; - - return; - } # if - - for (@{$existingWatchers->{watchers}}) { - # Cleanup: Mike Admin Cogan was added as a watcher for each issue imported. - # If he's on the list then remove him. - if ($_->{name} eq 'mcoganAdmin') { - $jira->DELETE ("/issue/$issue/watchers?username=$_->{name}"); - - $main::total{"mcoganAdmin's destroyed"}++; - } # if - - # Delete any matching watchers - delete $watchers{$_->{name}} if $watchers{$_->{name}}; - } # for - - return if keys %watchers == 0; - - my $issueUpdated; - - for (keys %watchers) { - if ($main::opts{exec}) { - eval {$jira->POST ("/issue/$issue/watchers", undef, $_)}; - - if ($@) { - error "Unable to add user $_ as a watcher to JIRA Issue $issue"; - - $main::total{'Watchers skipped'}++; - } else { - $main::total{'Watchers added'}++; - - $issueUpdated = 1; - } # if - } else { - $main::log->msg ("Would have added user $_ as a watcher to JIRA Issue $issue"); - - $main::total{'Watchers that would have been added'}++; - } # if - } # for - - $main::total{'Issues updated'}++ if $issueUpdated; - - return; -} # updateWatchers -=cut - -1; diff --git a/etc/LDAP.conf b/etc/LDAP.conf new file mode 100644 index 0000000..f5315da --- /dev/null +++ b/etc/LDAP.conf @@ -0,0 +1,30 @@ +################################################################################ +# +# File: LDAP.conf +# Description: Configuration file for LDAP +# Author: Andrew DeFaria +# Version: 1.0 +# Created: Fri Oct 3 18:16:26 PDT 2014 +# Modified: $Date: 2014/10/03 18:17:20 $ +# Language: Conf +# +# This file contains configurable parameters that are unique to your site that +# identify the LDAP parms needed to retrieve user information from Active +# Directory. +# +################################################################################ +AD_HOST: ad-vip.audience.local +AD_PORT: 389 +AD_BINDDN: CN=jenkins cm,OU=Service Accounts,DC=audience,DC=local +AD_BINDPW: j55ln8Y6 +AD_BASEDN: DC=audience,DC=local +AD_PRINCIPAL: audience.local + +#KNOWLES_AD_HOST: kmvdcgc01.knowles.com +KNOWLES_AD_HOST: 10.252.2.28 +KNOWLES_AD_PORT: 389 +KNOWLES_AD_BINDDN: CN=AD Reader,OU=Users,OU=KMV,OU=Knowles,DC=knowles,DC=com +KNOWLES_AD_BINDPW: @Dre@D2015 +KNOWLES_AD_BASEDN: DC=knowles,DC=com +KNOWLES_AD_PRINCIPAL: knowles.com +KNOWLES_AD_FILTER: (sAMAccountName=%s) diff --git a/rmc/Audience.png b/rmc/Audience.png new file mode 100644 index 0000000000000000000000000000000000000000..5420ea19245b6fd3973d55b859a3576bb6f8c6f9 GIT binary patch literal 4231 zcmV;25P0v2P)^9zsHilqgf85*nnEG|4PeZZee$Q4)!wQ4}i4 zecttbj=kIabWTpg{oH;y>+@Nkj(zs1=xOyBl}G=ij$2gWZ#^KD&oj%Etb z$siQq0_a?Xi=I0zkvz>)$j9<9d81pTkjM07ay2EOXb)Ya8xMBZ>$*{=cO5Bt~^yH86J!O0O? z>XJ#TJ&w~l-&_h_kWV3tPH^9_D4&An=To3}jsWK|TI86{jOjA@0L}dOAiJ(~KTb_A z-j0jWRaI4FVPQq9R;{MS#-<4|kQo_AY5H_?xmQ+J%HN$hae{1YY-#FLQ<^cuoDLmI zoG^=MLV~gK_Dfn|pRNf?zW)kD;eHng##<>EaQ(bI+O(vAwyZqOZaix3d5RQ$o9Gb% zXKC~DQ?y}WK1bXDF7&3${@_EXaC>-6!%r!g7a-``JxfopkUXU?>vf`XGY zF!0Yu?njQKvrSSjFRzflOH526GqV|d+{9!WZQZ(ULfAYZ!Pwo^OYz%^glmmwFxrZM z1Q9gAxk@k^m@wS`6h*B&Psw{q=vL7)4ovEwwA16N7P@-&F~vm`(N+OU1TBF+8bE>} zp(DYv03=-bgR-$P*^L`F$jZt_p4gzEV0PIP8H`}^xpUq5xSgFnUB6ypM4mn{)(Su& zPY8Vn;JEy8fb}1AVX$z)5v$J7?u{2YhuGKKFSOsF_`^p8J?{meb5ie8WZ*dlE*=f7{?zUAWs z1Ao!koGOZ3b5^+Q6YSpS2~Y#vQciN6Up}?I?jDt~*RNlv*|X;`BPu68BxDl@3A!5W z>ged8rluzDuU@^qUc(nO1p6n&HKC;bMunzMQ>n0Tz7OL!qjoJ;16Z3+x z`21NzPLq0j`YC$TC7HS4R(-4wb1(uxZ4<10GMg%noXyEc^e$|eM%CBlk93|@m z%RZs{rm1)w6N`M~@!S&Yio38+YLQDi<6)0o?nsV_6I=Wvi*F5%xOq zy`7z%>Ey|iY&*sZLDRHp)A?TJ<`(4dzmmVx1_yvqTzs9DELp~$M)@AN7TWyi(M(=Y z+IByF{FtJn_lR+rHH0K!x2>%`rKe}mvuDp#i}TMvi+RkiUcJhIML;(R_2yNc*791C4`lJ3f>#y~(1T{$ zCfs$9Umh(J+R@WIne⪙O`VaA2Arq%gf2dWj4R0wY3dJMQvBTID#5o4UD;deUN;f zm6erFFlJzO?MkU{WbkO=S=k|)96-MgdNEkkacKYyM<2swgx#61~|U@m}U=1;NgQduwh z=2G~|GeS>3~xtcsZy?LOehw*Cb?xS20%!Dj1;KaDE zaQn0Vn;`4Pf}{6F1V#*og@vX3Qk|V|Skqxrl>i6`STn9*#CaI3sZ$3hJ2-fQP8+pt zF%*noI^}b(T)8sDJ`e%4vUHxa^K80*|GrKz?%NkP#5RCZX&YD%Iv;ZiCR2V_RCG$qTS=;<5l<1&p*M( zwZWK`m8CN`Yic?uF@l0Na1dHsTdTU~(xuCWod>WQfCyI&hL*1L@$uzAN0;3xDJiOF zWM^l~0n0aQ)A|}Fu(tMzpx#7E&&Z^N1BXQ5q5&gZH9#S`NhG!lGAe1ktQk`jnlYB{ z-e@owh;W0D0eBGvO?ZX`Cb+5{J9f&c9=Irg2dj8Iz=%nN6p@Z2P|z2Rjs}buFJ6+} zFS@VqJYLY5nVCcSh7BRA`@>cD^z<48Mod1&MjMQ2X=$o^rl+SHG$(NF%KJetL%Qg) zJ3M@g>KQwB?9dyTPZ~)O0oQo-Y7y;@ju8)uV6lwJa&w!@s$d)Y;tX1H)qz<>DsXVeEqu!J%? zmt3CiuGXZOWN6BU!3OzJd zFhZMR0VpHg@dV@L%U4ux8mSH`JvNd}BclalOiYZ*=G@#ogNqN!`yqO7Z5{i*)o}+S zBJa3}tF+YlD0y3^Xlm2nYx0ZGk2UuVt0TkJB8MkUpr79YPCD!BE-rIa1B?6^G@mvY zkuTG=GWBUjfB#jA0LD~r1!pv1M3z9ARzdC-QSxx*La=p9ON*?##sbEoq9WB|M`SJ$qtwZvPst z0;3O%7%x<_l-D5u(^oD8r~shw@NkY^#|lO~M_Tac2o#leYo~T{a&jmyFONS*MMbIX z4hsv{Yd01@*c9)oFB>uLV7y=2$nF^%ak0Tx3R=*E7oMQDX2Y~1vh!YE-tvorwZ6&7 zf+-d+UZMgW1`o%TK$G%p2!t?5NYKJPN!1z#f1Vc4N^Z6@)Yt?i&&kGORlvH!xO}zoK>XayfEFwS{*8 zeRaa(thC#~VHSf_X*YsUcXtmyf4EJ> zV+JEOT9jO>V?F5jTLM>cBsRoOZv-*;xvQZK#dx@7OfC|_hOnZt;E$TF=2}h1E^7b^ z+x{_d00V%Ij?btHfE_UK(lyYPU6jThcU7Z_2>lWYUC0))y< zD6k26T#SkO+0%JEP5Ir;@vq&Y)f5VPD)<{j;e<8dP$?eZn(bG)FZTW=PuqM1IEL4 zEnEk2t-MZK|KR$&cgtDJA|(;1rLHwlE%`}Mu zeR3%!riAX6G*C-Z7mH(KGq%4a&xjr2s5yh}+FQD*@p&h`sPCX=;f6aqKJx#=GuodM d7@M)7{U6Gurk$H{=|lhk002ovPDHLkV1hrn7y1AI literal 0 HcmV?d00001 diff --git a/rmc/Makefile b/rmc/Makefile new file mode 100644 index 0000000..45defae --- /dev/null +++ b/rmc/Makefile @@ -0,0 +1,56 @@ +################################################################################ +# +# File: Makefile +# Revision: $Revision: 1 $ +# Description: Makefile for Devops/Web/rmc +# Author: Andrew@Clearscm.com +# Created: Mon, Jun 01, 2015 12:19:02 PM +# Modified: $Date: 2012/09/20 06:52:37 $ +# Language: Makefile +# +# (c) Copyright 2015, Audience, Inc., all rights reserved. +# +# Aside from the standard make targets, the following additional targets exist: +# +# setup: Set up rmc web app +# +################################################################################ +include ../../make.inc + +WEBAPPS := rmc +HTTPCONF := /etc/httpd/conf.d +SERVER := $(shell hostname -s) +PORT := 8000 +TEMPFILE := $(shell mktemp --tmpdir $(TMP) -u rmc.conf.XXXX) + +define helpText +Aside from the standard make targets, the following additional targets exist:\n\\n\ +install: Set up rmc web app\n\ +uninstall: Remove rmc web app\n +endef + +all: + install + +help: + @echo -e "$(helpText)" + +test: + @read -p "Enter SERVER:" SERVER;\ + echo "SERVER = $$SERVER";\ + exit 1; + +install: + @read -p "Enter server name for this instance (Default: $(SERVER)):" SERVER; \ + read -p "Enter port number for this instance (Default: $(PORT)):" PORT; \ + $(SUDO) $(RMF) $(HTTPCONF)/rmc.conf; \ + $(SED) "s//$$SERVER/" rmc.conf > $(TEMPFILE); \ + $(SED) "s//$$PORT/" $(TEMPFILE) > /tmp/rmc.conf; \ + $(SUDO) $(RMF) $(TEMPFILE); \ + $(SUDO) chown root.root /tmp/rmc.conf; \ + $(SUDO) $(MV) /tmp/rmc.conf $(HTTPCONF)/rmc.conf; \ + $(SUDO) $(SERVICE) httpd reload + +uninstall: + $(SUDO) $(RMF) $(HTTPCONF)/rmc.conf + $(SUDO) $(SERVICE) httpd reload diff --git a/rmc/index.pl b/rmc/index.pl new file mode 100644 index 0000000..be8403e --- /dev/null +++ b/rmc/index.pl @@ -0,0 +1,1043 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use CGI qw ( + :standard + :cgi-lib + start_div end_div + *table + start_Tr end_Tr + start_td end_td + start_pre end_pre + start_thead end_thead +); + +=pod + +=head1 NAME rmc.pl + +Release Mission Control: Customized Release Notes + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #7 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2015/07/22 $ + +=back + +=head1 SYNOPSIS + + $ rmc.pl [-username ] [-password ] + [-client client] [-port] [-[no]html] [-csv] + [-comments] [-files] [-description] + + -from [-to ] + [-branchpath ] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -deb|ug: Display debugging information + -he|lp: Display full help + -usa|ge: Display usage + -p|ort: Perforce server and port (Default: Env P4PORT). + -use|rname: Name of the user to connect to Perforce with with + (Default:Env P4USER). + -p|assword: Password for the user to connect to Perforce with + (Default: Env P4PASSWD). + -cl|ient: Perforce Client (Default: Env P4CLIENT) + -co|mments: Include comments in output + -fi|les: Include files in output + -cs|v: Produce a csv file + -des|cription: Include description from Bugzilla + -fr|om: From revSpec + -l|ong: Shorthand for -comments & -files + -t|o: To revSpec (Default: @now) + -b|ranchpath: Path to limit changes to + -[no]ht|ml: Whether or not to produce html output + +Note that revSpecs are Perforce's way of handling changelist/label/dates. For +more info see p4 help revisions. For your reference: + + #rev - A revision number or one of the following keywords: + #none - A nonexistent revision (also #0). + #head - The current head revision (also @now). + #have - The revision on the current client. + @change - A change number: the revision as of that change. + @client - A client name: the revision on the client. + @label - A label name: the revision in the label. + @date - A date or date/time: the revision as of that time. + Either yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss + Note that yyyy/mm/dd means yyyy/mm/dd:00:00:00. + To include all events on a day, specify the next day. + +=head1 DESCRIPTION + +This script produces release notes on the web or in a .csv file. You can also +produce an .html file by using -html and redirecting stdout. + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; + +use P4; + +use lib "$FindBin::Bin/../../Web/common/lib"; +use lib "$FindBin::Bin/../../common/lib"; +use lib "$FindBin::Bin/../../lib"; +use lib "$FindBin::Bin/../lib"; + +use Display; +use DateUtils; +use JIRAUtils; +use Utils; + +#use webutils; + +# Globals +my $VERSION = '$Revision: #7 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4; +my @labels; +my $headerPrinted; +my $p4ticketsFile = '/opt/audience/perforce/p4tickets'; + +my $bugsweb = 'http://bugs.audience.local/show_bug.cgi?id='; +my $p4web = 'http://p4web.audience.local:8080'; +my $jiraWeb = 'http://jira.audience.local/browse/'; +my %opts; + +my $changesCommand = ''; + +local $| = 1; + +my $title = 'Release Mission Control'; +my $subtitle = 'Select from and to revspecs to see the bugs changes between them'; +my $helpStr = 'Both From and To are Perforce ' + . i ('revSpecs') + . '. You can use changelists, labels, dates or clients. For more' + . ' see p4 help revisions or ' + . a { + href => 'http://www.perforce.com/perforce/r12.2/manuals/cmdref/o.fspecs.html#1047453', + target => 'rmcHelp', + }, + 'Perforce File Specifications', + . '.' + . br + . b ('revSpec examples') + . ': <change>, <client>, <label>, ' + . '<date> - yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss' + . br + . b ('Note:') + . ' To show all changes after label1 but before label2 use >label1 for From and @label2 for To. Or specify To as now'; +my @columnHeadings = ( + '#', + 'Changelist', + 'Bug ID', + 'Issue', + 'Type', + 'Status', + 'Fix Versions', + 'User ID', + 'Date', + '# of Files', + 'Summary', + 'Checkin Comments', + 'Files', +); + +sub displayForm (;$$$); + +sub footing (;$) { + my ($startTime) = @_; + + print '
', a { + href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}", + }, 'Export CSV
' if $opts{from} or $opts{to}; + + print end_form; + + my $script = $FindBin::Script =~ /index.pl/ + ? 'rmc.pl' + : $FindBin::Script; + + my ($sec, $min, $hour, $mday, $mon, $year) = + localtime ((stat ($script))[9]); + + $year += 1900; + $mon++; + + my $dateModified = "$mon/$mday/$year @ $hour:$min"; + my $secondsElapsed = $startTime ? time () - $startTime . ' secs' : ''; + + print end_div; + + print start_div {-class => 'copyright'}; + print "$script version $VERSION: Last modified: $dateModified"; + print " ($secondsElapsed)" if $secondsElapsed; + print br "Copyright © $year, Audience - All rights reserved - Design by ClearSCM"; + print end_div; + + print end_html; + + return; +} # footing + +sub errorMsg ($;$) { + my ($msg, $exit) = @_; + + unless ($opts{html}) { + error ($msg, $exit); + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted =1; + } # unless + + print font ({class => 'error'}, '
ERROR: ') . $msg; + + if ($exit) { + footing; + exit $exit; + } # if +} # errorMsg + +sub debugMsg ($) { + my ($msg) = @_; + + return unless $opts{debug}; + + unless ($opts{html}) { + debug $msg; + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted = 1; + } # unless + + print font ({class => 'error'}, '
DEBUG: ') . $msg; +} # debugMsg + +sub formatTimestamp (;$) { + my ($time) = @_; + + my $date = YMDHMS ($time); + my $formattedDate = substr ($date, 0, 4) . '-' + . substr ($date, 4, 2) . '-' + . substr ($date, 6, 2) . ' ' + . substr ($date, 9); + + return $formattedDate; +} # formatTimestamp + +sub p4errors ($) { + my ($cmd) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + $msg .= $opts{html} ? '
' : "\n"; + + if ($p4->ErrorCount) { + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + errorMsg $msg . $p4->Errors, $p4->ErrorCount; + } # if + + return; +} # p4errors + +sub p4connect () { + $p4 = P4->new; + + $p4->SetUser ($opts{username}); + $p4->SetClient ($opts{client}) if $opts{client}; + $p4->SetPort ($opts{port}); + + if ($opts{username} eq 'shared') { + $p4->SetTicketFile ($p4ticketsFile); + } else { + $p4->SetPassword ($opts{password}); + } # if + + verbose_nolf "Connecting to Perforce server $opts{port}..."; + $p4->Connect or die "Unable to connect to Perforce Server\n"; + verbose 'done'; + + unless ($opts{username} eq 'shared') { + verbose_nolf "Logging in as $opts{username}\@$opts{port}..."; + + $p4->RunLogin; + + p4errors 'login'; + + verbose 'done'; + } # unless + + return $p4; +} # p4connect + +sub getChanges ($;$$) { + my ($from, $to, $branchpath) = @_; + + $from = "\@$from" unless $from =~ /^@/; + $to = "\@$to" unless $to =~ /^@/; + + my $args; + #$args = '-s shelved '; + $args .= $branchpath if $branchpath; + $args .= $from; + $args .= ",$to" if $to; + + my $cmd = 'changes'; + my $changes = $p4->Run ($cmd, $args); + + $changesCommand = "p4 $cmd $args" if $opts{debug}; + + p4errors "$cmd $args"; + + unless (@$changes) { + if ($to =~ /\@now/i) { + verbose "No changes since $from"; + } else { + verbose "No changes between $from - $to"; + } # if + + return; + } else { + return @$changes; + } # unless +} # getChanges + +sub getJobInfo ($) { + my ($job) = @_; + + my $jobs = $p4->IterateJobs ("-e $job"); + + p4errors "jobs -e $job"; + + $job = $jobs->next if $jobs->hasNext; + + return $job; +} # getJobInfo + +sub getComments ($) { + my ($changelist) = @_; + + my $change = $p4->FetchChange ($changelist); + + p4errors "change $changelist"; + + return $change->{Description}; +} # getComments + +sub getFiles ($) { + my ($changelist) = @_; + + my $files = $p4->Run ('files', "\@=$changelist"); + + p4errors "files \@=$changelist"; + + my @files; + + push @files, $_->{depotFile} . '#' . $_->{rev} for @$files; + + return @files; +} # getFiles + +sub displayForm (;$$$) { + my ($from, $to, $branchpath) = @_; + + $from //= ''; + $to //= ''; + + print p {align => 'center', class => 'dim'}, $helpStr; + + print start_form { + method => 'get', + actions => $FindBin::Script, + }; + + print start_table { + class => 'table', + align => 'center', + cellspacing => 1, + width => '95%', + }; + + print Tr [th ['From', 'To']]; + print start_Tr; + print td {align => 'center'}, textfield ( + -name => 'from', + value => $from, + size => 60, + ); + print td {align => 'center'}, textfield ( + -name => 'to', + value => $to, + size => 60, + ); + print end_Tr; + + print Tr [th {colspan => 2}, 'Branch/Path']; + print start_Tr; + print td { + colspan => 2, + align => 'center', + }, textfield ( + -name => 'branchpath', + value => $branchpath, + size => 136, + ); + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, b ('Options:'), checkbox ( + -name => 'comments', + id => 'comments', + onclick => 'toggleOption ("comments");', + label => 'Comments', + checked => $opts{comments} ? 'checked' : '', + value => $opts{comments}, + ), checkbox ( + -name => 'files', + id => 'files', + onclick => 'toggleOption ("files");', + label => 'Files', + checked => $opts{files} ? 'checked' : '', + value => $opts{files}, +# ), checkbox ( +# -name => 'group', +# id => 'group', +# onclick => 'groupIndicate();', +# label => 'Group Indicate', +# checked => 'checked', +# value => $opts{group}, + ); + + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, input { + type => 'Submit', + value => 'Submit', + }; + print end_Tr; + print end_table; + print p; + + return; +} # displayForm + +sub displayChangesHTML (@) { + my (@changes) = @_; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $displayComments = $opts{comments} ? '' : 'none'; + my $displayFiles = $opts{files} ? '' : 'none'; + + debugMsg "Changes command used: $changesCommand"; + + print start_table { + class => 'table-autosort', + align => 'center', + width => '95%', + }; + + print start_thead; + print start_Tr; + print th '#'; + print + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, 'Changelist', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Bug ID', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Issue', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Type', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Status', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Fix Version', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'User ID', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Date', + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, '# of Files', + th 'Summary', + th { + id => 'comments0', + style => "display: $displayComments", + }, 'Checkin Comments', + th { + id => 'files0', + style => "display: $displayFiles", + }, 'Files'; + print end_Tr; + print end_thead; + + my $i = 0; + + for (sort {$b->{change} <=> $a->{change}} @changes) { + my %change = %$_; + + my @files = getFiles $change{change}; + + my $job; + + for ($p4->Run ('fixes', "-c$change{change}")) { # Why am I uses fixes here? + $job = getJobInfo $_->{Job}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job->{Description} = font {color => '#aaa'}, 'N/A' unless $job; + + my ($bugid, $jiraIssue); + + if ($job->{Job}) { + if ($job->{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job->{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + # Using the following does not guarantee the ordering of the elements + # emitted! + # + # my $buglink = a { + # href => "$bugsweb$bugid", + # target => 'bugzilla', + # }, $bugid; + # + # IOW sometimes I get and other times I + # get ! Not cool because the JavaScript + # later on is comparing innerHTML and this messes that up. So we write this + # out by hand instead. + my $buglink = $bugid + ? "$bugid" + : font {color => '#aaa'}, 'N/A'; + my $jiralink = $jiraIssue + ? "$jiraIssue" + : font {color => '#aaa'}, 'N/A'; + my $cllink = "{change}?ac=133\" target=\"p4web\">$_->{change}"; + my $userid = $_->{user}; + my $description = $job->{Description}; + my $jiraStatus = font {color => '#aaa'}, 'N/A'; + my $issueType = font {color => '#aaa'}, 'N/A'; + my $fixVersions = font {color => '#aaa'}, 'N/A'; + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join '
', @fixVersions; + } # unless + } # if + + print start_Tr {id => ++$i}; + + # Attempting to "right size" the columns... + print + td {width => '10px', align => 'center'}, $i, + td {width => '15px', align => 'center', id => "changelist$i"}, $cllink, + td {width => '60px', align => 'center', id => "bugzilla$i"}, $buglink, + td {width => '80px', align => 'center', id => "jira$i"}, $jiralink, + td {width => '50px', align => 'center', id => "type$i"}, $issueType, + td {width => '50px', align => 'center', id => "jirastatus$i"}, $jiraStatus, + td {width => '50px', align => 'center', id => "fixVersion$i"}, $fixVersions, + td {width => '30px', align => 'center', id => "userid$i"}, a {href => "mailto:$userid\@audience.com" }, $userid, + td {width => '130px', align => 'center'}, formatTimestamp ($_->{time}), + td {width => '10px', align => 'center'}, scalar @files; + + if ($description =~ /N\/A/) { + print td {id => "description$i", align => 'center'}, $description; + } else { + print td {id => "description$i"}, $description; + } # if + + print + td {id => "comments$i", + valign => 'top', + style => "display: $displayComments", + }, pre {class => 'code'}, getComments ($_->{change}); + + print start_td { + id => "files$i", + valign => 'top', + style => "display: $displayFiles" + }; + + print start_pre {class => 'code'}; + + for my $file (@files) { + my ($filelink) = ($file =~ /(.*)\#/); + my ($revision) = ($file =~ /\#(\d+)/); + + # Note: For a Perforce "Add to Source Control" operation, revision is + # actually "none". Let's fix this. + $revision = 1 unless $revision; + + if ($revision == 1) { + print a { + href => '#', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "There's nothing to diff since this is the first revision", + }; + } else { + print a { + href => "$p4web$filelink?ac=19&rev1=$revision&rev2=" . ($revision - 1), + target => 'p4web', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "Diff rev #$revision vs. rev #" . ($revision -1), + }; + } # if + + print a { + href => "$p4web$filelink?ac=22", + target => 'p4web', + }, "$file
"; + } # for + + print end_pre; + print end_td; + + print end_Tr; + } # for + + print end_table; + + return; +} # displayChangesHTML + +sub displayChange (\%;$) { + my ($change, $nbr) = @_; + + $nbr //= 1; + + # Note: $change must about -c! + my $args = "-c$change->{change}"; + my $cmd = 'fixes'; + my $fix = $p4->Run ($cmd, $args); + + p4errors "$cmd $args"; + + errorMsg "Change $change->{change} is associated with multiple jobs. This case is not handled yet" if @$fix > 1; + + $fix = $$fix[0]; + + # If there was no fix associated with this change we will use the change data. + unless ($fix) { + $fix->{Change} = $change->{change}; + $fix->{User} = $change->{user}; + $fix->{Date} = $change->{time}; + } # unless + + my $job; + $job = getJobInfo ($fix->{Job}) if $fix->{Job}; + + unless ($job) { + chomp $change->{desc}; + + $job = { + Description => $change->{desc}, + Job => 'Unknown', + }; + } # unless + + my ($bugid) = ($job->{Job} =~ /^(\d+)/); + + chomp $job->{Description}; + + my $description = "$change->{change}"; + $description .= "/$bugid" if $bugid; + $description .= ": $job->{Description} ($fix->{User} "; + $description .= ymdhms ($fix->{Date}) . ')'; + + display $nbr++ . ") $description"; + + if ($opts{comments}) { + print "\n"; + print "Comments:\n" . '-'x80 . "\n" . getComments ($fix->{Change}) . "\n"; + } # if + + if ($opts{description}) { + display ''; + display "Description:\n" . '-'x80 . "\n" . $job->{Description}; + } # if + + if ($opts{files}) { + display "Files:\n" . '-'x80; + + for (getFiles $fix->{Change}) { + display "\t$_"; + } # for + + display ''; + } # if + + return $nbr; +} # displayChangesHTML + +sub displayChanges (@) { + my (@changes) = @_; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $i; + + debugMsg "Changes command used: $changesCommand"; + + $i = displayChange %$_, $i for @changes; + + return; +} # displayChanges + +sub heading ($;$) { + my ($title, $subtitle) = @_; + + print header unless $headerPrinted; + + $headerPrinted = 1; + + print start_html { + title => $title, + head => Link ({ + rel => 'shortcut icon', + href => 'http://p4web.audience.local:8080/favicon.ico', + type => 'image/x-icon', + }), + author => 'Andrew DeFaria ', + script => [{ + language => 'JavaScript', + src => 'rmc.js', + }, { + language => 'JavaScript', + src => 'rmctable.js', + }], + style => ['rmc.css'], + onload => 'setOptions();', + }, $title; + + print h1 {class => 'title'}, "
$title
"; + print h3 "
$subtitle
" if $subtitle; + + return; +} # heading + +sub exportCSV ($@) { + my ($filename, @data) = @_; + + print header ( + -type => 'application/octect-stream', + -attachment => $filename, + ); + + # Print heading line + my $columns; + + # Note that we do not include the '#' column so start at 1 + for (my $i = 1; $i < @columnHeadings; $i++) { + $columns .= "\"$columnHeadings[$i]\""; + + $columns .= ',' unless $i == @columnHeadings; + } # for + + print "$columns\n"; + + for (sort {$b->{change} <=> $a->{change}} @data) { + my %change = %$_; + + ## TODO: This code is duplicated (See displayChange). Consider refactoring. + # Note: $change must be right next to the -c! + my (%job, $jiraStatus, $issueType, $fixVersions); + + for ($p4->Run ('fixes', "-c$change{change}")) { + %job = %{getJobInfo $_->{Job}}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job{Description} = '' unless %job; + + my ($bugid, $jiraIssue); + + if ($job{Job}) { + if ($job{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join "\n", @fixVersions; + } # unless + } # if + + my $job; + + unless ($job) { + chomp $change{desc}; + + $job = { + Description => $change{desc}, + Job => 'Unknown', + }; + } # unless + ## End of refactor code + + $job{Description} = join ("\r\n", split "\n", $job{Description}); + + my @files = getFiles $change{change}; + my $comments = join ("\r\n", split "\n", getComments ($change{change})); + + # Fix up double quotes in description and comments + $job{Description} =~ s/"/""/g; + $comments =~ s/"/""/g; + + print "$change{change},"; + print $bugid ? "$bugid," : ','; + print $jiraIssue ? "$jiraIssue," : ','; + print $issueType ? "$issueType," : ','; + print $jiraStatus ? "$jiraStatus," : ','; + print $fixVersions ? "$fixVersions," : ','; + print "$change{user},"; + print '"' . formatTimestamp ($change{time}) . '",'; + print scalar @files . ','; + print "\"$job{Description}\","; + print "\"$comments\","; + print '"' . join ("\n", @files) . "\""; + print "\n"; + } # for + + return; +} # exportCSV + +sub main { + # Standard opts + $opts{usage} = sub { pod2usage }; + $opts{help} = sub { pod2usage (-verbose => 2)}; + + # Opts for this script + $opts{username} //= $ENV{P4USER} || $ENV{USERNAME} || $ENV{USER}; + $opts{client} //= $ENV{P4CLIENT}; + $opts{port} //= $ENV{P4PORT} || 'perforce:1666'; + $opts{password} //= $ENV{P4PASSWD}; + $opts{html} //= $ENV{HTML} || param ('html') || 1; + $opts{html} = (-t) ? 0 : 1; + $opts{debug} //= $ENV{DEBUG} || param ('debug') || sub { set_debug }; + $opts{verbose} //= $ENV{VERBOSE} || param ('verbose') || sub { set_verbose }; + $opts{jiraserver} //= 'jira'; + $opts{from} = param 'from'; + $opts{to} = param 'to'; + $opts{branchpath} = param ('branchpath') || '//AudEngr/Import/VSS/...'; + $opts{group} = param 'group'; + $opts{comments} //= $ENV{COMMENTS} || param 'comments'; + $opts{files} //= $ENV{FILES} || param 'files'; + $opts{long} //= $ENV{LONG} || param 'long'; + $opts{csv} //= $ENV{CSV} || param 'csv'; + + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'help', + 'usage', + 'port=s', + 'username=s', + 'password=s', + 'client=s', + 'comments', + 'files', + 'description', + 'long', + 'from=s', + 'to=s', + 'branchpath=s', + 'html!', + 'csv', + ) || pod2usage; + + $opts{comments} = $opts{files} = 1 if $opts{long}; + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + # Are we doing HTML? + if ($opts{html}) { + require CGI::Carp; + + CGI::Carp->import ('fatalsToBrowser'); + + $opts{username} ||= 'shared'; + } # if + + # Needed if using the shared user + if ($opts{username} eq 'shared') { + unless (-f $p4ticketsFile) { + errorMsg "Using 'shared' user but there is no P4TICKETS file ($p4ticketsFile)", 1; + } # unless + } else { + if ($opts{username} and not $opts{password}) { + $opts{password} = GetPassword "I need the Perforce password for $opts{username}"; + } # if + } # if + + p4connect; + + my $jira = Connect2JIRA (undef, undef, $opts{jiraserver}); + + unless ($opts{from} or $opts{to}) { + if ($opts{html}) { + heading $title, $subtitle; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + footing; + + exit; + } # if + } # unless + + my $ymd = YMD; + my $midnight = substr ($ymd, 0, 4) . '/' + . substr ($ymd, 4, 2) . '/' + . substr ($ymd, 6, 2) . ':00:00:00'; + + $opts{to} //= 'now'; + $opts{from} //= $midnight; + + $opts{to} = 'now' if ($opts{to} eq '' or $opts{to} eq ''); + $opts{from} = $midnight if ($opts{from} eq '' or $opts{from} eq ''); + + my $msg = 'Changes made '; + + if ($opts{to} =~ /^now$/i and $opts{from} eq $midnight) { + $msg .= 'today'; + } elsif ($opts{to} =~ /^now$/i) { + $msg .= "since $opts{from}"; + } else { + $msg .= "between $opts{from} and $opts{to}"; + } # if + + if ($opts{csv}) { + my $filename = "$opts{from}_$opts{to}.csv"; + + $filename =~ s/\//-/g; + $filename =~ s/\@//g; + + debug "branchpath = $opts{branchpath}"; + + exportCSV $filename, getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + return; + } # if + + if ($opts{html}) { + heading 'Release Mission Control', $msg; + } else { + display "$msg\n"; + } # if + + if ($opts{html}) { + my $startTime = time; + displayChangesHTML getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + footing $startTime; + } else { + displayChanges getChanges $opts{from}, $opts{to}, $opts{branchpath}; + } # if + + return; +} # main + +main; + +exit; + diff --git a/rmc/rmc.conf b/rmc/rmc.conf new file mode 100644 index 0000000..e35ff64 --- /dev/null +++ b/rmc/rmc.conf @@ -0,0 +1,34 @@ +################################################################################ +# +# File: rmc.conf +# Revision: $Revision: 1 $ +# Description: Apache conf file for RMC +# Author: Andrew@Clearscm.com +# Created: Mon, Jun 01, 2015 12:19:02 PM +# Modified: $Date: 2012/09/20 06:52:37 $ +# Language: Apache +# +# (c) Copyright 2015, Audience, Inc., all rights reserved. +# +# This file defines the RMC web app for Apache. Generally it is symlinked into +# /etc/httpd/conf.d +# +################################################################################ +Listen + +> + ServerName .audience.local: + ServerAlias + ErrorLog "/var/log/httpd/rmc.error.log" + CustomLog "/var/log/httpd/rmc.access.log" common + DocumentRoot "/opt/audience/Web/rmc" + + + Options Indexes MultiViews FollowSymLinks ExecCGI + DirectoryIndex index.html index.pl + AllowOverride None + Order allow,deny + Allow from all + AddHandler cgi-script .pl + + diff --git a/rmc/rmc.css b/rmc/rmc.css new file mode 100644 index 0000000..0ee6b8b --- /dev/null +++ b/rmc/rmc.css @@ -0,0 +1,355 @@ +/******************************************************************************* +* +* File: $RCSfile: rmc.css,v $ +* Revision: $Revision: 1 $ +* Description: Cascading Style Sheet definitions for rmc +* Author: Andrew@ClearSCM.com +* Created: Tue May 27 20:48:21 PDT 2014 +* Modified: $Date: $ +* Language: Cascading Style Sheet +* +* (c) Copyright 2014, Audience, all rights reserved. +* +*******************************************************************************/ +body { + font-size: 11px; + color: black; + /*background-color:#f1f1ed;*/ + margin: 0px; + /*padding: 0; */ + /*overflow:scroll;*/ +} + +body,p,h1,h2,h3,table,td,th,ul,ol,textarea,input { + font-family: verdana, helvetica, arial, sans-serif; +} + +h1.title { + font-size: 3em; + color: #838; + text-shadow: 3px 2px 5px #666; +} +.nobr { + white-space: nowrap; + wrap: off; +} + +table.noborder { + border: none; +} + +table.noborder td { + border: none; +} + +td.noboder { + border-top: none; +} + +table.invisible1 { + border: none; + border-collapse: collapse; +} + +.indent { + display: block; + margin-left: 50px; +} + +table { + border: 1px solid #c3c3c3; + padding: 3px; + border-collapse: collapse; +} + +table.framework { + /*border:none;*/ + border-collapse: collapse; + /*width:100%;*/ +} + +table.frameworktop { + border: none; + border-collapse: collapse; + width: 100%; +} + +tr.highlighthover:hover { + background-color: #FFF8F0; + color: #000; +} + +td.highlighthover:hover { + background-color: #F8F0F0; + color: #000; +} + +caption { + font-size: 130%; + font-weight: bold; + text-align: left; + padding-bottom: 2px; +} + +.envbanner { + color: RED; + font-weight: bold; + font-style: Italic; +} + +.statuserrorfont { + color: RED; + /*font-weight:Bold;*/ +} + +.statusinprogressfont { + color: Blue; + font-style: Italic +} + +.statusgoodfont { + color: #10B618; +} + +span.rightalign { + /* display:block; */ + display: block; + text-align: right; + padding: 0px; + margin: 0px; +} + +table th { + vertical-align: top; + text-align: center; + border: 1px solid #c3c3c3; + padding: 3px; + background-color: #f0f0f0; +} + +table td { + vertical-align: top; + /*text-align: left;*/ + border: 1px solid #c3c3c3; + padding: 3px; +} + +/* Sorting */ +th.table-sortable { + cursor:pointer; + /*background-image:url("/icons/sortable.gif");*/ + background-position:center left; + background-repeat:no-repeat; + padding-left:12px; +} +th.table-sorted-asc { + background-image:url("/icons/sorted_up.gif"); + background-position:center left; + background-repeat:no-repeat; +} +th.table-sorted-desc { + background-image:url("/icons/sorted_down.gif"); + background-position:center left; + background-repeat:no-repeat; +} + +table.framework td.frameworkleft /*td.frameworkleft */ { + /*background: #ffff00;*/ + width: 15em; + min-width: 15em; + max-width: 15em; + vertical-align: top; + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 1px; + border-top-width: 0px; + border-bottom-width: 0px; + /*width:15%;*/ + /*max-width:80px;*/ +} + +table.framework td.frameworkcenter { + /*border:3px red;*/ + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; + border-bottom-width: 0px; + /*width:75%;*/ +} + +table.frameworktop td.frameworktop { + border-collapse: collapse; + border: none; +} + +table.framework td.frameworkright { + border-collapse: collapse; + border-left-width: 0px; + border-right-width: 0px; + border-top-width: 0px; + border-bottom-width: 0px; + width: 5%; /* modify if there is content */ +} +smallfiltericon { + width: 8px; + min-width: 8px; + max-width: 8px; + width: 8px; + visibility: hidden; +} + +h1,h2,h3 { + background-color: transparent; + color: #000000; + margin-bottom: 3px; +} + +pre { + font-family: "Courier New", monospace; + margin-left: 0; + margin-bottom: 0; + white-space: pre-wrap; +} + +span.show { + display: block; +} + +span.hide { + visibility: hidden; +} + +a:link { + text-decoration: none; + color: #5150F7; +} + +a:visited { + color: #8E0094; +} + +a:hover,a:active { + text-decoration: none; + border-bottom-width: 1px; + border-bottom-style: dotted; +} + +a.downloadgood { + color: #10B618; +} + + +hr { + background-color: #d4d4d4; + color: #d4d4d4; + height: 1px; + border: 0px; +} + + +span.insert { + color: #e80000; + background-color: transparent; +} + +span.highlight_code { + color: #e80000; + background-color: transparent; +} + +a.m_item:link { + text-decoration: none; + color: white; + background-color: transparent +} + +a.m_item:visited { + text-decoration: none; + color: white; + background-color: transparent +} + +a.m_item:active { + text-decoration: underline; + color: white; + background-color: transparent +} + +a.m_item:hover { + text-decoration: underline; + color: white; + background-color: transparent +} + +a.bold { + font-weight: bold; +} + +table.noborder td { + border: none; +} + +table.dashboardtable th { + border: none; + background-color: none; + border-bottom: 1px solid #000000; + border-top: 1px solid #000000; +} + +div.showdiv { + display: block; +} + +div.hidediv { + display: none; +} + +div.inline { + display: inline; +} + +greyfont { + font-color: red; +} + +img { + border: none; +} + +/* Copyright block */ +.copyright { + border-bottom:1px dotted #ccc; + border-top:1px dotted #ccc; + color:#999; + font-family:verdana, arial, sans-serif; + font-size:10px; + margin-top:5px; + text-align:center; + width:auto; +} + +.copyright a:link, a:visited { + color:#666; + font-weight:bold; + text-decoration:none; +} + +.copyright a:hover { + color:#333; +} + +.error { + color:red; + font-weight:bold; +} + +.dim { + color: #aaa; +} + +pre.code { + display: inline; + white-space: pre-line; + font-family: monospace; +} \ No newline at end of file diff --git a/rmc/rmc.js b/rmc/rmc.js new file mode 100644 index 0000000..49d5343 --- /dev/null +++ b/rmc/rmc.js @@ -0,0 +1,107 @@ +var comments; +var files; + +function setOptions () { + comments = document.getElementById ("comments").checked; + files = document.getElementById ("files").checked; + //group = document.getElementById ("group").checked; +} // setOptions + +function colorLines () { + return; // not used + if (comments || files) { + color = '#ffc'; + } else { + color = 'white'; + } // if + + i = 1; + + while (element = document.getElementById (i++)) { + element.style.backgroundColor = color; + } // while +} // colorLines + +function hideElement (elementName) { + var i = 0; + + while ((element = document.getElementById (elementName + i++)) != null) { + element.style.display = "none"; + } // while +} // hideElement + +function showElement (elementName) { + var i = 0; + + while ((element = document.getElementById (elementName + i++)) != null) { + element.colSpan = 7; + element.style.display = ""; + } // while +} // showElement + +function toggleOption (option) { + if (option == "comments") { + if (comments) { + hideElement (option); + + comments = false; + } else { + showElement (option); + + comments = true; + } // if + } else if (option == "files") { + if (files) { + hideElement (option); + + files = false; + } else { + showElement (option); + + files = true; + } // if + } // if + + //colorLines (); +} // toggleOption + +function groupIndicate () { + var fields = ['bugzilla', 'changelist', 'userid', 'summary']; + var values = []; + + // Seed values + for (var i = 0; i < fields.length; i++) { + values[fields[i]] = document.getElementById (fields[i] + 1).innerHTML; + } // for + + i = 1; + + while (document.getElementById (i) != null) { + i++; + + for (var j = 0; j < fields.length; j++) { + var element = document.getElementById (fields[j] + i); + + if (element == null) break; + + if (group) { + if (element.innerHTML == values[fields[j]]) { + element.innerHTML = ''; + } else { + values[fields[j]] = element.innerHTML; + } // if + } else { + if (element.innerHTML == '') { + element.innerHTML = values[fields[j]]; + } // if + } // if + } // for + } // while + + // Toggle group + if (group) { + group = false; + } else { + group = true; + } // if +} // groupIndicate \ No newline at end of file diff --git a/rmc/rmc.pl b/rmc/rmc.pl new file mode 100644 index 0000000..be8403e --- /dev/null +++ b/rmc/rmc.pl @@ -0,0 +1,1043 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use CGI qw ( + :standard + :cgi-lib + start_div end_div + *table + start_Tr end_Tr + start_td end_td + start_pre end_pre + start_thead end_thead +); + +=pod + +=head1 NAME rmc.pl + +Release Mission Control: Customized Release Notes + +=head1 VERSION + +=over + +=item Author + +Andrew DeFaria + +=item Revision + +$Revision: #7 $ + +=item Created + +Thu Mar 20 10:11:53 PDT 2014 + +=item Modified + +$Date: 2015/07/22 $ + +=back + +=head1 SYNOPSIS + + $ rmc.pl [-username ] [-password ] + [-client client] [-port] [-[no]html] [-csv] + [-comments] [-files] [-description] + + -from [-to ] + [-branchpath ] + + [-verbose] [-debug] [-help] [-usage] + + Where: + + -v|erbose: Display progress output + -deb|ug: Display debugging information + -he|lp: Display full help + -usa|ge: Display usage + -p|ort: Perforce server and port (Default: Env P4PORT). + -use|rname: Name of the user to connect to Perforce with with + (Default:Env P4USER). + -p|assword: Password for the user to connect to Perforce with + (Default: Env P4PASSWD). + -cl|ient: Perforce Client (Default: Env P4CLIENT) + -co|mments: Include comments in output + -fi|les: Include files in output + -cs|v: Produce a csv file + -des|cription: Include description from Bugzilla + -fr|om: From revSpec + -l|ong: Shorthand for -comments & -files + -t|o: To revSpec (Default: @now) + -b|ranchpath: Path to limit changes to + -[no]ht|ml: Whether or not to produce html output + +Note that revSpecs are Perforce's way of handling changelist/label/dates. For +more info see p4 help revisions. For your reference: + + #rev - A revision number or one of the following keywords: + #none - A nonexistent revision (also #0). + #head - The current head revision (also @now). + #have - The revision on the current client. + @change - A change number: the revision as of that change. + @client - A client name: the revision on the client. + @label - A label name: the revision in the label. + @date - A date or date/time: the revision as of that time. + Either yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss + Note that yyyy/mm/dd means yyyy/mm/dd:00:00:00. + To include all events on a day, specify the next day. + +=head1 DESCRIPTION + +This script produces release notes on the web or in a .csv file. You can also +produce an .html file by using -html and redirecting stdout. + +=cut + +use FindBin; +use Getopt::Long; +use Pod::Usage; + +use P4; + +use lib "$FindBin::Bin/../../Web/common/lib"; +use lib "$FindBin::Bin/../../common/lib"; +use lib "$FindBin::Bin/../../lib"; +use lib "$FindBin::Bin/../lib"; + +use Display; +use DateUtils; +use JIRAUtils; +use Utils; + +#use webutils; + +# Globals +my $VERSION = '$Revision: #7 $'; + ($VERSION) = ($VERSION =~ /\$Revision: (.*) /); + +my $p4; +my @labels; +my $headerPrinted; +my $p4ticketsFile = '/opt/audience/perforce/p4tickets'; + +my $bugsweb = 'http://bugs.audience.local/show_bug.cgi?id='; +my $p4web = 'http://p4web.audience.local:8080'; +my $jiraWeb = 'http://jira.audience.local/browse/'; +my %opts; + +my $changesCommand = ''; + +local $| = 1; + +my $title = 'Release Mission Control'; +my $subtitle = 'Select from and to revspecs to see the bugs changes between them'; +my $helpStr = 'Both From and To are Perforce ' + . i ('revSpecs') + . '. You can use changelists, labels, dates or clients. For more' + . ' see p4 help revisions or ' + . a { + href => 'http://www.perforce.com/perforce/r12.2/manuals/cmdref/o.fspecs.html#1047453', + target => 'rmcHelp', + }, + 'Perforce File Specifications', + . '.' + . br + . b ('revSpec examples') + . ': <change>, <client>, <label>, ' + . '<date> - yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss' + . br + . b ('Note:') + . ' To show all changes after label1 but before label2 use >label1 for From and @label2 for To. Or specify To as now'; +my @columnHeadings = ( + '#', + 'Changelist', + 'Bug ID', + 'Issue', + 'Type', + 'Status', + 'Fix Versions', + 'User ID', + 'Date', + '# of Files', + 'Summary', + 'Checkin Comments', + 'Files', +); + +sub displayForm (;$$$); + +sub footing (;$) { + my ($startTime) = @_; + + print '
', a { + href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}", + }, 'Export CSV
' if $opts{from} or $opts{to}; + + print end_form; + + my $script = $FindBin::Script =~ /index.pl/ + ? 'rmc.pl' + : $FindBin::Script; + + my ($sec, $min, $hour, $mday, $mon, $year) = + localtime ((stat ($script))[9]); + + $year += 1900; + $mon++; + + my $dateModified = "$mon/$mday/$year @ $hour:$min"; + my $secondsElapsed = $startTime ? time () - $startTime . ' secs' : ''; + + print end_div; + + print start_div {-class => 'copyright'}; + print "$script version $VERSION: Last modified: $dateModified"; + print " ($secondsElapsed)" if $secondsElapsed; + print br "Copyright © $year, Audience - All rights reserved - Design by ClearSCM"; + print end_div; + + print end_html; + + return; +} # footing + +sub errorMsg ($;$) { + my ($msg, $exit) = @_; + + unless ($opts{html}) { + error ($msg, $exit); + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted =1; + } # unless + + print font ({class => 'error'}, '
ERROR: ') . $msg; + + if ($exit) { + footing; + exit $exit; + } # if +} # errorMsg + +sub debugMsg ($) { + my ($msg) = @_; + + return unless $opts{debug}; + + unless ($opts{html}) { + debug $msg; + + return + } # if + + unless ($headerPrinted) { + print header; + print start_html; + + $headerPrinted = 1; + } # unless + + print font ({class => 'error'}, '
DEBUG: ') . $msg; +} # debugMsg + +sub formatTimestamp (;$) { + my ($time) = @_; + + my $date = YMDHMS ($time); + my $formattedDate = substr ($date, 0, 4) . '-' + . substr ($date, 4, 2) . '-' + . substr ($date, 6, 2) . ' ' + . substr ($date, 9); + + return $formattedDate; +} # formatTimestamp + +sub p4errors ($) { + my ($cmd) = @_; + + my $msg = "Unable to run \"p4 $cmd\""; + $msg .= $opts{html} ? '
' : "\n"; + + if ($p4->ErrorCount) { + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + errorMsg $msg . $p4->Errors, $p4->ErrorCount; + } # if + + return; +} # p4errors + +sub p4connect () { + $p4 = P4->new; + + $p4->SetUser ($opts{username}); + $p4->SetClient ($opts{client}) if $opts{client}; + $p4->SetPort ($opts{port}); + + if ($opts{username} eq 'shared') { + $p4->SetTicketFile ($p4ticketsFile); + } else { + $p4->SetPassword ($opts{password}); + } # if + + verbose_nolf "Connecting to Perforce server $opts{port}..."; + $p4->Connect or die "Unable to connect to Perforce Server\n"; + verbose 'done'; + + unless ($opts{username} eq 'shared') { + verbose_nolf "Logging in as $opts{username}\@$opts{port}..."; + + $p4->RunLogin; + + p4errors 'login'; + + verbose 'done'; + } # unless + + return $p4; +} # p4connect + +sub getChanges ($;$$) { + my ($from, $to, $branchpath) = @_; + + $from = "\@$from" unless $from =~ /^@/; + $to = "\@$to" unless $to =~ /^@/; + + my $args; + #$args = '-s shelved '; + $args .= $branchpath if $branchpath; + $args .= $from; + $args .= ",$to" if $to; + + my $cmd = 'changes'; + my $changes = $p4->Run ($cmd, $args); + + $changesCommand = "p4 $cmd $args" if $opts{debug}; + + p4errors "$cmd $args"; + + unless (@$changes) { + if ($to =~ /\@now/i) { + verbose "No changes since $from"; + } else { + verbose "No changes between $from - $to"; + } # if + + return; + } else { + return @$changes; + } # unless +} # getChanges + +sub getJobInfo ($) { + my ($job) = @_; + + my $jobs = $p4->IterateJobs ("-e $job"); + + p4errors "jobs -e $job"; + + $job = $jobs->next if $jobs->hasNext; + + return $job; +} # getJobInfo + +sub getComments ($) { + my ($changelist) = @_; + + my $change = $p4->FetchChange ($changelist); + + p4errors "change $changelist"; + + return $change->{Description}; +} # getComments + +sub getFiles ($) { + my ($changelist) = @_; + + my $files = $p4->Run ('files', "\@=$changelist"); + + p4errors "files \@=$changelist"; + + my @files; + + push @files, $_->{depotFile} . '#' . $_->{rev} for @$files; + + return @files; +} # getFiles + +sub displayForm (;$$$) { + my ($from, $to, $branchpath) = @_; + + $from //= ''; + $to //= ''; + + print p {align => 'center', class => 'dim'}, $helpStr; + + print start_form { + method => 'get', + actions => $FindBin::Script, + }; + + print start_table { + class => 'table', + align => 'center', + cellspacing => 1, + width => '95%', + }; + + print Tr [th ['From', 'To']]; + print start_Tr; + print td {align => 'center'}, textfield ( + -name => 'from', + value => $from, + size => 60, + ); + print td {align => 'center'}, textfield ( + -name => 'to', + value => $to, + size => 60, + ); + print end_Tr; + + print Tr [th {colspan => 2}, 'Branch/Path']; + print start_Tr; + print td { + colspan => 2, + align => 'center', + }, textfield ( + -name => 'branchpath', + value => $branchpath, + size => 136, + ); + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, b ('Options:'), checkbox ( + -name => 'comments', + id => 'comments', + onclick => 'toggleOption ("comments");', + label => 'Comments', + checked => $opts{comments} ? 'checked' : '', + value => $opts{comments}, + ), checkbox ( + -name => 'files', + id => 'files', + onclick => 'toggleOption ("files");', + label => 'Files', + checked => $opts{files} ? 'checked' : '', + value => $opts{files}, +# ), checkbox ( +# -name => 'group', +# id => 'group', +# onclick => 'groupIndicate();', +# label => 'Group Indicate', +# checked => 'checked', +# value => $opts{group}, + ); + + print end_Tr; + + print start_Tr; + print td {align => 'center', colspan => 2}, input { + type => 'Submit', + value => 'Submit', + }; + print end_Tr; + print end_table; + print p; + + return; +} # displayForm + +sub displayChangesHTML (@) { + my (@changes) = @_; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $displayComments = $opts{comments} ? '' : 'none'; + my $displayFiles = $opts{files} ? '' : 'none'; + + debugMsg "Changes command used: $changesCommand"; + + print start_table { + class => 'table-autosort', + align => 'center', + width => '95%', + }; + + print start_thead; + print start_Tr; + print th '#'; + print + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, 'Changelist', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Bug ID', + th { + class => 'table-sortable:numeric', + title => 'Click to sort', + }, 'Issue', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Type', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Status', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Fix Version', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'User ID', + th { + class => 'table-sortable:default table-sortable', + title => 'Click to sort', + }, 'Date', + th { + class => 'table-sortable:numeric table-sortable', + title => 'Click to sort', + }, '# of Files', + th 'Summary', + th { + id => 'comments0', + style => "display: $displayComments", + }, 'Checkin Comments', + th { + id => 'files0', + style => "display: $displayFiles", + }, 'Files'; + print end_Tr; + print end_thead; + + my $i = 0; + + for (sort {$b->{change} <=> $a->{change}} @changes) { + my %change = %$_; + + my @files = getFiles $change{change}; + + my $job; + + for ($p4->Run ('fixes', "-c$change{change}")) { # Why am I uses fixes here? + $job = getJobInfo $_->{Job}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job->{Description} = font {color => '#aaa'}, 'N/A' unless $job; + + my ($bugid, $jiraIssue); + + if ($job->{Job}) { + if ($job->{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job->{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + # Using the following does not guarantee the ordering of the elements + # emitted! + # + # my $buglink = a { + # href => "$bugsweb$bugid", + # target => 'bugzilla', + # }, $bugid; + # + # IOW sometimes I get and other times I + # get ! Not cool because the JavaScript + # later on is comparing innerHTML and this messes that up. So we write this + # out by hand instead. + my $buglink = $bugid + ? "$bugid" + : font {color => '#aaa'}, 'N/A'; + my $jiralink = $jiraIssue + ? "$jiraIssue" + : font {color => '#aaa'}, 'N/A'; + my $cllink = "{change}?ac=133\" target=\"p4web\">$_->{change}"; + my $userid = $_->{user}; + my $description = $job->{Description}; + my $jiraStatus = font {color => '#aaa'}, 'N/A'; + my $issueType = font {color => '#aaa'}, 'N/A'; + my $fixVersions = font {color => '#aaa'}, 'N/A'; + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join '
', @fixVersions; + } # unless + } # if + + print start_Tr {id => ++$i}; + + # Attempting to "right size" the columns... + print + td {width => '10px', align => 'center'}, $i, + td {width => '15px', align => 'center', id => "changelist$i"}, $cllink, + td {width => '60px', align => 'center', id => "bugzilla$i"}, $buglink, + td {width => '80px', align => 'center', id => "jira$i"}, $jiralink, + td {width => '50px', align => 'center', id => "type$i"}, $issueType, + td {width => '50px', align => 'center', id => "jirastatus$i"}, $jiraStatus, + td {width => '50px', align => 'center', id => "fixVersion$i"}, $fixVersions, + td {width => '30px', align => 'center', id => "userid$i"}, a {href => "mailto:$userid\@audience.com" }, $userid, + td {width => '130px', align => 'center'}, formatTimestamp ($_->{time}), + td {width => '10px', align => 'center'}, scalar @files; + + if ($description =~ /N\/A/) { + print td {id => "description$i", align => 'center'}, $description; + } else { + print td {id => "description$i"}, $description; + } # if + + print + td {id => "comments$i", + valign => 'top', + style => "display: $displayComments", + }, pre {class => 'code'}, getComments ($_->{change}); + + print start_td { + id => "files$i", + valign => 'top', + style => "display: $displayFiles" + }; + + print start_pre {class => 'code'}; + + for my $file (@files) { + my ($filelink) = ($file =~ /(.*)\#/); + my ($revision) = ($file =~ /\#(\d+)/); + + # Note: For a Perforce "Add to Source Control" operation, revision is + # actually "none". Let's fix this. + $revision = 1 unless $revision; + + if ($revision == 1) { + print a { + href => '#', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "There's nothing to diff since this is the first revision", + }; + } else { + print a { + href => "$p4web$filelink?ac=19&rev1=$revision&rev2=" . ($revision - 1), + target => 'p4web', + }, img { + src => "$p4web/rundiffprevsmallIcon?ac=20", + title => "Diff rev #$revision vs. rev #" . ($revision -1), + }; + } # if + + print a { + href => "$p4web$filelink?ac=22", + target => 'p4web', + }, "$file
"; + } # for + + print end_pre; + print end_td; + + print end_Tr; + } # for + + print end_table; + + return; +} # displayChangesHTML + +sub displayChange (\%;$) { + my ($change, $nbr) = @_; + + $nbr //= 1; + + # Note: $change must about -c! + my $args = "-c$change->{change}"; + my $cmd = 'fixes'; + my $fix = $p4->Run ($cmd, $args); + + p4errors "$cmd $args"; + + errorMsg "Change $change->{change} is associated with multiple jobs. This case is not handled yet" if @$fix > 1; + + $fix = $$fix[0]; + + # If there was no fix associated with this change we will use the change data. + unless ($fix) { + $fix->{Change} = $change->{change}; + $fix->{User} = $change->{user}; + $fix->{Date} = $change->{time}; + } # unless + + my $job; + $job = getJobInfo ($fix->{Job}) if $fix->{Job}; + + unless ($job) { + chomp $change->{desc}; + + $job = { + Description => $change->{desc}, + Job => 'Unknown', + }; + } # unless + + my ($bugid) = ($job->{Job} =~ /^(\d+)/); + + chomp $job->{Description}; + + my $description = "$change->{change}"; + $description .= "/$bugid" if $bugid; + $description .= ": $job->{Description} ($fix->{User} "; + $description .= ymdhms ($fix->{Date}) . ')'; + + display $nbr++ . ") $description"; + + if ($opts{comments}) { + print "\n"; + print "Comments:\n" . '-'x80 . "\n" . getComments ($fix->{Change}) . "\n"; + } # if + + if ($opts{description}) { + display ''; + display "Description:\n" . '-'x80 . "\n" . $job->{Description}; + } # if + + if ($opts{files}) { + display "Files:\n" . '-'x80; + + for (getFiles $fix->{Change}) { + display "\t$_"; + } # for + + display ''; + } # if + + return $nbr; +} # displayChangesHTML + +sub displayChanges (@) { + my (@changes) = @_; + + unless (@changes) { + my $msg = "No changes found between $opts{from} and $opts{to}"; + $msg .= " for $opts{branchpath}"; + print p $msg; + + return; + } # unless + + my $i; + + debugMsg "Changes command used: $changesCommand"; + + $i = displayChange %$_, $i for @changes; + + return; +} # displayChanges + +sub heading ($;$) { + my ($title, $subtitle) = @_; + + print header unless $headerPrinted; + + $headerPrinted = 1; + + print start_html { + title => $title, + head => Link ({ + rel => 'shortcut icon', + href => 'http://p4web.audience.local:8080/favicon.ico', + type => 'image/x-icon', + }), + author => 'Andrew DeFaria ', + script => [{ + language => 'JavaScript', + src => 'rmc.js', + }, { + language => 'JavaScript', + src => 'rmctable.js', + }], + style => ['rmc.css'], + onload => 'setOptions();', + }, $title; + + print h1 {class => 'title'}, "
$title
"; + print h3 "
$subtitle
" if $subtitle; + + return; +} # heading + +sub exportCSV ($@) { + my ($filename, @data) = @_; + + print header ( + -type => 'application/octect-stream', + -attachment => $filename, + ); + + # Print heading line + my $columns; + + # Note that we do not include the '#' column so start at 1 + for (my $i = 1; $i < @columnHeadings; $i++) { + $columns .= "\"$columnHeadings[$i]\""; + + $columns .= ',' unless $i == @columnHeadings; + } # for + + print "$columns\n"; + + for (sort {$b->{change} <=> $a->{change}} @data) { + my %change = %$_; + + ## TODO: This code is duplicated (See displayChange). Consider refactoring. + # Note: $change must be right next to the -c! + my (%job, $jiraStatus, $issueType, $fixVersions); + + for ($p4->Run ('fixes', "-c$change{change}")) { + %job = %{getJobInfo $_->{Job}}; + last; # FIXME - doesn't handle muliple jobs. + } # for + + $job{Description} = '' unless %job; + + my ($bugid, $jiraIssue); + + if ($job{Job}) { + if ($job{Job} =~ /^(\d+)/) { + $bugid = $1; + } elsif ($job{Job} =~ /^(\w+-\d+)/) { + $jiraIssue = $1; + } + } # if + + if ($jiraIssue) { + my $issue; + + eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))}; + + unless ($@) { + $jiraStatus = $issue->{fields}{status}{name}; + $issueType = $issue->{fields}{issuetype}{name}; + + my @fixVersions; + + push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}}; + + $fixVersions = join "\n", @fixVersions; + } # unless + } # if + + my $job; + + unless ($job) { + chomp $change{desc}; + + $job = { + Description => $change{desc}, + Job => 'Unknown', + }; + } # unless + ## End of refactor code + + $job{Description} = join ("\r\n", split "\n", $job{Description}); + + my @files = getFiles $change{change}; + my $comments = join ("\r\n", split "\n", getComments ($change{change})); + + # Fix up double quotes in description and comments + $job{Description} =~ s/"/""/g; + $comments =~ s/"/""/g; + + print "$change{change},"; + print $bugid ? "$bugid," : ','; + print $jiraIssue ? "$jiraIssue," : ','; + print $issueType ? "$issueType," : ','; + print $jiraStatus ? "$jiraStatus," : ','; + print $fixVersions ? "$fixVersions," : ','; + print "$change{user},"; + print '"' . formatTimestamp ($change{time}) . '",'; + print scalar @files . ','; + print "\"$job{Description}\","; + print "\"$comments\","; + print '"' . join ("\n", @files) . "\""; + print "\n"; + } # for + + return; +} # exportCSV + +sub main { + # Standard opts + $opts{usage} = sub { pod2usage }; + $opts{help} = sub { pod2usage (-verbose => 2)}; + + # Opts for this script + $opts{username} //= $ENV{P4USER} || $ENV{USERNAME} || $ENV{USER}; + $opts{client} //= $ENV{P4CLIENT}; + $opts{port} //= $ENV{P4PORT} || 'perforce:1666'; + $opts{password} //= $ENV{P4PASSWD}; + $opts{html} //= $ENV{HTML} || param ('html') || 1; + $opts{html} = (-t) ? 0 : 1; + $opts{debug} //= $ENV{DEBUG} || param ('debug') || sub { set_debug }; + $opts{verbose} //= $ENV{VERBOSE} || param ('verbose') || sub { set_verbose }; + $opts{jiraserver} //= 'jira'; + $opts{from} = param 'from'; + $opts{to} = param 'to'; + $opts{branchpath} = param ('branchpath') || '//AudEngr/Import/VSS/...'; + $opts{group} = param 'group'; + $opts{comments} //= $ENV{COMMENTS} || param 'comments'; + $opts{files} //= $ENV{FILES} || param 'files'; + $opts{long} //= $ENV{LONG} || param 'long'; + $opts{csv} //= $ENV{CSV} || param 'csv'; + + GetOptions ( + \%opts, + 'verbose', + 'debug', + 'help', + 'usage', + 'port=s', + 'username=s', + 'password=s', + 'client=s', + 'comments', + 'files', + 'description', + 'long', + 'from=s', + 'to=s', + 'branchpath=s', + 'html!', + 'csv', + ) || pod2usage; + + $opts{comments} = $opts{files} = 1 if $opts{long}; + $opts{debug} = get_debug if ref $opts{debug} eq 'CODE'; + $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE'; + + # Are we doing HTML? + if ($opts{html}) { + require CGI::Carp; + + CGI::Carp->import ('fatalsToBrowser'); + + $opts{username} ||= 'shared'; + } # if + + # Needed if using the shared user + if ($opts{username} eq 'shared') { + unless (-f $p4ticketsFile) { + errorMsg "Using 'shared' user but there is no P4TICKETS file ($p4ticketsFile)", 1; + } # unless + } else { + if ($opts{username} and not $opts{password}) { + $opts{password} = GetPassword "I need the Perforce password for $opts{username}"; + } # if + } # if + + p4connect; + + my $jira = Connect2JIRA (undef, undef, $opts{jiraserver}); + + unless ($opts{from} or $opts{to}) { + if ($opts{html}) { + heading $title, $subtitle; + + displayForm $opts{from}, $opts{to}, $opts{branchpath}; + + footing; + + exit; + } # if + } # unless + + my $ymd = YMD; + my $midnight = substr ($ymd, 0, 4) . '/' + . substr ($ymd, 4, 2) . '/' + . substr ($ymd, 6, 2) . ':00:00:00'; + + $opts{to} //= 'now'; + $opts{from} //= $midnight; + + $opts{to} = 'now' if ($opts{to} eq '' or $opts{to} eq ''); + $opts{from} = $midnight if ($opts{from} eq '' or $opts{from} eq ''); + + my $msg = 'Changes made '; + + if ($opts{to} =~ /^now$/i and $opts{from} eq $midnight) { + $msg .= 'today'; + } elsif ($opts{to} =~ /^now$/i) { + $msg .= "since $opts{from}"; + } else { + $msg .= "between $opts{from} and $opts{to}"; + } # if + + if ($opts{csv}) { + my $filename = "$opts{from}_$opts{to}.csv"; + + $filename =~ s/\//-/g; + $filename =~ s/\@//g; + + debug "branchpath = $opts{branchpath}"; + + exportCSV $filename, getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + return; + } # if + + if ($opts{html}) { + heading 'Release Mission Control', $msg; + } else { + display "$msg\n"; + } # if + + if ($opts{html}) { + my $startTime = time; + displayChangesHTML getChanges $opts{from}, $opts{to}, $opts{branchpath}; + + footing $startTime; + } else { + displayChanges getChanges $opts{from}, $opts{to}, $opts{branchpath}; + } # if + + return; +} # main + +main; + +exit; + diff --git a/rmc/rmctable.js b/rmc/rmctable.js new file mode 100644 index 0000000..778ccb1 --- /dev/null +++ b/rmc/rmctable.js @@ -0,0 +1,1002 @@ +/** + * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com) + * + * Dual licensed under the MIT and GPL licenses. + * This basically means you can use this code however you want for + * free, but don't claim to have written it yourself! + * Donations always accepted: http://www.JavascriptToolbox.com/donate/ + * + * Please do not link to the .js files on javascripttoolbox.com from + * your site. Copy the files locally to your server instead. + * + */ +/** + * Table.js + * Functions for interactive Tables + * + * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com) + * Dual licensed under the MIT and GPL licenses. + * + * @version 0.981 + * + * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats + * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin. + * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes + * @history 0.958 2007-02-28 Added auto functionality based on class names + * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality + * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality. + * @history 0.950 2006-11-15 First BETA release. + * + * @todo Add more date format parsers + * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column + * @todo Correct for colspans in data rows (this may slow it down) + * @todo Fix for IE losing form control values after sort? + */ + +/** + * Sort Functions + */ +var Sort = (function(){ + var sort = {}; + // Default alpha-numeric sort + // -------------------------- + sort.alphanumeric = function(a,b) { + return (a==b)?0:(a0) { + var rows = section.rows; + for (var j=0,L2=rows.length; j0) { + var cells = row.cells; + for (var k=0,L3=cells.length; k1 && cells[cells.length-1].cellIndex>0) { + // Define the new function, overwrite the one we're running now, and then run the new one + (this.getCellIndex = function(td) { + return td.cellIndex; + })(td); + } + // Safari will always go through this slower block every time. Oh well. + for (var i=0,L=cells.length; i=0 && node.options) { + // Sort select elements by the visible text + return node.options[node.selectedIndex].text; + } + return ""; + }, + 'IMG':function(node) { + return node.name || ""; + } + }; + + /** + * Get the text value of a cell. Only use innerText if explicitly told to, because + * otherwise we want to be able to handle sorting on inputs and other types + */ + table.getCellValue = function(td,useInnerText) { + if (useInnerText && def(td.innerText)) { + return td.innerText; + } + if (!td.childNodes) { + return ""; + } + var childNodes=td.childNodes; + var ret = ""; + for (var i=0,L=childNodes.length; i-1) { + filters={ 'filter':filters.options[filters.selectedIndex].value }; + } + // Also allow for a regular input + if (filters.nodeName=="INPUT" && filters.type=="text") { + filters={ 'filter':"/^"+filters.value+"/" }; + } + // Force filters to be an array + if (typeof(filters)=="object" && !filters.length) { + filters = [filters]; + } + + // Convert regular expression strings to RegExp objects and function strings to function objects + for (var i=0,L=filters.length; ipageend) { + hideRow = true; + } + } + } + + row.style.display = hideRow?"none":""; + } + } + + if (def(page)) { + // Check to see if filtering has put us past the requested page index. If it has, + // then go back to the last page and show it. + if (pagestart>=unfilteredrowcount) { + pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize); + tdata.page = page = pagestart/pagesize; + for (var i=pagestart,L=unfilteredrows.length; i0) { + if (typeof(args.insert)=="function") { + func.insert(cell,colValues); + } + else { + var sel = ''; + cell.innerHTML += "
"+sel; + } + } + } + }); + if (val = classValue(t,table.FilteredRowcountPrefix)) { + tdata.container_filtered_count = document.getElementById(val); + } + if (val = classValue(t,table.RowcountPrefix)) { + tdata.container_all_count = document.getElementById(val); + } + }; + + /** + * Attach the auto event so it happens on load. + * use jQuery's ready() function if available + */ + if (typeof(jQuery)!="undefined") { + jQuery(table.auto); + } + else if (window.addEventListener) { + window.addEventListener( "load", table.auto, false ); + } + else if (window.attachEvent) { + window.attachEvent( "onload", table.auto ); + } + + return table; +})(); diff --git a/test/testConfluence.pl b/test/testConfluence.pl new file mode 100755 index 0000000..6ec842e --- /dev/null +++ b/test/testConfluence.pl @@ -0,0 +1,20 @@ +#/usr/bin/env perl +use strict; +use warnings; + +use FindBin; + +use lib "$FindBin::Bin/../lib"; + +use Confluence; + +my $confluence = Confluence->new ( + username => 'adefaria', + server => 'confluence', + port => 8080, +); + +my $content = $confluence->getContent ( + title => 'Knowles Migration', +); + diff --git a/test/testldap.pl b/test/testldap.pl new file mode 100755 index 0000000..a5c7602 --- /dev/null +++ b/test/testldap.pl @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Net::LDAP; +use Carp; + +sub getUserEmail ($) { + my ($userid) = @_; + + my (@entries, $result); + + my %opts = ( + KNOWLES_AD_HOST => '10.252.2.28', + KNOWLES_AD_PORT => 389, + KNOWLES_AD_BASEDN => 'DC=knowles,DC=com', + KNOWLES_AD_BINDDN => 'CN=AD Reader,OU=Users,OU=KMV,OU=Knowles,DC=knowles,DC=com', + KNOWLES_AD_BINDPW => '@Dre@D2015', + ); + + my $mailAttribute = 'mail'; + + print "Creating new LDAP object for Knowles\n"; + my $knowlesLDAP = Net::LDAP->new ( + $opts{KNOWLES_AD_HOST}, ( + host => $opts{KNOWLES_AD_HOST}, + port => $opts{KNOWLES_AD_PORT}, + basedn => $opts{KNOWLES_AD_BASEDN}, + #binddn => $opts{KNOWLES_AD_BINDDN}, + #bindpw => $opts{KNOWLES_AD_BINDPW}, + ) + ) or croak $@; + + print "Binding anonymously\n"; +# if ($opts{KNOWLES_AD_BINDDN}) { + $result = $knowlesLDAP->bind ( +# dn => $opts{KNOWLES_AD_BINDDN}, +# password => $opts{KNOWLES_AD_BINDPW}, + ) or croak "Unable to bind\n$@"; + + croak "Unable to bind (Error " . $result->code . "\n" . $result->error + if $result->code; + + print "Searching for $userid\n"; + $result = $knowlesLDAP->search ( + base => $opts{KNOWLES_AD_BASEDN}, + filter => "sAMAccountName=$userid", + ); + + print "Getting entries\n"; + @entries = ($result->entries); + + if ($entries[0]) { + return $entries[0]->get_value ($mailAttribute); + } else { + return 'Unknown'; + } # if +} # getUserEmail + +print getUserEmail ('adefari'); +print "\n"; \ No newline at end of file -- 2.17.1