2008-11-04 Anders Carlsson <andersca@apple.com>
[webkit/qt.git] / WebKitTools / Scripts / svn-apply
blobd43d525e24c36d1ff900f4c07ade078a9f4e380e
1 #!/usr/bin/perl -w
3 # Copyright (C) 2005, 2006, 2007 Apple Inc. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
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.
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.
29 # "patch" script for WebKit Open Source Project, used to apply patches.
31 # Differences from invoking "patch -p0":
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
49 # Missing features:
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
59 use strict;
60 use warnings;
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);
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 setChangeLogDateAndReviewer($$);
80 sub svnStatus($);
82 # Project time zone for Cupertino, CA, US
83 my $changeLogTimeZone = "PST8PDT";
85 my $merge = 0;
86 my $showHelp = 0;
87 my $reviewer;
88 if (!GetOptions("merge!" => \$merge, "help!" => \$showHelp, "reviewer=s" => \$reviewer) || $showHelp) {
89 print STDERR basename($0) . " [-h|--help] [-m|--merge] [-r|--reviewer name] patch1 [patch2 ...]\n";
90 exit 1;
93 my %removeDirectoryIgnoreList = (
94 '.' => 1,
95 '..' => 1,
96 '.svn' => 1,
97 '_svn' => 1,
100 my %checkedDirectories;
101 my %copiedFiles;
102 my @patches;
103 my %versions;
105 my $copiedFromPath;
106 my $filter;
107 my $indexPath;
108 my $patch;
109 while (<>) {
110 s/([\n\r]+)$//mg;
111 my $eol = $1;
112 if (!defined($indexPath) && m#^diff --git a/#) {
113 $filter = \&gitdiff2svndiff;
115 $_ = &$filter($_) if $filter;
116 if (/^Index: (.+)/) {
117 $indexPath = $1;
118 if ($patch) {
119 if (!$copiedFromPath) {
120 push @patches, $patch;
122 $copiedFromPath = "";
123 $patch = "";
126 if ($indexPath) {
127 # Fix paths on diff, ---, and +++ lines to match preceding Index: line.
128 s/\S+$/$indexPath/ if /^diff/;
129 s/^--- \S+/--- $indexPath/;
130 if (/^--- .+\(from (\S+):(\d+)\)$/) {
131 $copiedFromPath = $1;
132 $copiedFiles{$indexPath} = $copiedFromPath;
133 $versions{$copiedFromPath} = $2 if ($2 != 0);
135 elsif (/^--- .+\(revision (\d+)\)$/) {
136 $versions{$indexPath} = $1 if ($1 != 0);
138 if (s/^\+\+\+ \S+/+++ $indexPath/) {
139 $indexPath = "";
142 $patch .= $_;
143 $patch .= $eol;
146 if ($patch && !$copiedFromPath) {
147 push @patches, $patch;
150 if ($merge) {
151 for my $file (sort keys %versions) {
152 print "Getting version $versions{$file} of $file\n";
153 system "svn", "update", "-r", $versions{$file}, $file;
157 # Handle copied and moved files first since moved files may have their source deleted before the move.
158 for my $file (keys %copiedFiles) {
159 addDirectoriesIfNeeded(dirname($file));
160 system "svn", "copy", $copiedFiles{$file}, $file;
163 for $patch (@patches) {
164 patch($patch);
167 removeDirectoriesIfNeeded();
169 exit 0;
171 sub addDirectoriesIfNeeded($)
173 my ($path) = @_;
174 my @dirs = File::Spec->splitdir($path);
175 my $dir = ".";
176 while (scalar @dirs) {
177 $dir = File::Spec->catdir($dir, shift @dirs);
178 next if exists $checkedDirectories{$dir};
179 if (! -e $dir) {
180 mkdir $dir or die "Failed to create required directory '$dir' for path '$path'\n";
181 system "svn", "add", $dir;
182 $checkedDirectories{$dir} = 1;
184 elsif (-d $dir) {
185 my $svnOutput = svnStatus($dir);
186 if ($svnOutput && $svnOutput =~ m#\?\s+$dir\n#) {
187 system "svn", "add", $dir;
189 $checkedDirectories{$dir} = 1;
191 else {
192 die "'$dir' is not a directory";
197 sub applyPatch($$;$)
199 my ($patch, $fullPath, $options) = @_;
200 $options = [] if (! $options);
201 my $command = "patch " . join(" ", "-p0", @{$options});
202 open PATCH, "| $command" or die "Failed to patch $fullPath\n";
203 print PATCH $patch;
204 close PATCH;
207 sub checksum($)
209 my $file = shift;
210 open(FILE, $file) or die "Can't open '$file': $!";
211 binmode(FILE);
212 my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
213 close(FILE);
214 return $checksum;
217 sub fixChangeLogPatch($)
219 my $patch = shift;
220 my $contextLineCount = 3;
222 return $patch if $patch !~ /\n@@ -1,(\d+) \+1,(\d+) @@\n( .*\n)+(\+.*\n)+( .*\n){$contextLineCount}$/m;
223 my ($oldLineCount, $newLineCount) = ($1, $2);
224 return $patch if $oldLineCount <= $contextLineCount;
226 # The diff(1) command is greedy when matching lines, so a new ChangeLog entry will
227 # have lines of context at the top of a patch when the existing entry has the same
228 # date and author as the new entry. This nifty loop alters a ChangeLog patch so
229 # that the added lines ("+") in the patch always start at the beginning of the
230 # patch and there are no initial lines of context.
231 my $newPatch;
232 my $lineCountInState = 0;
233 my $oldContentLineCountReduction = $oldLineCount - $contextLineCount;
234 my $newContentLineCountWithoutContext = $newLineCount - $oldLineCount - $oldContentLineCountReduction;
235 my ($stateHeader, $statePreContext, $stateNewChanges, $statePostContext) = (1..4);
236 my $state = $stateHeader;
237 foreach my $line (split(/\n/, $patch)) {
238 $lineCountInState++;
239 if ($state == $stateHeader && $line =~ /^@@ -1,$oldLineCount \+1,$newLineCount @\@$/) {
240 $line = "@@ -1,$contextLineCount +1," . ($newLineCount - $oldContentLineCountReduction) . " @@";
241 $lineCountInState = 0;
242 $state = $statePreContext;
243 } elsif ($state == $statePreContext && substr($line, 0, 1) eq " ") {
244 $line = "+" . substr($line, 1);
245 if ($lineCountInState == $oldContentLineCountReduction) {
246 $lineCountInState = 0;
247 $state = $stateNewChanges;
249 } elsif ($state == $stateNewChanges && substr($line, 0, 1) eq "+") {
250 # No changes to these lines
251 if ($lineCountInState == $newContentLineCountWithoutContext) {
252 $lineCountInState = 0;
253 $state = $statePostContext;
255 } elsif ($state == $statePostContext) {
256 if (substr($line, 0, 1) eq "+" && $lineCountInState <= $oldContentLineCountReduction) {
257 $line = " " . substr($line, 1);
258 } elsif ($lineCountInState > $contextLineCount && substr($line, 0, 1) eq " ") {
259 next; # Discard
262 $newPatch .= $line . "\n";
265 return $newPatch;
268 sub gitdiff2svndiff($)
270 $_ = shift @_;
271 if (m#^diff --git a/(.+) b/(.+)#) {
272 return "Index: $1";
273 } elsif (m/^new file.*/) {
274 return "";
275 } elsif (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) {
276 return "===================================================================";
277 } elsif (m#^--- a/(.+)#) {
278 return "--- $1";
279 } elsif (m#^\+\+\+ b/(.+)#) {
280 return "+++ $1";
282 return $_;
285 sub handleBinaryChange($$)
287 my ($fullPath, $contents) = @_;
288 if ($contents =~ m#((\n[A-Za-z0-9+/]{76})+\n[A-Za-z0-9+/=]{4,76}\n)#) {
289 # Addition or Modification
290 open FILE, ">", $fullPath or die;
291 print FILE decode_base64($1);
292 close FILE;
293 my $svnOutput = svnStatus($fullPath);
294 if ($svnOutput && substr($svnOutput, 0, 1) eq "?") {
295 # Addition
296 system "svn", "add", $fullPath;
297 } else {
298 # Modification
299 print $svnOutput if $svnOutput;
301 } else {
302 # Deletion
303 system "svn", "rm", $fullPath;
307 sub isDirectoryEmptyForRemoval($)
309 my ($dir) = @_;
310 my $directoryIsEmpty = 1;
311 opendir DIR, $dir or die "Could not open '$dir' to list files: $?";
312 for (my $item = readdir DIR; $item && $directoryIsEmpty; $item = readdir DIR) {
313 next if exists $removeDirectoryIgnoreList{$item};
314 if (! -d File::Spec->catdir($dir, $item)) {
315 $directoryIsEmpty = 0;
316 } else {
317 my $svnOutput = svnStatus(File::Spec->catdir($dir, $item));
318 next if $svnOutput && substr($svnOutput, 0, 1) eq "D";
319 $directoryIsEmpty = 0;
322 closedir DIR;
323 return $directoryIsEmpty;
326 sub patch($)
328 my ($patch) = @_;
329 return if !$patch;
331 unless ($patch =~ m|^Index: ([^\n]+)|) {
332 my $separator = '-' x 67;
333 warn "Failed to find 'Index:' in:\n$separator\n$patch\n$separator\n";
334 return;
336 my $fullPath = $1;
338 my $deletion = 0;
339 my $addition = 0;
340 my $isBinary = 0;
342 $addition = 1 if $patch =~ /\n--- .+\(revision 0\)\n/;
343 $deletion = 1 if $patch =~ /\n@@ .* \+0,0 @@/;
344 $isBinary = 1 if $patch =~ /\nCannot display: file marked as a binary type\./;
346 if (!$addition && !$deletion && !$isBinary) {
347 # Standard patch, patch tool can handle this.
348 if (basename($fullPath) eq "ChangeLog") {
349 my $changeLogDotOrigExisted = -f "${fullPath}.orig";
350 applyPatch(setChangeLogDateAndReviewer(fixChangeLogPatch($patch), $reviewer), $fullPath, ["--fuzz=3"]);
351 unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
352 } else {
353 applyPatch($patch, $fullPath);
355 } else {
356 # Either a deletion, an addition or a binary change.
358 addDirectoriesIfNeeded(dirname($fullPath));
360 if ($isBinary) {
361 # Binary change
362 handleBinaryChange($fullPath, $patch);
363 } elsif ($deletion) {
364 # Deletion
365 applyPatch($patch, $fullPath, ["--force"]);
366 system "svn", "rm", "--force", $fullPath;
367 } else {
368 # Addition
369 rename($fullPath, "$fullPath.orig") if -e $fullPath;
370 applyPatch($patch, $fullPath);
371 unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
372 system "svn", "add", $fullPath;
373 system "svn", "stat", "$fullPath.orig" if -e "$fullPath.orig";
378 sub removeDirectoriesIfNeeded()
380 foreach my $dir (reverse sort keys %checkedDirectories) {
381 if (isDirectoryEmptyForRemoval($dir)) {
382 my $svnOutput;
383 open SVN, "svn rm '$dir' |" or die;
384 # Only save the last line since Subversion lists all changed statuses below $dir
385 while (<SVN>) {
386 $svnOutput = $_;
388 close SVN;
389 print $svnOutput if $svnOutput;
394 sub setChangeLogDateAndReviewer($$)
396 my $patch = shift;
397 my $reviewer = shift;
398 my $savedTimeZone = $ENV{'TZ'};
399 # Set TZ temporarily so that localtime() is in that time zone
400 $ENV{'TZ'} = $changeLogTimeZone;
401 my $newDate = strftime("%Y-%m-%d", localtime());
402 if (defined $savedTimeZone) {
403 $ENV{'TZ'} = $savedTimeZone;
404 } else {
405 delete $ENV{'TZ'};
407 $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}( )/$1$newDate$2/;
408 if (defined($reviewer)) {
409 $patch =~ s/NOBODY \(OOPS!\)/$reviewer/;
411 return $patch;
414 sub svnStatus($)
416 my ($fullPath) = @_;
417 my $svnStatus;
418 open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
419 if (-d $fullPath) {
420 # When running "svn stat" on a directory, we can't assume that only one
421 # status will be returned (since any files with a status below the
422 # directory will be returned), and we can't assume that the directory will
423 # be first (since any files with unknown status will be listed first).
424 my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
425 while (<SVN>) {
426 chomp;
427 my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
428 if ($normalizedFullPath eq $normalizedStatPath) {
429 $svnStatus = $_;
430 last;
433 # Read the rest of the svn command output to avoid a broken pipe warning.
434 local $/ = undef;
435 <SVN>;
437 else {
438 # Files will have only one status returned.
439 $svnStatus = <SVN>;
441 close SVN;
442 return $svnStatus;