3 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved.
4 # Copyright (C) 2009 Torch Mobile Inc. All rights reserved.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
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.
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.
30 # Script to put change log comments in as default check-in comment.
37 use lib $FindBin::Bin;
41 sub createCommitMessage(@);
43 sub loadTermReadKey();
44 sub normalizeLineEndings($$);
45 sub patchAuthorshipString($$$);
46 sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
47 sub isCommitLogEditor($);
53 my $programName = basename($0);
55 Usage: $programName [--regenerate-log] <log file>
56 $programName --print-log <ChangeLog file> [<ChangeLog file>...]
64 my $regenerateLog = 0;
66 my $getOptionsResult = GetOptions(
68 'print-log' => \$printLog,
69 'regenerate-log' => \$regenerateLog,
72 if (!$getOptionsResult || $help) {
76 die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog;
79 printUsageAndExit() unless @ARGV;
80 print createCommitMessage(@ARGV);
91 my $baseDir = baseProductDir();
93 my $editor = $ENV{SVN_LOG_EDITOR};
94 $editor = $ENV{CVS_LOG_EDITOR} if !$editor;
95 $editor = "" if $editor && isCommitLogEditor($editor);
99 my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
100 if (-x $builtEditorApplication) {
101 $editor = $builtEditorApplication;
106 my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
107 if (-x $builtEditorApplication) {
108 $editor = $builtEditorApplication;
113 my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
114 if (-x $builtEditorApplication) {
115 $editor = $builtEditorApplication;
120 $editor = $ENV{EDITOR} if !$editor;
121 $editor = "/usr/bin/vi" if !$editor;
125 @editor = split ' ', $editor;
130 my $inChangesToBeCommitted = !isGit();
132 my $logContents = "";
134 open LOG, $log or die "Could not open the log file.";
137 if (/^# Changes to be committed:$/) {
138 $inChangesToBeCommitted = 1;
139 } elsif ($inChangesToBeCommitted && /^# \S/) {
140 $inChangesToBeCommitted = 0;
144 if (!isGit() || /^#/) { #
147 # $_ contains the current git log message
148 # (without the log comment info). We don't need it.
150 $existingLog = isGit() && !(/^#/ || /^\s*$/) unless $existingLog;
152 push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && (/^(?:M|A)....(.*ChangeLog)\r?\n?$/ || /^#\t(?:modified|new file): (.*ChangeLog)$/) && !/-ChangeLog$/;
156 # We want to match the line endings of the existing log file in case they're
157 # different from perl's line endings.
158 $endl = $1 if $logContents =~ /(\r?\n)/;
160 my $keepExistingLog = 1;
161 if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0 && loadTermReadKey()) {
162 print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
163 Term::ReadKey::ReadMode('cbreak');
164 my $key = Term::ReadKey::ReadKey(0);
165 Term::ReadKey::ReadMode('normal');
166 $keepExistingLog = 0 if ($key eq "r");
169 # Don't change anything if there's already a log message (as can happen with git-commit --amend).
170 exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
173 open NEWLOG, ">$log.edit" or die;
174 if (isGit() && @changeLogs == 0) {
175 # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
176 my $branch = gitBranch();
177 chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
178 if ($webkitGenerateCommitMessage eq "") {
179 chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
181 if ($webkitGenerateCommitMessage ne "false") {
182 open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
183 while (<CHANGELOG_ENTRIES>) {
184 print NEWLOG normalizeLineEndings($_, $endl);
186 close CHANGELOG_ENTRIES;
189 print NEWLOG createCommitMessage(@changeLogs);
191 print NEWLOG $logContents;
194 system (@editor, "$log.edit");
196 open NEWLOG, "$log.edit" or exit;
197 my $foundComment = 0;
199 $foundComment = 1 if (/\S/ && !/^CVS:/);
204 open NEWLOG, "$log.edit" or die;
205 open LOG, ">$log" or die;
215 sub createCommitMessage(@)
219 my $topLevel = determineVCSRoot();
222 my %changeLogContents;
223 for my $changeLog (@changeLogs) {
224 open CHANGELOG, $changeLog or die "Can't open $changeLog";
231 my $hasAuthorInfoToWrite = 0;
232 while (<CHANGELOG>) {
237 $contents .= $blankLines if $contents;
242 # Remove indentation spaces
245 # Grab the author and the date line
246 if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
250 $hasAuthorInfoToWrite = 1;
254 if ($hasAuthorInfoToWrite) {
255 my $isReviewedByLine = $line =~ m/^(?:Reviewed|Rubber[ \-]?stamped) by/;
256 my $isModifiedFileLine = $line =~ m/^\* .*:/;
258 # Insert the authorship line if needed just above the "Reviewed by" line or the
259 # first modified file (whichever comes first).
260 if ($isReviewedByLine || $isModifiedFileLine) {
261 $hasAuthorInfoToWrite = 0;
262 my $authorshipString = patchAuthorshipString($author, $email, $date);
263 if ($authorshipString) {
264 $contents .= "$authorshipString\n";
265 $contents .= "\n" if $isModifiedFileLine;
277 if ($hasAuthorInfoToWrite) {
278 # We didn't find anywhere to put the authorship info, so just put it at the end.
279 my $authorshipString = patchAuthorshipString($author, $email, $date);
280 $contents .= "\n$authorshipString\n" if $authorshipString;
281 $hasAuthorInfoToWrite = 0;
286 $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
288 my $label = dirname($changeLog);
289 $label = "top level" unless length $label;
291 my $sortKey = lc $label;
292 if ($label eq "top level") {
294 } elsif ($label eq "LayoutTests") {
295 $sortKey = lc "~, LayoutTests last";
298 $changeLogSort{$sortKey} = $label;
299 $changeLogContents{$label} = $contents;
302 my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
306 push @result, normalizeLineEndings($commonPrefix, $endl);
307 for my $sortKey (sort keys %changeLogSort) {
308 my $label = $changeLogSort{$sortKey};
309 if (keys %changeLogSort > 1) {
310 push @result, normalizeLineEndings("\n", $endl) if !$first;
312 push @result, normalizeLineEndings("$label: ", $endl);
314 push @result, normalizeLineEndings($changeLogContents{$label}, $endl);
317 return join '', @result;
322 return unless isMsys() && isGit();
324 # When this script gets run from inside git commit, msys-style paths in the
325 # environment will have been turned into Windows-style paths with forward
326 # slashes. This screws up functions like File::Spec->rel2abs, which seem to
327 # rely on $PWD having an msys-style path. We convert the paths back to
328 # msys-style here by transforming "c:/foo" to "/c/foo" (e.g.). See
329 # <http://webkit.org/b/48527>.
330 foreach my $key (keys %ENV) {
331 $ENV{$key} =~ s#^([[:alpha:]]):/#/$1/#;
335 sub loadTermReadKey()
337 eval { require Term::ReadKey; };
341 sub normalizeLineEndings($$)
343 my ($string, $endl) = @_;
344 $string =~ s/\r?\n/$endl/g;
348 sub patchAuthorshipString($$$)
350 my ($authorName, $authorEmail, $authorDate) = @_;
352 return if $authorEmail eq changeLogEmailAddress();
353 return "Patch by $authorName <$authorEmail> on $authorDate";
356 sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
358 my ($hashOfStrings) = @_;
360 my @strings = values %{$hashOfStrings};
361 return "" unless @strings > 1;
363 my $prefix = shift @strings;
364 my $prefixLength = length $prefix;
365 foreach my $string (@strings) {
366 while ($prefixLength) {
367 last if substr($string, 0, $prefixLength) eq $prefix;
369 $prefix = substr($prefix, 0, -1);
371 last unless $prefixLength;
374 return "" unless $prefixLength;
376 my $lastDoubleNewline = rindex($prefix, "\n\n");
377 return "" unless $lastDoubleNewline > 0;
379 foreach my $key (keys %{$hashOfStrings}) {
380 $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
382 return substr($prefix, 0, $lastDoubleNewline + 2);
385 sub isCommitLogEditor($)
388 return $editor =~ m/commit-log-editor/;