20 Release Mission Control: Customized Release Notes
28 Andrew DeFaria <Andrew@ClearSCM.com>
36 Thu Mar 20 10:11:53 PDT 2014
46 $ rmc.pl [-username <username>] [-password <password>]
47 [-client client] [-port] [-[no]html] [-csv]
48 [-comments] [-files] [-description]
50 -from <revRange> [-to <revRange>]
53 [-verbose] [-debug] [-help] [-usage]
57 -v|erbose: Display progress output
58 -deb|ug: Display debugging information
59 -he|lp: Display full help
60 -usa|ge: Display usage
61 -p|ort: Perforce server and port (Default: Env P4PORT).
62 -use|rname: Name of the user to connect to Perforce with with
64 -p|assword: Password for the user to connect to Perforce with
65 (Default: Env P4PASSWD).
66 -cl|ient: Perforce Client (Default: Env P4CLIENT)
67 -co|mments: Include comments in output
68 -fi|les: Include files in output
69 -cs|v: Produce a csv file
70 -des|cription: Include description from Bugzilla
72 -l|ong: Shorthand for -comments & -files
73 -t|o: To revSpec (Default: @now)
74 -b|ranchpath: Path to limit changes to
75 -[no]ht|ml: Whether or not to produce html output
77 Note that revSpecs are Perforce's way of handling changelist/label/dates. For
78 more info see p4 help revisions. For your reference:
80 #rev - A revision number or one of the following keywords:
81 #none - A nonexistent revision (also #0).
82 #head - The current head revision (also @now).
83 #have - The revision on the current client.
84 @change - A change number: the revision as of that change.
85 @client - A client name: the revision on the client.
86 @label - A label name: the revision in the label.
87 @date - A date or date/time: the revision as of that time.
88 Either yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss
89 Note that yyyy/mm/dd means yyyy/mm/dd:00:00:00.
90 To include all events on a day, specify the next day.
94 This script produces release notes on the web or in a .csv file. You can also
95 produce an .html file by using -html and redirecting stdout.
105 use lib "$FindBin::Bin/../../Web/common/lib";
106 use lib "$FindBin::Bin/../../common/lib";
107 use lib "$FindBin::Bin/../../lib";
108 use lib "$FindBin::Bin/../lib";
118 my $VERSION = '$Revision: #7 $';
119 ($VERSION) = ($VERSION =~ /\$Revision: (.*) /);
124 my $p4ticketsFile = '/opt/audience/perforce/p4tickets';
126 my $bugsweb = 'http://bugs.audience.local/show_bug.cgi?id=';
127 my $p4web = 'http://p4web.audience.local:8080';
128 my $jiraWeb = 'http://jira.audience.local/browse/';
131 my $changesCommand = '';
135 my $title = 'Release Mission Control';
136 my $subtitle = 'Select from and to revspecs to see the bugs changes between them';
137 my $helpStr = 'Both From and To are Perforce '
139 . '. You can use changelists, labels, dates or clients. For more'
140 . ' see p4 help revisions or '
142 href => 'http://www.perforce.com/perforce/r12.2/manuals/cmdref/o.fspecs.html#1047453',
145 'Perforce File Specifications',
148 . b ('revSpec examples')
149 . ': <change>, <client>, <label>, '
150 . '<date> - yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss'
153 . ' To show all changes after label1 but before label2 use >label1 for From and @label2 for To. Or specify To as now';
154 my @columnHeadings = (
170 sub displayForm (;$$$);
173 my ($startTime) = @_;
175 print '<center>', a {
176 href => url (-relative => 1). "?csv=1&from=$opts{from}&to=$opts{to}&branchpath=$opts{branchpath}",
177 }, 'Export CSV</center>' if $opts{from} or $opts{to};
181 my $script = $FindBin::Script =~ /index.pl/
185 my ($sec, $min, $hour, $mday, $mon, $year) =
186 localtime ((stat ($script))[9]);
191 my $dateModified = "$mon/$mday/$year @ $hour:$min";
192 my $secondsElapsed = $startTime ? time () - $startTime . ' secs' : '';
196 print start_div {-class => 'copyright'};
197 print "$script version $VERSION: Last modified: $dateModified";
198 print " ($secondsElapsed)" if $secondsElapsed;
199 print br "Copyright © $year, Audience - All rights reserved - Design by ClearSCM";
208 my ($msg, $exit) = @_;
210 unless ($opts{html}) {
216 unless ($headerPrinted) {
223 print font ({class => 'error'}, '<br>ERROR: ') . $msg;
234 return unless $opts{debug};
236 unless ($opts{html}) {
242 unless ($headerPrinted) {
249 print font ({class => 'error'}, '<br>DEBUG: ') . $msg;
252 sub formatTimestamp (;$) {
255 my $date = YMDHMS ($time);
256 my $formattedDate = substr ($date, 0, 4) . '-'
257 . substr ($date, 4, 2) . '-'
258 . substr ($date, 6, 2) . ' '
261 return $formattedDate;
267 my $msg = "Unable to run \"p4 $cmd\"";
268 $msg .= $opts{html} ? '<br>' : "\n";
270 if ($p4->ErrorCount) {
271 displayForm $opts{from}, $opts{to}, $opts{branchpath};
273 errorMsg $msg . $p4->Errors, $p4->ErrorCount;
282 $p4->SetUser ($opts{username});
283 $p4->SetClient ($opts{client}) if $opts{client};
284 $p4->SetPort ($opts{port});
286 if ($opts{username} eq 'shared') {
287 $p4->SetTicketFile ($p4ticketsFile);
289 $p4->SetPassword ($opts{password});
292 verbose_nolf "Connecting to Perforce server $opts{port}...";
293 $p4->Connect or die "Unable to connect to Perforce Server\n";
296 unless ($opts{username} eq 'shared') {
297 verbose_nolf "Logging in as $opts{username}\@$opts{port}...";
309 sub getChanges ($;$$) {
310 my ($from, $to, $branchpath) = @_;
312 $from = "\@$from" unless $from =~ /^@/;
313 $to = "\@$to" unless $to =~ /^@/;
316 #$args = '-s shelved ';
317 $args .= $branchpath if $branchpath;
319 $args .= ",$to" if $to;
322 my $changes = $p4->Run ($cmd, $args);
324 $changesCommand = "p4 $cmd $args" if $opts{debug};
326 p4errors "$cmd $args";
329 if ($to =~ /\@now/i) {
330 verbose "No changes since $from";
332 verbose "No changes between $from - $to";
344 my $jobs = $p4->IterateJobs ("-e $job");
346 p4errors "jobs -e $job";
348 $job = $jobs->next if $jobs->hasNext;
353 sub getComments ($) {
354 my ($changelist) = @_;
356 my $change = $p4->FetchChange ($changelist);
358 p4errors "change $changelist";
360 return $change->{Description};
364 my ($changelist) = @_;
366 my $files = $p4->Run ('files', "\@=$changelist");
368 p4errors "files \@=$changelist";
372 push @files, $_->{depotFile} . '#' . $_->{rev} for @$files;
377 sub displayForm (;$$$) {
378 my ($from, $to, $branchpath) = @_;
383 print p {align => 'center', class => 'dim'}, $helpStr;
387 actions => $FindBin::Script,
397 print Tr [th ['From', 'To']];
399 print td {align => 'center'}, textfield (
404 print td {align => 'center'}, textfield (
411 print Tr [th {colspan => 2}, 'Branch/Path'];
417 -name => 'branchpath',
418 value => $branchpath,
424 print td {align => 'center', colspan => 2}, b ('Options:'), checkbox (
427 onclick => 'toggleOption ("comments");',
429 checked => $opts{comments} ? 'checked' : '',
430 value => $opts{comments},
434 onclick => 'toggleOption ("files");',
436 checked => $opts{files} ? 'checked' : '',
437 value => $opts{files},
441 # onclick => 'groupIndicate();',
442 # label => 'Group Indicate',
443 # checked => 'checked',
444 # value => $opts{group},
450 print td {align => 'center', colspan => 2}, input {
461 sub displayChangesHTML (@) {
464 displayForm $opts{from}, $opts{to}, $opts{branchpath};
467 my $msg = "No changes found between $opts{from} and $opts{to}";
468 $msg .= " for $opts{branchpath}";
474 my $displayComments = $opts{comments} ? '' : 'none';
475 my $displayFiles = $opts{files} ? '' : 'none';
477 debugMsg "Changes command used: $changesCommand";
480 class => 'table-autosort',
490 class => 'table-sortable:numeric table-sortable',
491 title => 'Click to sort',
494 class => 'table-sortable:numeric',
495 title => 'Click to sort',
498 class => 'table-sortable:numeric',
499 title => 'Click to sort',
502 class => 'table-sortable:default table-sortable',
503 title => 'Click to sort',
506 class => 'table-sortable:default table-sortable',
507 title => 'Click to sort',
510 class => 'table-sortable:default table-sortable',
511 title => 'Click to sort',
514 class => 'table-sortable:default table-sortable',
515 title => 'Click to sort',
518 class => 'table-sortable:default table-sortable',
519 title => 'Click to sort',
522 class => 'table-sortable:numeric table-sortable',
523 title => 'Click to sort',
528 style => "display: $displayComments",
529 }, 'Checkin Comments',
532 style => "display: $displayFiles",
539 for (sort {$b->{change} <=> $a->{change}} @changes) {
542 my @files = getFiles $change{change};
546 for ($p4->Run ('fixes', "-c$change{change}")) { # Why am I uses fixes here?
547 $job = getJobInfo $_->{Job};
548 last; # FIXME - doesn't handle muliple jobs.
551 $job->{Description} = font {color => '#aaa'}, 'N/A' unless $job;
553 my ($bugid, $jiraIssue);
556 if ($job->{Job} =~ /^(\d+)/) {
558 } elsif ($job->{Job} =~ /^(\w+-\d+)/) {
563 # Using the following does not guarantee the ordering of the elements
567 # href => "$bugsweb$bugid",
568 # target => 'bugzilla',
571 # IOW sometimes I get <a href="..." target="bugzilla"> and other times I
572 # get <a target="bugzilla" href="...">! Not cool because the JavaScript
573 # later on is comparing innerHTML and this messes that up. So we write this
574 # out by hand instead.
576 ? "<a href=\"$bugsweb$bugid\" target=\"bugzilla\">$bugid</a>"
577 : font {color => '#aaa'}, 'N/A';
578 my $jiralink = $jiraIssue
579 ? "<a href=\"$jiraWeb$jiraIssue\" target=\"jira\">$jiraIssue</a>"
580 : font {color => '#aaa'}, 'N/A';
581 my $cllink = "<a href=\"$p4web/$_->{change}?ac=133\" target=\"p4web\">$_->{change}</a>";
582 my $userid = $_->{user};
583 my $description = $job->{Description};
584 my $jiraStatus = font {color => '#aaa'}, 'N/A';
585 my $issueType = font {color => '#aaa'}, 'N/A';
586 my $fixVersions = font {color => '#aaa'}, 'N/A';
591 eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))};
594 $jiraStatus = $issue->{fields}{status}{name};
595 $issueType = $issue->{fields}{issuetype}{name};
599 push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}};
601 $fixVersions = join '<br>', @fixVersions;
605 print start_Tr {id => ++$i};
607 # Attempting to "right size" the columns...
609 td {width => '10px', align => 'center'}, $i,
610 td {width => '15px', align => 'center', id => "changelist$i"}, $cllink,
611 td {width => '60px', align => 'center', id => "bugzilla$i"}, $buglink,
612 td {width => '80px', align => 'center', id => "jira$i"}, $jiralink,
613 td {width => '50px', align => 'center', id => "type$i"}, $issueType,
614 td {width => '50px', align => 'center', id => "jirastatus$i"}, $jiraStatus,
615 td {width => '50px', align => 'center', id => "fixVersion$i"}, $fixVersions,
616 td {width => '30px', align => 'center', id => "userid$i"}, a {href => "mailto:$userid\@audience.com" }, $userid,
617 td {width => '130px', align => 'center'}, formatTimestamp ($_->{time}),
618 td {width => '10px', align => 'center'}, scalar @files;
620 if ($description =~ /N\/A/) {
621 print td {id => "description$i", align => 'center'}, $description;
623 print td {id => "description$i"}, $description;
627 td {id => "comments$i",
629 style => "display: $displayComments",
630 }, pre {class => 'code'}, getComments ($_->{change});
635 style => "display: $displayFiles"
638 print start_pre {class => 'code'};
640 for my $file (@files) {
641 my ($filelink) = ($file =~ /(.*)\#/);
642 my ($revision) = ($file =~ /\#(\d+)/);
644 # Note: For a Perforce "Add to Source Control" operation, revision is
645 # actually "none". Let's fix this.
646 $revision = 1 unless $revision;
648 if ($revision == 1) {
652 src => "$p4web/rundiffprevsmallIcon?ac=20",
653 title => "There's nothing to diff since this is the first revision",
657 href => "$p4web$filelink?ac=19&rev1=$revision&rev2=" . ($revision - 1),
660 src => "$p4web/rundiffprevsmallIcon?ac=20",
661 title => "Diff rev #$revision vs. rev #" . ($revision -1),
666 href => "$p4web$filelink?ac=22",
680 } # displayChangesHTML
682 sub displayChange (\%;$) {
683 my ($change, $nbr) = @_;
687 # Note: $change must about -c!
688 my $args = "-c$change->{change}";
690 my $fix = $p4->Run ($cmd, $args);
692 p4errors "$cmd $args";
694 errorMsg "Change $change->{change} is associated with multiple jobs. This case is not handled yet" if @$fix > 1;
698 # If there was no fix associated with this change we will use the change data.
700 $fix->{Change} = $change->{change};
701 $fix->{User} = $change->{user};
702 $fix->{Date} = $change->{time};
706 $job = getJobInfo ($fix->{Job}) if $fix->{Job};
709 chomp $change->{desc};
712 Description => $change->{desc},
717 my ($bugid) = ($job->{Job} =~ /^(\d+)/);
719 chomp $job->{Description};
721 my $description = "$change->{change}";
722 $description .= "/$bugid" if $bugid;
723 $description .= ": $job->{Description} ($fix->{User} ";
724 $description .= ymdhms ($fix->{Date}) . ')';
726 display $nbr++ . ") $description";
728 if ($opts{comments}) {
730 print "Comments:\n" . '-'x80 . "\n" . getComments ($fix->{Change}) . "\n";
733 if ($opts{description}) {
735 display "Description:\n" . '-'x80 . "\n" . $job->{Description};
739 display "Files:\n" . '-'x80;
741 for (getFiles $fix->{Change}) {
749 } # displayChangesHTML
751 sub displayChanges (@) {
755 my $msg = "No changes found between $opts{from} and $opts{to}";
756 $msg .= " for $opts{branchpath}";
764 debugMsg "Changes command used: $changesCommand";
766 $i = displayChange %$_, $i for @changes;
772 my ($title, $subtitle) = @_;
774 print header unless $headerPrinted;
781 rel => 'shortcut icon',
782 href => 'http://p4web.audience.local:8080/favicon.ico',
783 type => 'image/x-icon',
785 author => 'Andrew DeFaria <Andrew@ClearSCM.com>',
787 language => 'JavaScript',
790 language => 'JavaScript',
791 src => 'rmctable.js',
793 style => ['rmc.css'],
794 onload => 'setOptions();',
797 print h1 {class => 'title'}, "<center><img src=\"Audience.png\"> $title</center>";
798 print h3 "<center><font color='#838'>$subtitle</font></center>" if $subtitle;
804 my ($filename, @data) = @_;
807 -type => 'application/octect-stream',
808 -attachment => $filename,
814 # Note that we do not include the '#' column so start at 1
815 for (my $i = 1; $i < @columnHeadings; $i++) {
816 $columns .= "\"$columnHeadings[$i]\"";
818 $columns .= ',' unless $i == @columnHeadings;
823 for (sort {$b->{change} <=> $a->{change}} @data) {
826 ## TODO: This code is duplicated (See displayChange). Consider refactoring.
827 # Note: $change must be right next to the -c!
828 my (%job, $jiraStatus, $issueType, $fixVersions);
830 for ($p4->Run ('fixes', "-c$change{change}")) {
831 %job = %{getJobInfo $_->{Job}};
832 last; # FIXME - doesn't handle muliple jobs.
835 $job{Description} = '' unless %job;
837 my ($bugid, $jiraIssue);
840 if ($job{Job} =~ /^(\d+)/) {
842 } elsif ($job{Job} =~ /^(\w+-\d+)/) {
850 eval {$issue = getIssue ($jiraIssue, qw (status issuetype fixVersions))};
853 $jiraStatus = $issue->{fields}{status}{name};
854 $issueType = $issue->{fields}{issuetype}{name};
858 push @fixVersions, $_->{name} for @{$issue->{fields}{fixVersions}};
860 $fixVersions = join "\n", @fixVersions;
870 Description => $change{desc},
874 ## End of refactor code
876 $job{Description} = join ("\r\n", split "\n", $job{Description});
878 my @files = getFiles $change{change};
879 my $comments = join ("\r\n", split "\n", getComments ($change{change}));
881 # Fix up double quotes in description and comments
882 $job{Description} =~ s/"/""/g;
883 $comments =~ s/"/""/g;
885 print "$change{change},";
886 print $bugid ? "$bugid," : ',';
887 print $jiraIssue ? "$jiraIssue," : ',';
888 print $issueType ? "$issueType," : ',';
889 print $jiraStatus ? "$jiraStatus," : ',';
890 print $fixVersions ? "$fixVersions," : ',';
891 print "$change{user},";
892 print '"' . formatTimestamp ($change{time}) . '",';
893 print scalar @files . ',';
894 print "\"$job{Description}\",";
895 print "\"$comments\",";
896 print '"' . join ("\n", @files) . "\"";
905 $opts{usage} = sub { pod2usage };
906 $opts{help} = sub { pod2usage (-verbose => 2)};
908 # Opts for this script
909 $opts{username} //= $ENV{P4USER} || $ENV{USERNAME} || $ENV{USER};
910 $opts{client} //= $ENV{P4CLIENT};
911 $opts{port} //= $ENV{P4PORT} || 'perforce:1666';
912 $opts{password} //= $ENV{P4PASSWD};
913 $opts{html} //= $ENV{HTML} || param ('html') || 1;
914 $opts{html} = (-t) ? 0 : 1;
915 $opts{debug} //= $ENV{DEBUG} || param ('debug') || sub { set_debug };
916 $opts{verbose} //= $ENV{VERBOSE} || param ('verbose') || sub { set_verbose };
917 $opts{jiraserver} //= 'jira';
918 $opts{from} = param 'from';
919 $opts{to} = param 'to';
920 $opts{branchpath} = param ('branchpath') || '//AudEngr/Import/VSS/...';
921 $opts{group} = param 'group';
922 $opts{comments} //= $ENV{COMMENTS} || param 'comments';
923 $opts{files} //= $ENV{FILES} || param 'files';
924 $opts{long} //= $ENV{LONG} || param 'long';
925 $opts{csv} //= $ENV{CSV} || param 'csv';
948 $opts{comments} = $opts{files} = 1 if $opts{long};
949 $opts{debug} = get_debug if ref $opts{debug} eq 'CODE';
950 $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE';
956 CGI::Carp->import ('fatalsToBrowser');
958 $opts{username} ||= 'shared';
961 # Needed if using the shared user
962 if ($opts{username} eq 'shared') {
963 unless (-f $p4ticketsFile) {
964 errorMsg "Using 'shared' user but there is no P4TICKETS file ($p4ticketsFile)", 1;
967 if ($opts{username} and not $opts{password}) {
968 $opts{password} = GetPassword "I need the Perforce password for $opts{username}";
974 my $jira = Connect2JIRA (undef, undef, $opts{jiraserver});
976 unless ($opts{from} or $opts{to}) {
978 heading $title, $subtitle;
980 displayForm $opts{from}, $opts{to}, $opts{branchpath};
989 my $midnight = substr ($ymd, 0, 4) . '/'
990 . substr ($ymd, 4, 2) . '/'
991 . substr ($ymd, 6, 2) . ':00:00:00';
994 $opts{from} //= $midnight;
996 $opts{to} = 'now' if ($opts{to} eq '<today>' or $opts{to} eq '');
997 $opts{from} = $midnight if ($opts{from} eq '<today>' or $opts{from} eq '');
999 my $msg = 'Changes made ';
1001 if ($opts{to} =~ /^now$/i and $opts{from} eq $midnight) {
1003 } elsif ($opts{to} =~ /^now$/i) {
1004 $msg .= "since $opts{from}";
1006 $msg .= "between $opts{from} and $opts{to}";
1010 my $filename = "$opts{from}_$opts{to}.csv";
1012 $filename =~ s/\//-/g;
1013 $filename =~ s/\@//g;
1015 debug "branchpath = $opts{branchpath}";
1017 exportCSV $filename, getChanges $opts{from}, $opts{to}, $opts{branchpath};
1023 heading 'Release Mission Control', $msg;
1029 my $startTime = time;
1030 displayChangesHTML getChanges $opts{from}, $opts{to}, $opts{branchpath};
1034 displayChanges getChanges $opts{from}, $opts{to}, $opts{branchpath};