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# "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
59use strict;
60use warnings;
61
62use Cwd;
63use Digest::MD5;
64use Fcntl qw(:DEFAULT :seek);
65use File::Basename;
66use File::Spec;
67use File::Temp qw(tempfile);
68use Getopt::Long;
69
70use FindBin;
71use lib $FindBin::Bin;
72use VCSUtils;
73
74sub checksum($);
75sub patch($);
76sub revertDirectories();
77sub unapplyPatch($$;$);
78sub unsetChangeLogDate($$);
79
80my $force = 0;
81my $showHelp = 0;
82
83my $optionParseSuccess = GetOptions(
84    "force!" => \$force,
85    "help!" => \$showHelp
86);
87
88if (!$optionParseSuccess || $showHelp) {
89    print STDERR basename($0) . " [-h|--help] [--force] patch1 [patch2 ...]\n";
90    exit 1;
91}
92
93my $globalExitStatus = 0;
94
95my $repositoryRootPath = determineVCSRoot();
96
97my @copiedFiles;
98my %directoriesToCheck;
99
100my $copiedFromPath;
101my $filter;
102my $indexPath;
103my $patch;
104while (<>) {
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
137if ($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
146for $patch (@copiedFiles) {
147    patch($patch);
148}
149
150if (isSVN()) {
151    revertDirectories();
152}
153
154exit $globalExitStatus;
155
156sub 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
166sub 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
238sub 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.
277sub 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
290sub 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