|
1 #!/usr/bin/perl -w |
|
2 |
|
3 # Copyright (C) 2005, 2006, 2007 Apple 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 # "patch" script for WebKit Open Source Project, used to apply patches. |
|
30 |
|
31 # Differences from invoking "patch -p0": |
|
32 # |
|
33 # Handles added files (does a svn add with logic to handle local changes). |
|
34 # Handles added directories (does a svn add). |
|
35 # Handles removed files (does a svn rm with logic to handle local changes). |
|
36 # Handles removed directories--those with no more files or directories left in them |
|
37 # (does a svn rm). |
|
38 # Has mode where it will roll back to svn version numbers in the patch file so svn |
|
39 # can do a 3-way merge. |
|
40 # Paths from Index: lines are used rather than the paths on the patch lines, which |
|
41 # makes patches generated by "cvs diff" work (increasingly unimportant since we |
|
42 # use Subversion now). |
|
43 # ChangeLog patches use --fuzz=3 to prevent rejects, and the entry date is set in |
|
44 # the patch to today's date using $changeLogTimeZone. |
|
45 # Handles binary files (requires patches made by svn-create-patch). |
|
46 # Handles copied and moved files (requires patches made by svn-create-patch). |
|
47 # Handles git-diff patches (without binary changes) created at the top-level directory |
|
48 # |
|
49 # Missing features: |
|
50 # |
|
51 # Handle property changes. |
|
52 # Handle copied and moved directories (would require patches made by svn-create-patch). |
|
53 # When doing a removal, check that old file matches what's being removed. |
|
54 # Notice a patch that's being applied at the "wrong level" and make it work anyway. |
|
55 # Do a dry run on the whole patch and don't do anything if part of the patch is |
|
56 # going to fail (probably too strict unless we exclude ChangeLog). |
|
57 # Handle git-diff patches with binary changes |
|
58 |
|
59 use strict; |
|
60 use warnings; |
|
61 |
|
62 use Cwd; |
|
63 use Digest::MD5; |
|
64 use File::Basename; |
|
65 use File::Spec; |
|
66 use Getopt::Long; |
|
67 use MIME::Base64; |
|
68 use POSIX qw(strftime); |
|
69 |
|
70 sub addDirectoriesIfNeeded($); |
|
71 sub applyPatch($$;$); |
|
72 sub checksum($); |
|
73 sub fixChangeLogPatch($); |
|
74 sub gitdiff2svndiff($); |
|
75 sub handleBinaryChange($$); |
|
76 sub isDirectoryEmptyForRemoval($); |
|
77 sub patch($); |
|
78 sub removeDirectoriesIfNeeded(); |
|
79 sub setChangeLogDate($); |
|
80 sub svnStatus($); |
|
81 |
|
82 # Project time zone for Cupertino, CA, US |
|
83 my $changeLogTimeZone = "PST8PDT"; |
|
84 |
|
85 my $merge = 0; |
|
86 my $showHelp = 0; |
|
87 if (!GetOptions("merge!" => \$merge, "help!" => \$showHelp) || $showHelp) { |
|
88 print STDERR basename($0) . " [-h|--help] [-m|--merge] patch1 [patch2 ...]\n"; |
|
89 exit 1; |
|
90 } |
|
91 |
|
92 my %removeDirectoryIgnoreList = ( |
|
93 '.' => 1, |
|
94 '..' => 1, |
|
95 '.svn' => 1, |
|
96 '_svn' => 1, |
|
97 ); |
|
98 |
|
99 my %checkedDirectories; |
|
100 my %copiedFiles; |
|
101 my @patches; |
|
102 my %versions; |
|
103 |
|
104 my $copiedFromPath; |
|
105 my $filter; |
|
106 my $indexPath; |
|
107 my $patch; |
|
108 while (<>) { |
|
109 s/\r//g; |
|
110 chomp; |
|
111 if (!defined($indexPath) && m#^diff --git a/#) { |
|
112 $filter = \&gitdiff2svndiff; |
|
113 } |
|
114 $_ = &$filter($_) if $filter; |
|
115 if (/^Index: (.+)/) { |
|
116 $indexPath = $1; |
|
117 if ($patch) { |
|
118 if (!$copiedFromPath) { |
|
119 push @patches, $patch; |
|
120 } |
|
121 $copiedFromPath = ""; |
|
122 $patch = ""; |
|
123 } |
|
124 } |
|
125 if ($indexPath) { |
|
126 # Fix paths on diff, ---, and +++ lines to match preceding Index: line. |
|
127 s/\S+$/$indexPath/ if /^diff/; |
|
128 s/^--- \S+/--- $indexPath/; |
|
129 if (/^--- .+\(from (\S+):(\d+)\)$/) { |
|
130 $copiedFromPath = $1; |
|
131 $copiedFiles{$indexPath} = $copiedFromPath; |
|
132 $versions{$copiedFromPath} = $2 if ($2 != 0); |
|
133 } |
|
134 elsif (/^--- .+\(revision (\d+)\)$/) { |
|
135 $versions{$indexPath} = $1 if ($1 != 0); |
|
136 } |
|
137 if (s/^\+\+\+ \S+/+++ $indexPath/) { |
|
138 $indexPath = ""; |
|
139 } |
|
140 } |
|
141 $patch .= $_; |
|
142 $patch .= "\n"; |
|
143 } |
|
144 |
|
145 if ($patch && !$copiedFromPath) { |
|
146 push @patches, $patch; |
|
147 } |
|
148 |
|
149 if ($merge) { |
|
150 for my $file (sort keys %versions) { |
|
151 print "Getting version $versions{$file} of $file\n"; |
|
152 system "svn", "update", "-r", $versions{$file}, $file; |
|
153 } |
|
154 } |
|
155 |
|
156 # Handle copied and moved files first since moved files may have their source deleted before the move. |
|
157 for my $file (keys %copiedFiles) { |
|
158 addDirectoriesIfNeeded(dirname($file)); |
|
159 system "svn", "copy", $copiedFiles{$file}, $file; |
|
160 } |
|
161 |
|
162 for $patch (@patches) { |
|
163 patch($patch); |
|
164 } |
|
165 |
|
166 removeDirectoriesIfNeeded(); |
|
167 |
|
168 exit 0; |
|
169 |
|
170 sub addDirectoriesIfNeeded($) |
|
171 { |
|
172 my ($path) = @_; |
|
173 my @dirs = File::Spec->splitdir($path); |
|
174 my $dir = "."; |
|
175 while (scalar @dirs) { |
|
176 $dir = File::Spec->catdir($dir, shift @dirs); |
|
177 next if exists $checkedDirectories{$dir}; |
|
178 if (! -e $dir) { |
|
179 mkdir $dir or die "Failed to create required directory '$dir' for path '$path'\n"; |
|
180 system "svn", "add", $dir; |
|
181 $checkedDirectories{$dir} = 1; |
|
182 } |
|
183 elsif (-d $dir) { |
|
184 my $svnOutput = svnStatus($dir); |
|
185 if ($svnOutput && $svnOutput =~ m#\?\s+$dir\n#) { |
|
186 system "svn", "add", $dir; |
|
187 } |
|
188 $checkedDirectories{$dir} = 1; |
|
189 } |
|
190 else { |
|
191 die "'$dir' is not a directory"; |
|
192 } |
|
193 } |
|
194 } |
|
195 |
|
196 sub applyPatch($$;$) |
|
197 { |
|
198 my ($patch, $fullPath, $options) = @_; |
|
199 $options = [] if (! $options); |
|
200 my $command = "patch " . join(" ", "-p0", @{$options}); |
|
201 open PATCH, "| $command" or die "Failed to patch $fullPath\n"; |
|
202 print PATCH $patch; |
|
203 close PATCH; |
|
204 } |
|
205 |
|
206 sub checksum($) |
|
207 { |
|
208 my $file = shift; |
|
209 open(FILE, $file) or die "Can't open '$file': $!"; |
|
210 binmode(FILE); |
|
211 my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest(); |
|
212 close(FILE); |
|
213 return $checksum; |
|
214 } |
|
215 |
|
216 sub fixChangeLogPatch($) |
|
217 { |
|
218 my $patch = shift; |
|
219 my $contextLineCount = 3; |
|
220 |
|
221 return $patch if $patch !~ /\n@@ -1,(\d+) \+1,(\d+) @@\n( .*\n)+(\+.*\n)+( .*\n){$contextLineCount}$/m; |
|
222 my ($oldLineCount, $newLineCount) = ($1, $2); |
|
223 return $patch if $oldLineCount <= $contextLineCount; |
|
224 |
|
225 # The diff(1) command is greedy when matching lines, so a new ChangeLog entry will |
|
226 # have lines of context at the top of a patch when the existing entry has the same |
|
227 # date and author as the new entry. This nifty loop alters a ChangeLog patch so |
|
228 # that the added lines ("+") in the patch always start at the beginning of the |
|
229 # patch and there are no initial lines of context. |
|
230 my $newPatch; |
|
231 my $lineCountInState = 0; |
|
232 my $oldContentLineCountReduction = $oldLineCount - $contextLineCount; |
|
233 my $newContentLineCountWithoutContext = $newLineCount - $oldLineCount - $oldContentLineCountReduction; |
|
234 my ($stateHeader, $statePreContext, $stateNewChanges, $statePostContext) = (1..4); |
|
235 my $state = $stateHeader; |
|
236 foreach my $line (split(/\n/, $patch)) { |
|
237 $lineCountInState++; |
|
238 if ($state == $stateHeader && $line =~ /^@@ -1,$oldLineCount \+1,$newLineCount @\@$/) { |
|
239 $line = "@@ -1,$contextLineCount +1," . ($newLineCount - $oldContentLineCountReduction) . " @@"; |
|
240 $lineCountInState = 0; |
|
241 $state = $statePreContext; |
|
242 } elsif ($state == $statePreContext && substr($line, 0, 1) eq " ") { |
|
243 $line = "+" . substr($line, 1); |
|
244 if ($lineCountInState == $oldContentLineCountReduction) { |
|
245 $lineCountInState = 0; |
|
246 $state = $stateNewChanges; |
|
247 } |
|
248 } elsif ($state == $stateNewChanges && substr($line, 0, 1) eq "+") { |
|
249 # No changes to these lines |
|
250 if ($lineCountInState == $newContentLineCountWithoutContext) { |
|
251 $lineCountInState = 0; |
|
252 $state = $statePostContext; |
|
253 } |
|
254 } elsif ($state == $statePostContext) { |
|
255 if (substr($line, 0, 1) eq "+" && $lineCountInState <= $oldContentLineCountReduction) { |
|
256 $line = " " . substr($line, 1); |
|
257 } elsif ($lineCountInState > $contextLineCount && substr($line, 0, 1) eq " ") { |
|
258 next; # Discard |
|
259 } |
|
260 } |
|
261 $newPatch .= $line . "\n"; |
|
262 } |
|
263 |
|
264 return $newPatch; |
|
265 } |
|
266 |
|
267 sub gitdiff2svndiff($) |
|
268 { |
|
269 $_ = shift @_; |
|
270 if (m#^diff --git a/(.+) b/(.+)#) { |
|
271 return "Index: $1"; |
|
272 } elsif (m/^new file.*/) { |
|
273 return ""; |
|
274 } elsif (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) { |
|
275 return "==================================================================="; |
|
276 } elsif (m#^--- a/(.+)#) { |
|
277 return "--- $1"; |
|
278 } elsif (m#^\+\+\+ b/(.+)#) { |
|
279 return "+++ $1"; |
|
280 } |
|
281 return $_; |
|
282 } |
|
283 |
|
284 sub handleBinaryChange($$) |
|
285 { |
|
286 my ($fullPath, $contents) = @_; |
|
287 if ($contents =~ m#((\n[A-Za-z0-9+/]{76})+\n[A-Za-z0-9+/=]{4,76}\n)#) { |
|
288 # Addition or Modification |
|
289 open FILE, ">", $fullPath or die; |
|
290 print FILE decode_base64($1); |
|
291 close FILE; |
|
292 my $svnOutput = svnStatus($fullPath); |
|
293 if ($svnOutput && substr($svnOutput, 0, 1) eq "?") { |
|
294 # Addition |
|
295 system "svn", "add", $fullPath; |
|
296 } else { |
|
297 # Modification |
|
298 print $svnOutput if $svnOutput; |
|
299 } |
|
300 } else { |
|
301 # Deletion |
|
302 system "svn", "rm", $fullPath; |
|
303 } |
|
304 } |
|
305 |
|
306 sub isDirectoryEmptyForRemoval($) |
|
307 { |
|
308 my ($dir) = @_; |
|
309 my $directoryIsEmpty = 1; |
|
310 opendir DIR, $dir or die "Could not open '$dir' to list files: $?"; |
|
311 for (my $item = readdir DIR; $item && $directoryIsEmpty; $item = readdir DIR) { |
|
312 next if exists $removeDirectoryIgnoreList{$item}; |
|
313 if (! -d File::Spec->catdir($dir, $item)) { |
|
314 $directoryIsEmpty = 0; |
|
315 } else { |
|
316 my $svnOutput = svnStatus(File::Spec->catdir($dir, $item)); |
|
317 next if $svnOutput && substr($svnOutput, 0, 1) eq "D"; |
|
318 $directoryIsEmpty = 0; |
|
319 } |
|
320 } |
|
321 closedir DIR; |
|
322 return $directoryIsEmpty; |
|
323 } |
|
324 |
|
325 sub patch($) |
|
326 { |
|
327 my ($patch) = @_; |
|
328 return if !$patch; |
|
329 |
|
330 $patch =~ m|^Index: ([^\n]+)| or die "Failed to find 'Index:' in \"$patch\"\n"; |
|
331 my $fullPath = $1; |
|
332 |
|
333 my $deletion = 0; |
|
334 my $addition = 0; |
|
335 my $isBinary = 0; |
|
336 |
|
337 $addition = 1 if $patch =~ /\n--- .+\(revision 0\)\n/; |
|
338 $deletion = 1 if $patch =~ /\n@@ .* \+0,0 @@/; |
|
339 $isBinary = 1 if $patch =~ /\nCannot display: file marked as a binary type\./; |
|
340 |
|
341 if (!$addition && !$deletion && !$isBinary) { |
|
342 # Standard patch, patch tool can handle this. |
|
343 if (basename($fullPath) eq "ChangeLog") { |
|
344 my $changeLogDotOrigExisted = -f "${fullPath}.orig"; |
|
345 applyPatch(setChangeLogDate(fixChangeLogPatch($patch)), $fullPath, ["--fuzz=3"]); |
|
346 unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted); |
|
347 } else { |
|
348 applyPatch($patch, $fullPath); |
|
349 } |
|
350 } else { |
|
351 # Either a deletion, an addition or a binary change. |
|
352 |
|
353 addDirectoriesIfNeeded(dirname($fullPath)); |
|
354 |
|
355 if ($isBinary) { |
|
356 # Binary change |
|
357 handleBinaryChange($fullPath, $patch); |
|
358 } elsif ($deletion) { |
|
359 # Deletion |
|
360 applyPatch($patch, $fullPath, ["--force"]); |
|
361 system "svn", "rm", "--force", $fullPath; |
|
362 } else { |
|
363 # Addition |
|
364 rename($fullPath, "$fullPath.orig") if -e $fullPath; |
|
365 applyPatch($patch, $fullPath); |
|
366 unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig"); |
|
367 system "svn", "add", $fullPath; |
|
368 system "svn", "stat", "$fullPath.orig" if -e "$fullPath.orig"; |
|
369 } |
|
370 } |
|
371 } |
|
372 |
|
373 sub removeDirectoriesIfNeeded() |
|
374 { |
|
375 foreach my $dir (reverse sort keys %checkedDirectories) { |
|
376 if (isDirectoryEmptyForRemoval($dir)) { |
|
377 my $svnOutput; |
|
378 open SVN, "svn rm '$dir' |" or die; |
|
379 # Only save the last line since Subversion lists all changed statuses below $dir |
|
380 while (<SVN>) { |
|
381 $svnOutput = $_; |
|
382 } |
|
383 close SVN; |
|
384 print $svnOutput if $svnOutput; |
|
385 } |
|
386 } |
|
387 } |
|
388 |
|
389 sub setChangeLogDate($) |
|
390 { |
|
391 my $patch = shift; |
|
392 my $savedTimeZone = $ENV{'TZ'}; |
|
393 # Set TZ temporarily so that localtime() is in that time zone |
|
394 $ENV{'TZ'} = $changeLogTimeZone; |
|
395 my $newDate = strftime("%Y-%m-%d", localtime()); |
|
396 if (defined $savedTimeZone) { |
|
397 $ENV{'TZ'} = $savedTimeZone; |
|
398 } else { |
|
399 delete $ENV{'TZ'}; |
|
400 } |
|
401 $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}( )/$1$newDate$2/; |
|
402 return $patch; |
|
403 } |
|
404 |
|
405 sub svnStatus($) |
|
406 { |
|
407 my ($fullPath) = @_; |
|
408 my $svnStatus; |
|
409 open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die; |
|
410 if (-d $fullPath) { |
|
411 # When running "svn stat" on a directory, we can't assume that only one |
|
412 # status will be returned (since any files with a status below the |
|
413 # directory will be returned), and we can't assume that the directory will |
|
414 # be first (since any files with unknown status will be listed first). |
|
415 my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath)); |
|
416 while (<SVN>) { |
|
417 chomp; |
|
418 my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7))); |
|
419 if ($normalizedFullPath eq $normalizedStatPath) { |
|
420 $svnStatus = $_; |
|
421 last; |
|
422 } |
|
423 } |
|
424 # Read the rest of the svn command output to avoid a broken pipe warning. |
|
425 local $/ = undef; |
|
426 <SVN>; |
|
427 } |
|
428 else { |
|
429 # Files will have only one status returned. |
|
430 $svnStatus = <SVN>; |
|
431 } |
|
432 close SVN; |
|
433 return $svnStatus; |
|
434 } |