|
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 |
|
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 # Need to use a typeglob to pass the file handle as a parameter, |
|
101 # otherwise get a bareword error. |
|
102 my @diffHashRefs = parsePatch(*ARGV); |
|
103 |
|
104 print "Parsed " . @diffHashRefs . " diffs from patch file(s).\n"; |
|
105 |
|
106 my $preparedPatchHash = prepareParsedPatch($force, @diffHashRefs); |
|
107 |
|
108 my @copyDiffHashRefs = @{$preparedPatchHash->{copyDiffHashRefs}}; |
|
109 my @nonCopyDiffHashRefs = @{$preparedPatchHash->{nonCopyDiffHashRefs}}; |
|
110 |
|
111 for my $diffHashRef (@nonCopyDiffHashRefs) { |
|
112 patch($diffHashRef); |
|
113 } |
|
114 |
|
115 # Handle copied and moved files last since they may have had post-copy changes that have now been unapplied |
|
116 for my $diffHashRef (@copyDiffHashRefs) { |
|
117 patch($diffHashRef); |
|
118 } |
|
119 |
|
120 if (isSVN()) { |
|
121 revertDirectories(); |
|
122 } |
|
123 |
|
124 exit $globalExitStatus; |
|
125 |
|
126 sub checksum($) |
|
127 { |
|
128 my $file = shift; |
|
129 open(FILE, $file) or die "Can't open '$file': $!"; |
|
130 binmode(FILE); |
|
131 my $checksum = Digest::MD5->new->addfile(*FILE)->hexdigest(); |
|
132 close(FILE); |
|
133 return $checksum; |
|
134 } |
|
135 |
|
136 # Args: |
|
137 # $diffHashRef: a diff hash reference of the type returned by parsePatch(). |
|
138 sub patch($) |
|
139 { |
|
140 my ($diffHashRef) = @_; |
|
141 |
|
142 my $patch = $diffHashRef->{svnConvertedText}; |
|
143 |
|
144 my $fullPath = $diffHashRef->{indexPath}; |
|
145 my $isSvnBinary = $diffHashRef->{isBinary} && $diffHashRef->{isSvn}; |
|
146 |
|
147 $directoriesToCheck{dirname($fullPath)} = 1; |
|
148 |
|
149 my $deletion = 0; |
|
150 my $addition = 0; |
|
151 |
|
152 $addition = 1 if ($diffHashRef->{isNew} || $diffHashRef->{copiedFromPath} || $patch =~ /\n@@ -0,0 .* @@/); |
|
153 $deletion = 1 if ($diffHashRef->{isDeletion} || $patch =~ /\n@@ .* \+0,0 @@/); |
|
154 |
|
155 if (!$addition && !$deletion && !$isSvnBinary) { |
|
156 # Standard patch, patch tool can handle this. |
|
157 if (basename($fullPath) eq "ChangeLog") { |
|
158 my $changeLogDotOrigExisted = -f "${fullPath}.orig"; |
|
159 unapplyPatch(unsetChangeLogDate($fullPath, fixChangeLogPatch($patch)), $fullPath, ["--fuzz=3"]); |
|
160 unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted); |
|
161 } else { |
|
162 unapplyPatch($patch, $fullPath); |
|
163 } |
|
164 } else { |
|
165 # Either a deletion, an addition or a binary change. |
|
166 |
|
167 # FIXME: Add support for Git binary files. |
|
168 if ($isSvnBinary) { |
|
169 # Reverse binary change |
|
170 unlink($fullPath) if (-e $fullPath); |
|
171 system "svn", "revert", $fullPath; |
|
172 } elsif ($deletion) { |
|
173 # Reverse deletion |
|
174 rename($fullPath, "$fullPath.orig") if -e $fullPath; |
|
175 |
|
176 unapplyPatch($patch, $fullPath); |
|
177 |
|
178 # If we don't ask for the filehandle here, we always get a warning. |
|
179 my ($fh, $tempPath) = tempfile(basename($fullPath) . "-XXXXXXXX", |
|
180 DIR => dirname($fullPath), UNLINK => 1); |
|
181 close($fh); |
|
182 |
|
183 # Keep the version from the patch in case it's different from svn. |
|
184 rename($fullPath, $tempPath); |
|
185 system "svn", "revert", $fullPath; |
|
186 rename($tempPath, $fullPath); |
|
187 |
|
188 # This works around a bug in the svn client. |
|
189 # [Issue 1960] file modifications get lost due to FAT 2s time resolution |
|
190 # http://subversion.tigris.org/issues/show_bug.cgi?id=1960 |
|
191 system "touch", $fullPath; |
|
192 |
|
193 # Remove $fullPath.orig if it is the same as $fullPath |
|
194 unlink("$fullPath.orig") if -e "$fullPath.orig" && checksum($fullPath) eq checksum("$fullPath.orig"); |
|
195 |
|
196 # Show status if the file is modifed |
|
197 system "svn", "stat", $fullPath; |
|
198 } else { |
|
199 # Reverse addition |
|
200 # |
|
201 # FIXME: This should use the same logic as svn-apply's deletion |
|
202 # code. In particular, svn-apply's scmRemove() subroutine |
|
203 # should be used here. |
|
204 unapplyPatch($patch, $fullPath, ["--force"]); |
|
205 unlink($fullPath) if -z $fullPath; |
|
206 system "svn", "revert", $fullPath; |
|
207 } |
|
208 } |
|
209 |
|
210 scmToggleExecutableBit($fullPath, -1 * $diffHashRef->{executableBitDelta}) if defined($diffHashRef->{executableBitDelta}); |
|
211 } |
|
212 |
|
213 sub revertDirectories() |
|
214 { |
|
215 chdir $repositoryRootPath; |
|
216 my %checkedDirectories; |
|
217 foreach my $path (reverse sort keys %directoriesToCheck) { |
|
218 my @dirs = File::Spec->splitdir($path); |
|
219 while (scalar @dirs) { |
|
220 my $dir = File::Spec->catdir(@dirs); |
|
221 pop(@dirs); |
|
222 next if (exists $checkedDirectories{$dir}); |
|
223 if (-d $dir) { |
|
224 my $svnOutput = svnStatus($dir); |
|
225 if ($svnOutput && $svnOutput =~ m#A\s+$dir\n#) { |
|
226 system "svn", "revert", $dir; |
|
227 rmdir $dir; |
|
228 } |
|
229 elsif ($svnOutput && $svnOutput =~ m#D\s+$dir\n#) { |
|
230 system "svn", "revert", $dir; |
|
231 } |
|
232 else { |
|
233 # Modification |
|
234 print $svnOutput if $svnOutput; |
|
235 } |
|
236 $checkedDirectories{$dir} = 1; |
|
237 } |
|
238 else { |
|
239 die "'$dir' is not a directory"; |
|
240 } |
|
241 } |
|
242 } |
|
243 } |
|
244 |
|
245 # Args: |
|
246 # $patch: a patch string. |
|
247 # $pathRelativeToRoot: the path of the file to be patched, relative to the |
|
248 # repository root. This should normally be the path |
|
249 # found in the patch's "Index:" line. |
|
250 # $options: a reference to an array of options to pass to the patch command. |
|
251 # Do not include --reverse in this array. |
|
252 sub unapplyPatch($$;$) |
|
253 { |
|
254 my ($patch, $pathRelativeToRoot, $options) = @_; |
|
255 |
|
256 my $optionalArgs = {options => $options, ensureForce => $force, shouldReverse => 1}; |
|
257 |
|
258 my $exitStatus = runPatchCommand($patch, $repositoryRootPath, $pathRelativeToRoot, $optionalArgs); |
|
259 |
|
260 if ($exitStatus) { |
|
261 $globalExitStatus = $exitStatus; |
|
262 } |
|
263 } |
|
264 |
|
265 sub unsetChangeLogDate($$) |
|
266 { |
|
267 my $fullPath = shift; |
|
268 my $patch = shift; |
|
269 my $newDate; |
|
270 sysopen(CHANGELOG, $fullPath, O_RDONLY) or die "Failed to open $fullPath: $!"; |
|
271 sysseek(CHANGELOG, 0, SEEK_SET); |
|
272 my $byteCount = sysread(CHANGELOG, $newDate, 10); |
|
273 die "Failed reading $fullPath: $!" if !$byteCount || $byteCount != 10; |
|
274 close(CHANGELOG); |
|
275 $patch =~ s/(\n\+)\d{4}-[^-]{2}-[^-]{2}( )/$1$newDate$2/; |
|
276 return $patch; |
|
277 } |