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