|
1 #!/usr/bin/perl -w |
|
2 |
|
3 # Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved. |
|
4 # |
|
5 # Redistribution and use in source and binary forms, with or without |
|
6 # modification, are permitted provided that the following conditions |
|
7 # are met: |
|
8 # |
|
9 # 1. Redistributions of source code must retain the above copyright |
|
10 # notice, this list of conditions and the following disclaimer. |
|
11 # 2. Redistributions in binary form must reproduce the above copyright |
|
12 # notice, this list of conditions and the following disclaimer in the |
|
13 # documentation and/or other materials provided with the distribution. |
|
14 # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
|
15 # its contributors may be used to endorse or promote products derived |
|
16 # from this software without specific prior written permission. |
|
17 # |
|
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
|
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
|
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
|
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
28 |
|
29 # Extended "svn diff" script for WebKit Open Source Project, used to make patches. |
|
30 |
|
31 # Differences from standard "svn diff": |
|
32 # |
|
33 # Uses the real diff, not svn's built-in diff. |
|
34 # Always passes "-p" to diff so it will try to include function names. |
|
35 # Handles binary files (encoded as a base64 chunk of text). |
|
36 # Sorts the diffs alphabetically by text files, then binary files. |
|
37 # Handles copied and moved files. |
|
38 # |
|
39 # Missing features: |
|
40 # |
|
41 # Handle copied and moved directories. |
|
42 |
|
43 use strict; |
|
44 use warnings; |
|
45 |
|
46 use Config; |
|
47 use File::Basename; |
|
48 use File::Spec; |
|
49 use File::stat; |
|
50 use FindBin; |
|
51 use Getopt::Long; |
|
52 use lib $FindBin::Bin; |
|
53 use MIME::Base64; |
|
54 use POSIX qw(:errno_h); |
|
55 use Time::gmtime; |
|
56 use VCSUtils; |
|
57 |
|
58 sub binarycmp($$); |
|
59 sub diffOptionsForFile($); |
|
60 sub findBaseUrl($); |
|
61 sub findMimeType($;$); |
|
62 sub findModificationType($); |
|
63 sub findSourceFileAndRevision($); |
|
64 sub generateDiff($$); |
|
65 sub generateFileList($\%); |
|
66 sub hunkHeaderLineRegExForFile($); |
|
67 sub isBinaryMimeType($); |
|
68 sub manufacturePatchForAdditionWithHistory($); |
|
69 sub numericcmp($$); |
|
70 sub outputBinaryContent($); |
|
71 sub patchpathcmp($$); |
|
72 sub pathcmp($$); |
|
73 sub processPaths(\@); |
|
74 sub splitpath($); |
|
75 sub testfilecmp($$); |
|
76 |
|
77 $ENV{'LC_ALL'} = 'C'; |
|
78 |
|
79 my $showHelp; |
|
80 my $ignoreChangelogs = 0; |
|
81 my $devNull = File::Spec->devnull(); |
|
82 |
|
83 my $result = GetOptions( |
|
84 "help" => \$showHelp, |
|
85 "ignore-changelogs" => \$ignoreChangelogs |
|
86 ); |
|
87 if (!$result || $showHelp) { |
|
88 print STDERR basename($0) . " [-h|--help] [--ignore-changelogs] [svndir1 [svndir2 ...]]\n"; |
|
89 exit 1; |
|
90 } |
|
91 |
|
92 # Sort the diffs for easier reviewing. |
|
93 my %paths = processPaths(@ARGV); |
|
94 |
|
95 # Generate a list of files requiring diffs. |
|
96 my %diffFiles; |
|
97 for my $path (keys %paths) { |
|
98 generateFileList($path, %diffFiles); |
|
99 } |
|
100 |
|
101 my $svnRoot = determineSVNRoot(); |
|
102 my $prefix = chdirReturningRelativePath($svnRoot); |
|
103 |
|
104 my $patchSize = 0; |
|
105 |
|
106 # Generate the diffs, in a order chosen for easy reviewing. |
|
107 for my $path (sort patchpathcmp values %diffFiles) { |
|
108 $patchSize += generateDiff($path, $prefix); |
|
109 } |
|
110 |
|
111 if ($patchSize > 20480) { |
|
112 print STDERR "WARNING: Patch's size is " . int($patchSize/1024) . " kbytes.\n"; |
|
113 print STDERR "Patches 20k or smaller are more likely to be reviewed. Larger patches may sit unreviewed for a long time.\n"; |
|
114 } |
|
115 |
|
116 exit 0; |
|
117 |
|
118 # Overall sort, considering multiple criteria. |
|
119 sub patchpathcmp($$) |
|
120 { |
|
121 my ($a, $b) = @_; |
|
122 |
|
123 # All binary files come after all non-binary files. |
|
124 my $result = binarycmp($a, $b); |
|
125 return $result if $result; |
|
126 |
|
127 # All test files come after all non-test files. |
|
128 $result = testfilecmp($a, $b); |
|
129 return $result if $result; |
|
130 |
|
131 # Final sort is a "smart" sort by directory and file name. |
|
132 return pathcmp($a, $b); |
|
133 } |
|
134 |
|
135 # Sort so text files appear before binary files. |
|
136 sub binarycmp($$) |
|
137 { |
|
138 my ($fileDataA, $fileDataB) = @_; |
|
139 return $fileDataA->{isBinary} <=> $fileDataB->{isBinary}; |
|
140 } |
|
141 |
|
142 sub diffOptionsForFile($) |
|
143 { |
|
144 my ($file) = @_; |
|
145 |
|
146 my $options = "uaNp"; |
|
147 |
|
148 if (my $hunkHeaderLineRegEx = hunkHeaderLineRegExForFile($file)) { |
|
149 $options .= "F'$hunkHeaderLineRegEx'"; |
|
150 } |
|
151 |
|
152 return $options; |
|
153 } |
|
154 |
|
155 sub findBaseUrl($) |
|
156 { |
|
157 my ($infoPath) = @_; |
|
158 my $baseUrl; |
|
159 open INFO, "svn info '$infoPath' |" or die; |
|
160 while (<INFO>) { |
|
161 if (/^URL: (.+?)[\r\n]*$/) { |
|
162 $baseUrl = $1; |
|
163 } |
|
164 } |
|
165 close INFO; |
|
166 return $baseUrl; |
|
167 } |
|
168 |
|
169 sub findMimeType($;$) |
|
170 { |
|
171 my ($file, $revision) = @_; |
|
172 my $args = $revision ? "--revision $revision" : ""; |
|
173 open PROPGET, "svn propget svn:mime-type $args '$file' |" or die; |
|
174 my $mimeType = <PROPGET>; |
|
175 close PROPGET; |
|
176 # svn may output a different EOL sequence than $/, so avoid chomp. |
|
177 if ($mimeType) { |
|
178 $mimeType =~ s/[\r\n]+$//g; |
|
179 } |
|
180 return $mimeType; |
|
181 } |
|
182 |
|
183 sub findModificationType($) |
|
184 { |
|
185 my ($stat) = @_; |
|
186 my $fileStat = substr($stat, 0, 1); |
|
187 my $propertyStat = substr($stat, 1, 1); |
|
188 if ($fileStat eq "A" || $fileStat eq "R") { |
|
189 my $additionWithHistory = substr($stat, 3, 1); |
|
190 return $additionWithHistory eq "+" ? "additionWithHistory" : "addition"; |
|
191 } |
|
192 return "modification" if ($fileStat eq "M" || $propertyStat eq "M"); |
|
193 return "deletion" if ($fileStat eq "D"); |
|
194 return undef; |
|
195 } |
|
196 |
|
197 sub findSourceFileAndRevision($) |
|
198 { |
|
199 my ($file) = @_; |
|
200 my $baseUrl = findBaseUrl("."); |
|
201 my $sourceFile; |
|
202 my $sourceRevision; |
|
203 open INFO, "svn info '$file' |" or die; |
|
204 while (<INFO>) { |
|
205 if (/^Copied From URL: (.+?)[\r\n]*$/) { |
|
206 $sourceFile = File::Spec->abs2rel($1, $baseUrl); |
|
207 } elsif (/^Copied From Rev: ([0-9]+)/) { |
|
208 $sourceRevision = $1; |
|
209 } |
|
210 } |
|
211 close INFO; |
|
212 return ($sourceFile, $sourceRevision); |
|
213 } |
|
214 |
|
215 sub generateDiff($$) |
|
216 { |
|
217 my ($fileData, $prefix) = @_; |
|
218 my $file = File::Spec->catdir($prefix, $fileData->{path}); |
|
219 |
|
220 if ($ignoreChangelogs && basename($file) eq "ChangeLog") { |
|
221 return 0; |
|
222 } |
|
223 |
|
224 my $patch = ""; |
|
225 if ($fileData->{modificationType} eq "additionWithHistory") { |
|
226 manufacturePatchForAdditionWithHistory($fileData); |
|
227 } |
|
228 |
|
229 my $diffOptions = diffOptionsForFile($file); |
|
230 open DIFF, "svn diff --diff-cmd diff -x -$diffOptions '$file' |" or die; |
|
231 while (<DIFF>) { |
|
232 $patch .= $_; |
|
233 } |
|
234 close DIFF; |
|
235 $patch = fixChangeLogPatch($patch) if basename($file) eq "ChangeLog"; |
|
236 print $patch; |
|
237 if ($fileData->{isBinary}) { |
|
238 print "\n" if ($patch && $patch =~ m/\n\S+$/m); |
|
239 outputBinaryContent($file); |
|
240 } |
|
241 return length($patch); |
|
242 } |
|
243 |
|
244 sub generateFileList($\%) |
|
245 { |
|
246 my ($statPath, $diffFiles) = @_; |
|
247 my %testDirectories = map { $_ => 1 } qw(LayoutTests); |
|
248 open STAT, "svn stat '$statPath' |" or die; |
|
249 while (my $line = <STAT>) { |
|
250 # svn may output a different EOL sequence than $/, so avoid chomp. |
|
251 $line =~ s/[\r\n]+$//g; |
|
252 my $stat; |
|
253 my $path; |
|
254 if (isSVNVersion16OrNewer()) { |
|
255 $stat = substr($line, 0, 8); |
|
256 $path = substr($line, 8); |
|
257 } else { |
|
258 $stat = substr($line, 0, 7); |
|
259 $path = substr($line, 7); |
|
260 } |
|
261 next if -d $path; |
|
262 my $modificationType = findModificationType($stat); |
|
263 if ($modificationType) { |
|
264 $diffFiles->{$path}->{path} = $path; |
|
265 $diffFiles->{$path}->{modificationType} = $modificationType; |
|
266 $diffFiles->{$path}->{isBinary} = isBinaryMimeType($path); |
|
267 $diffFiles->{$path}->{isTestFile} = exists $testDirectories{(File::Spec->splitdir($path))[0]} ? 1 : 0; |
|
268 if ($modificationType eq "additionWithHistory") { |
|
269 my ($sourceFile, $sourceRevision) = findSourceFileAndRevision($path); |
|
270 $diffFiles->{$path}->{sourceFile} = $sourceFile; |
|
271 $diffFiles->{$path}->{sourceRevision} = $sourceRevision; |
|
272 } |
|
273 } else { |
|
274 print STDERR $line, "\n"; |
|
275 } |
|
276 } |
|
277 close STAT; |
|
278 } |
|
279 |
|
280 sub hunkHeaderLineRegExForFile($) |
|
281 { |
|
282 my ($file) = @_; |
|
283 |
|
284 my $startOfObjCInterfaceRegEx = "@(implementation\\|interface\\|protocol)"; |
|
285 return "^[-+]\\|$startOfObjCInterfaceRegEx" if $file =~ /\.mm?$/; |
|
286 return "^$startOfObjCInterfaceRegEx" if $file =~ /^(.*\/)?(mac|objc)\// && $file =~ /\.h$/; |
|
287 } |
|
288 |
|
289 sub isBinaryMimeType($) |
|
290 { |
|
291 my ($file) = @_; |
|
292 my $mimeType = findMimeType($file); |
|
293 return 0 if (!$mimeType || substr($mimeType, 0, 5) eq "text/"); |
|
294 return 1; |
|
295 } |
|
296 |
|
297 sub manufacturePatchForAdditionWithHistory($) |
|
298 { |
|
299 my ($fileData) = @_; |
|
300 my $file = $fileData->{path}; |
|
301 print "Index: ${file}\n"; |
|
302 print "=" x 67, "\n"; |
|
303 my $sourceFile = $fileData->{sourceFile}; |
|
304 my $sourceRevision = $fileData->{sourceRevision}; |
|
305 print "--- ${file}\t(revision ${sourceRevision})\t(from ${sourceFile}:${sourceRevision})\n"; |
|
306 print "+++ ${file}\t(working copy)\n"; |
|
307 if ($fileData->{isBinary}) { |
|
308 print "\nCannot display: file marked as a binary type.\n"; |
|
309 my $mimeType = findMimeType($file, $sourceRevision); |
|
310 print "svn:mime-type = ${mimeType}\n\n"; |
|
311 } else { |
|
312 print `svn cat ${sourceFile} | diff -u $devNull - | tail -n +3`; |
|
313 } |
|
314 } |
|
315 |
|
316 # Sort numeric parts of strings as numbers, other parts as strings. |
|
317 # Makes 1.33 come after 1.3, which is cool. |
|
318 sub numericcmp($$) |
|
319 { |
|
320 my ($aa, $bb) = @_; |
|
321 |
|
322 my @a = split /(\d+)/, $aa; |
|
323 my @b = split /(\d+)/, $bb; |
|
324 |
|
325 # Compare one chunk at a time. |
|
326 # Each chunk is either all numeric digits, or all not numeric digits. |
|
327 while (@a && @b) { |
|
328 my $a = shift @a; |
|
329 my $b = shift @b; |
|
330 |
|
331 # Use numeric comparison if chunks are non-equal numbers. |
|
332 return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b; |
|
333 |
|
334 # Use string comparison if chunks are any other kind of non-equal string. |
|
335 return $a cmp $b if $a ne $b; |
|
336 } |
|
337 |
|
338 # One of the two is now empty; compare lengths for result in this case. |
|
339 return @a <=> @b; |
|
340 } |
|
341 |
|
342 sub outputBinaryContent($) |
|
343 { |
|
344 my ($path) = @_; |
|
345 # Deletion |
|
346 return if (! -e $path); |
|
347 # Addition or Modification |
|
348 my $buffer; |
|
349 open BINARY, $path or die; |
|
350 while (read(BINARY, $buffer, 60*57)) { |
|
351 print encode_base64($buffer); |
|
352 } |
|
353 close BINARY; |
|
354 print "\n"; |
|
355 } |
|
356 |
|
357 # Sort first by directory, then by file, so all paths in one directory are grouped |
|
358 # rather than being interspersed with items from subdirectories. |
|
359 # Use numericcmp to sort directory and filenames to make order logical. |
|
360 # Also include a special case for ChangeLog, which comes first in any directory. |
|
361 sub pathcmp($$) |
|
362 { |
|
363 my ($fileDataA, $fileDataB) = @_; |
|
364 |
|
365 my ($dira, $namea) = splitpath($fileDataA->{path}); |
|
366 my ($dirb, $nameb) = splitpath($fileDataB->{path}); |
|
367 |
|
368 return numericcmp($dira, $dirb) if $dira ne $dirb; |
|
369 return -1 if $namea eq "ChangeLog" && $nameb ne "ChangeLog"; |
|
370 return +1 if $namea ne "ChangeLog" && $nameb eq "ChangeLog"; |
|
371 return numericcmp($namea, $nameb); |
|
372 } |
|
373 |
|
374 sub processPaths(\@) |
|
375 { |
|
376 my ($paths) = @_; |
|
377 return ("." => 1) if (!@{$paths}); |
|
378 |
|
379 my %result = (); |
|
380 |
|
381 for my $file (@{$paths}) { |
|
382 die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file); |
|
383 die "can't handle empty string path\n" if $file eq ""; |
|
384 die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy) |
|
385 |
|
386 my $untouchedFile = $file; |
|
387 |
|
388 $file = canonicalizePath($file); |
|
389 |
|
390 die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|; |
|
391 |
|
392 $result{$file} = 1; |
|
393 } |
|
394 |
|
395 return ("." => 1) if ($result{"."}); |
|
396 |
|
397 # Remove any paths that also have a parent listed. |
|
398 for my $path (keys %result) { |
|
399 for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) { |
|
400 if ($result{$parent}) { |
|
401 delete $result{$path}; |
|
402 last; |
|
403 } |
|
404 } |
|
405 } |
|
406 |
|
407 return %result; |
|
408 } |
|
409 |
|
410 # Break up a path into the directory (with slash) and base name. |
|
411 sub splitpath($) |
|
412 { |
|
413 my ($path) = @_; |
|
414 |
|
415 my $pathSeparator = "/"; |
|
416 my $dirname = dirname($path) . $pathSeparator; |
|
417 $dirname = "" if $dirname eq "." . $pathSeparator; |
|
418 |
|
419 return ($dirname, basename($path)); |
|
420 } |
|
421 |
|
422 # Sort so source code files appear before test files. |
|
423 sub testfilecmp($$) |
|
424 { |
|
425 my ($fileDataA, $fileDataB) = @_; |
|
426 return $fileDataA->{isTestFile} <=> $fileDataB->{isTestFile}; |
|
427 } |
|
428 |