svn-create-patch 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. #!/usr/bin/perl -w
  2. # Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions
  6. # are met:
  7. #
  8. # 1. Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # 2. Redistributions in binary form must reproduce the above copyright
  11. # notice, this list of conditions and the following disclaimer in the
  12. # documentation and/or other materials provided with the distribution.
  13. # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
  14. # its contributors may be used to endorse or promote products derived
  15. # from this software without specific prior written permission.
  16. #
  17. # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
  18. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
  21. # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  26. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. # Extended "svn diff" script for WebKit Open Source Project, used to make patches.
  28. # Differences from standard "svn diff":
  29. #
  30. # Uses the real diff, not svn's built-in diff.
  31. # Always passes "-p" to diff so it will try to include function names.
  32. # Handles binary files (encoded as a base64 chunk of text).
  33. # Sorts the diffs alphabetically by text files, then binary files.
  34. # Handles copied and moved files.
  35. #
  36. # Missing features:
  37. #
  38. # Handle copied and moved directories.
  39. use strict;
  40. use warnings;
  41. use Config;
  42. use File::Basename;
  43. use File::Spec;
  44. use File::stat;
  45. use FindBin;
  46. use Getopt::Long;
  47. use lib $FindBin::Bin;
  48. use MIME::Base64;
  49. use POSIX qw(:errno_h);
  50. use Time::gmtime;
  51. use VCSUtils;
  52. sub binarycmp($$);
  53. sub diffOptionsForFile($);
  54. sub findBaseUrl($);
  55. sub findMimeType($;$);
  56. sub findModificationType($);
  57. sub findSourceFileAndRevision($);
  58. sub generateDiff($$);
  59. sub generateFileList($\%);
  60. sub hunkHeaderLineRegExForFile($);
  61. sub isBinaryMimeType($);
  62. sub manufacturePatchForAdditionWithHistory($);
  63. sub numericcmp($$);
  64. sub outputBinaryContent($);
  65. sub patchpathcmp($$);
  66. sub pathcmp($$);
  67. sub processPaths(\@);
  68. sub splitpath($);
  69. sub testfilecmp($$);
  70. $ENV{'LC_ALL'} = 'C';
  71. my $showHelp;
  72. my $ignoreChangelogs = 0;
  73. my $devNull = File::Spec->devnull();
  74. my $result = GetOptions(
  75. "help" => \$showHelp,
  76. "ignore-changelogs" => \$ignoreChangelogs
  77. );
  78. if (!$result || $showHelp) {
  79. print STDERR basename($0) . " [-h|--help] [--ignore-changelogs] [svndir1 [svndir2 ...]]\n";
  80. exit 1;
  81. }
  82. # Sort the diffs for easier reviewing.
  83. my %paths = processPaths(@ARGV);
  84. # Generate a list of files requiring diffs.
  85. my %diffFiles;
  86. for my $path (keys %paths) {
  87. generateFileList($path, %diffFiles);
  88. }
  89. my $svnRoot = determineSVNRoot();
  90. my $prefix = chdirReturningRelativePath($svnRoot);
  91. # Generate the diffs, in an order chosen for ease of reviewing.
  92. for my $path (sort patchpathcmp values %diffFiles) {
  93. generateDiff($path, $prefix);
  94. }
  95. exit 0;
  96. # Overall sort, considering multiple criteria.
  97. sub patchpathcmp($$)
  98. {
  99. my ($a, $b) = @_;
  100. # All binary files come after all non-binary files.
  101. my $result = binarycmp($a, $b);
  102. return $result if $result;
  103. # All test files come after all non-test files.
  104. $result = testfilecmp($a, $b);
  105. return $result if $result;
  106. # Final sort is a "smart" sort by directory and file name.
  107. return pathcmp($a, $b);
  108. }
  109. # Sort so text files appear before binary files.
  110. sub binarycmp($$)
  111. {
  112. my ($fileDataA, $fileDataB) = @_;
  113. return $fileDataA->{isBinary} <=> $fileDataB->{isBinary};
  114. }
  115. sub diffOptionsForFile($)
  116. {
  117. my ($file) = @_;
  118. my $options = "uaNp";
  119. if (my $hunkHeaderLineRegEx = hunkHeaderLineRegExForFile($file)) {
  120. $options .= "F'$hunkHeaderLineRegEx'";
  121. }
  122. return $options;
  123. }
  124. sub findBaseUrl($)
  125. {
  126. my ($infoPath) = @_;
  127. my $baseUrl;
  128. my $escapedInfoPath = escapeSubversionPath($infoPath);
  129. open INFO, "svn info '$escapedInfoPath' |" or die;
  130. while (<INFO>) {
  131. if (/^URL: (.+?)[\r\n]*$/) {
  132. $baseUrl = $1;
  133. }
  134. }
  135. close INFO;
  136. return $baseUrl;
  137. }
  138. sub findMimeType($;$)
  139. {
  140. my ($file, $revision) = @_;
  141. my $args = $revision ? "--revision $revision" : "";
  142. my $escapedFile = escapeSubversionPath($file);
  143. open PROPGET, "svn propget svn:mime-type $args '$escapedFile' |" or die;
  144. my $mimeType = <PROPGET>;
  145. close PROPGET;
  146. # svn may output a different EOL sequence than $/, so avoid chomp.
  147. if ($mimeType) {
  148. $mimeType =~ s/[\r\n]+$//g;
  149. }
  150. return $mimeType;
  151. }
  152. sub findModificationType($)
  153. {
  154. my ($stat) = @_;
  155. my $fileStat = substr($stat, 0, 1);
  156. my $propertyStat = substr($stat, 1, 1);
  157. if ($fileStat eq "A" || $fileStat eq "R") {
  158. my $additionWithHistory = substr($stat, 3, 1);
  159. return $additionWithHistory eq "+" ? "additionWithHistory" : "addition";
  160. }
  161. return "modification" if ($fileStat eq "M" || $propertyStat eq "M");
  162. return "deletion" if ($fileStat eq "D");
  163. return undef;
  164. }
  165. sub findSourceFileAndRevision($)
  166. {
  167. my ($file) = @_;
  168. my $baseUrl = findBaseUrl(".");
  169. my $sourceFile;
  170. my $sourceRevision;
  171. my $escapedFile = escapeSubversionPath($file);
  172. open INFO, "svn info '$escapedFile' |" or die;
  173. while (<INFO>) {
  174. if (/^Copied From URL: (.+?)[\r\n]*$/) {
  175. $sourceFile = File::Spec->abs2rel($1, $baseUrl);
  176. } elsif (/^Copied From Rev: ([0-9]+)/) {
  177. $sourceRevision = $1;
  178. }
  179. }
  180. close INFO;
  181. return ($sourceFile, $sourceRevision);
  182. }
  183. sub generateDiff($$)
  184. {
  185. my ($fileData, $prefix) = @_;
  186. my $file = File::Spec->catdir($prefix, $fileData->{path});
  187. if ($ignoreChangelogs && basename($file) eq "ChangeLog") {
  188. return 0;
  189. }
  190. my $patch = "";
  191. if ($fileData->{modificationType} eq "additionWithHistory") {
  192. manufacturePatchForAdditionWithHistory($fileData);
  193. }
  194. my $diffOptions = diffOptionsForFile($file);
  195. my $escapedFile = escapeSubversionPath($file);
  196. open DIFF, "svn diff --diff-cmd diff -x -$diffOptions '$escapedFile' |" or die;
  197. while (<DIFF>) {
  198. $patch .= $_;
  199. }
  200. close DIFF;
  201. if (basename($file) eq "ChangeLog") {
  202. my $changeLogHash = fixChangeLogPatch($patch);
  203. $patch = $changeLogHash->{patch};
  204. }
  205. print $patch;
  206. if ($fileData->{isBinary}) {
  207. print "\n" if ($patch && $patch =~ m/\n\S+$/m);
  208. outputBinaryContent($file);
  209. }
  210. }
  211. sub generateFileList($\%)
  212. {
  213. my ($statPath, $diffFiles) = @_;
  214. my %testDirectories = map { $_ => 1 } qw(LayoutTests);
  215. my $escapedStatPath = escapeSubversionPath($statPath);
  216. open STAT, "svn stat '$escapedStatPath' |" or die;
  217. while (my $line = <STAT>) {
  218. # svn may output a different EOL sequence than $/, so avoid chomp.
  219. $line =~ s/[\r\n]+$//g;
  220. my $stat;
  221. my $path;
  222. if (isSVNVersion16OrNewer()) {
  223. $stat = substr($line, 0, 8);
  224. $path = substr($line, 8);
  225. } else {
  226. $stat = substr($line, 0, 7);
  227. $path = substr($line, 7);
  228. }
  229. next if -d $path;
  230. my $modificationType = findModificationType($stat);
  231. if ($modificationType) {
  232. $diffFiles->{$path}->{path} = $path;
  233. $diffFiles->{$path}->{modificationType} = $modificationType;
  234. $diffFiles->{$path}->{isBinary} = isBinaryMimeType($path);
  235. $diffFiles->{$path}->{isTestFile} = exists $testDirectories{(File::Spec->splitdir($path))[0]} ? 1 : 0;
  236. if ($modificationType eq "additionWithHistory") {
  237. my ($sourceFile, $sourceRevision) = findSourceFileAndRevision($path);
  238. $diffFiles->{$path}->{sourceFile} = $sourceFile;
  239. $diffFiles->{$path}->{sourceRevision} = $sourceRevision;
  240. }
  241. } else {
  242. print STDERR $line, "\n";
  243. }
  244. }
  245. close STAT;
  246. }
  247. sub hunkHeaderLineRegExForFile($)
  248. {
  249. my ($file) = @_;
  250. my $startOfObjCInterfaceRegEx = "@(implementation\\|interface\\|protocol)";
  251. return "^[-+]\\|$startOfObjCInterfaceRegEx" if $file =~ /\.mm?$/;
  252. return "^$startOfObjCInterfaceRegEx" if $file =~ /^(.*\/)?(mac|objc)\// && $file =~ /\.h$/;
  253. }
  254. sub isBinaryMimeType($)
  255. {
  256. my ($file) = @_;
  257. my $mimeType = findMimeType($file);
  258. return 0 if (!$mimeType || substr($mimeType, 0, 5) eq "text/");
  259. return 1;
  260. }
  261. sub manufacturePatchForAdditionWithHistory($)
  262. {
  263. my ($fileData) = @_;
  264. my $file = $fileData->{path};
  265. print "Index: ${file}\n";
  266. print "=" x 67, "\n";
  267. my $sourceFile = $fileData->{sourceFile};
  268. my $sourceRevision = $fileData->{sourceRevision};
  269. print "--- ${file}\t(revision ${sourceRevision})\t(from ${sourceFile}:${sourceRevision})\n";
  270. print "+++ ${file}\t(working copy)\n";
  271. if ($fileData->{isBinary}) {
  272. print "\nCannot display: file marked as a binary type.\n";
  273. my $mimeType = findMimeType($file, $sourceRevision);
  274. print "svn:mime-type = ${mimeType}\n\n";
  275. } else {
  276. my $escapedSourceFile = escapeSubversionPath($sourceFile);
  277. print `svn cat ${escapedSourceFile} | diff -u $devNull - | tail -n +3`;
  278. }
  279. }
  280. # Sort numeric parts of strings as numbers, other parts as strings.
  281. # Makes 1.33 come after 1.3, which is cool.
  282. sub numericcmp($$)
  283. {
  284. my ($aa, $bb) = @_;
  285. my @a = split /(\d+)/, $aa;
  286. my @b = split /(\d+)/, $bb;
  287. # Compare one chunk at a time.
  288. # Each chunk is either all numeric digits, or all not numeric digits.
  289. while (@a && @b) {
  290. my $a = shift @a;
  291. my $b = shift @b;
  292. # Use numeric comparison if chunks are non-equal numbers.
  293. return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
  294. # Use string comparison if chunks are any other kind of non-equal string.
  295. return $a cmp $b if $a ne $b;
  296. }
  297. # One of the two is now empty; compare lengths for result in this case.
  298. return @a <=> @b;
  299. }
  300. sub outputBinaryContent($)
  301. {
  302. my ($path) = @_;
  303. # Deletion
  304. return if (! -e $path);
  305. # Addition or Modification
  306. my $buffer;
  307. open BINARY, $path or die;
  308. while (read(BINARY, $buffer, 60*57)) {
  309. print encode_base64($buffer);
  310. }
  311. close BINARY;
  312. print "\n";
  313. }
  314. # Sort first by directory, then by file, so all paths in one directory are grouped
  315. # rather than being interspersed with items from subdirectories.
  316. # Use numericcmp to sort directory and filenames to make order logical.
  317. # Also include a special case for ChangeLog, which comes first in any directory.
  318. sub pathcmp($$)
  319. {
  320. my ($fileDataA, $fileDataB) = @_;
  321. my ($dira, $namea) = splitpath($fileDataA->{path});
  322. my ($dirb, $nameb) = splitpath($fileDataB->{path});
  323. return numericcmp($dira, $dirb) if $dira ne $dirb;
  324. return -1 if $namea eq "ChangeLog" && $nameb ne "ChangeLog";
  325. return +1 if $namea ne "ChangeLog" && $nameb eq "ChangeLog";
  326. return numericcmp($namea, $nameb);
  327. }
  328. sub processPaths(\@)
  329. {
  330. my ($paths) = @_;
  331. return ("." => 1) if (!@{$paths});
  332. my %result = ();
  333. for my $file (@{$paths}) {
  334. die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
  335. die "can't handle empty string path\n" if $file eq "";
  336. die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
  337. my $untouchedFile = $file;
  338. $file = canonicalizePath($file);
  339. die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
  340. $result{$file} = 1;
  341. }
  342. return ("." => 1) if ($result{"."});
  343. # Remove any paths that also have a parent listed.
  344. for my $path (keys %result) {
  345. for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
  346. if ($result{$parent}) {
  347. delete $result{$path};
  348. last;
  349. }
  350. }
  351. }
  352. return %result;
  353. }
  354. # Break up a path into the directory (with slash) and base name.
  355. sub splitpath($)
  356. {
  357. my ($path) = @_;
  358. my $pathSeparator = "/";
  359. my $dirname = dirname($path) . $pathSeparator;
  360. $dirname = "" if $dirname eq "." . $pathSeparator;
  361. return ($dirname, basename($path));
  362. }
  363. # Sort so source code files appear before test files.
  364. sub testfilecmp($$)
  365. {
  366. my ($fileDataA, $fileDataB) = @_;
  367. return $fileDataA->{isTestFile} <=> $fileDataB->{isTestFile};
  368. }