webengine/osswebengine/WebKitTools/Scripts/svn-apply
changeset 0 dd21522fd290
equal deleted inserted replaced
-1:000000000000 0:dd21522fd290
       
     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 }