123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- #!/usr/bin/perl -w
- # Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
- #
- # 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.
- # Extended "svn diff" script for WebKit Open Source Project, used to make patches.
- # Differences from standard "svn diff":
- #
- # Uses the real diff, not svn's built-in diff.
- # Always passes "-p" to diff so it will try to include function names.
- # Handles binary files (encoded as a base64 chunk of text).
- # Sorts the diffs alphabetically by text files, then binary files.
- # Handles copied and moved files.
- #
- # Missing features:
- #
- # Handle copied and moved directories.
- use strict;
- use warnings;
- use Config;
- use File::Basename;
- use File::Spec;
- use File::stat;
- use FindBin;
- use Getopt::Long;
- use lib $FindBin::Bin;
- use MIME::Base64;
- use POSIX qw(:errno_h);
- use Time::gmtime;
- use VCSUtils;
- sub binarycmp($$);
- sub diffOptionsForFile($);
- sub findBaseUrl($);
- sub findMimeType($;$);
- sub findModificationType($);
- sub findSourceFileAndRevision($);
- sub generateDiff($$);
- sub generateFileList($\%);
- sub hunkHeaderLineRegExForFile($);
- sub isBinaryMimeType($);
- sub manufacturePatchForAdditionWithHistory($);
- sub numericcmp($$);
- sub outputBinaryContent($);
- sub patchpathcmp($$);
- sub pathcmp($$);
- sub processPaths(\@);
- sub splitpath($);
- sub testfilecmp($$);
- $ENV{'LC_ALL'} = 'C';
- my $showHelp;
- my $ignoreChangelogs = 0;
- my $devNull = File::Spec->devnull();
- my $result = GetOptions(
- "help" => \$showHelp,
- "ignore-changelogs" => \$ignoreChangelogs
- );
- if (!$result || $showHelp) {
- print STDERR basename($0) . " [-h|--help] [--ignore-changelogs] [svndir1 [svndir2 ...]]\n";
- exit 1;
- }
- # Sort the diffs for easier reviewing.
- my %paths = processPaths(@ARGV);
- # Generate a list of files requiring diffs.
- my %diffFiles;
- for my $path (keys %paths) {
- generateFileList($path, %diffFiles);
- }
- my $svnRoot = determineSVNRoot();
- my $prefix = chdirReturningRelativePath($svnRoot);
- # Generate the diffs, in an order chosen for ease of reviewing.
- for my $path (sort patchpathcmp values %diffFiles) {
- generateDiff($path, $prefix);
- }
- exit 0;
- # Overall sort, considering multiple criteria.
- sub patchpathcmp($$)
- {
- my ($a, $b) = @_;
- # All binary files come after all non-binary files.
- my $result = binarycmp($a, $b);
- return $result if $result;
- # All test files come after all non-test files.
- $result = testfilecmp($a, $b);
- return $result if $result;
- # Final sort is a "smart" sort by directory and file name.
- return pathcmp($a, $b);
- }
- # Sort so text files appear before binary files.
- sub binarycmp($$)
- {
- my ($fileDataA, $fileDataB) = @_;
- return $fileDataA->{isBinary} <=> $fileDataB->{isBinary};
- }
- sub diffOptionsForFile($)
- {
- my ($file) = @_;
- my $options = "uaNp";
- if (my $hunkHeaderLineRegEx = hunkHeaderLineRegExForFile($file)) {
- $options .= "F'$hunkHeaderLineRegEx'";
- }
- return $options;
- }
- sub findBaseUrl($)
- {
- my ($infoPath) = @_;
- my $baseUrl;
- my $escapedInfoPath = escapeSubversionPath($infoPath);
- open INFO, "svn info '$escapedInfoPath' |" or die;
- while (<INFO>) {
- if (/^URL: (.+?)[\r\n]*$/) {
- $baseUrl = $1;
- }
- }
- close INFO;
- return $baseUrl;
- }
- sub findMimeType($;$)
- {
- my ($file, $revision) = @_;
- my $args = $revision ? "--revision $revision" : "";
- my $escapedFile = escapeSubversionPath($file);
- open PROPGET, "svn propget svn:mime-type $args '$escapedFile' |" or die;
- my $mimeType = <PROPGET>;
- close PROPGET;
- # svn may output a different EOL sequence than $/, so avoid chomp.
- if ($mimeType) {
- $mimeType =~ s/[\r\n]+$//g;
- }
- return $mimeType;
- }
- sub findModificationType($)
- {
- my ($stat) = @_;
- my $fileStat = substr($stat, 0, 1);
- my $propertyStat = substr($stat, 1, 1);
- if ($fileStat eq "A" || $fileStat eq "R") {
- my $additionWithHistory = substr($stat, 3, 1);
- return $additionWithHistory eq "+" ? "additionWithHistory" : "addition";
- }
- return "modification" if ($fileStat eq "M" || $propertyStat eq "M");
- return "deletion" if ($fileStat eq "D");
- return undef;
- }
- sub findSourceFileAndRevision($)
- {
- my ($file) = @_;
- my $baseUrl = findBaseUrl(".");
- my $sourceFile;
- my $sourceRevision;
- my $escapedFile = escapeSubversionPath($file);
- open INFO, "svn info '$escapedFile' |" or die;
- while (<INFO>) {
- if (/^Copied From URL: (.+?)[\r\n]*$/) {
- $sourceFile = File::Spec->abs2rel($1, $baseUrl);
- } elsif (/^Copied From Rev: ([0-9]+)/) {
- $sourceRevision = $1;
- }
- }
- close INFO;
- return ($sourceFile, $sourceRevision);
- }
- sub generateDiff($$)
- {
- my ($fileData, $prefix) = @_;
- my $file = File::Spec->catdir($prefix, $fileData->{path});
-
- if ($ignoreChangelogs && basename($file) eq "ChangeLog") {
- return 0;
- }
-
- my $patch = "";
- if ($fileData->{modificationType} eq "additionWithHistory") {
- manufacturePatchForAdditionWithHistory($fileData);
- }
- my $diffOptions = diffOptionsForFile($file);
- my $escapedFile = escapeSubversionPath($file);
- open DIFF, "svn diff --diff-cmd diff -x -$diffOptions '$escapedFile' |" or die;
- while (<DIFF>) {
- $patch .= $_;
- }
- close DIFF;
- if (basename($file) eq "ChangeLog") {
- my $changeLogHash = fixChangeLogPatch($patch);
- $patch = $changeLogHash->{patch};
- }
- print $patch;
- if ($fileData->{isBinary}) {
- print "\n" if ($patch && $patch =~ m/\n\S+$/m);
- outputBinaryContent($file);
- }
- }
- sub generateFileList($\%)
- {
- my ($statPath, $diffFiles) = @_;
- my %testDirectories = map { $_ => 1 } qw(LayoutTests);
- my $escapedStatPath = escapeSubversionPath($statPath);
- open STAT, "svn stat '$escapedStatPath' |" or die;
- while (my $line = <STAT>) {
- # svn may output a different EOL sequence than $/, so avoid chomp.
- $line =~ s/[\r\n]+$//g;
- my $stat;
- my $path;
- if (isSVNVersion16OrNewer()) {
- $stat = substr($line, 0, 8);
- $path = substr($line, 8);
- } else {
- $stat = substr($line, 0, 7);
- $path = substr($line, 7);
- }
- next if -d $path;
- my $modificationType = findModificationType($stat);
- if ($modificationType) {
- $diffFiles->{$path}->{path} = $path;
- $diffFiles->{$path}->{modificationType} = $modificationType;
- $diffFiles->{$path}->{isBinary} = isBinaryMimeType($path);
- $diffFiles->{$path}->{isTestFile} = exists $testDirectories{(File::Spec->splitdir($path))[0]} ? 1 : 0;
- if ($modificationType eq "additionWithHistory") {
- my ($sourceFile, $sourceRevision) = findSourceFileAndRevision($path);
- $diffFiles->{$path}->{sourceFile} = $sourceFile;
- $diffFiles->{$path}->{sourceRevision} = $sourceRevision;
- }
- } else {
- print STDERR $line, "\n";
- }
- }
- close STAT;
- }
- sub hunkHeaderLineRegExForFile($)
- {
- my ($file) = @_;
- my $startOfObjCInterfaceRegEx = "@(implementation\\|interface\\|protocol)";
- return "^[-+]\\|$startOfObjCInterfaceRegEx" if $file =~ /\.mm?$/;
- return "^$startOfObjCInterfaceRegEx" if $file =~ /^(.*\/)?(mac|objc)\// && $file =~ /\.h$/;
- }
- sub isBinaryMimeType($)
- {
- my ($file) = @_;
- my $mimeType = findMimeType($file);
- return 0 if (!$mimeType || substr($mimeType, 0, 5) eq "text/");
- return 1;
- }
- sub manufacturePatchForAdditionWithHistory($)
- {
- my ($fileData) = @_;
- my $file = $fileData->{path};
- print "Index: ${file}\n";
- print "=" x 67, "\n";
- my $sourceFile = $fileData->{sourceFile};
- my $sourceRevision = $fileData->{sourceRevision};
- print "--- ${file}\t(revision ${sourceRevision})\t(from ${sourceFile}:${sourceRevision})\n";
- print "+++ ${file}\t(working copy)\n";
- if ($fileData->{isBinary}) {
- print "\nCannot display: file marked as a binary type.\n";
- my $mimeType = findMimeType($file, $sourceRevision);
- print "svn:mime-type = ${mimeType}\n\n";
- } else {
- my $escapedSourceFile = escapeSubversionPath($sourceFile);
- print `svn cat ${escapedSourceFile} | diff -u $devNull - | tail -n +3`;
- }
- }
- # Sort numeric parts of strings as numbers, other parts as strings.
- # Makes 1.33 come after 1.3, which is cool.
- sub numericcmp($$)
- {
- my ($aa, $bb) = @_;
- my @a = split /(\d+)/, $aa;
- my @b = split /(\d+)/, $bb;
- # Compare one chunk at a time.
- # Each chunk is either all numeric digits, or all not numeric digits.
- while (@a && @b) {
- my $a = shift @a;
- my $b = shift @b;
-
- # Use numeric comparison if chunks are non-equal numbers.
- return $a <=> $b if $a =~ /^\d/ && $b =~ /^\d/ && $a != $b;
- # Use string comparison if chunks are any other kind of non-equal string.
- return $a cmp $b if $a ne $b;
- }
-
- # One of the two is now empty; compare lengths for result in this case.
- return @a <=> @b;
- }
- sub outputBinaryContent($)
- {
- my ($path) = @_;
- # Deletion
- return if (! -e $path);
- # Addition or Modification
- my $buffer;
- open BINARY, $path or die;
- while (read(BINARY, $buffer, 60*57)) {
- print encode_base64($buffer);
- }
- close BINARY;
- print "\n";
- }
- # Sort first by directory, then by file, so all paths in one directory are grouped
- # rather than being interspersed with items from subdirectories.
- # Use numericcmp to sort directory and filenames to make order logical.
- # Also include a special case for ChangeLog, which comes first in any directory.
- sub pathcmp($$)
- {
- my ($fileDataA, $fileDataB) = @_;
- my ($dira, $namea) = splitpath($fileDataA->{path});
- my ($dirb, $nameb) = splitpath($fileDataB->{path});
- return numericcmp($dira, $dirb) if $dira ne $dirb;
- return -1 if $namea eq "ChangeLog" && $nameb ne "ChangeLog";
- return +1 if $namea ne "ChangeLog" && $nameb eq "ChangeLog";
- return numericcmp($namea, $nameb);
- }
- sub processPaths(\@)
- {
- my ($paths) = @_;
- return ("." => 1) if (!@{$paths});
- my %result = ();
- for my $file (@{$paths}) {
- die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
- die "can't handle empty string path\n" if $file eq "";
- die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
- my $untouchedFile = $file;
- $file = canonicalizePath($file);
- die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
- $result{$file} = 1;
- }
- return ("." => 1) if ($result{"."});
- # Remove any paths that also have a parent listed.
- for my $path (keys %result) {
- for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
- if ($result{$parent}) {
- delete $result{$path};
- last;
- }
- }
- }
- return %result;
- }
- # Break up a path into the directory (with slash) and base name.
- sub splitpath($)
- {
- my ($path) = @_;
- my $pathSeparator = "/";
- my $dirname = dirname($path) . $pathSeparator;
- $dirname = "" if $dirname eq "." . $pathSeparator;
- return ($dirname, basename($path));
- }
- # Sort so source code files appear before test files.
- sub testfilecmp($$)
- {
- my ($fileDataA, $fileDataB) = @_;
- return $fileDataA->{isTestFile} <=> $fileDataB->{isTestFile};
- }
|