Home | History | Annotate | Download | only in Scripts
      1 #!/usr/bin/perl -w
      2 
      3 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc.  All rights reserved.
      4 # Copyright (C) 2009 Torch Mobile Inc. All rights reserved.
      5 #
      6 # Redistribution and use in source and binary forms, with or without
      7 # modification, are permitted provided that the following conditions
      8 # are met:
      9 #
     10 # 1.  Redistributions of source code must retain the above copyright
     11 #     notice, this list of conditions and the following disclaimer.
     12 # 2.  Redistributions in binary form must reproduce the above copyright
     13 #     notice, this list of conditions and the following disclaimer in the
     14 #     documentation and/or other materials provided with the distribution.
     15 # 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     16 #     its contributors may be used to endorse or promote products derived
     17 #     from this software without specific prior written permission.
     18 #
     19 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     20 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     21 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     22 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     23 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     24 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     26 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 # Script to put change log comments in as default check-in comment.
     31 
     32 use strict;
     33 use Getopt::Long;
     34 use File::Basename;
     35 use File::Spec;
     36 use FindBin;
     37 use lib $FindBin::Bin;
     38 use VCSUtils;
     39 use webkitdirs;
     40 
     41 sub createCommitMessage(@);
     42 sub loadTermReadKey();
     43 sub normalizeLineEndings($$);
     44 sub patchAuthorshipString($$$);
     45 sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
     46 sub isCommitLogEditor($);
     47 
     48 my $endl = "\n";
     49 
     50 sub printUsageAndExit
     51 {
     52     my $programName = basename($0);
     53     print STDERR <<EOF;
     54 Usage: $programName [--regenerate-log] <log file>
     55        $programName --print-log <ChangeLog file> [<ChangeLog file>...]
     56        $programName --help
     57 EOF
     58     exit 1;
     59 }
     60 
     61 my $help = 0;
     62 my $printLog = 0;
     63 my $regenerateLog = 0;
     64 
     65 my $getOptionsResult = GetOptions(
     66     'help' => \$help,
     67     'print-log' => \$printLog,
     68     'regenerate-log' => \$regenerateLog,
     69 );
     70 
     71 if (!$getOptionsResult || $help) {
     72     printUsageAndExit();
     73 }
     74 
     75 die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog;
     76 
     77 if ($printLog) {
     78     printUsageAndExit() unless @ARGV;
     79     print createCommitMessage(@ARGV);
     80     exit 0;
     81 }
     82 
     83 my $log = $ARGV[0];
     84 if (!$log) {
     85     printUsageAndExit();
     86 }
     87 
     88 my $baseDir = baseProductDir();
     89 
     90 my $editor = $ENV{SVN_LOG_EDITOR};
     91 $editor = $ENV{CVS_LOG_EDITOR} if !$editor;
     92 $editor = "" if $editor && isCommitLogEditor($editor);
     93 
     94 my $splitEditor = 1;
     95 if (!$editor) {
     96     my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
     97     if (-x $builtEditorApplication) {
     98         $editor = $builtEditorApplication;
     99         $splitEditor = 0;
    100     }
    101 }
    102 if (!$editor) {
    103     my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
    104     if (-x $builtEditorApplication) {
    105         $editor = $builtEditorApplication;
    106         $splitEditor = 0;
    107     }
    108 }
    109 if (!$editor) {
    110     my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
    111     if (-x $builtEditorApplication) {
    112         $editor = $builtEditorApplication;
    113         $splitEditor = 0;
    114     }
    115 }
    116 
    117 $editor = $ENV{EDITOR} if !$editor;
    118 $editor = "/usr/bin/vi" if !$editor;
    119 
    120 my @editor;
    121 if ($splitEditor) {
    122     @editor = split ' ', $editor;
    123 } else {
    124     @editor = ($editor);
    125 }
    126 
    127 my $inChangesToBeCommitted = !isGit();
    128 my @changeLogs = ();
    129 my $logContents = "";
    130 my $existingLog = 0;
    131 open LOG, $log or die "Could not open the log file.";
    132 while (my $curLine = <LOG>) {
    133     if (isGit()) {
    134         if ($curLine =~ /^# Changes to be committed:$/) {
    135             $inChangesToBeCommitted = 1;
    136         } elsif ($inChangesToBeCommitted && $curLine =~ /^# \S/) {
    137             $inChangesToBeCommitted = 0;
    138         }
    139     }
    140 
    141     if (!isGit() || $curLine =~ /^#/) {
    142         $logContents .= $curLine;
    143     } else {
    144         # $_ contains the current git log message
    145         # (without the log comment info). We don't need it.
    146     }
    147     $existingLog = isGit() && !($curLine =~ /^#/ || $curLine =~ /^\s*$/) unless $existingLog;
    148     my $changeLogFileName = changeLogFileName();
    149     push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && ($curLine =~ /^(?:M|A)....(.*$changeLogFileName)\r?\n?$/ || $curLine =~ /^#\t(?:modified|new file):   (.*$changeLogFileName)$/) && $curLine !~ /-$changeLogFileName$/;
    150 }
    151 close LOG;
    152 
    153 # We want to match the line endings of the existing log file in case they're
    154 # different from perl's line endings.
    155 $endl = $1 if $logContents =~ /(\r?\n)/;
    156 
    157 my $keepExistingLog = 1;
    158 if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0 && loadTermReadKey()) {
    159     print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
    160     Term::ReadKey::ReadMode('cbreak');
    161     my $key = Term::ReadKey::ReadKey(0);
    162     Term::ReadKey::ReadMode('normal');
    163     $keepExistingLog = 0 if ($key eq "r");
    164 }
    165 
    166 # Don't change anything if there's already a log message (as can happen with git-commit --amend).
    167 exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
    168 
    169 my $first = 1;
    170 open NEWLOG, ">$log.edit" or die;
    171 if (isGit() && @changeLogs == 0) {
    172     # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
    173     my $branch = gitBranch();
    174     chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
    175     if ($webkitGenerateCommitMessage eq "") {
    176         chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
    177     }
    178     if ($webkitGenerateCommitMessage ne "false") {
    179         open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
    180         while (<CHANGELOG_ENTRIES>) {
    181             print NEWLOG normalizeLineEndings($_, $endl);
    182         }
    183         close CHANGELOG_ENTRIES;
    184     }
    185 } else {
    186     print NEWLOG createCommitMessage(@changeLogs);
    187 }
    188 print NEWLOG $logContents;
    189 close NEWLOG;
    190 
    191 system (@editor, "$log.edit");
    192 
    193 open NEWLOG, "$log.edit" or exit;
    194 my $foundComment = 0;
    195 while (<NEWLOG>) {
    196     $foundComment = 1 if (/\S/ && !/^CVS:/);
    197 }
    198 close NEWLOG;
    199 
    200 if ($foundComment) {
    201     open NEWLOG, "$log.edit" or die;
    202     open LOG, ">$log" or die;
    203     while (<NEWLOG>) {
    204         print LOG;
    205     }
    206     close LOG;
    207     close NEWLOG;
    208 }
    209 
    210 unlink "$log.edit";
    211 
    212 sub createCommitMessage(@)
    213 {
    214     my @changeLogs = @_;
    215 
    216     my $topLevel = determineVCSRoot();
    217 
    218     my %changeLogSort;
    219     my %changeLogContents;
    220     for my $changeLog (@changeLogs) {
    221         open CHANGELOG, $changeLog or die "Can't open $changeLog";
    222         my $contents = "";
    223         my $blankLines = "";
    224         my $lineCount = 0;
    225         my $date = "";
    226         my $author = "";
    227         my $email = "";
    228         my $hasAuthorInfoToWrite = 0;
    229         while (<CHANGELOG>) {
    230             if (/^\S/) {
    231                 last if $contents;
    232             }
    233             if (/\S/) {
    234                 $contents .= $blankLines if $contents;
    235                 $blankLines = "";
    236 
    237                 my $line = $_;
    238 
    239                 # Remove indentation spaces
    240                 $line =~ s/^ {8}//;
    241 
    242                 # Grab the author and the date line
    243                 if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
    244                     $date = $1;
    245                     $author = $2;
    246                     $email = $3;
    247                     $hasAuthorInfoToWrite = 1;
    248                     next;
    249                 }
    250 
    251                 if ($hasAuthorInfoToWrite) {
    252                     my $isReviewedByLine = $line =~ m/^(?:Reviewed|Rubber[ \-]?stamped) by/;
    253                     my $isModifiedFileLine = $line =~ m/^\* .*:/;
    254 
    255                     # Insert the authorship line if needed just above the "Reviewed by" line or the
    256                     # first modified file (whichever comes first).
    257                     if ($isReviewedByLine || $isModifiedFileLine) {
    258                         $hasAuthorInfoToWrite = 0;
    259                         my $authorshipString = patchAuthorshipString($author, $email, $date);
    260                         if ($authorshipString) {
    261                             $contents .= "$authorshipString\n";
    262                             $contents .= "\n" if $isModifiedFileLine;
    263                         }
    264                     }
    265                 }
    266 
    267 
    268                 $lineCount++;
    269                 $contents .= $line;
    270             } else {
    271                 $blankLines .= $_;
    272             }
    273         }
    274         if ($hasAuthorInfoToWrite) {
    275             # We didn't find anywhere to put the authorship info, so just put it at the end.
    276             my $authorshipString = patchAuthorshipString($author, $email, $date);
    277             $contents .= "\n$authorshipString\n" if $authorshipString;
    278             $hasAuthorInfoToWrite = 0;
    279         }
    280 
    281         close CHANGELOG;
    282 
    283         $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
    284 
    285         my $label = dirname($changeLog);
    286         $label = "top level" unless length $label;
    287 
    288         my $sortKey = lc $label;
    289         if ($label eq "top level") {
    290             $sortKey = "";
    291         } elsif ($label eq "LayoutTests") {
    292             $sortKey = lc "~, LayoutTests last";
    293         }
    294 
    295         $changeLogSort{$sortKey} = $label;
    296         $changeLogContents{$label} = $contents;
    297     }
    298 
    299     my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
    300 
    301     my $first = 1;
    302     my @result;
    303     push @result, normalizeLineEndings($commonPrefix, $endl);
    304     for my $sortKey (sort keys %changeLogSort) {
    305         my $label = $changeLogSort{$sortKey};
    306         if (keys %changeLogSort > 1) {
    307             push @result, normalizeLineEndings("\n", $endl) if !$first;
    308             $first = 0;
    309             push @result, normalizeLineEndings("$label: ", $endl);
    310         }
    311         push @result, normalizeLineEndings($changeLogContents{$label}, $endl);
    312     }
    313 
    314     return join '', @result;
    315 }
    316 
    317 sub loadTermReadKey()
    318 {
    319     eval { require Term::ReadKey; };
    320     return !$@;
    321 }
    322 
    323 sub normalizeLineEndings($$)
    324 {
    325     my ($string, $endl) = @_;
    326     $string =~ s/\r?\n/$endl/g;
    327     return $string;
    328 }
    329 
    330 sub patchAuthorshipString($$$)
    331 {
    332     my ($authorName, $authorEmail, $authorDate) = @_;
    333 
    334     return if $authorEmail eq changeLogEmailAddress();
    335     return "Patch by $authorName <$authorEmail> on $authorDate";
    336 }
    337 
    338 sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
    339 {
    340     my ($hashOfStrings) = @_;
    341 
    342     my @strings = values %{$hashOfStrings};
    343     return "" unless @strings > 1;
    344 
    345     my $prefix = shift @strings;
    346     my $prefixLength = length $prefix;
    347     foreach my $string (@strings) {
    348         while ($prefixLength) {
    349             last if substr($string, 0, $prefixLength) eq $prefix;
    350             --$prefixLength;
    351             $prefix = substr($prefix, 0, -1);
    352         }
    353         last unless $prefixLength;
    354     }
    355 
    356     return "" unless $prefixLength;
    357 
    358     my $lastDoubleNewline = rindex($prefix, "\n\n");
    359     return "" unless $lastDoubleNewline > 0;
    360 
    361     foreach my $key (keys %{$hashOfStrings}) {
    362         $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
    363     }
    364     return substr($prefix, 0, $lastDoubleNewline + 2);
    365 }
    366 
    367 sub isCommitLogEditor($)
    368 {
    369     my $editor = shift;
    370     return $editor =~ m/commit-log-editor/;
    371 }
    372