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 32use strict; 33use Getopt::Long; 34use File::Basename; 35use File::Spec; 36use FindBin; 37use lib $FindBin::Bin; 38use VCSUtils; 39use webkitdirs; 40 41sub createCommitMessage(@); 42sub loadTermReadKey(); 43sub normalizeLineEndings($$); 44sub patchAuthorshipString($$$); 45sub removeLongestCommonPrefixEndingInDoubleNewline(\%); 46sub isCommitLogEditor($); 47 48my $endl = "\n"; 49 50sub printUsageAndExit 51{ 52 my $programName = basename($0); 53 print STDERR <<EOF; 54Usage: $programName [--regenerate-log] <log file> 55 $programName --print-log <ChangeLog file> [<ChangeLog file>...] 56 $programName --help 57EOF 58 exit 1; 59} 60 61my $help = 0; 62my $printLog = 0; 63my $regenerateLog = 0; 64 65my $getOptionsResult = GetOptions( 66 'help' => \$help, 67 'print-log' => \$printLog, 68 'regenerate-log' => \$regenerateLog, 69); 70 71if (!$getOptionsResult || $help) { 72 printUsageAndExit(); 73} 74 75die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog; 76 77if ($printLog) { 78 printUsageAndExit() unless @ARGV; 79 print createCommitMessage(@ARGV); 80 exit 0; 81} 82 83my $log = $ARGV[0]; 84if (!$log) { 85 printUsageAndExit(); 86} 87 88my $baseDir = baseProductDir(); 89 90my $editor = $ENV{SVN_LOG_EDITOR}; 91$editor = $ENV{CVS_LOG_EDITOR} if !$editor; 92$editor = "" if $editor && isCommitLogEditor($editor); 93 94my $splitEditor = 1; 95if (!$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} 102if (!$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} 109if (!$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 120my @editor; 121if ($splitEditor) { 122 @editor = split ' ', $editor; 123} else { 124 @editor = ($editor); 125} 126 127my $inChangesToBeCommitted = !isGit(); 128my @changeLogs = (); 129my $logContents = ""; 130my $existingLog = 0; 131open LOG, $log or die "Could not open the log file."; 132while (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} 151close 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 157my $keepExistingLog = 1; 158if ($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). 167exec (@editor, @ARGV) if $existingLog && $keepExistingLog; 168 169my $first = 1; 170open NEWLOG, ">$log.edit" or die; 171if (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} 188print NEWLOG $logContents; 189close NEWLOG; 190 191system (@editor, "$log.edit"); 192 193open NEWLOG, "$log.edit" or exit; 194my $foundComment = 0; 195while (<NEWLOG>) { 196 $foundComment = 1 if (/\S/ && !/^CVS:/); 197} 198close NEWLOG; 199 200if ($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 210unlink "$log.edit"; 211 212sub 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 317sub loadTermReadKey() 318{ 319 eval { require Term::ReadKey; }; 320 return !$@; 321} 322 323sub normalizeLineEndings($$) 324{ 325 my ($string, $endl) = @_; 326 $string =~ s/\r?\n/$endl/g; 327 return $string; 328} 329 330sub patchAuthorshipString($$$) 331{ 332 my ($authorName, $authorEmail, $authorDate) = @_; 333 334 return if $authorEmail eq changeLogEmailAddress(); 335 return "Patch by $authorName <$authorEmail> on $authorDate"; 336} 337 338sub 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 367sub isCommitLogEditor($) 368{ 369 my $editor = shift; 370 return $editor =~ m/commit-log-editor/; 371} 372