WebKitTools/Scripts/VCSUtils.pm
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     1 # Copyright (C) 2007, 2008, 2009 Apple Inc.  All rights reserved.
       
     2 # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
       
     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 
       
    28 # Module to share code to work with various version control systems.
       
    29 package VCSUtils;
       
    30 
       
    31 use strict;
       
    32 use warnings;
       
    33 
       
    34 use Cwd qw();  # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
       
    35 use English; # for $POSTMATCH, etc.
       
    36 use File::Basename;
       
    37 use File::Spec;
       
    38 use POSIX;
       
    39 
       
    40 BEGIN {
       
    41     use Exporter   ();
       
    42     our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
       
    43     $VERSION     = 1.00;
       
    44     @ISA         = qw(Exporter);
       
    45     @EXPORT      = qw(
       
    46         &callSilently
       
    47         &canonicalizePath
       
    48         &changeLogEmailAddress
       
    49         &changeLogName
       
    50         &chdirReturningRelativePath
       
    51         &decodeGitBinaryPatch
       
    52         &determineSVNRoot
       
    53         &determineVCSRoot
       
    54         &exitStatus
       
    55         &fixChangeLogPatch
       
    56         &gitBranch
       
    57         &gitdiff2svndiff
       
    58         &isGit
       
    59         &isGitBranchBuild
       
    60         &isGitDirectory
       
    61         &isSVN
       
    62         &isSVNDirectory
       
    63         &isSVNVersion16OrNewer
       
    64         &makeFilePathRelative
       
    65         &mergeChangeLogs
       
    66         &normalizePath
       
    67         &parsePatch
       
    68         &pathRelativeToSVNRepositoryRootForPath
       
    69         &prepareParsedPatch
       
    70         &runPatchCommand
       
    71         &scmToggleExecutableBit
       
    72         &setChangeLogDateAndReviewer
       
    73         &svnRevisionForDirectory
       
    74         &svnStatus
       
    75     );
       
    76     %EXPORT_TAGS = ( );
       
    77     @EXPORT_OK   = ();
       
    78 }
       
    79 
       
    80 our @EXPORT_OK;
       
    81 
       
    82 my $gitBranch;
       
    83 my $gitRoot;
       
    84 my $isGit;
       
    85 my $isGitBranchBuild;
       
    86 my $isSVN;
       
    87 my $svnVersion;
       
    88 
       
    89 # Project time zone for Cupertino, CA, US
       
    90 my $changeLogTimeZone = "PST8PDT";
       
    91 
       
    92 my $gitDiffStartRegEx = qr#^diff --git (\w/)?(.+) (\w/)?([^\r\n]+)#;
       
    93 my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#;
       
    94 my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path.
       
    95 my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property.
       
    96 my $svnPropertyValueStartRegEx = qr#^   (\+|-) ([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines).
       
    97 
       
    98 # This method is for portability. Return the system-appropriate exit
       
    99 # status of a child process.
       
   100 #
       
   101 # Args: pass the child error status returned by the last pipe close,
       
   102 #       for example "$?".
       
   103 sub exitStatus($)
       
   104 {
       
   105     my ($returnvalue) = @_;
       
   106     if ($^O eq "MSWin32") {
       
   107         return $returnvalue >> 8;
       
   108     }
       
   109     return WEXITSTATUS($returnvalue);
       
   110 }
       
   111 
       
   112 # Call a function while suppressing STDERR, and return the return values
       
   113 # as an array.
       
   114 sub callSilently($@) {
       
   115     my ($func, @args) = @_;
       
   116 
       
   117     # The following pattern was taken from here:
       
   118     #   http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html
       
   119     #
       
   120     # Also see this Perl documentation (search for "open OLDERR"):
       
   121     #   http://perldoc.perl.org/functions/open.html
       
   122     open(OLDERR, ">&STDERR");
       
   123     close(STDERR);
       
   124     my @returnValue = &$func(@args);
       
   125     open(STDERR, ">&OLDERR");
       
   126     close(OLDERR);
       
   127 
       
   128     return @returnValue;
       
   129 }
       
   130 
       
   131 # Note, this method will not error if the file corresponding to the path does not exist.
       
   132 sub scmToggleExecutableBit
       
   133 {
       
   134     my ($path, $executableBitDelta) = @_;
       
   135     return if ! -e $path;
       
   136     if ($executableBitDelta == 1) {
       
   137         scmAddExecutableBit($path);
       
   138     } elsif ($executableBitDelta == -1) {
       
   139         scmRemoveExecutableBit($path);
       
   140     }
       
   141 }
       
   142 
       
   143 sub scmAddExecutableBit($)
       
   144 {
       
   145     my ($path) = @_;
       
   146 
       
   147     if (isSVN()) {
       
   148         system("svn", "propset", "svn:executable", "on", $path) == 0 or die "Failed to run 'svn propset svn:executable on $path'.";
       
   149     } elsif (isGit()) {
       
   150         chmod(0755, $path);
       
   151     }
       
   152 }
       
   153 
       
   154 sub scmRemoveExecutableBit($)
       
   155 {
       
   156     my ($path) = @_;
       
   157 
       
   158     if (isSVN()) {
       
   159         system("svn", "propdel", "svn:executable", $path) == 0 or die "Failed to run 'svn propdel svn:executable $path'.";
       
   160     } elsif (isGit()) {
       
   161         chmod(0664, $path);
       
   162     }
       
   163 }
       
   164 
       
   165 sub isGitDirectory($)
       
   166 {
       
   167     my ($dir) = @_;
       
   168     return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
       
   169 }
       
   170 
       
   171 sub isGit()
       
   172 {
       
   173     return $isGit if defined $isGit;
       
   174 
       
   175     $isGit = isGitDirectory(".");
       
   176     return $isGit;
       
   177 }
       
   178 
       
   179 sub gitBranch()
       
   180 {
       
   181     unless (defined $gitBranch) {
       
   182         chomp($gitBranch = `git symbolic-ref -q HEAD`);
       
   183         $gitBranch = "" if exitStatus($?);
       
   184         $gitBranch =~ s#^refs/heads/##;
       
   185         $gitBranch = "" if $gitBranch eq "master";
       
   186     }
       
   187 
       
   188     return $gitBranch;
       
   189 }
       
   190 
       
   191 sub isGitBranchBuild()
       
   192 {
       
   193     my $branch = gitBranch();
       
   194     chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
       
   195     return 1 if $override eq "true";
       
   196     return 0 if $override eq "false";
       
   197 
       
   198     unless (defined $isGitBranchBuild) {
       
   199         chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
       
   200         $isGitBranchBuild = $gitBranchBuild eq "true";
       
   201     }
       
   202 
       
   203     return $isGitBranchBuild;
       
   204 }
       
   205 
       
   206 sub isSVNDirectory($)
       
   207 {
       
   208     my ($dir) = @_;
       
   209 
       
   210     return -d File::Spec->catdir($dir, ".svn");
       
   211 }
       
   212 
       
   213 sub isSVN()
       
   214 {
       
   215     return $isSVN if defined $isSVN;
       
   216 
       
   217     $isSVN = isSVNDirectory(".");
       
   218     return $isSVN;
       
   219 }
       
   220 
       
   221 sub svnVersion()
       
   222 {
       
   223     return $svnVersion if defined $svnVersion;
       
   224 
       
   225     if (!isSVN()) {
       
   226         $svnVersion = 0;
       
   227     } else {
       
   228         chomp($svnVersion = `svn --version --quiet`);
       
   229     }
       
   230     return $svnVersion;
       
   231 }
       
   232 
       
   233 sub isSVNVersion16OrNewer()
       
   234 {
       
   235     my $version = svnVersion();
       
   236     return eval "v$version" ge v1.6;
       
   237 }
       
   238 
       
   239 sub chdirReturningRelativePath($)
       
   240 {
       
   241     my ($directory) = @_;
       
   242     my $previousDirectory = Cwd::getcwd();
       
   243     chdir $directory;
       
   244     my $newDirectory = Cwd::getcwd();
       
   245     return "." if $newDirectory eq $previousDirectory;
       
   246     return File::Spec->abs2rel($previousDirectory, $newDirectory);
       
   247 }
       
   248 
       
   249 sub determineGitRoot()
       
   250 {
       
   251     chomp(my $gitDir = `git rev-parse --git-dir`);
       
   252     return dirname($gitDir);
       
   253 }
       
   254 
       
   255 sub determineSVNRoot()
       
   256 {
       
   257     my $last = '';
       
   258     my $path = '.';
       
   259     my $parent = '..';
       
   260     my $repositoryRoot;
       
   261     my $repositoryUUID;
       
   262     while (1) {
       
   263         my $thisRoot;
       
   264         my $thisUUID;
       
   265         # Ignore error messages in case we've run past the root of the checkout.
       
   266         open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die;
       
   267         while (<INFO>) {
       
   268             if (/^Repository Root: (.+)/) {
       
   269                 $thisRoot = $1;
       
   270             }
       
   271             if (/^Repository UUID: (.+)/) {
       
   272                 $thisUUID = $1;
       
   273             }
       
   274             if ($thisRoot && $thisUUID) {
       
   275                 local $/ = undef;
       
   276                 <INFO>; # Consume the rest of the input.
       
   277             }
       
   278         }
       
   279         close INFO;
       
   280 
       
   281         # It's possible (e.g. for developers of some ports) to have a WebKit
       
   282         # checkout in a subdirectory of another checkout.  So abort if the
       
   283         # repository root or the repository UUID suddenly changes.
       
   284         last if !$thisUUID;
       
   285         $repositoryUUID = $thisUUID if !$repositoryUUID;
       
   286         last if $thisUUID ne $repositoryUUID;
       
   287 
       
   288         last if !$thisRoot;
       
   289         $repositoryRoot = $thisRoot if !$repositoryRoot;
       
   290         last if $thisRoot ne $repositoryRoot;
       
   291 
       
   292         $last = $path;
       
   293         $path = File::Spec->catdir($parent, $path);
       
   294     }
       
   295 
       
   296     return File::Spec->rel2abs($last);
       
   297 }
       
   298 
       
   299 sub determineVCSRoot()
       
   300 {
       
   301     if (isGit()) {
       
   302         return determineGitRoot();
       
   303     }
       
   304 
       
   305     if (!isSVN()) {
       
   306         # Some users have a workflow where svn-create-patch, svn-apply and
       
   307         # svn-unapply are used outside of multiple svn working directores,
       
   308         # so warn the user and assume Subversion is being used in this case.
       
   309         warn "Unable to determine VCS root; assuming Subversion";
       
   310         $isSVN = 1;
       
   311     }
       
   312 
       
   313     return determineSVNRoot();
       
   314 }
       
   315 
       
   316 sub svnRevisionForDirectory($)
       
   317 {
       
   318     my ($dir) = @_;
       
   319     my $revision;
       
   320 
       
   321     if (isSVNDirectory($dir)) {
       
   322         my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`;
       
   323         ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
       
   324     } elsif (isGitDirectory($dir)) {
       
   325         my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`;
       
   326         ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
       
   327     }
       
   328     die "Unable to determine current SVN revision in $dir" unless (defined $revision);
       
   329     return $revision;
       
   330 }
       
   331 
       
   332 sub pathRelativeToSVNRepositoryRootForPath($)
       
   333 {
       
   334     my ($file) = @_;
       
   335     my $relativePath = File::Spec->abs2rel($file);
       
   336 
       
   337     my $svnInfo;
       
   338     if (isSVN()) {
       
   339         $svnInfo = `LC_ALL=C svn info $relativePath`;
       
   340     } elsif (isGit()) {
       
   341         $svnInfo = `LC_ALL=C git svn info $relativePath`;
       
   342     }
       
   343 
       
   344     $svnInfo =~ /.*^URL: (.*?)$/m;
       
   345     my $svnURL = $1;
       
   346 
       
   347     $svnInfo =~ /.*^Repository Root: (.*?)$/m;
       
   348     my $repositoryRoot = $1;
       
   349 
       
   350     $svnURL =~ s/$repositoryRoot\///;
       
   351     return $svnURL;
       
   352 }
       
   353 
       
   354 sub makeFilePathRelative($)
       
   355 {
       
   356     my ($path) = @_;
       
   357     return $path unless isGit();
       
   358 
       
   359     unless (defined $gitRoot) {
       
   360         chomp($gitRoot = `git rev-parse --show-cdup`);
       
   361     }
       
   362     return $gitRoot . $path;
       
   363 }
       
   364 
       
   365 sub normalizePath($)
       
   366 {
       
   367     my ($path) = @_;
       
   368     $path =~ s/\\/\//g;
       
   369     return $path;
       
   370 }
       
   371 
       
   372 sub canonicalizePath($)
       
   373 {
       
   374     my ($file) = @_;
       
   375 
       
   376     # Remove extra slashes and '.' directories in path
       
   377     $file = File::Spec->canonpath($file);
       
   378 
       
   379     # Remove '..' directories in path
       
   380     my @dirs = ();
       
   381     foreach my $dir (File::Spec->splitdir($file)) {
       
   382         if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
       
   383             pop(@dirs);
       
   384         } else {
       
   385             push(@dirs, $dir);
       
   386         }
       
   387     }
       
   388     return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
       
   389 }
       
   390 
       
   391 sub removeEOL($)
       
   392 {
       
   393     my ($line) = @_;
       
   394 
       
   395     $line =~ s/[\r\n]+$//g;
       
   396     return $line;
       
   397 }
       
   398 
       
   399 sub svnStatus($)
       
   400 {
       
   401     my ($fullPath) = @_;
       
   402     my $svnStatus;
       
   403     open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
       
   404     if (-d $fullPath) {
       
   405         # When running "svn stat" on a directory, we can't assume that only one
       
   406         # status will be returned (since any files with a status below the
       
   407         # directory will be returned), and we can't assume that the directory will
       
   408         # be first (since any files with unknown status will be listed first).
       
   409         my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
       
   410         while (<SVN>) {
       
   411             # Input may use a different EOL sequence than $/, so avoid chomp.
       
   412             $_ = removeEOL($_);
       
   413             my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
       
   414             if ($normalizedFullPath eq $normalizedStatPath) {
       
   415                 $svnStatus = "$_\n";
       
   416                 last;
       
   417             }
       
   418         }
       
   419         # Read the rest of the svn command output to avoid a broken pipe warning.
       
   420         local $/ = undef;
       
   421         <SVN>;
       
   422     }
       
   423     else {
       
   424         # Files will have only one status returned.
       
   425         $svnStatus = removeEOL(<SVN>) . "\n";
       
   426     }
       
   427     close SVN;
       
   428     return $svnStatus;
       
   429 }
       
   430 
       
   431 # Return whether the given file mode is executable in the source control
       
   432 # sense.  We make this determination based on whether the executable bit
       
   433 # is set for "others" rather than the stronger condition that it be set
       
   434 # for the user, group, and others.  This is sufficient for distinguishing
       
   435 # the default behavior in Git and SVN.
       
   436 #
       
   437 # Args:
       
   438 #   $fileMode: A number or string representing a file mode in octal notation.
       
   439 sub isExecutable($)
       
   440 {
       
   441     my $fileMode = shift;
       
   442 
       
   443     return $fileMode % 2;
       
   444 }
       
   445 
       
   446 # Parse the next Git diff header from the given file handle, and advance
       
   447 # the handle so the last line read is the first line after the header.
       
   448 #
       
   449 # This subroutine dies if given leading junk.
       
   450 #
       
   451 # Args:
       
   452 #   $fileHandle: advanced so the last line read from the handle is the first
       
   453 #                line of the header to parse.  This should be a line
       
   454 #                beginning with "diff --git".
       
   455 #   $line: the line last read from $fileHandle
       
   456 #
       
   457 # Returns ($headerHashRef, $lastReadLine):
       
   458 #   $headerHashRef: a hash reference representing a diff header, as follows--
       
   459 #     copiedFromPath: the path from which the file was copied or moved if
       
   460 #                     the diff is a copy or move.
       
   461 #     executableBitDelta: the value 1 or -1 if the executable bit was added or
       
   462 #                         removed, respectively.  New and deleted files have
       
   463 #                         this value only if the file is executable, in which
       
   464 #                         case the value is 1 and -1, respectively.
       
   465 #     indexPath: the path of the target file.
       
   466 #     isBinary: the value 1 if the diff is for a binary file.
       
   467 #     isDeletion: the value 1 if the diff is a file deletion.
       
   468 #     isCopyWithChanges: the value 1 if the file was copied or moved and
       
   469 #                        the target file was changed in some way after being
       
   470 #                        copied or moved (e.g. if its contents or executable
       
   471 #                        bit were changed).
       
   472 #     isNew: the value 1 if the diff is for a new file.
       
   473 #     shouldDeleteSource: the value 1 if the file was copied or moved and
       
   474 #                         the source file was deleted -- i.e. if the copy
       
   475 #                         was actually a move.
       
   476 #     svnConvertedText: the header text with some lines converted to SVN
       
   477 #                       format.  Git-specific lines are preserved.
       
   478 #   $lastReadLine: the line last read from $fileHandle.
       
   479 sub parseGitDiffHeader($$)
       
   480 {
       
   481     my ($fileHandle, $line) = @_;
       
   482 
       
   483     $_ = $line;
       
   484 
       
   485     my $indexPath;
       
   486     if (/$gitDiffStartRegEx/) {
       
   487         # The first and second paths can differ in the case of copies
       
   488         # and renames.  We use the second file path because it is the
       
   489         # destination path.
       
   490         $indexPath = $4;
       
   491         # Use $POSTMATCH to preserve the end-of-line character.
       
   492         $_ = "Index: $indexPath$POSTMATCH"; # Convert to SVN format.
       
   493     } else {
       
   494         die("Could not parse leading \"diff --git\" line: \"$line\".");
       
   495     }
       
   496 
       
   497     my $copiedFromPath;
       
   498     my $foundHeaderEnding;
       
   499     my $isBinary;
       
   500     my $isDeletion;
       
   501     my $isNew;
       
   502     my $newExecutableBit = 0;
       
   503     my $oldExecutableBit = 0;
       
   504     my $shouldDeleteSource = 0;
       
   505     my $similarityIndex = 0;
       
   506     my $svnConvertedText;
       
   507     while (1) {
       
   508         # Temporarily strip off any end-of-line characters to simplify
       
   509         # regex matching below.
       
   510         s/([\n\r]+)$//;
       
   511         my $eol = $1;
       
   512 
       
   513         if (/^(deleted file|old) mode (\d+)/) {
       
   514             $oldExecutableBit = (isExecutable($2) ? 1 : 0);
       
   515             $isDeletion = 1 if $1 eq "deleted file";
       
   516         } elsif (/^new( file)? mode (\d+)/) {
       
   517             $newExecutableBit = (isExecutable($2) ? 1 : 0);
       
   518             $isNew = 1 if $1;
       
   519         } elsif (/^similarity index (\d+)%/) {
       
   520             $similarityIndex = $1;
       
   521         } elsif (/^copy from (\S+)/) {
       
   522             $copiedFromPath = $1;
       
   523         } elsif (/^rename from (\S+)/) {
       
   524             # FIXME: Record this as a move rather than as a copy-and-delete.
       
   525             #        This will simplify adding rename support to svn-unapply.
       
   526             #        Otherwise, the hash for a deletion would have to know
       
   527             #        everything about the file being deleted in order to
       
   528             #        support undoing itself.  Recording as a move will also
       
   529             #        permit us to use "svn move" and "git move".
       
   530             $copiedFromPath = $1;
       
   531             $shouldDeleteSource = 1;
       
   532         } elsif (/^--- \S+/) {
       
   533             $_ = "--- $indexPath"; # Convert to SVN format.
       
   534         } elsif (/^\+\+\+ \S+/) {
       
   535             $_ = "+++ $indexPath"; # Convert to SVN format.
       
   536             $foundHeaderEnding = 1;
       
   537         } elsif (/^GIT binary patch$/ ) {
       
   538             $isBinary = 1;
       
   539             $foundHeaderEnding = 1;
       
   540         # The "git diff" command includes a line of the form "Binary files
       
   541         # <path1> and <path2> differ" if the --binary flag is not used.
       
   542         } elsif (/^Binary files / ) {
       
   543             die("Error: the Git diff contains a binary file without the binary data in ".
       
   544                 "line: \"$_\".  Be sure to use the --binary flag when invoking \"git diff\" ".
       
   545                 "with diffs containing binary files.");
       
   546         }
       
   547 
       
   548         $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
       
   549 
       
   550         $_ = <$fileHandle>; # Not defined if end-of-file reached.
       
   551 
       
   552         last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding);
       
   553     }
       
   554 
       
   555     my $executableBitDelta = $newExecutableBit - $oldExecutableBit;
       
   556 
       
   557     my %header;
       
   558 
       
   559     $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
       
   560     $header{executableBitDelta} = $executableBitDelta if $executableBitDelta;
       
   561     $header{indexPath} = $indexPath;
       
   562     $header{isBinary} = $isBinary if $isBinary;
       
   563     $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta));
       
   564     $header{isDeletion} = $isDeletion if $isDeletion;
       
   565     $header{isNew} = $isNew if $isNew;
       
   566     $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource;
       
   567     $header{svnConvertedText} = $svnConvertedText;
       
   568 
       
   569     return (\%header, $_);
       
   570 }
       
   571 
       
   572 # Parse the next SVN diff header from the given file handle, and advance
       
   573 # the handle so the last line read is the first line after the header.
       
   574 #
       
   575 # This subroutine dies if given leading junk or if it could not detect
       
   576 # the end of the header block.
       
   577 #
       
   578 # Args:
       
   579 #   $fileHandle: advanced so the last line read from the handle is the first
       
   580 #                line of the header to parse.  This should be a line
       
   581 #                beginning with "Index:".
       
   582 #   $line: the line last read from $fileHandle
       
   583 #
       
   584 # Returns ($headerHashRef, $lastReadLine):
       
   585 #   $headerHashRef: a hash reference representing a diff header, as follows--
       
   586 #     copiedFromPath: the path from which the file was copied if the diff
       
   587 #                     is a copy.
       
   588 #     indexPath: the path of the target file, which is the path found in
       
   589 #                the "Index:" line.
       
   590 #     isBinary: the value 1 if the diff is for a binary file.
       
   591 #     isNew: the value 1 if the diff is for a new file.
       
   592 #     sourceRevision: the revision number of the source, if it exists.  This
       
   593 #                     is the same as the revision number the file was copied
       
   594 #                     from, in the case of a file copy.
       
   595 #     svnConvertedText: the header text converted to a header with the paths
       
   596 #                       in some lines corrected.
       
   597 #   $lastReadLine: the line last read from $fileHandle.
       
   598 sub parseSvnDiffHeader($$)
       
   599 {
       
   600     my ($fileHandle, $line) = @_;
       
   601 
       
   602     $_ = $line;
       
   603 
       
   604     my $indexPath;
       
   605     if (/$svnDiffStartRegEx/) {
       
   606         $indexPath = $1;
       
   607     } else {
       
   608         die("First line of SVN diff does not begin with \"Index \": \"$_\"");
       
   609     }
       
   610 
       
   611     my $copiedFromPath;
       
   612     my $foundHeaderEnding;
       
   613     my $isBinary;
       
   614     my $isNew;
       
   615     my $sourceRevision;
       
   616     my $svnConvertedText;
       
   617     while (1) {
       
   618         # Temporarily strip off any end-of-line characters to simplify
       
   619         # regex matching below.
       
   620         s/([\n\r]+)$//;
       
   621         my $eol = $1;
       
   622 
       
   623         # Fix paths on ""---" and "+++" lines to match the leading
       
   624         # index line.
       
   625         if (s/^--- \S+/--- $indexPath/) {
       
   626             # ---
       
   627             if (/^--- .+\(revision (\d+)\)/) {
       
   628                 $sourceRevision = $1;
       
   629                 $isNew = 1 if !$sourceRevision; # if revision 0.
       
   630                 if (/\(from (\S+):(\d+)\)$/) {
       
   631                     # The "from" clause is created by svn-create-patch, in
       
   632                     # which case there is always also a "revision" clause.
       
   633                     $copiedFromPath = $1;
       
   634                     die("Revision number \"$2\" in \"from\" clause does not match " .
       
   635                         "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
       
   636                 }
       
   637             }
       
   638         } elsif (s/^\+\+\+ \S+/+++ $indexPath/) {
       
   639             $foundHeaderEnding = 1;
       
   640         } elsif (/^Cannot display: file marked as a binary type.$/) {
       
   641             $isBinary = 1;
       
   642             $foundHeaderEnding = 1;
       
   643         }
       
   644 
       
   645         $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
       
   646 
       
   647         $_ = <$fileHandle>; # Not defined if end-of-file reached.
       
   648 
       
   649         last if (!defined($_) || /$svnDiffStartRegEx/ || $foundHeaderEnding);
       
   650     }
       
   651 
       
   652     if (!$foundHeaderEnding) {
       
   653         die("Did not find end of header block corresponding to index path \"$indexPath\".");
       
   654     }
       
   655 
       
   656     my %header;
       
   657 
       
   658     $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
       
   659     $header{indexPath} = $indexPath;
       
   660     $header{isBinary} = $isBinary if $isBinary;
       
   661     $header{isNew} = $isNew if $isNew;
       
   662     $header{sourceRevision} = $sourceRevision if $sourceRevision;
       
   663     $header{svnConvertedText} = $svnConvertedText;
       
   664 
       
   665     return (\%header, $_);
       
   666 }
       
   667 
       
   668 # Parse the next diff header from the given file handle, and advance
       
   669 # the handle so the last line read is the first line after the header.
       
   670 #
       
   671 # This subroutine dies if given leading junk or if it could not detect
       
   672 # the end of the header block.
       
   673 #
       
   674 # Args:
       
   675 #   $fileHandle: advanced so the last line read from the handle is the first
       
   676 #                line of the header to parse.  For SVN-formatted diffs, this
       
   677 #                is a line beginning with "Index:".  For Git, this is a line
       
   678 #                beginning with "diff --git".
       
   679 #   $line: the line last read from $fileHandle
       
   680 #
       
   681 # Returns ($headerHashRef, $lastReadLine):
       
   682 #   $headerHashRef: a hash reference representing a diff header
       
   683 #     copiedFromPath: the path from which the file was copied if the diff
       
   684 #                     is a copy.
       
   685 #     executableBitDelta: the value 1 or -1 if the executable bit was added or
       
   686 #                         removed, respectively.  New and deleted files have
       
   687 #                         this value only if the file is executable, in which
       
   688 #                         case the value is 1 and -1, respectively.
       
   689 #     indexPath: the path of the target file.
       
   690 #     isBinary: the value 1 if the diff is for a binary file.
       
   691 #     isGit: the value 1 if the diff is Git-formatted.
       
   692 #     isSvn: the value 1 if the diff is SVN-formatted.
       
   693 #     sourceRevision: the revision number of the source, if it exists.  This
       
   694 #                     is the same as the revision number the file was copied
       
   695 #                     from, in the case of a file copy.
       
   696 #     svnConvertedText: the header text with some lines converted to SVN
       
   697 #                       format.  Git-specific lines are preserved.
       
   698 #   $lastReadLine: the line last read from $fileHandle.
       
   699 sub parseDiffHeader($$)
       
   700 {
       
   701     my ($fileHandle, $line) = @_;
       
   702 
       
   703     my $header;  # This is a hash ref.
       
   704     my $isGit;
       
   705     my $isSvn;
       
   706     my $lastReadLine;
       
   707 
       
   708     if ($line =~ $svnDiffStartRegEx) {
       
   709         $isSvn = 1;
       
   710         ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line);
       
   711     } elsif ($line =~ $gitDiffStartRegEx) {
       
   712         $isGit = 1;
       
   713         ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line);
       
   714     } else {
       
   715         die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\"");
       
   716     }
       
   717 
       
   718     $header->{isGit} = $isGit if $isGit;
       
   719     $header->{isSvn} = $isSvn if $isSvn;
       
   720 
       
   721     return ($header, $lastReadLine);
       
   722 }
       
   723 
       
   724 # FIXME: The %diffHash "object" should not have an svnConvertedText property.
       
   725 #        Instead, the hash object should store its information in a
       
   726 #        structured way as properties.  This should be done in a way so
       
   727 #        that, if necessary, the text of an SVN or Git patch can be
       
   728 #        reconstructed from the information in those hash properties.
       
   729 #
       
   730 # A %diffHash is a hash representing a source control diff of a single
       
   731 # file operation (e.g. a file modification, copy, or delete).
       
   732 #
       
   733 # These hashes appear, for example, in the parseDiff(), parsePatch(),
       
   734 # and prepareParsedPatch() subroutines of this package.
       
   735 #
       
   736 # The corresponding values are--
       
   737 #
       
   738 #   copiedFromPath: the path from which the file was copied if the diff
       
   739 #                   is a copy.
       
   740 #   executableBitDelta: the value 1 or -1 if the executable bit was added or
       
   741 #                       removed from the target file, respectively.
       
   742 #   indexPath: the path of the target file.  For SVN-formatted diffs,
       
   743 #              this is the same as the path in the "Index:" line.
       
   744 #   isBinary: the value 1 if the diff is for a binary file.
       
   745 #   isDeletion: the value 1 if the diff is known from the header to be a deletion.
       
   746 #   isGit: the value 1 if the diff is Git-formatted.
       
   747 #   isNew: the value 1 if the dif is known from the header to be a new file.
       
   748 #   isSvn: the value 1 if the diff is SVN-formatted.
       
   749 #   sourceRevision: the revision number of the source, if it exists.  This
       
   750 #                   is the same as the revision number the file was copied
       
   751 #                   from, in the case of a file copy.
       
   752 #   svnConvertedText: the diff with some lines converted to SVN format.
       
   753 #                     Git-specific lines are preserved.
       
   754 
       
   755 # Parse one diff from a patch file created by svn-create-patch, and
       
   756 # advance the file handle so the last line read is the first line
       
   757 # of the next header block.
       
   758 #
       
   759 # This subroutine preserves any leading junk encountered before the header.
       
   760 #
       
   761 # Composition of an SVN diff
       
   762 #
       
   763 # There are three parts to an SVN diff: the header, the property change, and
       
   764 # the binary contents, in that order. Either the header or the property change
       
   765 # may be ommitted, but not both. If there are binary changes, then you always
       
   766 # have all three.
       
   767 #
       
   768 # Args:
       
   769 #   $fileHandle: a file handle advanced to the first line of the next
       
   770 #                header block. Leading junk is okay.
       
   771 #   $line: the line last read from $fileHandle.
       
   772 #
       
   773 # Returns ($diffHashRefs, $lastReadLine):
       
   774 #   $diffHashRefs: A reference to an array of references to %diffHash hashes.
       
   775 #                  See the %diffHash documentation above.
       
   776 #   $lastReadLine: the line last read from $fileHandle
       
   777 sub parseDiff($$)
       
   778 {
       
   779     # FIXME: Adjust this method so that it dies if the first line does not
       
   780     #        match the start of a diff.  This will require a change to
       
   781     #        parsePatch() so that parsePatch() skips over leading junk.
       
   782     my ($fileHandle, $line) = @_;
       
   783 
       
   784     my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default
       
   785 
       
   786     my $headerHashRef; # Last header found, as returned by parseDiffHeader().
       
   787     my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties().
       
   788     my $svnText;
       
   789     while (defined($line)) {
       
   790         if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) {
       
   791             # Then assume all diffs in the patch are Git-formatted. This
       
   792             # block was made to be enterable at most once since we assume
       
   793             # all diffs in the patch are formatted the same (SVN or Git).
       
   794             $headerStartRegEx = $gitDiffStartRegEx;
       
   795         }
       
   796 
       
   797         if ($line =~ $svnPropertiesStartRegEx) {
       
   798             my $propertyPath = $1;
       
   799             if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) {
       
   800                 # This is the start of the second diff in the while loop, which happens to
       
   801                 # be a property diff.  If $svnPropertiesHasRef is defined, then this is the
       
   802                 # second consecutive property diff, otherwise it's the start of a property
       
   803                 # diff for a file that only has property changes.
       
   804                 last;
       
   805             }
       
   806             ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line);
       
   807             next;
       
   808         }
       
   809         if ($line !~ $headerStartRegEx) {
       
   810             # Then we are in the body of the diff.
       
   811             $svnText .= $line;
       
   812             $line = <$fileHandle>;
       
   813             next;
       
   814         } # Otherwise, we found a diff header.
       
   815 
       
   816         if ($svnPropertiesHashRef || $headerHashRef) {
       
   817             # Then either we just processed an SVN property change or this
       
   818             # is the start of the second diff header of this while loop.
       
   819             last;
       
   820         }
       
   821 
       
   822         ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
       
   823 
       
   824         $svnText .= $headerHashRef->{svnConvertedText};
       
   825     }
       
   826 
       
   827     my @diffHashRefs;
       
   828 
       
   829     if ($headerHashRef->{shouldDeleteSource}) {
       
   830         my %deletionHash;
       
   831         $deletionHash{indexPath} = $headerHashRef->{copiedFromPath};
       
   832         $deletionHash{isDeletion} = 1;
       
   833         push @diffHashRefs, \%deletionHash;
       
   834     }
       
   835     if ($headerHashRef->{copiedFromPath}) {
       
   836         my %copyHash;
       
   837         $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath};
       
   838         $copyHash{indexPath} = $headerHashRef->{indexPath};
       
   839         $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
       
   840         if ($headerHashRef->{isSvn}) {
       
   841             $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
       
   842         }
       
   843         push @diffHashRefs, \%copyHash;
       
   844     }
       
   845 
       
   846     # Note, the order of evaluation for the following if conditional has been explicitly chosen so that
       
   847     # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that
       
   848     # only has property changes).
       
   849     if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) {
       
   850         # Then add the usual file modification.
       
   851         my %diffHash;
       
   852         # FIXME: We should expand this code to support other properties.  In the future,
       
   853         #        parseSvnDiffProperties may return a hash whose keys are the properties.
       
   854         if ($headerHashRef->{isSvn}) {
       
   855             # SVN records the change to the executable bit in a separate property change diff
       
   856             # that follows the contents of the diff, except for binary diffs.  For binary
       
   857             # diffs, the property change diff follows the diff header.
       
   858             $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
       
   859         } elsif ($headerHashRef->{isGit}) {
       
   860             # Git records the change to the executable bit in the header of a diff.
       
   861             $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta};
       
   862         }
       
   863         $diffHash{indexPath} = $headerHashRef->{indexPath};
       
   864         $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary};
       
   865         $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion};
       
   866         $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit};
       
   867         $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew};
       
   868         $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn};
       
   869         if (!$headerHashRef->{copiedFromPath}) {
       
   870             # If the file was copied, then we have already incorporated the
       
   871             # sourceRevision information into the change.
       
   872             $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
       
   873         }
       
   874         # FIXME: Remove the need for svnConvertedText.  See the %diffHash
       
   875         #        code comments above for more information.
       
   876         #
       
   877         # Note, we may not always have SVN converted text since we intend
       
   878         # to deprecate it in the future.  For example, a property change
       
   879         # diff for a file that only has property changes will not return
       
   880         # any SVN converted text.
       
   881         $diffHash{svnConvertedText} = $svnText if $svnText;
       
   882         push @diffHashRefs, \%diffHash;
       
   883     }
       
   884 
       
   885     if (!%$headerHashRef && $svnPropertiesHashRef) {
       
   886         # A property change diff for a file that only has property changes.
       
   887         my %propertyChangeHash;
       
   888         $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
       
   889         $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath};
       
   890         $propertyChangeHash{isSvn} = 1;
       
   891         push @diffHashRefs, \%propertyChangeHash;
       
   892     }
       
   893 
       
   894     return (\@diffHashRefs, $line);
       
   895 }
       
   896 
       
   897 # Parse an SVN property change diff from the given file handle, and advance
       
   898 # the handle so the last line read is the first line after this diff.
       
   899 #
       
   900 # For the case of an SVN binary diff, the binary contents will follow the
       
   901 # the property changes.
       
   902 #
       
   903 # This subroutine dies if the first line does not begin with "Property changes on"
       
   904 # or if the separator line that follows this line is missing.
       
   905 #
       
   906 # Args:
       
   907 #   $fileHandle: advanced so the last line read from the handle is the first
       
   908 #                line of the footer to parse.  This line begins with
       
   909 #                "Property changes on".
       
   910 #   $line: the line last read from $fileHandle.
       
   911 #
       
   912 # Returns ($propertyHashRef, $lastReadLine):
       
   913 #   $propertyHashRef: a hash reference representing an SVN diff footer.
       
   914 #     propertyPath: the path of the target file.
       
   915 #     executableBitDelta: the value 1 or -1 if the executable bit was added or
       
   916 #                         removed from the target file, respectively.
       
   917 #   $lastReadLine: the line last read from $fileHandle.
       
   918 sub parseSvnDiffProperties($$)
       
   919 {
       
   920     my ($fileHandle, $line) = @_;
       
   921 
       
   922     $_ = $line;
       
   923 
       
   924     my %footer;
       
   925     if (/$svnPropertiesStartRegEx/) {
       
   926         $footer{propertyPath} = $1;
       
   927     } else {
       
   928         die("Failed to find start of SVN property change, \"Property changes on \": \"$_\"");
       
   929     }
       
   930 
       
   931     # We advance $fileHandle two lines so that the next line that
       
   932     # we process is $svnPropertyStartRegEx in a well-formed footer.
       
   933     # A well-formed footer has the form:
       
   934     # Property changes on: FileA
       
   935     # ___________________________________________________________________
       
   936     # Added: svn:executable
       
   937     #    + *
       
   938     $_ = <$fileHandle>; # Not defined if end-of-file reached.
       
   939     my $separator = "_" x 67;
       
   940     if (defined($_) && /^$separator[\r\n]+$/) {
       
   941         $_ = <$fileHandle>;
       
   942     } else {
       
   943         die("Failed to find separator line: \"$_\".");
       
   944     }
       
   945 
       
   946     # FIXME: We should expand this to support other SVN properties
       
   947     #        (e.g. return a hash of property key-values that represents
       
   948     #        all properties).
       
   949     #
       
   950     # Notice, we keep processing until we hit end-of-file or some
       
   951     # line that does not resemble $svnPropertyStartRegEx, such as
       
   952     # the empty line that precedes the start of the binary contents
       
   953     # of a patch, or the start of the next diff (e.g. "Index:").
       
   954     my $propertyHashRef;
       
   955     while (defined($_) && /$svnPropertyStartRegEx/) {
       
   956         ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_);
       
   957         if ($propertyHashRef->{name} eq "svn:executable") {
       
   958             # Notice, for SVN properties, propertyChangeDelta is always non-zero
       
   959             # because a property can only be added or removed.
       
   960             $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta};   
       
   961         }
       
   962     }
       
   963 
       
   964     return(\%footer, $_);
       
   965 }
       
   966 
       
   967 # Parse the next SVN property from the given file handle, and advance the handle so the last
       
   968 # line read is the first line after the property.
       
   969 #
       
   970 # This subroutine dies if the first line is not a valid start of an SVN property,
       
   971 # or the property is missing a value, or the property change type (e.g. "Added")
       
   972 # does not correspond to the property value type (e.g. "+").
       
   973 #
       
   974 # Args:
       
   975 #   $fileHandle: advanced so the last line read from the handle is the first
       
   976 #                line of the property to parse.  This should be a line
       
   977 #                that matches $svnPropertyStartRegEx.
       
   978 #   $line: the line last read from $fileHandle.
       
   979 #
       
   980 # Returns ($propertyHashRef, $lastReadLine):
       
   981 #   $propertyHashRef: a hash reference representing a SVN property.
       
   982 #     name: the name of the property.
       
   983 #     value: the last property value.  For instance, suppose the property is "Modified".
       
   984 #            Then it has both a '-' and '+' property value in that order.  Therefore,
       
   985 #            the value of this key is the value of the '+' property by ordering (since
       
   986 #            it is the last value).
       
   987 #     propertyChangeDelta: the value 1 or -1 if the property was added or
       
   988 #                          removed, respectively.
       
   989 #   $lastReadLine: the line last read from $fileHandle.
       
   990 sub parseSvnProperty($$)
       
   991 {
       
   992     my ($fileHandle, $line) = @_;
       
   993 
       
   994     $_ = $line;
       
   995 
       
   996     my $propertyName;
       
   997     my $propertyChangeType;
       
   998     if (/$svnPropertyStartRegEx/) {
       
   999         $propertyChangeType = $1;
       
  1000         $propertyName = $2;
       
  1001     } else {
       
  1002         die("Failed to find SVN property: \"$_\".");
       
  1003     }
       
  1004 
       
  1005     $_ = <$fileHandle>; # Not defined if end-of-file reached.
       
  1006 
       
  1007     # The "svn diff" command neither inserts newline characters between property values
       
  1008     # nor between successive properties.
       
  1009     #
       
  1010     # FIXME: We do not support property values that contain tailing newline characters
       
  1011     #        as it is difficult to disambiguate these trailing newlines from the empty
       
  1012     #        line that precedes the contents of a binary patch.
       
  1013     my $propertyValue;
       
  1014     my $propertyValueType;
       
  1015     while (defined($_) && /$svnPropertyValueStartRegEx/) {
       
  1016         # Note, a '-' property may be followed by a '+' property in the case of a "Modified"
       
  1017         # or "Name" property.  We only care about the ending value (i.e. the '+' property)
       
  1018         # in such circumstances.  So, we take the property value for the property to be its
       
  1019         # last parsed property value.
       
  1020         #
       
  1021         # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or
       
  1022         #        add error checking to prevent '+', '+', ..., '+' and other invalid combinations.
       
  1023         $propertyValueType = $1;
       
  1024         ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_);
       
  1025     }
       
  1026 
       
  1027     if (!$propertyValue) {
       
  1028         die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\".");
       
  1029     }
       
  1030 
       
  1031     my $propertyChangeDelta;
       
  1032     if ($propertyValueType eq '+') {
       
  1033         $propertyChangeDelta = 1;
       
  1034     } elsif ($propertyValueType eq '-') {
       
  1035         $propertyChangeDelta = -1;
       
  1036     } else {
       
  1037         die("Not reached.");
       
  1038     }
       
  1039 
       
  1040     # We perform a simple validation that an "Added" or "Deleted" property
       
  1041     # change type corresponds with a "+" and "-" value type, respectively.
       
  1042     my $expectedChangeDelta;
       
  1043     if ($propertyChangeType eq "Added") {
       
  1044         $expectedChangeDelta = 1;
       
  1045     } elsif ($propertyChangeType eq "Deleted") {
       
  1046         $expectedChangeDelta = -1;
       
  1047     }
       
  1048 
       
  1049     if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) {
       
  1050         die("The final property value type found \"$propertyValueType\" does not " .
       
  1051             "correspond to the property change type found \"$propertyChangeType\".");
       
  1052     }
       
  1053 
       
  1054     my %propertyHash;
       
  1055     $propertyHash{name} = $propertyName;
       
  1056     $propertyHash{propertyChangeDelta} = $propertyChangeDelta;
       
  1057     $propertyHash{value} = $propertyValue;
       
  1058     return (\%propertyHash, $_);
       
  1059 }
       
  1060 
       
  1061 # Parse the value of an SVN property from the given file handle, and advance
       
  1062 # the handle so the last line read is the first line after the property value.
       
  1063 #
       
  1064 # This subroutine dies if the first line is an invalid SVN property value line
       
  1065 # (i.e. a line that does not begin with "   +" or "   -").
       
  1066 #
       
  1067 # Args:
       
  1068 #   $fileHandle: advanced so the last line read from the handle is the first
       
  1069 #                line of the property value to parse.  This should be a line
       
  1070 #                beginning with "   +" or "   -".
       
  1071 #   $line: the line last read from $fileHandle.
       
  1072 #
       
  1073 # Returns ($propertyValue, $lastReadLine):
       
  1074 #   $propertyValue: the value of the property.
       
  1075 #   $lastReadLine: the line last read from $fileHandle.
       
  1076 sub parseSvnPropertyValue($$)
       
  1077 {
       
  1078     my ($fileHandle, $line) = @_;
       
  1079 
       
  1080     $_ = $line;
       
  1081 
       
  1082     my $propertyValue;
       
  1083     my $eol;
       
  1084     if (/$svnPropertyValueStartRegEx/) {
       
  1085         $propertyValue = $2; # Does not include the end-of-line character(s).
       
  1086         $eol = $POSTMATCH;
       
  1087     } else {
       
  1088         die("Failed to find property value beginning with '+' or '-': \"$_\".");
       
  1089     }
       
  1090 
       
  1091     while (<$fileHandle>) {
       
  1092         if (/^$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/) {
       
  1093             # Note, we may encounter an empty line before the contents of a binary patch.
       
  1094             # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be
       
  1095             # followed by a '+' property in the case of a "Modified" or "Name" property.
       
  1096             # We check for $svnPropertyStartRegEx because it indicates the start of the
       
  1097             # next property to parse.
       
  1098             last;
       
  1099         }
       
  1100 
       
  1101         # Temporarily strip off any end-of-line characters. We add the end-of-line characters
       
  1102         # from the previously processed line to the start of this line so that the last line
       
  1103         # of the property value does not end in end-of-line characters.
       
  1104         s/([\n\r]+)$//;
       
  1105         $propertyValue .= "$eol$_";
       
  1106         $eol = $1;
       
  1107     }
       
  1108 
       
  1109     return ($propertyValue, $_);
       
  1110 }
       
  1111 
       
  1112 # Parse a patch file created by svn-create-patch.
       
  1113 #
       
  1114 # Args:
       
  1115 #   $fileHandle: A file handle to the patch file that has not yet been
       
  1116 #                read from.
       
  1117 #
       
  1118 # Returns:
       
  1119 #   @diffHashRefs: an array of diff hash references.
       
  1120 #                  See the %diffHash documentation above.
       
  1121 sub parsePatch($)
       
  1122 {
       
  1123     my ($fileHandle) = @_;
       
  1124 
       
  1125     my $newDiffHashRefs;
       
  1126     my @diffHashRefs; # return value
       
  1127 
       
  1128     my $line = <$fileHandle>;
       
  1129 
       
  1130     while (defined($line)) { # Otherwise, at EOF.
       
  1131 
       
  1132         ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line);
       
  1133 
       
  1134         push @diffHashRefs, @$newDiffHashRefs;
       
  1135     }
       
  1136 
       
  1137     return @diffHashRefs;
       
  1138 }
       
  1139 
       
  1140 # Prepare the results of parsePatch() for use in svn-apply and svn-unapply.
       
  1141 #
       
  1142 # Args:
       
  1143 #   $shouldForce: Whether to continue processing if an unexpected
       
  1144 #                 state occurs.
       
  1145 #   @diffHashRefs: An array of references to %diffHashes.
       
  1146 #                  See the %diffHash documentation above.
       
  1147 #
       
  1148 # Returns $preparedPatchHashRef:
       
  1149 #   copyDiffHashRefs: A reference to an array of the $diffHashRefs in
       
  1150 #                     @diffHashRefs that represent file copies. The original
       
  1151 #                     ordering is preserved.
       
  1152 #   nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in
       
  1153 #                        @diffHashRefs that do not represent file copies.
       
  1154 #                        The original ordering is preserved.
       
  1155 #   sourceRevisionHash: A reference to a hash of source path to source
       
  1156 #                       revision number.
       
  1157 sub prepareParsedPatch($@)
       
  1158 {
       
  1159     my ($shouldForce, @diffHashRefs) = @_;
       
  1160 
       
  1161     my %copiedFiles;
       
  1162 
       
  1163     # Return values
       
  1164     my @copyDiffHashRefs = ();
       
  1165     my @nonCopyDiffHashRefs = ();
       
  1166     my %sourceRevisionHash = ();
       
  1167     for my $diffHashRef (@diffHashRefs) {
       
  1168         my $copiedFromPath = $diffHashRef->{copiedFromPath};
       
  1169         my $indexPath = $diffHashRef->{indexPath};
       
  1170         my $sourceRevision = $diffHashRef->{sourceRevision};
       
  1171         my $sourcePath;
       
  1172 
       
  1173         if (defined($copiedFromPath)) {
       
  1174             # Then the diff is a copy operation.
       
  1175             $sourcePath = $copiedFromPath;
       
  1176 
       
  1177             # FIXME: Consider printing a warning or exiting if
       
  1178             #        exists($copiedFiles{$indexPath}) is true -- i.e. if
       
  1179             #        $indexPath appears twice as a copy target.
       
  1180             $copiedFiles{$indexPath} = $sourcePath;
       
  1181 
       
  1182             push @copyDiffHashRefs, $diffHashRef;
       
  1183         } else {
       
  1184             # Then the diff is not a copy operation.
       
  1185             $sourcePath = $indexPath;
       
  1186 
       
  1187             push @nonCopyDiffHashRefs, $diffHashRef;
       
  1188         }
       
  1189 
       
  1190         if (defined($sourceRevision)) {
       
  1191             if (exists($sourceRevisionHash{$sourcePath}) &&
       
  1192                 ($sourceRevisionHash{$sourcePath} != $sourceRevision)) {
       
  1193                 if (!$shouldForce) {
       
  1194                     die "Two revisions of the same file required as a source:\n".
       
  1195                         "    $sourcePath:$sourceRevisionHash{$sourcePath}\n".
       
  1196                         "    $sourcePath:$sourceRevision";
       
  1197                 }
       
  1198             }
       
  1199             $sourceRevisionHash{$sourcePath} = $sourceRevision;
       
  1200         }
       
  1201     }
       
  1202 
       
  1203     my %preparedPatchHash;
       
  1204 
       
  1205     $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs;
       
  1206     $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs;
       
  1207     $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash;
       
  1208 
       
  1209     return \%preparedPatchHash;
       
  1210 }
       
  1211 
       
  1212 # Return localtime() for the project's time zone, given an integer time as
       
  1213 # returned by Perl's time() function.
       
  1214 sub localTimeInProjectTimeZone($)
       
  1215 {
       
  1216     my $epochTime = shift;
       
  1217 
       
  1218     # Change the time zone temporarily for the localtime() call.
       
  1219     my $savedTimeZone = $ENV{'TZ'};
       
  1220     $ENV{'TZ'} = $changeLogTimeZone;
       
  1221     my @localTime = localtime($epochTime);
       
  1222     if (defined $savedTimeZone) {
       
  1223          $ENV{'TZ'} = $savedTimeZone;
       
  1224     } else {
       
  1225          delete $ENV{'TZ'};
       
  1226     }
       
  1227 
       
  1228     return @localTime;
       
  1229 }
       
  1230 
       
  1231 # Set the reviewer and date in a ChangeLog patch, and return the new patch.
       
  1232 #
       
  1233 # Args:
       
  1234 #   $patch: a ChangeLog patch as a string.
       
  1235 #   $reviewer: the name of the reviewer, or undef if the reviewer should not be set.
       
  1236 #   $epochTime: an integer time as returned by Perl's time() function.
       
  1237 sub setChangeLogDateAndReviewer($$$)
       
  1238 {
       
  1239     my ($patch, $reviewer, $epochTime) = @_;
       
  1240 
       
  1241     my @localTime = localTimeInProjectTimeZone($epochTime);
       
  1242     my $newDate = strftime("%Y-%m-%d", @localTime);
       
  1243 
       
  1244     my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}(  )#;
       
  1245     $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/;
       
  1246 
       
  1247     if (defined($reviewer)) {
       
  1248         # We include a leading plus ("+") in the regular expression to make
       
  1249         # the regular expression less likely to match text in the leading junk
       
  1250         # for the patch, if the patch has leading junk.
       
  1251         $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/;
       
  1252     }
       
  1253 
       
  1254     return $patch;
       
  1255 }
       
  1256 
       
  1257 # If possible, returns a ChangeLog patch equivalent to the given one,
       
  1258 # but with the newest ChangeLog entry inserted at the top of the
       
  1259 # file -- i.e. no leading context and all lines starting with "+".
       
  1260 #
       
  1261 # If given a patch string not representable as a patch with the above
       
  1262 # properties, it returns the input back unchanged.
       
  1263 #
       
  1264 # WARNING: This subroutine can return an inequivalent patch string if
       
  1265 # both the beginning of the new ChangeLog file matches the beginning
       
  1266 # of the source ChangeLog, and the source beginning was modified.
       
  1267 # Otherwise, it is guaranteed to return an equivalent patch string,
       
  1268 # if it returns.
       
  1269 #
       
  1270 # Applying this subroutine to ChangeLog patches allows svn-apply to
       
  1271 # insert new ChangeLog entries at the top of the ChangeLog file.
       
  1272 # svn-apply uses patch with --fuzz=3 to do this. We need to apply
       
  1273 # this subroutine because the diff(1) command is greedy when matching
       
  1274 # lines. A new ChangeLog entry with the same date and author as the
       
  1275 # previous will match and cause the diff to have lines of starting
       
  1276 # context.
       
  1277 #
       
  1278 # This subroutine has unit tests in VCSUtils_unittest.pl.
       
  1279 sub fixChangeLogPatch($)
       
  1280 {
       
  1281     my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
       
  1282 
       
  1283     $patch =~ /(\r?\n)/;
       
  1284     my $lineEnding = $1;
       
  1285     my @lines = split(/$lineEnding/, $patch);
       
  1286 
       
  1287     my $i = 0; # We reuse the same index throughout.
       
  1288 
       
  1289     # Skip to beginning of first chunk.
       
  1290     for (; $i < @lines; ++$i) {
       
  1291         if (substr($lines[$i], 0, 1) eq "@") {
       
  1292             last;
       
  1293         }
       
  1294     }
       
  1295     my $chunkStartIndex = ++$i;
       
  1296 
       
  1297     # Optimization: do not process if new lines already begin the chunk.
       
  1298     if (substr($lines[$i], 0, 1) eq "+") {
       
  1299         return $patch;
       
  1300     }
       
  1301 
       
  1302     # Skip to first line of newly added ChangeLog entry.
       
  1303     # For example, +2009-06-03  Eric Seidel  <eric@webkit.org>
       
  1304     my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
       
  1305                          . '\s+(.+)\s+' # name
       
  1306                          . '<([^<>]+)>$'; # e-mail address
       
  1307 
       
  1308     for (; $i < @lines; ++$i) {
       
  1309         my $line = $lines[$i];
       
  1310         my $firstChar = substr($line, 0, 1);
       
  1311         if ($line =~ /$dateStartRegEx/) {
       
  1312             last;
       
  1313         } elsif ($firstChar eq " " or $firstChar eq "+") {
       
  1314             next;
       
  1315         }
       
  1316         return $patch; # Do not change if, for example, "-" or "@" found.
       
  1317     }
       
  1318     if ($i >= @lines) {
       
  1319         return $patch; # Do not change if date not found.
       
  1320     }
       
  1321     my $dateStartIndex = $i;
       
  1322 
       
  1323     # Rewrite overlapping lines to lead with " ".
       
  1324     my @overlappingLines = (); # These will include a leading "+".
       
  1325     for (; $i < @lines; ++$i) {
       
  1326         my $line = $lines[$i];
       
  1327         if (substr($line, 0, 1) ne "+") {
       
  1328           last;
       
  1329         }
       
  1330         push(@overlappingLines, $line);
       
  1331         $lines[$i] = " " . substr($line, 1);
       
  1332     }
       
  1333 
       
  1334     # Remove excess ending context, if necessary.
       
  1335     my $shouldTrimContext = 1;
       
  1336     for (; $i < @lines; ++$i) {
       
  1337         my $firstChar = substr($lines[$i], 0, 1);
       
  1338         if ($firstChar eq " ") {
       
  1339             next;
       
  1340         } elsif ($firstChar eq "@") {
       
  1341             last;
       
  1342         }
       
  1343         $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
       
  1344         last;
       
  1345     }
       
  1346     my $deletedLineCount = 0;
       
  1347     if ($shouldTrimContext) { # Also occurs if end of file reached.
       
  1348         splice(@lines, $i - @overlappingLines, @overlappingLines);
       
  1349         $deletedLineCount = @overlappingLines;
       
  1350     }
       
  1351 
       
  1352     # Work backwards, shifting overlapping lines towards front
       
  1353     # while checking that patch stays equivalent.
       
  1354     for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) {
       
  1355         my $line = $lines[$i];
       
  1356         if (substr($line, 0, 1) ne " ") {
       
  1357             next;
       
  1358         }
       
  1359         my $text = substr($line, 1);
       
  1360         my $newLine = pop(@overlappingLines);
       
  1361         if ($text ne substr($newLine, 1)) {
       
  1362             return $patch; # Unexpected difference.
       
  1363         }
       
  1364         $lines[$i] = "+$text";
       
  1365     }
       
  1366 
       
  1367     # Finish moving whatever overlapping lines remain, and update
       
  1368     # the initial chunk range.
       
  1369     my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@
       
  1370     if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) {
       
  1371         # FIXME: Handle errors differently from ChangeLog files that
       
  1372         # are okay but should not be altered. That way we can find out
       
  1373         # if improvements to the script ever become necessary.
       
  1374         return $patch; # Error: unexpected patch string format.
       
  1375     }
       
  1376     my $skippedFirstLineCount = $1 - 1;
       
  1377     my $oldSourceLineCount = $2;
       
  1378     my $oldTargetLineCount = $3;
       
  1379 
       
  1380     if (@overlappingLines != $skippedFirstLineCount) {
       
  1381         # This can happen, for example, when deliberately inserting
       
  1382         # a new ChangeLog entry earlier in the file.
       
  1383         return $patch;
       
  1384     }
       
  1385     # If @overlappingLines > 0, this is where we make use of the
       
  1386     # assumption that the beginning of the source file was not modified.
       
  1387     splice(@lines, $chunkStartIndex, 0, @overlappingLines);
       
  1388 
       
  1389     my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
       
  1390     my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
       
  1391     $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
       
  1392 
       
  1393     return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
       
  1394 }
       
  1395 
       
  1396 # This is a supporting method for runPatchCommand.
       
  1397 #
       
  1398 # Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
       
  1399 #
       
  1400 # Returns ($patchCommand, $isForcing).
       
  1401 #
       
  1402 # This subroutine has unit tests in VCSUtils_unittest.pl.
       
  1403 sub generatePatchCommand($)
       
  1404 {
       
  1405     my ($passedArgsHashRef) = @_;
       
  1406 
       
  1407     my $argsHashRef = { # Defaults
       
  1408         ensureForce => 0,
       
  1409         shouldReverse => 0,
       
  1410         options => []
       
  1411     };
       
  1412     
       
  1413     # Merges hash references. It's okay here if passed hash reference is undefined.
       
  1414     @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
       
  1415     
       
  1416     my $ensureForce = $argsHashRef->{ensureForce};
       
  1417     my $shouldReverse = $argsHashRef->{shouldReverse};
       
  1418     my $options = $argsHashRef->{options};
       
  1419 
       
  1420     if (! $options) {
       
  1421         $options = [];
       
  1422     } else {
       
  1423         $options = [@{$options}]; # Copy to avoid side effects.
       
  1424     }
       
  1425 
       
  1426     my $isForcing = 0;
       
  1427     if (grep /^--force$/, @{$options}) {
       
  1428         $isForcing = 1;
       
  1429     } elsif ($ensureForce) {
       
  1430         push @{$options}, "--force";
       
  1431         $isForcing = 1;
       
  1432     }
       
  1433 
       
  1434     if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
       
  1435         push @{$options}, "--reverse";
       
  1436     }
       
  1437 
       
  1438     @{$options} = sort(@{$options}); # For easier testing.
       
  1439 
       
  1440     my $patchCommand = join(" ", "patch -p0", @{$options});
       
  1441 
       
  1442     return ($patchCommand, $isForcing);
       
  1443 }
       
  1444 
       
  1445 # Apply the given patch using the patch(1) command.
       
  1446 #
       
  1447 # On success, return the resulting exit status. Otherwise, exit with the
       
  1448 # exit status. If "--force" is passed as an option, however, then never
       
  1449 # exit and always return the exit status.
       
  1450 #
       
  1451 # Args:
       
  1452 #   $patch: a patch string.
       
  1453 #   $repositoryRootPath: an absolute path to the repository root.
       
  1454 #   $pathRelativeToRoot: the path of the file to be patched, relative to the
       
  1455 #                        repository root. This should normally be the path
       
  1456 #                        found in the patch's "Index:" line. It is passed
       
  1457 #                        explicitly rather than reparsed from the patch
       
  1458 #                        string for optimization purposes.
       
  1459 #                            This is used only for error reporting. The
       
  1460 #                        patch command gleans the actual file to patch
       
  1461 #                        from the patch string.
       
  1462 #   $args: a reference to a hash of optional arguments. The possible
       
  1463 #          keys are --
       
  1464 #            ensureForce: whether to ensure --force is passed (defaults to 0).
       
  1465 #            shouldReverse: whether to pass --reverse (defaults to 0).
       
  1466 #            options: a reference to an array of options to pass to the
       
  1467 #                     patch command. The subroutine passes the -p0 option
       
  1468 #                     no matter what. This should not include --reverse.
       
  1469 #
       
  1470 # This subroutine has unit tests in VCSUtils_unittest.pl.
       
  1471 sub runPatchCommand($$$;$)
       
  1472 {
       
  1473     my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
       
  1474 
       
  1475     my ($patchCommand, $isForcing) = generatePatchCommand($args);
       
  1476 
       
  1477     # Temporarily change the working directory since the path found
       
  1478     # in the patch's "Index:" line is relative to the repository root
       
  1479     # (i.e. the same as $pathRelativeToRoot).
       
  1480     my $cwd = Cwd::getcwd();
       
  1481     chdir $repositoryRootPath;
       
  1482 
       
  1483     open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
       
  1484     print PATCH $patch;
       
  1485     close PATCH;
       
  1486     my $exitStatus = exitStatus($?);
       
  1487 
       
  1488     chdir $cwd;
       
  1489 
       
  1490     if ($exitStatus && !$isForcing) {
       
  1491         print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
       
  1492               "status $exitStatus.  Pass --force to ignore patch failures.\n";
       
  1493         exit $exitStatus;
       
  1494     }
       
  1495 
       
  1496     return $exitStatus;
       
  1497 }
       
  1498 
       
  1499 # Merge ChangeLog patches using a three-file approach.
       
  1500 #
       
  1501 # This is used by resolve-ChangeLogs when it's operated as a merge driver
       
  1502 # and when it's used to merge conflicts after a patch is applied or after
       
  1503 # an svn update.
       
  1504 #
       
  1505 # It's also used for traditional rejected patches.
       
  1506 #
       
  1507 # Args:
       
  1508 #   $fileMine:  The merged version of the file.  Also known in git as the
       
  1509 #               other branch's version (%B) or "ours".
       
  1510 #               For traditional patch rejects, this is the *.rej file.
       
  1511 #   $fileOlder: The base version of the file.  Also known in git as the
       
  1512 #               ancestor version (%O) or "base".
       
  1513 #               For traditional patch rejects, this is the *.orig file.
       
  1514 #   $fileNewer: The current version of the file.  Also known in git as the
       
  1515 #               current version (%A) or "theirs".
       
  1516 #               For traditional patch rejects, this is the original-named
       
  1517 #               file.
       
  1518 #
       
  1519 # Returns 1 if merge was successful, else 0.
       
  1520 sub mergeChangeLogs($$$)
       
  1521 {
       
  1522     my ($fileMine, $fileOlder, $fileNewer) = @_;
       
  1523 
       
  1524     my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0;
       
  1525 
       
  1526     local $/ = undef;
       
  1527 
       
  1528     my $patch;
       
  1529     if ($traditionalReject) {
       
  1530         open(DIFF, "<", $fileMine) or die $!;
       
  1531         $patch = <DIFF>;
       
  1532         close(DIFF);
       
  1533         rename($fileMine, "$fileMine.save");
       
  1534         rename($fileOlder, "$fileOlder.save");
       
  1535     } else {
       
  1536         open(DIFF, "-|", qw(diff -u -a --binary), $fileOlder, $fileMine) or die $!;
       
  1537         $patch = <DIFF>;
       
  1538         close(DIFF);
       
  1539     }
       
  1540 
       
  1541     unlink("${fileNewer}.orig");
       
  1542     unlink("${fileNewer}.rej");
       
  1543 
       
  1544     open(PATCH, "| patch --force --fuzz=3 --binary $fileNewer > " . File::Spec->devnull()) or die $!;
       
  1545     print PATCH ($traditionalReject ? $patch : fixChangeLogPatch($patch));
       
  1546     close(PATCH);
       
  1547 
       
  1548     my $result = !exitStatus($?);
       
  1549 
       
  1550     # Refuse to merge the patch if it did not apply cleanly
       
  1551     if (-e "${fileNewer}.rej") {
       
  1552         unlink("${fileNewer}.rej");
       
  1553         if (-f "${fileNewer}.orig") {
       
  1554             unlink($fileNewer);
       
  1555             rename("${fileNewer}.orig", $fileNewer);
       
  1556         }
       
  1557     } else {
       
  1558         unlink("${fileNewer}.orig");
       
  1559     }
       
  1560 
       
  1561     if ($traditionalReject) {
       
  1562         rename("$fileMine.save", $fileMine);
       
  1563         rename("$fileOlder.save", $fileOlder);
       
  1564     }
       
  1565 
       
  1566     return $result;
       
  1567 }
       
  1568 
       
  1569 sub gitConfig($)
       
  1570 {
       
  1571     return unless $isGit;
       
  1572 
       
  1573     my ($config) = @_;
       
  1574 
       
  1575     my $result = `git config $config`;
       
  1576     if (($? >> 8)) {
       
  1577         $result = `git repo-config $config`;
       
  1578     }
       
  1579     chomp $result;
       
  1580     return $result;
       
  1581 }
       
  1582 
       
  1583 sub changeLogNameError($)
       
  1584 {
       
  1585     my ($message) = @_;
       
  1586     print STDERR "$message\nEither:\n";
       
  1587     print STDERR "  set CHANGE_LOG_NAME in your environment\n";
       
  1588     print STDERR "  OR pass --name= on the command line\n";
       
  1589     print STDERR "  OR set REAL_NAME in your environment";
       
  1590     print STDERR "  OR git users can set 'git config user.name'\n";
       
  1591     exit(1);
       
  1592 }
       
  1593 
       
  1594 sub changeLogName()
       
  1595 {
       
  1596     my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
       
  1597 
       
  1598     changeLogNameError("Failed to determine ChangeLog name.") unless $name;
       
  1599     # getpwuid seems to always succeed on windows, returning the username instead of the full name.  This check will catch that case.
       
  1600     changeLogNameError("'$name' does not contain a space!  ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
       
  1601 
       
  1602     return $name;
       
  1603 }
       
  1604 
       
  1605 sub changeLogEmailAddressError($)
       
  1606 {
       
  1607     my ($message) = @_;
       
  1608     print STDERR "$message\nEither:\n";
       
  1609     print STDERR "  set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
       
  1610     print STDERR "  OR pass --email= on the command line\n";
       
  1611     print STDERR "  OR set EMAIL_ADDRESS in your environment\n";
       
  1612     print STDERR "  OR git users can set 'git config user.email'\n";
       
  1613     exit(1);
       
  1614 }
       
  1615 
       
  1616 sub changeLogEmailAddress()
       
  1617 {
       
  1618     my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
       
  1619 
       
  1620     changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
       
  1621     changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
       
  1622 
       
  1623     return $emailAddress;
       
  1624 }
       
  1625 
       
  1626 # http://tools.ietf.org/html/rfc1924
       
  1627 sub decodeBase85($)
       
  1628 {
       
  1629     my ($encoded) = @_;
       
  1630     my %table;
       
  1631     my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
       
  1632     for (my $i = 0; $i < 85; $i++) {
       
  1633         $table{$characters[$i]} = $i;
       
  1634     }
       
  1635 
       
  1636     my $decoded = '';
       
  1637     my @encodedChars = $encoded =~ /./g;
       
  1638 
       
  1639     for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
       
  1640         my $digit = 0;
       
  1641         for (my $i = 0; $i < 5; $i++) {
       
  1642             $digit *= 85;
       
  1643             my $char = $encodedChars[$encodedIter];
       
  1644             $digit += $table{$char};
       
  1645             $encodedIter++;
       
  1646         }
       
  1647 
       
  1648         for (my $i = 0; $i < 4; $i++) {
       
  1649             $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
       
  1650         }
       
  1651     }
       
  1652 
       
  1653     return $decoded;
       
  1654 }
       
  1655 
       
  1656 sub decodeGitBinaryChunk($$)
       
  1657 {
       
  1658     my ($contents, $fullPath) = @_;
       
  1659 
       
  1660     # Load this module lazily in case the user don't have this module
       
  1661     # and won't handle git binary patches.
       
  1662     require Compress::Zlib;
       
  1663 
       
  1664     my $encoded = "";
       
  1665     my $compressedSize = 0;
       
  1666     while ($contents =~ /^([A-Za-z])(.*)$/gm) {
       
  1667         my $line = $2;
       
  1668         next if $line eq "";
       
  1669         die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
       
  1670         my $actualSize = length($2) / 5 * 4;
       
  1671         my $encodedExpectedSize = ord($1);
       
  1672         my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
       
  1673 
       
  1674         die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
       
  1675         $compressedSize += $expectedSize;
       
  1676         $encoded .= $line;
       
  1677     }
       
  1678 
       
  1679     my $compressed = decodeBase85($encoded);
       
  1680     $compressed = substr($compressed, 0, $compressedSize);
       
  1681     return Compress::Zlib::uncompress($compressed);
       
  1682 }
       
  1683 
       
  1684 sub decodeGitBinaryPatch($$)
       
  1685 {
       
  1686     my ($contents, $fullPath) = @_;
       
  1687 
       
  1688     # Git binary patch has two chunks. One is for the normal patching
       
  1689     # and another is for the reverse patching.
       
  1690     #
       
  1691     # Each chunk a line which starts from either "literal" or "delta",
       
  1692     # followed by a number which specifies decoded size of the chunk.
       
  1693     # The "delta" type chunks aren't supported by this function yet.
       
  1694     #
       
  1695     # Then, content of the chunk comes. To decode the content, we
       
  1696     # need decode it with base85 first, and then zlib.
       
  1697     my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
       
  1698     if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
       
  1699         die "$fullPath: unknown git binary patch format"
       
  1700     }
       
  1701 
       
  1702     my $binaryChunkType = $1;
       
  1703     my $binaryChunkExpectedSize = $2;
       
  1704     my $encodedChunk = $3;
       
  1705     my $reverseBinaryChunkType = $4;
       
  1706     my $reverseBinaryChunkExpectedSize = $5;
       
  1707     my $encodedReverseChunk = $6;
       
  1708 
       
  1709     my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
       
  1710     my $binaryChunkActualSize = length($binaryChunk);
       
  1711     my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
       
  1712     my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
       
  1713 
       
  1714     die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize);
       
  1715     die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
       
  1716 
       
  1717     return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
       
  1718 }
       
  1719 
       
  1720 1;