Home | History | Annotate | Download | only in Scripts
      1 #!/usr/bin/perl -w
      2 
      3 # Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved.
      4 # Copyright (C) 2009 Cameron McCormack <cam (at] mcc.id.au>
      5 # Copyright (C) 2010 Chris Jerdonek (chris.jerdonek (at] 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 # "unpatch" script for WebKit Open Source Project, used to remove patches.
     32 
     33 # Differences from invoking "patch -p0 -R":
     34 #
     35 #   Handles added files (does a svn revert with additional logic to handle local changes). 
     36 #   Handles added directories (does a svn revert and a rmdir).
     37 #   Handles removed files (does a svn revert with additional logic to handle local changes). 
     38 #   Handles removed directories (does a svn revert). 
     39 #   Paths from Index: lines are used rather than the paths on the patch lines, which
     40 #       makes patches generated by "cvs diff" work (increasingly unimportant since we
     41 #       use Subversion now).
     42 #   ChangeLog patches use --fuzz=3 to prevent rejects, and the entry date is reset in
     43 #       the patch before it is applied (svn-apply sets it when applying a patch).
     44 #   Handles binary files (requires patches made by svn-create-patch).
     45 #   Handles copied and moved files (requires patches made by svn-create-patch).
     46 #   Handles git-diff patches (without binary changes) created at the top-level directory
     47 #
     48 # Missing features:
     49 #
     50 #   Handle property changes.
     51 #   Handle copied and moved directories (would require patches made by svn-create-patch).
     52 #   Use version numbers in the patch file and do a 3-way merge.
     53 #   When reversing an addition, check that the file matches what's being removed.
     54 #   Notice a patch that's being unapplied 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 Fcntl qw(:DEFAULT :seek);
     65 use File::Basename;
     66 use File::Spec;
     67 use File::Temp qw(tempfile);
     68 use Getopt::Long;
     69 
     70 use FindBin;
     71 use lib $FindBin::Bin;
     72 use VCSUtils;
     73 
     74 sub checksum($);
     75 sub patch($);
     76 sub revertDirectories();
     77 sub unapplyPatch($$;$);
     78 sub unsetChangeLogDate($$);
     79 
     80 my $force = 0;
     81 my $showHelp = 0;
     82 
     83 my $optionParseSuccess = GetOptions(
     84     "force!" => \$force,
     85     "help!" => \$showHelp
     86 );
     87 
     88 if (!$optionParseSuccess || $showHelp) {
     89     print STDERR basename($0) . " [-h|--help] [--force] patch1 [patch2 ...]\n";
     90     exit 1;
     91 }
     92 
     93 my $globalExitStatus = 0;
     94 
     95 my $repositoryRootPath = determineVCSRoot();
     96 
     97 my @copiedFiles;
     98 my %directoriesToCheck;
     99 
    100 my $copiedFromPath;
    101 my $filter;
    102 my $indexPath;
    103 my $patch;
    104 while (<>) {
    105     s/([\n\r]+)$//mg;
    106     my $eol = $1;
    107     if (!defined($indexPath) && m#^diff --git \w/#) {
    108         $filter = \&gitdiff2svndiff;
    109     }
    110     $_ = &$filter($_) if $filter;
    111     if (/^Index: (.+)/) {
    112         $indexPath = $1;
    113         if ($patch) {
    114             if ($copiedFromPath) {
    115                 push @copiedFiles, $patch;
    116             } else {
    117                 patch($patch);
    118             }
    119             $copiedFromPath = "";
    120             $patch = "";
    121         }
    122     }
    123     if ($indexPath) {
    124         # Fix paths on diff, ---, and +++ lines to match preceding Index: line.
    125         s/^--- \S+/--- $indexPath/;
    126         if (/^--- .+\(from (\S+):\d+\)$/) {
    127             $copiedFromPath = $1;
    128         }
    129         if (s/^\+\+\+ \S+/+++ $indexPath/) {
    130             $indexPath = "";
    131         }
    132     }
    133     $patch .= $_;
    134     $patch .= $eol;
    135 }
    136 
    137 if ($patch) {
    138     if ($copiedFromPath) {
    139         push @copiedFiles, $patch;
    140     } else {
    141         patch($patch);
    142     }
    143 }
    144 
    145 # Handle copied and moved files last since they may have had post-copy changes that have now been unapplied
    146 for $patch (@copiedFiles) {
    147     patch($patch);
    148 }
    149 
    150 if (isSVN()) {
    151     revertDirectories();
    152 }
    153 
    154 exit $globalExitStatus;
    155 
    156 sub checksum($)
    157 {
    158     my $file = shift;
    159     open(FILE, $file) or die "Can't open '$file': $!";
    160     binmode(FILE);
    161     my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest();
    162     close(FILE);
    163     return $checksum;
    164 }
    165 
    166 sub patch($)
    167 {
    168     my ($patch) = @_;
    169     return if !$patch;
    170 
    171     unless ($patch =~ m|^Index: ([^\r\n]+)|) {
    172         my $separator = '-' x 67;
    173         warn "Failed to find 'Index:' in:\n$separator\n$patch\n$separator\n";
    174         return;
    175     }
    176     my $fullPath = $1;
    177     $directoriesToCheck{dirname($fullPath)} = 1;
    178 
    179     my $deletion = 0;
    180     my $addition = 0;
    181     my $isBinary = 0;
    182 
    183     $addition = 1 if ($patch =~ /\n--- .+\(revision 0\)\n/ || $patch =~ /\n@@ -0,0 .* @@/);
    184     $deletion = 1 if $patch =~ /\n@@ .* \+0,0 @@/;
    185     $isBinary = 1 if $patch =~ /\nCannot display: file marked as a binary type\./;
    186 
    187     if (!$addition && !$deletion && !$isBinary) {
    188         # Standard patch, patch tool can handle this.
    189         if (basename($fullPath) eq "ChangeLog") {
    190             my $changeLogDotOrigExisted = -f "${fullPath}.orig";
    191             unapplyPatch(unsetChangeLogDate($fullPath, fixChangeLogPatch($patch)), $fullPath, ["--fuzz=3"]);
    192             unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted);
    193         } else {
    194             unapplyPatch($patch, $fullPath);
    195         }
    196     } else {
    197         # Either a deletion, an addition or a binary change.
    198 
    199         if ($isBinary) {
    200             # Reverse binary change
    201             unlink($fullPath) if (-e $fullPath);
    202             system "svn", "revert", $fullPath;
    203         } elsif ($deletion) {
    204             # Reverse deletion
    205             rename($fullPath, "$fullPath.orig") if -e $fullPath;
    206 
    207             unapplyPatch($patch, $fullPath);
    208 
    209             # If we don't ask for the filehandle here, we always get a warning.
    210             my ($fh, $tempPath) = tempfile(basename($fullPath) . "-XXXXXXXX",
    211                                            DIR => dirname($fullPath), UNLINK => 1);
    212             close($fh);
    213 
    214             # Keep the version from the patch in case it's different from svn.
    215             rename($fullPath, $tempPath);
    216             system "svn", "revert", $fullPath;
    217             rename($tempPath, $fullPath);
    218 
    219             # This works around a bug in the svn client.
    220             # [Issue 1960] file modifications get lost due to FAT 2s time resolution
    221             # http://subversion.tigris.org/issues/show_bug.cgi?id=1960
    222             system "touch", $fullPath;
    223 
    224             # Remove $fullPath.orig if it is the same as $fullPath
    225             unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig");
    226 
    227             # Show status if the file is modifed
    228             system "svn", "stat", $fullPath;
    229         } else {
    230             # Reverse addition
    231             unapplyPatch($patch, $fullPath, ["--force"]);
    232             unlink($fullPath) if -z $fullPath;
    233             system "svn", "revert", $fullPath;
    234         }
    235     }
    236 }
    237 
    238 sub revertDirectories()
    239 {
    240     chdir $repositoryRootPath;
    241     my %checkedDirectories;
    242     foreach my $path (reverse sort keys %directoriesToCheck) {
    243         my @dirs = File::Spec->splitdir($path);
    244         while (scalar @dirs) {
    245             my $dir = File::Spec->catdir(@dirs);
    246             pop(@dirs);
    247             next if (exists $checkedDirectories{$dir});
    248             if (-d $dir) {
    249                 my $svnOutput = svnStatus($dir);
    250                 if ($svnOutput && $svnOutput =~ m#A\s+$dir\n#) {
    251                    system "svn", "revert", $dir;
    252                    rmdir $dir;
    253                 }
    254                 elsif ($svnOutput && $svnOutput =~ m#D\s+$dir\n#) {
    255                    system "svn", "revert", $dir;
    256                 }
    257                 else {
    258                     # Modification
    259                     print $svnOutput if $svnOutput;
    260                 }
    261                 $checkedDirectories{$dir} = 1;
    262             }
    263             else {
    264                 die "'$dir' is not a directory";
    265             }
    266         }
    267     }
    268 }
    269 
    270 # Args:
    271 #   $patch: a patch string.
    272 #   $pathRelativeToRoot: the path of the file to be patched, relative to the
    273 #                        repository root. This should normally be the path
    274 #                        found in the patch's "Index:" line.
    275 #   $options: a reference to an array of options to pass to the patch command.
    276 #             Do not include --reverse in this array.
    277 sub unapplyPatch($$;$)
    278 {
    279     my ($patch, $pathRelativeToRoot, $options) = @_;
    280 
    281     my $optionalArgs = {options => $options, ensureForce => $force, shouldReverse => 1};
    282 
    283     my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs);
    284 
    285     if ($exitStatus) {
    286         $globalExitStatus = $exitStatus;
    287     }
    288 }
    289 
    290 sub unsetChangeLogDate($$)
    291 {
    292     my $fullPath = shift;
    293     my $patch = shift;
    294     my $newDate;
    295     sysopen(CHANGELOG, $fullPath, O_RDONLY) or die "Failed to open $fullPath: $!";
    296     sysseek(CHANGELOG, 0, SEEK_SET);
    297     my $byteCount = sysread(CHANGELOG, $newDate, 10);
    298     die "Failed reading $fullPath: $!" if !$byteCount || $byteCount != 10;
    299     close(CHANGELOG);
    300     $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )/$1$newDate$2/;
    301     return $patch;
    302 }
    303