WebKitTools/Scripts/svn-apply
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     1 #!/usr/bin/perl -w
       
     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 Computer, 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::Spec;
       
    66 use Getopt::Long;
       
    67 use MIME::Base64;
       
    68 use POSIX qw(strftime);
       
    69 
       
    70 use FindBin;
       
    71 use lib $FindBin::Bin;
       
    72 use VCSUtils;
       
    73 
       
    74 sub addDirectoriesIfNeeded($);
       
    75 sub applyPatch($$;$);
       
    76 sub checksum($);
       
    77 sub handleBinaryChange($$);
       
    78 sub handleGitBinaryChange($$);
       
    79 sub isDirectoryEmptyForRemoval($);
       
    80 sub patch($);
       
    81 sub removeDirectoriesIfNeeded();
       
    82 
       
    83 # These should be replaced by an scm class/module:
       
    84 sub scmKnowsOfFile($);
       
    85 sub scmCopy($$);
       
    86 sub scmAdd($);
       
    87 sub scmRemove($);
       
    88 
       
    89 my $merge = 0;
       
    90 my $showHelp = 0;
       
    91 my $reviewer;
       
    92 my $force = 0;
       
    93 
       
    94 my $optionParseSuccess = GetOptions(
       
    95     "merge!" => \$merge,
       
    96     "help!" => \$showHelp,
       
    97     "reviewer=s" => \$reviewer,
       
    98     "force!" => \$force
       
    99 );
       
   100 
       
   101 if (!$optionParseSuccess || $showHelp) {
       
   102     print STDERR basename($0) . " [-h|--help] [--force] [-m|--merge] [-r|--reviewer name] patch1 [patch2 ...]\n";
       
   103     exit 1;
       
   104 }
       
   105 
       
   106 my %removeDirectoryIgnoreList = (
       
   107     '.' => 1,
       
   108     '..' => 1,
       
   109     '.git' => 1,
       
   110     '.svn' => 1,
       
   111     '_svn' => 1,
       
   112 );
       
   113 
       
   114 my $epochTime = time(); # This is used to set the date in ChangeLog files.
       
   115 my $globalExitStatus = 0;
       
   116 
       
   117 my $repositoryRootPath = determineVCSRoot();
       
   118 
       
   119 my %checkedDirectories;
       
   120 
       
   121 # Need to use a typeglob to pass the file handle as a parameter,
       
   122 # otherwise get a bareword error.
       
   123 my @diffHashRefs = parsePatch(*ARGV);
       
   124 
       
   125 print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n";
       
   126 
       
   127 my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs);
       
   128 
       
   129 my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}};
       
   130 my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}};
       
   131 my %sourceRevisions = %{$preparedPatchHash->{sourceRevisionHash}};
       
   132 
       
   133 if ($merge) {
       
   134     die "--merge is currently only supported for SVN" unless isSVN();
       
   135     # How do we handle Git patches applied to an SVN checkout here?
       
   136     for my $file (sort keys %sourceRevisions) {
       
   137         my $version = $sourceRevisions{$file};
       
   138         print "Getting version $version of $file\n";
       
   139         system("svn", "update", "-r", $version, $file) == 0 or die "Failed to run svn update -r $version $file.";
       
   140     }
       
   141 }
       
   142 
       
   143 # Handle copied and moved files first since moved files may have their
       
   144 # source deleted before the move.
       
   145 for my $copyDiffHashRef (@copyDiffHashRefs) {
       
   146     my $indexPath = $copyDiffHashRef->{indexPath};
       
   147     my $copiedFromPath = $copyDiffHashRef->{copiedFromPath};
       
   148 
       
   149     addDirectoriesIfNeeded(dirname($indexPath));
       
   150     scmCopy($copiedFromPath, $indexPath);
       
   151 }
       
   152 
       
   153 for my $diffHashRef (@nonCopyDiffHashRefs) {
       
   154     patch($diffHashRef);
       
   155 }
       
   156 
       
   157 removeDirectoriesIfNeeded();
       
   158 
       
   159 exit $globalExitStatus;
       
   160 
       
   161 sub addDirectoriesIfNeeded($)
       
   162 {
       
   163     my ($path) = @_;
       
   164     my @dirs = File::Spec->splitdir($path);
       
   165     my $dir = ".";
       
   166     while (scalar @dirs) {
       
   167         $dir = File::Spec->catdir($dir, shift @dirs);
       
   168         next if exists $checkedDirectories{$dir};
       
   169         if (! -e $dir) {
       
   170             mkdir $dir or die "Failed to create required directory '$dir' for path '$path'\n";
       
   171             scmAdd($dir);
       
   172             $checkedDirectories{$dir} = 1;
       
   173         }
       
   174         elsif (-d $dir) {
       
   175             # SVN prints "svn: warning: 'directory' is already under version control"
       
   176             # if you try and add a directory which is already in the repository.
       
   177             # Git will ignore the add, but re-adding large directories can be sloooow.
       
   178             # So we check first to see if the directory is under version control first.
       
   179             if (!scmKnowsOfFile($dir)) {
       
   180                 scmAdd($dir);
       
   181             }
       
   182             $checkedDirectories{$dir} = 1;
       
   183         }
       
   184         else {
       
   185             die "'$dir' exists, but is not a directory";
       
   186         }
       
   187     }
       
   188 }
       
   189 
       
   190 # Args:
       
   191 #   $patch: a patch string.
       
   192 #   $pathRelativeToRoot: the path of the file to be patched, relative to the
       
   193 #                        repository root. This should normally be the path
       
   194 #                        found in the patch's "Index:" line.
       
   195 #   $options: a reference to an array of options to pass to the patch command.
       
   196 sub applyPatch($$;$)
       
   197 {
       
   198     my ($patch, $pathRelativeToRoot, $options) = @_;
       
   199 
       
   200     my $optionalArgs = {options => $options, ensureForce => $force};
       
   201 
       
   202     my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs);
       
   203 
       
   204     if ($exitStatus) {
       
   205         $globalExitStatus = $exitStatus;
       
   206     }
       
   207 }
       
   208 
       
   209 sub checksum($)
       
   210 {
       
   211     my $file = shift;
       
   212     open(FILE, $file) or die "Can't open '$file': $!";
       
   213     binmode(FILE);
       
   214     my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
       
   215     close(FILE);
       
   216     return $checksum;
       
   217 }
       
   218 
       
   219 sub handleBinaryChange($$)
       
   220 {
       
   221     my ($fullPath, $contents) = @_;
       
   222     # [A-Za-z0-9+/] is the class of allowed base64 characters.
       
   223     # One or more lines, at most 76 characters in length.
       
   224     # The last line is allowed to have up to two '=' characters at the end (to signify padding).
       
   225     if ($contents =~ m#((\n[A-Za-z0-9+/]{76})*\n[A-Za-z0-9+/]{2,74}?[A-Za-z0-9+/=]{2}\n)#) {
       
   226         # Addition or Modification
       
   227         open FILE, ">", $fullPath or die "Failed to open $fullPath.";
       
   228         print FILE decode_base64($1);
       
   229         close FILE;
       
   230         if (!scmKnowsOfFile($fullPath)) {
       
   231             # Addition
       
   232             scmAdd($fullPath);
       
   233         }
       
   234     } else {
       
   235         # Deletion
       
   236         scmRemove($fullPath);
       
   237     }
       
   238 }
       
   239 
       
   240 sub handleGitBinaryChange($$)
       
   241 {
       
   242     my ($fullPath, $diffHashRef) = @_;
       
   243 
       
   244     my $contents = $diffHashRef->{svnConvertedText};
       
   245 
       
   246     my ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk) = decodeGitBinaryPatch($contents, $fullPath);
       
   247     # FIXME: support "delta" type.
       
   248     die "only literal type is supported now" if ($binaryChunkType ne "literal" || $reverseBinaryChunkType ne "literal");
       
   249 
       
   250     my $isFileAddition = $diffHashRef->{isNew};
       
   251     my $isFileDeletion = $diffHashRef->{isDeletion};
       
   252 
       
   253     my $originalContents = "";
       
   254     if (open FILE, $fullPath) {
       
   255         die "$fullPath already exists" if $isFileAddition;
       
   256 
       
   257         $originalContents = join("", <FILE>);
       
   258         close FILE;
       
   259     }
       
   260     die "Original content of $fullPath mismatches" if $originalContents ne $reverseBinaryChunk;
       
   261 
       
   262     if ($isFileDeletion) {
       
   263         scmRemove($fullPath);
       
   264     } else {
       
   265         # Addition or Modification
       
   266         open FILE, ">", $fullPath or die "Failed to open $fullPath.";
       
   267         print FILE $binaryChunk;
       
   268         close FILE;
       
   269         if ($isFileAddition) {
       
   270             scmAdd($fullPath);
       
   271         }
       
   272     }
       
   273 }
       
   274 
       
   275 sub isDirectoryEmptyForRemoval($)
       
   276 {
       
   277     my ($dir) = @_;
       
   278     return 1 unless -d $dir;
       
   279     my $directoryIsEmpty = 1;
       
   280     opendir DIR, $dir or die "Could not open '$dir' to list files: $?";
       
   281     for (my $item = readdir DIR; $item && $directoryIsEmpty; $item = readdir DIR) {
       
   282         next if exists $removeDirectoryIgnoreList{$item};
       
   283         if (! -d File::Spec->catdir($dir, $item)) {
       
   284             $directoryIsEmpty = 0;
       
   285         } else {
       
   286             next if (scmWillDeleteFile(File::Spec->catdir($dir, $item)));
       
   287             $directoryIsEmpty = 0;
       
   288         }
       
   289     }
       
   290     closedir DIR;
       
   291     return $directoryIsEmpty;
       
   292 }
       
   293 
       
   294 # Args:
       
   295 #   $diffHashRef: a diff hash reference of the type returned by parsePatch().
       
   296 sub patch($)
       
   297 {
       
   298     my ($diffHashRef) = @_;
       
   299 
       
   300     # Make sure $patch is initialized to some value.  A deletion can have no
       
   301     # svnConvertedText property in the case of a deletion resulting from a
       
   302     # Git rename.
       
   303     my $patch = $diffHashRef->{svnConvertedText} || "";
       
   304 
       
   305     my $fullPath = $diffHashRef->{indexPath};
       
   306     my $isBinary = $diffHashRef->{isBinary};
       
   307     my $isGit = $diffHashRef->{isGit};
       
   308 
       
   309     my $deletion = 0;
       
   310     my $addition = 0;
       
   311 
       
   312     $addition = 1 if ($diffHashRef->{isNew} || $patch =~ /\n@@ -0,0 .* @@/);
       
   313     $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/);
       
   314 
       
   315     if (!$addition && !$deletion && !$isBinary) {
       
   316         # Standard patch, patch tool can handle this.
       
   317         if (basename($fullPath) eq "ChangeLog") {
       
   318             my $changeLogDotOrigExisted = -f "${fullPath}.orig";
       
   319             my $newPatch = setChangeLogDateAndReviewer(fixChangeLogPatch($patch), $reviewer, $epochTime);
       
   320             applyPatch($newPatch, $fullPath, ["--fuzz=3"]);
       
   321             unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
       
   322         } else {
       
   323             applyPatch($patch, $fullPath) if $patch;
       
   324         }
       
   325     } else {
       
   326         # Either a deletion, an addition or a binary change.
       
   327 
       
   328         addDirectoriesIfNeeded(dirname($fullPath));
       
   329 
       
   330         if ($isBinary) {
       
   331             if ($isGit) {
       
   332                 handleGitBinaryChange($fullPath, $diffHashRef);
       
   333             } else {
       
   334                 handleBinaryChange($fullPath, $patch) if $patch;
       
   335             }
       
   336         } elsif ($deletion) {
       
   337             applyPatch($patch, $fullPath, ["--force"]) if $patch;
       
   338             scmRemove($fullPath);
       
   339         } else {
       
   340             # Addition
       
   341             rename($fullPath, "$fullPath.orig") if -e $fullPath;
       
   342             applyPatch($patch, $fullPath) if $patch;
       
   343             unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
       
   344             scmAdd($fullPath);
       
   345             # What is this for?
       
   346             system("svn", "stat", "$fullPath.orig") if isSVN() && -e "$fullPath.orig";
       
   347         }
       
   348     }
       
   349 
       
   350     scmToggleExecutableBit($fullPath, $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta});
       
   351 }
       
   352 
       
   353 sub removeDirectoriesIfNeeded()
       
   354 {
       
   355     foreach my $dir (reverse sort keys %checkedDirectories) {
       
   356         if (isDirectoryEmptyForRemoval($dir)) {
       
   357             scmRemove($dir);
       
   358         }
       
   359     }
       
   360 }
       
   361 
       
   362 # This could be made into a more general "status" call, except svn and git
       
   363 # have different ideas about "moving" files which might get confusing.
       
   364 sub scmWillDeleteFile($)
       
   365 {
       
   366     my ($path) = @_;
       
   367     if (isSVN()) {
       
   368         my $svnOutput = svnStatus($path);
       
   369         return 1 if $svnOutput && substr($svnOutput, 0, 1) eq "D";
       
   370     } elsif (isGit()) {
       
   371         my $gitOutput = `git diff-index --name-status HEAD -- $path`;
       
   372         return 1 if $gitOutput && substr($gitOutput, 0, 1) eq "D";
       
   373     }
       
   374     return 0;
       
   375 }
       
   376 
       
   377 # Return whether the file at the given path is known to Git.
       
   378 #
       
   379 # This method outputs a message like the following to STDERR when
       
   380 # returning false:
       
   381 #
       
   382 # "error: pathspec 'test.png' did not match any file(s) known to git.
       
   383 #  Did you forget to 'git add'?"
       
   384 sub gitKnowsOfFile($)
       
   385 {
       
   386     my $path = shift;
       
   387 
       
   388     `git ls-files --error-unmatch -- $path`;
       
   389     my $exitStatus = exitStatus($?);
       
   390     return $exitStatus == 0;
       
   391 }
       
   392 
       
   393 sub scmKnowsOfFile($)
       
   394 {
       
   395     my ($path) = @_;
       
   396     if (isSVN()) {
       
   397         my $svnOutput = svnStatus($path);
       
   398         # This will match more than intended.  ? might not be the first field in the status
       
   399         if ($svnOutput && $svnOutput =~ m#\?\s+$path\n#) {
       
   400             return 0;
       
   401         }
       
   402         # This does not handle errors well.
       
   403         return 1;
       
   404     } elsif (isGit()) {
       
   405         my @result = callSilently(\&gitKnowsOfFile, $path);
       
   406         return $result[0];
       
   407     }
       
   408 }
       
   409 
       
   410 sub scmCopy($$)
       
   411 {
       
   412     my ($source, $destination) = @_;
       
   413     if (isSVN()) {
       
   414         system("svn", "copy", $source, $destination) == 0 or die "Failed to svn copy $source $destination.";
       
   415     } elsif (isGit()) {
       
   416         system("cp", $source, $destination) == 0 or die "Failed to copy $source $destination.";
       
   417         system("git", "add", $destination) == 0 or die "Failed to git add $destination.";
       
   418     }
       
   419 }
       
   420 
       
   421 sub scmAdd($)
       
   422 {
       
   423     my ($path) = @_;
       
   424     if (isSVN()) {
       
   425         system("svn", "add", $path) == 0 or die "Failed to svn add $path.";
       
   426     } elsif (isGit()) {
       
   427         system("git", "add", $path) == 0 or die "Failed to git add $path.";
       
   428     }
       
   429 }
       
   430 
       
   431 sub scmRemove($)
       
   432 {
       
   433     my ($path) = @_;
       
   434     if (isSVN()) {
       
   435         # SVN is very verbose when removing directories.  Squelch all output except the last line.
       
   436         my $svnOutput;
       
   437         open SVN, "svn rm --force '$path' |" or die "svn rm --force '$path' failed!";
       
   438         # Only print the last line.  Subversion outputs all changed statuses below $dir
       
   439         while (<SVN>) {
       
   440             $svnOutput = $_;
       
   441         }
       
   442         close SVN;
       
   443         print $svnOutput if $svnOutput;
       
   444     } elsif (isGit()) {
       
   445         # Git removes a directory if it becomes empty when the last file it contains is
       
   446         # removed by `git rm`. In svn-apply this can happen when a directory is being
       
   447         # removed in a patch, and all of the files inside of the directory are removed
       
   448         # before attemping to remove the directory itself. In this case, Git will have 
       
   449         # already deleted the directory and `git rm` would exit with an error claiming
       
   450         # there was no file. The --ignore-unmatch switch gracefully handles this case.
       
   451         system("git", "rm", "--force", "--ignore-unmatch", $path) == 0 or die "Failed to git rm --force --ignore-unmatch $path.";
       
   452     }
       
   453 }