webengine/osswebengine/WebKitTools/Scripts/svn-unapply
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 # "unpatch" script for Web Kit Open Source Project, used to remove patches.
       
    30 
       
    31 # Differences from invoking "patch -p0 -R":
       
    32 #
       
    33 #   Handles added files (does a svn revert with additional logic to handle local changes). 
       
    34 #   Handles added directories (does a svn revert and a rmdir).
       
    35 #   Handles removed files (does a svn revert with additional logic to handle local changes). 
       
    36 #   Handles removed directories (does a svn revert). 
       
    37 #   Paths from Index: lines are used rather than the paths on the patch lines, which
       
    38 #       makes patches generated by "cvs diff" work (increasingly unimportant since we
       
    39 #       use Subversion now).
       
    40 #   ChangeLog patches use --fuzz=3 to prevent rejects, and the entry date is reset in
       
    41 #       the patch before it is applied (svn-apply sets it when applying a patch).
       
    42 #   Handles binary files (requires patches made by svn-create-patch).
       
    43 #   Handles copied and moved files (requires patches made by svn-create-patch).
       
    44 #   Handles git-diff patches (without binary changes) created at the top-level directory
       
    45 #
       
    46 # Missing features:
       
    47 #
       
    48 #   Handle property changes.
       
    49 #   Handle copied and moved directories (would require patches made by svn-create-patch).
       
    50 #   Use version numbers in the patch file and do a 3-way merge.
       
    51 #   When reversing an addition, check that the file matches what's being removed.
       
    52 #   Notice a patch that's being unapplied at the "wrong level" and make it work anyway.
       
    53 #   Do a dry run on the whole patch and don't do anything if part of the patch is
       
    54 #       going to fail (probably too strict unless we exclude ChangeLog).
       
    55 #   Handle git-diff patches with binary changes
       
    56 
       
    57 use strict;
       
    58 use warnings;
       
    59 
       
    60 use Cwd;
       
    61 use Digest::MD5;
       
    62 use Fcntl qw(:DEFAULT :seek);
       
    63 use File::Basename;
       
    64 use File::Spec;
       
    65 use File::Temp qw(tempfile);
       
    66 use Getopt::Long;
       
    67 
       
    68 sub checksum($);
       
    69 sub fixChangeLogPatch($);
       
    70 sub gitdiff2svndiff($);
       
    71 sub patch($);
       
    72 sub revertDirectories();
       
    73 sub svnStatus($);
       
    74 sub unapplyPatch($$;$);
       
    75 sub unsetChangeLogDate($$);
       
    76 
       
    77 my $showHelp = 0;
       
    78 if (!GetOptions("help!" => \$showHelp) || $showHelp) {
       
    79     print STDERR basename($0) . " [-h|--help] patch1 [patch2 ...]\n";
       
    80     exit 1;
       
    81 }
       
    82 
       
    83 my @copiedFiles;
       
    84 my %directoriesToCheck;
       
    85 
       
    86 my $copiedFromPath;
       
    87 my $filter;
       
    88 my $indexPath;
       
    89 my $patch;
       
    90 while (<>) {
       
    91     s/\r//g;
       
    92     chomp;
       
    93     if (!defined($indexPath) && m#^diff --git a/#) {
       
    94         $filter = \&gitdiff2svndiff;
       
    95     }
       
    96     $_ = &$filter($_) if $filter;
       
    97     if (/^Index: (.*)/) {
       
    98         $indexPath = $1;
       
    99         if ($patch) {
       
   100             if ($copiedFromPath) {
       
   101                 push @copiedFiles, $patch;
       
   102             } else {
       
   103                 patch($patch);
       
   104             }
       
   105             $copiedFromPath = "";
       
   106             $patch = "";
       
   107         }
       
   108     }
       
   109     if ($indexPath) {
       
   110         # Fix paths on diff, ---, and +++ lines to match preceding Index: line.
       
   111         s/^--- \S+/--- $indexPath/;
       
   112         if (/^--- .+\(from (\S+):\d+\)$/) {
       
   113             $copiedFromPath = $1;
       
   114         }
       
   115         if (s/^\+\+\+ \S+/+++ $indexPath/) {
       
   116             $indexPath = "";
       
   117         }
       
   118     }
       
   119     $patch .= $_;
       
   120     $patch .= "\n";
       
   121 }
       
   122 
       
   123 if ($patch) {
       
   124     if ($copiedFromPath) {
       
   125         push @copiedFiles, $patch;
       
   126     } else {
       
   127         patch($patch);
       
   128     }
       
   129 }
       
   130 
       
   131 # Handle copied and moved files last since they may have had post-copy changes that have now been unapplied
       
   132 for $patch (@copiedFiles) {
       
   133     patch($patch);
       
   134 }
       
   135 
       
   136 revertDirectories();
       
   137 
       
   138 exit 0;
       
   139 
       
   140 sub checksum($)
       
   141 {
       
   142     my $file = shift;
       
   143     open(FILE, $file) or die "Can't open '$file': $!";
       
   144     binmode(FILE);
       
   145     my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
       
   146     close(FILE);
       
   147     return $checksum;
       
   148 }
       
   149 
       
   150 sub fixChangeLogPatch($)
       
   151 {
       
   152     my $patch = shift;
       
   153     my $contextLineCount = 3;
       
   154 
       
   155     return $patch if $patch !~ /\n@@ -1,(\d+) \+1,(\d+) @@\n( .*\n)+(\+.*\n)+( .*\n){$contextLineCount}$/m;
       
   156     my ($oldLineCount, $newLineCount) = ($1, $2);
       
   157     return $patch if $oldLineCount <= $contextLineCount;
       
   158 
       
   159     # The diff(1) command is greedy when matching lines, so a new ChangeLog entry will
       
   160     # have lines of context at the top of a patch when the existing entry has the same
       
   161     # date and author as the new entry.  This nifty loop alters a ChangeLog patch so
       
   162     # that the added lines ("+") in the patch always start at the beginning of the
       
   163     # patch and there are no initial lines of context.
       
   164     my $newPatch;
       
   165     my $lineCountInState = 0;
       
   166     my $oldContentLineCountReduction = $oldLineCount - $contextLineCount;
       
   167     my $newContentLineCountWithoutContext = $newLineCount - $oldLineCount - $oldContentLineCountReduction;
       
   168     my ($stateHeader, $statePreContext, $stateNewChanges, $statePostContext) = (1..4);
       
   169     my $state = $stateHeader;
       
   170     foreach my $line (split(/\n/, $patch)) {
       
   171         $lineCountInState++;
       
   172         if ($state == $stateHeader && $line =~ /^@@ -1,$oldLineCount \+1,$newLineCount @\@$/) {
       
   173             $line = "@@ -1,$contextLineCount +1," . ($newLineCount - $oldContentLineCountReduction) . " @@";
       
   174             $lineCountInState = 0;
       
   175             $state = $statePreContext;
       
   176         } elsif ($state == $statePreContext && substr($line, 0, 1) eq " ") {
       
   177             $line = "+" . substr($line, 1);
       
   178             if ($lineCountInState == $oldContentLineCountReduction) {
       
   179                 $lineCountInState = 0;
       
   180                 $state = $stateNewChanges;
       
   181             }
       
   182         } elsif ($state == $stateNewChanges && substr($line, 0, 1) eq "+") {
       
   183             # No changes to these lines
       
   184             if ($lineCountInState == $newContentLineCountWithoutContext) {
       
   185                 $lineCountInState = 0;
       
   186                 $state = $statePostContext;
       
   187             }
       
   188         } elsif ($state == $statePostContext) {
       
   189             if (substr($line, 0, 1) eq "+" && $lineCountInState <= $oldContentLineCountReduction) {
       
   190                 $line = " " . substr($line, 1);
       
   191             } elsif ($lineCountInState > $contextLineCount && substr($line, 0, 1) eq " ") {
       
   192                 next; # Discard
       
   193             }
       
   194         }
       
   195         $newPatch .= $line . "\n";
       
   196     }
       
   197 
       
   198     return $newPatch;
       
   199 }
       
   200 
       
   201 sub gitdiff2svndiff($)
       
   202 {
       
   203     $_ = shift @_;
       
   204     if (m#^diff --git a/(.+) b/(.+)#) {
       
   205         return "Index: $1";
       
   206     } elsif (m/^new file.*/) {
       
   207         return "";
       
   208     } elsif (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) {
       
   209         return "===================================================================";
       
   210     } elsif (m#^--- a/(.+)#) {
       
   211         return "--- $1";
       
   212     } elsif (m#^\+\+\+ b/(.+)#) {
       
   213         return "+++ $1";
       
   214     }
       
   215     return $_;
       
   216 }
       
   217 
       
   218 sub patch($)
       
   219 {
       
   220     my ($patch) = @_;
       
   221     return if !$patch;
       
   222 
       
   223     $patch =~ m|^Index: ([^\n]+)| or die "Failed to find Index: in \"$patch\"\n";
       
   224     my $fullPath = $1;
       
   225     $directoriesToCheck{dirname($fullPath)} = 1;
       
   226 
       
   227     my $deletion = 0;
       
   228     my $addition = 0;
       
   229     my $isBinary = 0;
       
   230 
       
   231     $addition = 1 if ($patch =~ /\n--- .+\(revision 0\)\n/ || $patch =~ /\n@@ -0,0 .* @@/);
       
   232     $deletion = 1 if $patch =~ /\n@@ .* \+0,0 @@/;
       
   233     $isBinary = 1 if $patch =~ /\nCannot display: file marked as a binary type\./;
       
   234 
       
   235     if (!$addition && !$deletion && !$isBinary) {
       
   236         # Standard patch, patch tool can handle this.
       
   237         if (basename($fullPath) eq "ChangeLog") {
       
   238             my $changeLogDotOrigExisted = -f "${fullPath}.orig";
       
   239             unapplyPatch(unsetChangeLogDate($fullPath, fixChangeLogPatch($patch)), $fullPath, ["--fuzz=3"]);
       
   240             unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
       
   241         } else {
       
   242             unapplyPatch($patch, $fullPath);
       
   243         }
       
   244     } else {
       
   245         # Either a deletion, an addition or a binary change.
       
   246 
       
   247         if ($isBinary) {
       
   248             # Reverse binary change
       
   249             unlink($fullPath) if (-e $fullPath);
       
   250             system "svn", "revert", $fullPath;
       
   251         } elsif ($deletion) {
       
   252             # Reverse deletion
       
   253             rename($fullPath, "$fullPath.orig") if -e $fullPath;
       
   254 
       
   255             unapplyPatch($patch, $fullPath);
       
   256 
       
   257             # If we don't ask for the filehandle here, we always get a warning.
       
   258             my ($fh, $tempPath) = tempfile(basename($fullPath) . "-XXXXXXXX",
       
   259                                            DIR => dirname($fullPath), UNLINK => 1);
       
   260             close($fh);
       
   261 
       
   262             # Keep the version from the patch in case it's different from svn.
       
   263             rename($fullPath, $tempPath);
       
   264             system "svn", "revert", $fullPath;
       
   265             rename($tempPath, $fullPath);
       
   266 
       
   267             # This works around a bug in the svn client.
       
   268             # [Issue 1960] file modifications get lost due to FAT 2s time resolution
       
   269             # http://subversion.tigris.org/issues/show_bug.cgi?id=1960
       
   270             system "touch", $fullPath;
       
   271 
       
   272             # Remove $fullPath.orig if it is the same as $fullPath
       
   273             unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
       
   274 
       
   275             # Show status if the file is modifed
       
   276             system "svn", "stat", $fullPath;
       
   277         } else {
       
   278             # Reverse addition
       
   279             unapplyPatch($patch, $fullPath, ["--force"]);
       
   280             unlink($fullPath) if -z $fullPath;
       
   281             system "svn", "revert", $fullPath;
       
   282         }
       
   283     }
       
   284 }
       
   285 
       
   286 sub revertDirectories()
       
   287 {
       
   288     my %checkedDirectories;
       
   289     foreach my $path (reverse sort keys %directoriesToCheck) {
       
   290         my @dirs = File::Spec->splitdir($path);
       
   291         while (scalar @dirs) {
       
   292             my $dir = File::Spec->catdir(@dirs);
       
   293             pop(@dirs);
       
   294             next if (exists $checkedDirectories{$dir});
       
   295             if (-d $dir) {
       
   296                 my $svnOutput = svnStatus($dir);
       
   297                 if ($svnOutput && $svnOutput =~ m#A\s+$dir\n#) {
       
   298                    system "svn", "revert", $dir;
       
   299                    rmdir $dir;
       
   300                 }
       
   301                 elsif ($svnOutput && $svnOutput =~ m#D\s+$dir\n#) {
       
   302                    system "svn", "revert", $dir;
       
   303                 }
       
   304                 else {
       
   305                     # Modification
       
   306                     print $svnOutput if $svnOutput;
       
   307                 }
       
   308                 $checkedDirectories{$dir} = 1;
       
   309             }
       
   310             else {
       
   311                 die "'$dir' is not a directory";
       
   312             }
       
   313         }
       
   314     }
       
   315 }
       
   316 
       
   317 sub svnStatus($)
       
   318 {
       
   319     my ($fullPath) = @_;
       
   320     my $svnStatus;
       
   321     open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
       
   322     if (-d $fullPath) {
       
   323         # When running "svn stat" on a directory, we can't assume that only one
       
   324         # status will be returned (since any files with a status below the
       
   325         # directory will be returned), and we can't assume that the directory will
       
   326         # be first (since any files with unknown status will be listed first).
       
   327         my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
       
   328         while (<SVN>) {
       
   329             chomp;
       
   330             my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
       
   331             if ($normalizedFullPath eq $normalizedStatPath) {
       
   332                 $svnStatus = $_;
       
   333                 last;
       
   334             }
       
   335         }
       
   336         # Read the rest of the svn command output to avoid a broken pipe warning.
       
   337         local $/ = undef;
       
   338         <SVN>;
       
   339     }
       
   340     else {
       
   341         # Files will have only one status returned.
       
   342         $svnStatus = <SVN>;
       
   343     }
       
   344     close SVN;
       
   345     return $svnStatus;
       
   346 }
       
   347 
       
   348 sub unapplyPatch($$;$)
       
   349 {
       
   350     my ($patch, $fullPath, $options) = @_;
       
   351     $options = [] if (! $options);
       
   352     my $command = "patch " . join(" ", "-p0", "-R", @{$options});
       
   353     open PATCH, "| $command" or die "Failed to patch $fullPath: $!";
       
   354     print PATCH $patch;
       
   355     close PATCH;
       
   356 }
       
   357 
       
   358 sub unsetChangeLogDate($$)
       
   359 {
       
   360     my $fullPath = shift;
       
   361     my $patch = shift;
       
   362     my $newDate;
       
   363     sysopen(CHANGELOG, $fullPath, O_RDONLY) or die "Failed to open $fullPath: $!";
       
   364     sysseek(CHANGELOG, 0, SEEK_SET);
       
   365     my $byteCount = sysread(CHANGELOG, $newDate, 10);
       
   366     die "Failed reading $fullPath: $!" if !$byteCount || $byteCount != 10;
       
   367     close(CHANGELOG);
       
   368     $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )/$1$newDate$2/;
       
   369     return $patch;
       
   370 }