Ensure old tab state is cleared between iterations of run-benchmark
[WebKit-https.git] / Tools / Scripts / svn-apply
1 #!/usr/bin/env perl
2
3 # Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved.
4 # Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
5 # Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
10 #
11 # 1.  Redistributions of source code must retain the above copyright
12 #     notice, this list of conditions and the following disclaimer.
13 # 2.  Redistributions in binary form must reproduce the above copyright
14 #     notice, this list of conditions and the following disclaimer in the
15 #     documentation and/or other materials provided with the distribution. 
16 # 3.  Neither the name of Apple Inc. ("Apple") nor the names of
17 #     its contributors may be used to endorse or promote products derived
18 #     from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 # "patch" script for WebKit Open Source Project, used to apply patches.
32
33 # Differences from invoking "patch -p0":
34 #
35 #   Handles added files (does a svn add with logic to handle local changes).
36 #   Handles added directories (does a svn add).
37 #   Handles removed files (does a svn rm with logic to handle local changes).
38 #   Handles removed directories--those with no more files or directories left in them
39 #       (does a svn rm).
40 #   Has mode where it will roll back to svn version numbers in the patch file so svn
41 #       can do a 3-way merge.
42 #   Paths from Index: lines are used rather than the paths on the patch lines, which
43 #       makes patches generated by "cvs diff" work (increasingly unimportant since we
44 #       use Subversion now).
45 #   ChangeLog patches use --fuzz=3 to prevent rejects.
46 #   Handles binary files (requires patches made by svn-create-patch).
47 #   Handles copied and moved files (requires patches made by svn-create-patch).
48 #   Handles git-diff patches (without binary changes) created at the top-level directory
49 #
50 # Missing features:
51 #
52 #   Handle property changes.
53 #   Handle copied and moved directories (would require patches made by svn-create-patch).
54 #   When doing a removal, check that old file matches what's being removed.
55 #   Notice a patch that's being applied at the "wrong level" and make it work anyway.
56 #   Do a dry run on the whole patch and don't do anything if part of the patch is
57 #       going to fail (probably too strict unless we exclude ChangeLog).
58 #   Handle git-diff patches with binary delta
59
60 use strict;
61 use warnings;
62
63 use Digest::MD5;
64 use File::Basename;
65 use File::Copy qw(copy);
66 use File::Spec;
67 use Getopt::Long;
68 use MIME::Base64;
69 use POSIX qw(strftime);
70
71 use FindBin;
72 use lib $FindBin::Bin;
73 use VCSUtils;
74
75 sub addDirectoriesIfNeeded($);
76 sub applyPatch($$;$);
77 sub checksum($);
78 sub handleBinaryChange($$);
79 sub handleGitBinaryChange($$);
80 sub isDirectoryEmptyForRemoval($);
81 sub patch($);
82 sub removeDirectoriesIfNeeded();
83
84 # These should be replaced by an scm class/module:
85 sub scmKnowsOfFile($);
86 sub scmCopy($$);
87 sub scmAdd($);
88 sub scmRemove($);
89
90 my $merge = 0;
91 my $showHelp = 0;
92 my $reviewer;
93 my $force = 0;
94 my $skipChangeLogs = 0;
95
96 my $optionParseSuccess = GetOptions(
97     "merge!" => \$merge,
98     "help!" => \$showHelp,
99     "reviewer=s" => \$reviewer,
100     "force!" => \$force,
101     "skip-changelogs" => \$skipChangeLogs
102 );
103
104 if (!$optionParseSuccess || $showHelp) {
105     print STDERR basename($0) . " [-h|--help] [--force] [-m|--merge] [-r|--reviewer name] [--skip-changelogs] patch1 [patch2 ...]\n";
106     exit 1;
107 }
108
109 my %removeDirectoryIgnoreList = (
110     '.' => 1,
111     '..' => 1,
112     '.git' => 1,
113     '.svn' => 1,
114     '_svn' => 1,
115 );
116
117 my $epochTime = time(); # This is used to set the date in ChangeLog files.
118 my $globalExitStatus = 0;
119
120 my $repositoryRootPath = determineVCSRoot();
121
122 my %checkedDirectories;
123
124 # Need to use a typeglob to pass the file handle as a parameter,
125 # otherwise get a bareword error.
126 my @diffHashRefs = parsePatch(*ARGV);
127
128 print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n";
129 die "No diff found." unless @diffHashRefs;
130
131 my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs);
132
133 my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}};
134 my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}};
135 my %sourceRevisions = %{$preparedPatchHash->{sourceRevisionHash}};
136
137 if ($merge) {
138     die "--merge is currently only supported for SVN" unless isSVN();
139     # How do we handle Git patches applied to an SVN checkout here?
140     for my $file (sort keys %sourceRevisions) {
141         my $version = $sourceRevisions{$file};
142         print "Getting version $version of $file\n";
143         my $escapedFile = escapeSubversionPath($file);
144         system("svn", "update", "-r", $version, $escapedFile) == 0 or die "Failed to run svn update -r $version $escapedFile.";
145     }
146 }
147
148 # Handle copied and moved files first since moved files may have their
149 # source deleted before the move.
150 for my $copyDiffHashRef (@copyDiffHashRefs) {
151     my $indexPath = $copyDiffHashRef->{indexPath};
152     my $copiedFromPath = $copyDiffHashRef->{copiedFromPath};
153
154     addDirectoriesIfNeeded(dirname($indexPath));
155     scmCopy($copiedFromPath, $indexPath);
156 }
157
158 for my $diffHashRef (@nonCopyDiffHashRefs) {
159     patch($diffHashRef);
160 }
161
162 removeDirectoriesIfNeeded();
163
164 exit $globalExitStatus;
165
166 sub addDirectoriesIfNeeded($)
167 {
168     # Git removes a directory once the last file in it is removed. We need
169     # explicitly check for the existence of each directory along the path
170     # (and create it if it doesn't) so as to support patches that move all files in
171     # directory A to A/B. That is, we cannot depend on %checkedDirectories.
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 !isGit() && exists $checkedDirectories{$dir};
178         if (! -e $dir) {
179             mkdir $dir or die "Failed to create required directory '$dir' for path '$path'\n";
180             scmAdd($dir);
181             $checkedDirectories{$dir} = 1;
182         }
183         elsif (-d $dir) {
184             # SVN prints "svn: warning: 'directory' is already under version control"
185             # if you try and add a directory which is already in the repository.
186             # Git will ignore the add, but re-adding large directories can be sloooow.
187             # So we check first to see if the directory is under version control first.
188             if (!scmKnowsOfFile($dir)) {
189                 scmAdd($dir);
190             }
191             $checkedDirectories{$dir} = 1;
192         }
193         else {
194             die "'$dir' exists, but is not a directory";
195         }
196     }
197 }
198
199 # Args:
200 #   $patch: a patch string.
201 #   $pathRelativeToRoot: the path of the file to be patched, relative to the
202 #                        repository root. This should normally be the path
203 #                        found in the patch's "Index:" line.
204 #   $options: a reference to an array of options to pass to the patch command.
205 sub applyPatch($$;$)
206 {
207     my ($patch, $pathRelativeToRoot, $options) = @_;
208
209     my $optionalArgs = {options => $options, ensureForce => $force};
210
211     my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs);
212
213     if ($exitStatus) {
214         $globalExitStatus = $exitStatus;
215     }
216 }
217
218 sub checksum($)
219 {
220     my $file = shift;
221     open(FILE, $file) or die "Can't open '$file': $!";
222     binmode(FILE);
223     my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
224     close(FILE);
225     return $checksum;
226 }
227
228 sub handleBinaryChange($$)
229 {
230     my ($fullPath, $contents) = @_;
231     # [A-Za-z0-9+/] is the class of allowed base64 characters.
232     # One or more lines, at most 76 characters in length.
233     # The last line is allowed to have up to two '=' characters at the end (to signify padding).
234     if ($contents =~ m#((\n[A-Za-z0-9+/]{76})*\n[A-Za-z0-9+/]{2,74}?[A-Za-z0-9+/=]{2}\n)#) {
235         # Addition or Modification
236         open FILE, ">", $fullPath or die "Failed to open $fullPath.";
237         print FILE decode_base64($1);
238         close FILE;
239         if (!scmKnowsOfFile($fullPath)) {
240             # Addition
241             scmAdd($fullPath);
242         }
243     } else {
244         # Deletion
245         scmRemove($fullPath);
246     }
247 }
248
249 sub handleGitBinaryChange($$)
250 {
251     my ($fullPath, $diffHashRef) = @_;
252
253     my $contents = $diffHashRef->{svnConvertedText};
254
255     my ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk) = decodeGitBinaryPatch($contents, $fullPath);
256     if (!$binaryChunkType) {
257         die "$fullPath: unknown git binary patch format";
258     }
259
260     my $isFileAddition = $diffHashRef->{isNew};
261     my $isFileDeletion = $diffHashRef->{isDeletion};
262
263     my $originalContents = "";
264     if (open FILE, $fullPath) {
265         die "$fullPath already exists" if $isFileAddition;
266
267         binmode(FILE);
268         $originalContents = join("", <FILE>);
269         close FILE;
270     }
271
272     if ($reverseBinaryChunkType eq "literal") {
273         die "Original content of $fullPath mismatches" if $originalContents ne $reverseBinaryChunk;
274     }
275
276     if ($isFileDeletion) {
277         scmRemove($fullPath);
278     } else {
279         # Addition or Modification
280         my $out = "";
281         if ($binaryChunkType eq "delta") {
282             $out = applyGitBinaryPatchDelta($binaryChunk, $originalContents);
283         } else {
284             $out = $binaryChunk;
285         }
286         if ($reverseBinaryChunkType eq "delta") {
287             die "Original content of $fullPath mismatches" if $originalContents ne applyGitBinaryPatchDelta($reverseBinaryChunk, $out);
288         }
289         open FILE, ">", $fullPath or die "Failed to open $fullPath.";
290         binmode(FILE);
291         print FILE $out;
292         close FILE;
293         if ($isFileAddition) {
294             scmAdd($fullPath);
295         }
296     }
297 }
298
299 sub isDirectoryEmptyForRemoval($)
300 {
301     my ($dir) = @_;
302     return 1 unless -d $dir;
303     my $directoryIsEmpty = 1;
304     opendir DIR, $dir or die "Could not open '$dir' to list files: $?";
305     for (my $item = readdir DIR; $item && $directoryIsEmpty; $item = readdir DIR) {
306         next if exists $removeDirectoryIgnoreList{$item};
307         if (-d File::Spec->catdir($dir, $item)) {
308             $directoryIsEmpty = 0;
309         } else {
310             next if (scmWillDeleteFile(File::Spec->catdir($dir, $item)));
311             $directoryIsEmpty = 0;
312         }
313     }
314     closedir DIR;
315     return $directoryIsEmpty;
316 }
317
318 # Args:
319 #   $diffHashRef: a diff hash reference of the type returned by parsePatch().
320 sub patch($)
321 {
322     my ($diffHashRef) = @_;
323
324     # Make sure $patch is initialized to some value.  A deletion can have no
325     # svnConvertedText property in the case of a deletion resulting from a
326     # Git rename.
327     my $patch = $diffHashRef->{svnConvertedText} || "";
328
329     my $fullPath = $diffHashRef->{indexPath};
330     my $isBinary = $diffHashRef->{isBinary};
331     my $isGit = $diffHashRef->{isGit};
332     my $hasTextChunks = $patch && $diffHashRef->{numTextChunks};
333
334     my $deletion = 0;
335     my $addition = 0;
336
337     $addition = 1 if ($diffHashRef->{isNew} || $patch =~ /\n@@ -0,0 .* @@/);
338     $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/);
339
340     if (basename($fullPath) eq "ChangeLog" && $skipChangeLogs) {
341         print "Skipping '$fullPath' since --skip-changelogs was passed on the command line.";
342         return;
343     }
344
345     if (!$addition && !$deletion && !$isBinary && $hasTextChunks) {
346         # Standard patch, patch tool can handle this.
347         if (basename($fullPath) eq "ChangeLog") {
348             my $changeLogDotOrigExisted = -f "${fullPath}.orig";
349             my $changeLogHash = fixChangeLogPatch($patch);
350             my $newPatch = setChangeLogDateAndReviewer($changeLogHash->{patch}, $reviewer, $epochTime);
351             applyPatch($newPatch, $fullPath, ["--fuzz=3"]);
352             unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
353         } else {
354             applyPatch($patch, $fullPath);
355         }
356     } else {
357         # Either a deletion, an addition or a binary change.
358
359         addDirectoriesIfNeeded(dirname($fullPath));
360
361         if ($isBinary) {
362             if ($isGit) {
363                 handleGitBinaryChange($fullPath, $diffHashRef);
364             } else {
365                 handleBinaryChange($fullPath, $patch) if $patch;
366             }
367         } elsif ($deletion) {
368             applyPatch($patch, $fullPath, ["--force"]) if ($patch && $hasTextChunks);
369             scmRemove($fullPath);
370         } elsif ($addition && $hasTextChunks) {
371             # Addition
372             rename($fullPath, "$fullPath.orig") if -e $fullPath;
373             applyPatch($patch, $fullPath);
374             unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
375             scmAdd($fullPath);
376             my $escapedFullPath = escapeSubversionPath("$fullPath.orig");
377             # What is this for?
378             system("svn", "stat", "$escapedFullPath") if isSVN() && -e "$fullPath.orig";
379         } elsif ($addition && !$hasTextChunks) {
380             # Add empty file.
381             die "\"$fullPath\" already exists" if -e $fullPath;
382             open(my $FH, ">>", $fullPath) or die "Could not open \"$fullPath\" for writing: $!";
383             close($FH);
384             scmAdd($fullPath);
385         } else {
386             die "Can't handle patch for \"$fullPath\".";
387         }
388     }
389
390     scmToggleExecutableBit($fullPath, $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta});
391 }
392
393 sub removeDirectoriesIfNeeded()
394 {
395     foreach my $dir (reverse sort keys %checkedDirectories) {
396         if (isDirectoryEmptyForRemoval($dir)) {
397             scmRemove($dir);
398         }
399     }
400 }
401
402 # This could be made into a more general "status" call, except svn and git
403 # have different ideas about "moving" files which might get confusing.
404 sub scmWillDeleteFile($)
405 {
406     my ($path) = @_;
407     if (isSVN()) {
408         my $svnOutput = svnStatus($path);
409         return 1 if $svnOutput && substr($svnOutput, 0, 1) eq "D";
410     } elsif (isGit()) {
411         my $command = runCommand("git", "diff-index", "--name-status", "HEAD", "--", $path);
412         return 1 if $command->{stdout} && substr($command->{stdout}, 0, 1) eq "D";
413     }
414     return 0;
415 }
416
417 # Return whether the file at the given path is known to Git.
418 #
419 # This method outputs a message like the following to STDERR when
420 # returning false:
421 #
422 # "error: pathspec 'test.png' did not match any file(s) known to git.
423 #  Did you forget to 'git add'?"
424 sub gitKnowsOfFile($)
425 {
426     my $path = shift;
427
428     `git ls-files --error-unmatch -- $path`;
429     my $exitStatus = exitStatus($?);
430     return $exitStatus == 0;
431 }
432
433 sub scmKnowsOfFile($)
434 {
435     my ($path) = @_;
436     if (isSVN()) {
437         my $svnOutput = svnStatus($path);
438         # This will match more than intended.  ? might not be the first field in the status
439         if ($svnOutput && $svnOutput =~ m#\?\s+$path\n#) {
440             return 0;
441         }
442         # This does not handle errors well.
443         return 1;
444     } elsif (isGit()) {
445         my @result = callSilently(\&gitKnowsOfFile, $path);
446         return $result[0];
447     }
448 }
449
450 sub scmCopy($$)
451 {
452     my ($source, $destination) = @_;
453     if (isSVN()) {
454         my $escapedSource = escapeSubversionPath($source);
455         my $escapedDestination = escapeSubversionPath($destination);
456         system("svn", "copy", $escapedSource, $escapedDestination) == 0 or die "Failed to svn copy $escapedSource $escapedDestination.";
457     } elsif (isGit()) {
458         copy($source, $destination) or die "Failed to copy $source $destination.";
459         system("git", "add", $destination) == 0 or die "Failed to git add $destination.";
460     }
461 }
462
463 sub scmAdd($)
464 {
465     my ($path) = @_;
466     if (isSVN()) {
467         my $escapedPath = escapeSubversionPath($path);
468         system("svn", "add", $escapedPath) == 0 or die "Failed to svn add $escapedPath.";
469     } elsif (isGit()) {
470         system("git", "add", $path) == 0 or die "Failed to git add $path.";
471     }
472 }
473
474 sub scmRemove($)
475 {
476     my ($path) = @_;
477     if (isSVN()) {
478         # SVN is very verbose when removing directories.  Squelch all output except the last line.
479         my $svnOutput;
480         my $escapedPath = escapeSubversionPath($path);
481         open SVN, "svn rm --force '$escapedPath' |" or die "svn rm --force '$escapedPath' failed!";
482         # Only print the last line.  Subversion outputs all changed statuses below $dir
483         while (<SVN>) {
484             $svnOutput = $_;
485         }
486         close SVN;
487         print $svnOutput if $svnOutput;
488     } elsif (isGit()) {
489         # Git removes a directory if it becomes empty when the last file it contains is
490         # removed by `git rm`. In svn-apply this can happen when a directory is being
491         # removed in a patch, and all of the files inside of the directory are removed
492         # before attemping to remove the directory itself. In this case, Git will have 
493         # already deleted the directory and `git rm` would exit with an error claiming
494         # there was no file. The --ignore-unmatch switch gracefully handles this case.
495         system("git", "rm", "--force", "--ignore-unmatch", $path) == 0 or die "Failed to git rm --force --ignore-unmatch $path.";
496     }
497 }