diff options
Diffstat (limited to 'Tools/Scripts/svn-unapply')
| -rwxr-xr-x | Tools/Scripts/svn-unapply | 280 | 
1 files changed, 280 insertions, 0 deletions
diff --git a/Tools/Scripts/svn-unapply b/Tools/Scripts/svn-unapply new file mode 100755 index 0000000..1dca11c --- /dev/null +++ b/Tools/Scripts/svn-unapply @@ -0,0 +1,280 @@ +#!/usr/bin/perl -w + +# Copyright (C) 2005, 2006, 2007 Apple Inc.  All rights reserved. +# Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au> +# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1.  Redistributions of source code must retain the above copyright +#     notice, this list of conditions and the following disclaimer.  +# 2.  Redistributions in binary form must reproduce the above copyright +#     notice, this list of conditions and the following disclaimer in the +#     documentation and/or other materials provided with the distribution.  +# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of +#     its contributors may be used to endorse or promote products derived +#     from this software without specific prior written permission.  +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# "unpatch" script for WebKit Open Source Project, used to remove patches. + +# Differences from invoking "patch -p0 -R": +# +#   Handles added files (does a svn revert with additional logic to handle local changes).  +#   Handles added directories (does a svn revert and a rmdir). +#   Handles removed files (does a svn revert with additional logic to handle local changes).  +#   Handles removed directories (does a svn revert).  +#   Paths from Index: lines are used rather than the paths on the patch lines, which +#       makes patches generated by "cvs diff" work (increasingly unimportant since we +#       use Subversion now). +#   ChangeLog patches use --fuzz=3 to prevent rejects, and the entry date is reset in +#       the patch before it is applied (svn-apply sets it when applying a patch). +#   Handles binary files (requires patches made by svn-create-patch). +#   Handles copied and moved files (requires patches made by svn-create-patch). +#   Handles git-diff patches (without binary changes) created at the top-level directory +# +# Missing features: +# +#   Handle property changes. +#   Handle copied and moved directories (would require patches made by svn-create-patch). +#   Use version numbers in the patch file and do a 3-way merge. +#   When reversing an addition, check that the file matches what's being removed. +#   Notice a patch that's being unapplied at the "wrong level" and make it work anyway. +#   Do a dry run on the whole patch and don't do anything if part of the patch is +#       going to fail (probably too strict unless we exclude ChangeLog). +#   Handle git-diff patches with binary changes + +use strict; +use warnings; + +use Cwd; +use Digest::MD5; +use Fcntl qw(:DEFAULT :seek); +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile); +use Getopt::Long; + +use FindBin; +use lib $FindBin::Bin; +use VCSUtils; + +sub checksum($); +sub patch($); +sub revertDirectories(); +sub unapplyPatch($$;$); +sub unsetChangeLogDate($$); + +my $force = 0; +my $showHelp = 0; + +my $optionParseSuccess = GetOptions( +    "force!" => \$force, +    "help!" => \$showHelp +); + +if (!$optionParseSuccess || $showHelp) { +    print STDERR basename($0) . " [-h|--help] [--force] patch1 [patch2 ...]\n"; +    exit 1; +} + +my $globalExitStatus = 0; + +my $repositoryRootPath = determineVCSRoot(); + +my @copiedFiles; +my %directoriesToCheck; + +# Need to use a typeglob to pass the file handle as a parameter, +# otherwise get a bareword error. +my @diffHashRefs = parsePatch(*ARGV); + +print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n"; + +my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs); + +my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}}; +my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}}; + +for my $diffHashRef (@nonCopyDiffHashRefs) { +    patch($diffHashRef); +} + +# Handle copied and moved files last since they may have had post-copy changes that have now been unapplied +for my $diffHashRef (@copyDiffHashRefs) { +    patch($diffHashRef); +} + +if (isSVN()) { +    revertDirectories(); +} + +exit $globalExitStatus; + +sub checksum($) +{ +    my $file = shift; +    open(FILE, $file) or die "Can't open '$file': $!"; +    binmode(FILE); +    my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest(); +    close(FILE); +    return $checksum; +} + +# Args: +#   $diffHashRef: a diff hash reference of the type returned by parsePatch(). +sub patch($) +{ +    my ($diffHashRef) = @_; + +    # Make sure $patch is initialized to some value.  There is no +    # svnConvertedText when reversing an svn copy/move. +    my $patch = $diffHashRef->{svnConvertedText} || ""; + +    my $fullPath = $diffHashRef->{indexPath}; +    my $isSvnBinary = $diffHashRef->{isBinary} && $diffHashRef->{isSvn}; + +    $directoriesToCheck{dirname($fullPath)} = 1; + +    my $deletion = 0; +    my $addition = 0; + +    $addition = 1 if ($diffHashRef->{isNew} || $diffHashRef->{copiedFromPath} || $patch =~ /\n@@ -0,0 .* @@/); +    $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/); + +    if (!$addition && !$deletion && !$isSvnBinary) { +        # Standard patch, patch tool can handle this. +        if (basename($fullPath) eq "ChangeLog") { +            my $changeLogDotOrigExisted = -f "${fullPath}.orig"; +            my $changeLogHash = fixChangeLogPatch($patch); +            unapplyPatch(unsetChangeLogDate($fullPath, $changeLogHash->{patch}), $fullPath, ["--fuzz=3"]); +            unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted); +        } else { +            unapplyPatch($patch, $fullPath); +        } +    } else { +        # Either a deletion, an addition or a binary change. + +        # FIXME: Add support for Git binary files. +        if ($isSvnBinary) { +            # Reverse binary change +            unlink($fullPath) if (-e $fullPath); +            system "svn", "revert", $fullPath; +        } elsif ($deletion) { +            # Reverse deletion +            rename($fullPath, "$fullPath.orig") if -e $fullPath; + +            unapplyPatch($patch, $fullPath); + +            # If we don't ask for the filehandle here, we always get a warning. +            my ($fh, $tempPath) = tempfile(basename($fullPath) . "-XXXXXXXX", +                                           DIR => dirname($fullPath), UNLINK => 1); +            close($fh); + +            # Keep the version from the patch in case it's different from svn. +            rename($fullPath, $tempPath); +            system "svn", "revert", $fullPath; +            rename($tempPath, $fullPath); + +            # This works around a bug in the svn client. +            # [Issue 1960] file modifications get lost due to FAT 2s time resolution +            # http://subversion.tigris.org/issues/show_bug.cgi?id=1960 +            system "touch", $fullPath; + +            # Remove $fullPath.orig if it is the same as $fullPath +            unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig"); + +            # Show status if the file is modifed +            system "svn", "stat", $fullPath; +        } else { +            # Reverse addition +            # +            # FIXME: This should use the same logic as svn-apply's deletion +            #        code.  In particular, svn-apply's scmRemove() subroutine +            #        should be used here. +            unapplyPatch($patch, $fullPath, ["--force"]) if $patch; +            unlink($fullPath) if -z $fullPath; +            system "svn", "revert", $fullPath; +        } +    } + +    scmToggleExecutableBit($fullPath, -1 * $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta}); +} + +sub revertDirectories() +{ +    chdir $repositoryRootPath; +    my %checkedDirectories; +    foreach my $path (reverse sort keys %directoriesToCheck) { +        my @dirs = File::Spec->splitdir($path); +        while (scalar @dirs) { +            my $dir = File::Spec->catdir(@dirs); +            pop(@dirs); +            next if (exists $checkedDirectories{$dir}); +            if (-d $dir) { +                my $svnOutput = svnStatus($dir); +                if ($svnOutput && $svnOutput =~ m#A\s+$dir\n#) { +                   system "svn", "revert", $dir; +                   rmdir $dir; +                } +                elsif ($svnOutput && $svnOutput =~ m#D\s+$dir\n#) { +                   system "svn", "revert", $dir; +                } +                else { +                    # Modification +                    print $svnOutput if $svnOutput; +                } +                $checkedDirectories{$dir} = 1; +            } +            else { +                die "'$dir' is not a directory"; +            } +        } +    } +} + +# Args: +#   $patch: a patch string. +#   $pathRelativeToRoot: the path of the file to be patched, relative to the +#                        repository root. This should normally be the path +#                        found in the patch's "Index:" line. +#   $options: a reference to an array of options to pass to the patch command. +#             Do not include --reverse in this array. +sub unapplyPatch($$;$) +{ +    my ($patch, $pathRelativeToRoot, $options) = @_; + +    my $optionalArgs = {options => $options, ensureForce => $force, shouldReverse => 1}; + +    my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs); + +    if ($exitStatus) { +        $globalExitStatus = $exitStatus; +    } +} + +sub unsetChangeLogDate($$) +{ +    my $fullPath = shift; +    my $patch = shift; +    my $newDate; +    sysopen(CHANGELOG, $fullPath, O_RDONLY) or die "Failed to open $fullPath: $!"; +    sysseek(CHANGELOG, 0, SEEK_SET); +    my $byteCount = sysread(CHANGELOG, $newDate, 10); +    die "Failed reading $fullPath: $!" if !$byteCount || $byteCount != 10; +    close(CHANGELOG); +    $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )/$1$newDate$2/; +    return $patch; +}  | 
