Rename WebKitTools to Tools
[WebKit-https.git] / Tools / Scripts / commit-log-editor
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 File::Basename;
34 use File::Spec;
35 use FindBin;
36 use lib $FindBin::Bin;
37 use Term::ReadKey;
38 use VCSUtils;
39 use webkitdirs;
40
41 sub normalizeLineEndings($$);
42 sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
43 sub isCommitLogEditor($);
44
45 sub usage
46 {
47     print "Usage: [--help] [--regenerate-log] <log file>\n";
48     exit 1;
49 }
50
51 my $help = checkForArgumentAndRemoveFromARGV("--help");
52 if ($help) {
53     usage();
54 }
55
56 my $regenerateLog = checkForArgumentAndRemoveFromARGV("--regenerate-log");
57 my $log = $ARGV[0];
58 if (!$log) {
59     usage();
60 }
61
62 my $baseDir = baseProductDir();
63
64 my $editor = $ENV{SVN_LOG_EDITOR};
65 $editor = $ENV{CVS_LOG_EDITOR} if !$editor;
66 $editor = "" if $editor && isCommitLogEditor($editor);
67
68 my $splitEditor = 1;
69 if (!$editor) {
70     my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
71     if (-x $builtEditorApplication) {
72         $editor = $builtEditorApplication;
73         $splitEditor = 0;
74     }
75 }
76 if (!$editor) {
77     my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
78     if (-x $builtEditorApplication) {
79         $editor = $builtEditorApplication;
80         $splitEditor = 0;
81     }
82 }
83 if (!$editor) {
84     my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
85     if (-x $builtEditorApplication) {
86         $editor = $builtEditorApplication;
87         $splitEditor = 0;
88     }
89 }
90
91 $editor = $ENV{EDITOR} if !$editor;
92 $editor = "/usr/bin/vi" if !$editor;
93
94 my @editor;
95 if ($splitEditor) {
96     @editor = split ' ', $editor;
97 } else {
98     @editor = ($editor);
99 }
100
101 my $inChangesToBeCommitted = !isGit();
102 my @changeLogs = ();
103 my $logContents = "";
104 my $existingLog = 0;
105 open LOG, $log or die "Could not open the log file.";
106 while (<LOG>) {
107     if (isGit()) {
108         if (/^# Changes to be committed:$/) {
109             $inChangesToBeCommitted = 1;
110         } elsif ($inChangesToBeCommitted && /^# \S/) {
111             $inChangesToBeCommitted = 0;
112         }
113     }
114
115     if (!isGit() || /^#/) { #
116         $logContents .= $_;
117     } else {
118         # $_ contains the current git log message
119         # (without the log comment info). We don't need it.
120     }
121     $existingLog = isGit() && !(/^#/ || /^\s*$/) unless $existingLog;
122
123     push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && (/^(?:M|A)....(.*ChangeLog)\r?\n?$/ || /^#\t(?:modified|new file):   (.*ChangeLog)$/) && !/-ChangeLog$/;
124 }
125 close LOG;
126
127 # We want to match the line endings of the existing log file in case they're
128 # different from perl's line endings.
129 my $endl = "\n";
130 $endl = $1 if $logContents =~ /(\r?\n)/;
131
132 my $keepExistingLog = 1;
133 if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0) {
134     print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
135     ReadMode('cbreak');
136     my $key = ReadKey(0);
137     ReadMode('normal');
138     $keepExistingLog = 0 if ($key eq "r");
139 }
140
141 # Don't change anything if there's already a log message (as can happen with git-commit --amend).
142 exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
143
144 my $topLevel = determineVCSRoot();
145
146 my %changeLogSort;
147 my %changeLogContents;
148 for my $changeLog (@changeLogs) {
149     open CHANGELOG, $changeLog or die "Can't open $changeLog";
150     my $contents = "";
151     my $blankLines = "";
152     my $reviewedByLine = "";
153     my $lineCount = 0;
154     my $date = "";
155     my $author = "";
156     my $email = "";
157     my $hasAuthorInfoToWrite = 0;
158     while (<CHANGELOG>) {
159         if (/^\S/) {
160             last if $contents;
161         }
162         if (/\S/) {
163             my $previousLineWasBlank = 1 unless $blankLines eq "";
164             my $line = $_;
165             my $currentLineBlankLines = $blankLines;
166             $blankLines = "";
167
168             # Remove indentation spaces
169             $line =~ s/^ {8}//;
170
171             # Save the reviewed / rubber stamped by line.
172             if ($line =~ m/^Reviewed by .*/ || $line =~ m/^Rubber[ \-]?stamped by .*/) {
173                 $reviewedByLine = $line;
174                 next;
175             }
176
177             # Grab the author and the date line
178             if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
179                 $date = $1;
180                 $author = $2;
181                 $email = $3;
182                 $hasAuthorInfoToWrite = 1;
183                 next;
184             }
185
186             $contents .= $currentLineBlankLines if $contents;
187
188             # Attempt to insert the "patch by" line, after the first blank line.
189             if ($previousLineWasBlank && $hasAuthorInfoToWrite && $lineCount > 0) {
190                 my $committerEmail = changeLogEmailAddress();
191                 my $authorAndCommitterAreSamePerson = $email eq $committerEmail;
192                 if (!$authorAndCommitterAreSamePerson) {
193                     $contents .= "Patch by $author <$email> on $date\n";
194                     $hasAuthorInfoToWrite = 0;
195                 }
196             }
197
198             # Attempt to insert the "reviewed by" line, after the first blank line.
199             if ($previousLineWasBlank && $reviewedByLine && $lineCount > 0) {
200                 $contents .= $reviewedByLine . "\n";
201                 $reviewedByLine = "";
202             }
203
204             $lineCount++;
205             $contents .= $line;
206         } else {
207             $blankLines .= $_;
208         }
209     }
210     if ($reviewedByLine) {
211         $contents .= "\n".$reviewedByLine;
212     }
213     close CHANGELOG;
214
215     $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
216
217     my $label = dirname($changeLog);
218     $label = "top level" unless length $label;
219
220     my $sortKey = lc $label;
221     if ($label eq "top level") {
222         $sortKey = "";
223     } elsif ($label eq "LayoutTests") {
224         $sortKey = lc "~, LayoutTests last";
225     }
226
227     $changeLogSort{$sortKey} = $label;
228     $changeLogContents{$label} = $contents;
229 }
230
231 my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
232
233 my $first = 1;
234 open NEWLOG, ">$log.edit" or die;
235 if (isGit() && scalar keys %changeLogSort == 0) {
236     # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
237     my $branch = gitBranch();
238     chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
239     if ($webkitGenerateCommitMessage eq "") {
240         chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
241     }
242     if ($webkitGenerateCommitMessage ne "false") {
243         open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
244         while (<CHANGELOG_ENTRIES>) {
245             print NEWLOG normalizeLineEndings($_, $endl);
246         }
247         close CHANGELOG_ENTRIES;
248     }
249 } else {
250     print NEWLOG normalizeLineEndings($commonPrefix, $endl);
251     for my $sortKey (sort keys %changeLogSort) {
252         my $label = $changeLogSort{$sortKey};
253         if (keys %changeLogSort > 1) {
254             print NEWLOG normalizeLineEndings("\n", $endl) if !$first;
255             $first = 0;
256             print NEWLOG normalizeLineEndings("$label: ", $endl);
257         }
258         print NEWLOG normalizeLineEndings($changeLogContents{$label}, $endl);
259     }
260 }
261 print NEWLOG $logContents;
262 close NEWLOG;
263
264 system (@editor, "$log.edit");
265
266 open NEWLOG, "$log.edit" or exit;
267 my $foundComment = 0;
268 while (<NEWLOG>) {
269     $foundComment = 1 if (/\S/ && !/^CVS:/);
270 }
271 close NEWLOG;
272
273 if ($foundComment) {
274     open NEWLOG, "$log.edit" or die;
275     open LOG, ">$log" or die;
276     while (<NEWLOG>) {
277         print LOG;
278     }
279     close LOG;
280     close NEWLOG;
281 }
282
283 unlink "$log.edit";
284
285 sub normalizeLineEndings($$)
286 {
287     my ($string, $endl) = @_;
288     $string =~ s/\r?\n/$endl/g;
289     return $string;
290 }
291
292 sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
293 {
294     my ($hashOfStrings) = @_;
295
296     my @strings = values %{$hashOfStrings};
297     return "" unless @strings > 1;
298
299     my $prefix = shift @strings;
300     my $prefixLength = length $prefix;
301     foreach my $string (@strings) {
302         while ($prefixLength) {
303             last if substr($string, 0, $prefixLength) eq $prefix;
304             --$prefixLength;
305             $prefix = substr($prefix, 0, -1);
306         }
307         last unless $prefixLength;
308     }
309
310     return "" unless $prefixLength;
311
312     my $lastDoubleNewline = rindex($prefix, "\n\n");
313     return "" unless $lastDoubleNewline > 0;
314
315     foreach my $key (keys %{$hashOfStrings}) {
316         $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
317     }
318     return substr($prefix, 0, $lastDoubleNewline + 2);
319 }
320
321 sub isCommitLogEditor($)
322 {
323     my $editor = shift;
324     return $editor =~ m/commit-log-editor/;
325 }