|
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 Cwd; |
|
48 use File::Basename; |
|
49 use File::Spec; |
|
50 use File::stat; |
|
51 use Getopt::Long; |
|
52 use MIME::Base64; |
|
53 use POSIX qw(:errno_h); |
|
54 use Time::gmtime; |
|
55 |
|
56 sub binarycmp($$); |
|
57 sub canonicalizePath($); |
|
58 sub findBaseUrl($); |
|
59 sub findMimeType($;$); |
|
60 sub findModificationType($); |
|
61 sub findSourceFileAndRevision($); |
|
62 sub fixChangeLogPatch($); |
|
63 sub generateDiff($); |
|
64 sub generateFileList($\%); |
|
65 sub isBinaryMimeType($); |
|
66 sub manufacturePatchForAdditionWithHistory($); |
|
67 sub numericcmp($$); |
|
68 sub outputBinaryContent($); |
|
69 sub pathcmp($$); |
|
70 sub processPaths(\@); |
|
71 sub splitpath($); |
|
72 sub testfilecmp($$); |
|
73 |
|
74 $ENV{'LC_ALL'} = 'C'; |
|
75 |
|
76 my $showHelp; |
|
77 |
|
78 my $result = GetOptions( |
|
79 "help" => \$showHelp, |
|
80 ); |
|
81 if (!$result || $showHelp) { |
|
82 print STDERR basename($0) . " [-h|--help] [svndir1 [svndir2 ...]]\n"; |
|
83 exit 1; |
|
84 } |
|
85 |
|
86 my %paths = processPaths(@ARGV); |
|
87 |
|
88 # Generate a list of files requiring diffs |
|
89 my %diffFiles; |
|
90 for my $path (keys %paths) { |
|
91 generateFileList($path, %diffFiles); |
|
92 } |
|
93 |
|
94 # Generate the diff for source code files, test files then binary files for easy reviewing |
|
95 for my $fileData (sort binarycmp sort testfilecmp sort pathcmp values %diffFiles) { |
|
96 generateDiff($fileData); |
|
97 } |
|
98 |
|
99 exit 0; |
|
100 |
|
101 |
|
102 # Sort so text files appear before binary files. |
|
103 sub binarycmp($$) |
|
104 { |
|
105 my ($fileDataA, $fileDataB) = @_; |
|
106 return $fileDataA->{isBinary} <=> $fileDataB->{isBinary}; |
|
107 } |
|
108 |
|
109 sub canonicalizePath($) |
|
110 { |
|
111 my ($file) = @_; |
|
112 |
|
113 # Remove extra slashes and '.' directories in path |
|
114 $file = File::Spec->canonpath($file); |
|
115 |
|
116 # Remove '..' directories in path |
|
117 my @dirs = (); |
|
118 foreach my $dir (File::Spec->splitdir($file)) { |
|
119 if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') { |
|
120 pop(@dirs); |
|
121 } else { |
|
122 push(@dirs, $dir); |
|
123 } |
|
124 } |
|
125 return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : "."; |
|
126 } |
|
127 |
|
128 sub findBaseUrl($) |
|
129 { |
|
130 my ($infoPath) = @_; |
|
131 my $baseUrl; |
|
132 open INFO, "svn info '$infoPath' |" or die; |
|
133 while (<INFO>) { |
|
134 if (/^URL: (.+)/) { |
|
135 $baseUrl = $1; |
|
136 last; |
|
137 } |
|
138 } |
|
139 close INFO; |
|
140 return $baseUrl; |
|
141 } |
|
142 |
|
143 sub findMimeType($;$) |
|
144 { |
|
145 my ($file, $revision) = @_; |
|
146 my $args = $revision ? "--revision $revision" : ""; |
|
147 open PROPGET, "svn propget svn:mime-type $args '$file' |" or die; |
|
148 my $mimeType = <PROPGET>; |
|
149 close PROPGET; |
|
150 chomp $mimeType if $mimeType; |
|
151 return $mimeType; |
|
152 } |
|
153 |
|
154 sub findModificationType($) |
|
155 { |
|
156 my ($stat) = @_; |
|
157 my $fileStat = substr($stat, 0, 1); |
|
158 my $propertyStat = substr($stat, 1, 1); |
|
159 if ($fileStat eq "A") { |
|
160 my $additionWithHistory = substr($stat, 3, 1); |
|
161 return $additionWithHistory eq "+" ? "additionWithHistory" : "addition"; |
|
162 } |
|
163 return "modification" if ($fileStat eq "M" || $propertyStat eq "M"); |
|
164 return "deletion" if ($fileStat eq "D"); |
|
165 return undef; |
|
166 } |
|
167 |
|
168 sub findSourceFileAndRevision($) |
|
169 { |
|
170 my ($file) = @_; |
|
171 my $baseUrl = findBaseUrl("."); |
|
172 my $sourceFile; |
|
173 my $sourceRevision; |
|
174 open INFO, "svn info '$file' |" or die; |
|
175 while (<INFO>) { |
|
176 if (/^Copied From URL: (.+)/) { |
|
177 $sourceFile = File::Spec->abs2rel($1, $baseUrl); |
|
178 } elsif (/^Copied From Rev: ([0-9]+)/) { |
|
179 $sourceRevision = $1; |
|
180 } |
|
181 } |
|
182 close INFO; |
|
183 return ($sourceFile, $sourceRevision); |
|
184 } |
|
185 |
|
186 sub fixChangeLogPatch($) |
|
187 { |
|
188 my $patch = shift; |
|
189 my $contextLineCount = 3; |
|
190 |
|
191 return $patch if $patch !~ /\n@@ -1,(\d+) \+1,(\d+) @@\n( .*\n)+(\+.*\n)+( .*\n){$contextLineCount}$/m; |
|
192 my ($oldLineCount, $newLineCount) = ($1, $2); |
|
193 return $patch if $oldLineCount <= $contextLineCount; |
|
194 |
|
195 # The diff(1) command is greedy when matching lines, so a new ChangeLog entry will |
|
196 # have lines of context at the top of a patch when the existing entry has the same |
|
197 # date and author as the new entry. This nifty loop alters a ChangeLog patch so |
|
198 # that the added lines ("+") in the patch always start at the beginning of the |
|
199 # patch and there are no initial lines of context. |
|
200 my $newPatch; |
|
201 my $lineCountInState = 0; |
|
202 my $oldContentLineCountReduction = $oldLineCount - $contextLineCount; |
|
203 my $newContentLineCountWithoutContext = $newLineCount - $oldLineCount - $oldContentLineCountReduction; |
|
204 my ($stateHeader, $statePreContext, $stateNewChanges, $statePostContext) = (1..4); |
|
205 my $state = $stateHeader; |
|
206 foreach my $line (split(/\n/, $patch)) { |
|
207 $lineCountInState++; |
|
208 if ($state == $stateHeader && $line =~ /^@@ -1,$oldLineCount \+1,$newLineCount @\@$/) { |
|
209 $line = "@@ -1,$contextLineCount +1," . ($newLineCount - $oldContentLineCountReduction) . " @@"; |
|
210 $lineCountInState = 0; |
|
211 $state = $statePreContext; |
|
212 } elsif ($state == $statePreContext && substr($line, 0, 1) eq " ") { |
|
213 $line = "+" . substr($line, 1); |
|
214 if ($lineCountInState == $oldContentLineCountReduction) { |
|
215 $lineCountInState = 0; |
|
216 $state = $stateNewChanges; |
|
217 } |
|
218 } elsif ($state == $stateNewChanges && substr($line, 0, 1) eq "+") { |
|
219 # No changes to these lines |
|
220 if ($lineCountInState == $newContentLineCountWithoutContext) { |
|
221 $lineCountInState = 0; |
|
222 $state = $statePostContext; |
|
223 } |
|
224 } elsif ($state == $statePostContext) { |
|
225 if (substr($line, 0, 1) eq "+" && $lineCountInState <= $oldContentLineCountReduction) { |
|
226 $line = " " . substr($line, 1); |
|
227 } elsif ($lineCountInState > $contextLineCount && substr($line, 0, 1) eq " ") { |
|
228 next; # Discard |
|
229 } |
|
230 } |
|
231 $newPatch .= $line . "\n"; |
|
232 } |
|
233 |
|
234 return $newPatch; |
|
235 } |
|
236 |
|
237 sub generateDiff($) |
|
238 { |
|
239 my ($fileData) = @_; |
|
240 my $file = $fileData->{path}; |
|
241 my $patch; |
|
242 if ($fileData->{modificationType} eq "additionWithHistory") { |
|
243 manufacturePatchForAdditionWithHistory($fileData); |
|
244 } |
|
245 open DIFF, "svn diff --diff-cmd diff -x -uaNp '$file' |" or die; |
|
246 while (<DIFF>) { |
|
247 $patch .= $_; |
|
248 } |
|
249 close DIFF; |
|
250 $patch = fixChangeLogPatch($patch) if basename($file) eq "ChangeLog"; |
|
251 print $patch if $patch; |
|
252 if ($fileData->{isBinary}) { |
|
253 print "\n" if ($patch && $patch =~ m/\n\S+$/m); |
|
254 outputBinaryContent($file); |
|
255 } |
|
256 } |
|
257 |
|
258 sub generateFileList($\%) |
|
259 { |
|
260 my ($statPath, $diffFiles) = @_; |
|
261 my %testDirectories = map { $_ => 1 } qw(LayoutTests); |
|
262 open STAT, "svn stat '$statPath' |" or die; |
|
263 while (my $line = <STAT>) { |
|
264 chomp $line; |
|
265 my $stat = substr($line, 0, 7); |
|
266 my $path = substr($line, 7); |
|
267 next if -d $path; |
|
268 my $modificationType = findModificationType($stat); |
|
269 if ($modificationType) { |
|
270 $diffFiles->{$path}->{path} = $path; |
|
271 $diffFiles->{$path}->{modificationType} = $modificationType; |
|
272 $diffFiles->{$path}->{isBinary} = isBinaryMimeType($path); |
|
273 $diffFiles->{$path}->{isTestFile} = exists $testDirectories{(File::Spec->splitdir($path))[0]} ? 1 : 0; |
|
274 if ($modificationType eq "additionWithHistory") { |
|
275 my ($sourceFile, $sourceRevision) = findSourceFileAndRevision($path); |
|
276 $diffFiles->{$path}->{sourceFile} = $sourceFile; |
|
277 $diffFiles->{$path}->{sourceRevision} = $sourceRevision; |
|
278 } |
|
279 } else { |
|
280 print STDERR $line, "\n"; |
|
281 } |
|
282 } |
|
283 close STAT; |
|
284 } |
|
285 |
|
286 sub isBinaryMimeType($) |
|
287 { |
|
288 my ($file) = @_; |
|
289 my $mimeType = findMimeType($file); |
|
290 return 0 if (!$mimeType || substr($mimeType, 0, 5) eq "text/"); |
|
291 return 1; |
|
292 } |
|
293 |
|
294 sub manufacturePatchForAdditionWithHistory($) |
|
295 { |
|
296 my ($fileData) = @_; |
|
297 my $file = $fileData->{path}; |
|
298 print "Index: ${file}\n"; |
|
299 print "=" x 67, "\n"; |
|
300 my $sourceFile = $fileData->{sourceFile}; |
|
301 my $sourceRevision = $fileData->{sourceRevision}; |
|
302 print "--- ${file}\t(revision ${sourceRevision})\t(from ${sourceFile}:${sourceRevision})\n"; |
|
303 print "+++ ${file}\t(working copy)\n"; |
|
304 if ($fileData->{isBinary}) { |
|
305 print "\nCannot display: file marked as a binary type.\n"; |
|
306 my $mimeType = findMimeType($file, $sourceRevision); |
|
307 print "svn:mime-type = ${mimeType}\n\n"; |
|
308 } else { |
|
309 print `svn cat ${sourceFile} | diff -u /dev/null - | tail -n +3`; |
|
310 } |
|
311 } |
|
312 |
|
313 # Sort numeric parts of strings as numbers, other parts as strings. |
|
314 # Makes 1.33 come after 1.3, which is cool. |
|
315 sub numericcmp($$) |
|
316 { |
|
317 my ($aa, $bb) = @_; |
|
318 |
|
319 my @a = split /(\d+)/, $aa; |
|
320 my @b = split /(\d+)/, $bb; |
|
321 |
|
322 # Compare one chunk at a time. |
|
323 # Each chunk is either all numeric digits, or all not numeric digits. |
|
324 while (@a && @b) { |
|
325 my $a = shift @a; |
|
326 my $b = shift @b; |
|
327 |
|
328 # Use numeric comparison if chunks are non-equal numbers. |
|
329 return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b; |
|
330 |
|
331 # Use string comparison if chunks are any other kind of non-equal string. |
|
332 return $a cmp $b if $a ne $b; |
|
333 } |
|
334 |
|
335 # One of the two is now empty; compare lengths for result in this case. |
|
336 return @a <=> @b; |
|
337 } |
|
338 |
|
339 sub outputBinaryContent($) |
|
340 { |
|
341 my ($path) = @_; |
|
342 # Deletion |
|
343 return if (! -e $path); |
|
344 # Addition or Modification |
|
345 my $buffer; |
|
346 open BINARY, $path or die; |
|
347 while (read(BINARY, $buffer, 60*57)) { |
|
348 print encode_base64($buffer); |
|
349 } |
|
350 close BINARY; |
|
351 print "\n"; |
|
352 } |
|
353 |
|
354 # Sort first by directory, then by file, so all paths in one directory are grouped |
|
355 # rather than being interspersed with items from subdirectories. |
|
356 # Use numericcmp to sort directory and filenames to make order logical. |
|
357 sub pathcmp($$) |
|
358 { |
|
359 my ($fileDataA, $fileDataB) = @_; |
|
360 |
|
361 my ($dira, $namea) = splitpath($fileDataA->{path}); |
|
362 my ($dirb, $nameb) = splitpath($fileDataB->{path}); |
|
363 |
|
364 return numericcmp($dira, $dirb) if $dira ne $dirb; |
|
365 return numericcmp($namea, $nameb); |
|
366 } |
|
367 |
|
368 sub processPaths(\@) |
|
369 { |
|
370 my ($paths) = @_; |
|
371 return ("." => 1) if (!@{$paths}); |
|
372 |
|
373 my %result = (); |
|
374 |
|
375 for my $file (@{$paths}) { |
|
376 die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file); |
|
377 die "can't handle empty string path\n" if $file eq ""; |
|
378 die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy) |
|
379 |
|
380 my $untouchedFile = $file; |
|
381 |
|
382 $file = canonicalizePath($file); |
|
383 |
|
384 die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|; |
|
385 |
|
386 $result{$file} = 1; |
|
387 } |
|
388 |
|
389 return ("." => 1) if ($result{"."}); |
|
390 |
|
391 # Remove any paths that also have a parent listed. |
|
392 for my $path (keys %result) { |
|
393 for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) { |
|
394 if ($result{$parent}) { |
|
395 delete $result{$path}; |
|
396 last; |
|
397 } |
|
398 } |
|
399 } |
|
400 |
|
401 return %result; |
|
402 } |
|
403 |
|
404 # Break up a path into the directory (with slash) and base name. |
|
405 sub splitpath($) |
|
406 { |
|
407 my ($path) = @_; |
|
408 |
|
409 my $pathSeparator = "/"; |
|
410 my $dirname = dirname($path) . $pathSeparator; |
|
411 $dirname = "" if $dirname eq "." . $pathSeparator; |
|
412 |
|
413 return ($dirname, basename($path)); |
|
414 } |
|
415 |
|
416 # Sort so source code files appear before test files. |
|
417 sub testfilecmp($$) |
|
418 { |
|
419 my ($fileDataA, $fileDataB) = @_; |
|
420 return $fileDataA->{isTestFile} <=> $fileDataB->{isTestFile}; |
|
421 } |