Unreviewed. Add Silvia Pfeiffer to contributor list.
[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 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 }