Home | History | Annotate | Download | only in Scripts
      1 # Copyright (C) 2007, 2008, 2009 Apple Inc.  All rights reserved.
      2 # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek (at] 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         &canonicalizePath
     47         &changeLogEmailAddress
     48         &changeLogName
     49         &chdirReturningRelativePath
     50         &decodeGitBinaryPatch
     51         &determineSVNRoot
     52         &determineVCSRoot
     53         &exitStatus
     54         &fixChangeLogPatch
     55         &gitBranch
     56         &gitdiff2svndiff
     57         &isGit
     58         &isGitBranchBuild
     59         &isGitDirectory
     60         &isSVN
     61         &isSVNDirectory
     62         &isSVNVersion16OrNewer
     63         &makeFilePathRelative
     64         &normalizePath
     65         &parsePatch
     66         &pathRelativeToSVNRepositoryRootForPath
     67         &runPatchCommand
     68         &svnRevisionForDirectory
     69         &svnStatus
     70     );
     71     %EXPORT_TAGS = ( );
     72     @EXPORT_OK   = ();
     73 }
     74 
     75 our @EXPORT_OK;
     76 
     77 my $gitBranch;
     78 my $gitRoot;
     79 my $isGit;
     80 my $isGitBranchBuild;
     81 my $isSVN;
     82 my $svnVersion;
     83 
     84 # This method is for portability. Return the system-appropriate exit
     85 # status of a child process.
     86 #
     87 # Args: pass the child error status returned by the last pipe close,
     88 #       for example "$?".
     89 sub exitStatus($)
     90 {
     91     my ($returnvalue) = @_;
     92     if ($^O eq "MSWin32") {
     93         return $returnvalue >> 8;
     94     }
     95     return WEXITSTATUS($returnvalue);
     96 }
     97 
     98 sub isGitDirectory($)
     99 {
    100     my ($dir) = @_;
    101     return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
    102 }
    103 
    104 sub isGit()
    105 {
    106     return $isGit if defined $isGit;
    107 
    108     $isGit = isGitDirectory(".");
    109     return $isGit;
    110 }
    111 
    112 sub gitBranch()
    113 {
    114     unless (defined $gitBranch) {
    115         chomp($gitBranch = `git symbolic-ref -q HEAD`);
    116         $gitBranch = "" if exitStatus($?);
    117         $gitBranch =~ s#^refs/heads/##;
    118         $gitBranch = "" if $gitBranch eq "master";
    119     }
    120 
    121     return $gitBranch;
    122 }
    123 
    124 sub isGitBranchBuild()
    125 {
    126     my $branch = gitBranch();
    127     chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
    128     return 1 if $override eq "true";
    129     return 0 if $override eq "false";
    130 
    131     unless (defined $isGitBranchBuild) {
    132         chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
    133         $isGitBranchBuild = $gitBranchBuild eq "true";
    134     }
    135 
    136     return $isGitBranchBuild;
    137 }
    138 
    139 sub isSVNDirectory($)
    140 {
    141     my ($dir) = @_;
    142 
    143     return -d File::Spec->catdir($dir, ".svn");
    144 }
    145 
    146 sub isSVN()
    147 {
    148     return $isSVN if defined $isSVN;
    149 
    150     $isSVN = isSVNDirectory(".");
    151     return $isSVN;
    152 }
    153 
    154 sub svnVersion()
    155 {
    156     return $svnVersion if defined $svnVersion;
    157 
    158     if (!isSVN()) {
    159         $svnVersion = 0;
    160     } else {
    161         chomp($svnVersion = `svn --version --quiet`);
    162     }
    163     return $svnVersion;
    164 }
    165 
    166 sub isSVNVersion16OrNewer()
    167 {
    168     my $version = svnVersion();
    169     return eval "v$version" ge v1.6;
    170 }
    171 
    172 sub chdirReturningRelativePath($)
    173 {
    174     my ($directory) = @_;
    175     my $previousDirectory = Cwd::getcwd();
    176     chdir $directory;
    177     my $newDirectory = Cwd::getcwd();
    178     return "." if $newDirectory eq $previousDirectory;
    179     return File::Spec->abs2rel($previousDirectory, $newDirectory);
    180 }
    181 
    182 sub determineGitRoot()
    183 {
    184     chomp(my $gitDir = `git rev-parse --git-dir`);
    185     return dirname($gitDir);
    186 }
    187 
    188 sub determineSVNRoot()
    189 {
    190     my $last = '';
    191     my $path = '.';
    192     my $parent = '..';
    193     my $repositoryRoot;
    194     my $repositoryUUID;
    195     while (1) {
    196         my $thisRoot;
    197         my $thisUUID;
    198         # Ignore error messages in case we've run past the root of the checkout.
    199         open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die;
    200         while (<INFO>) {
    201             if (/^Repository Root: (.+)/) {
    202                 $thisRoot = $1;
    203             }
    204             if (/^Repository UUID: (.+)/) {
    205                 $thisUUID = $1;
    206             }
    207             if ($thisRoot && $thisUUID) {
    208                 local $/ = undef;
    209                 <INFO>; # Consume the rest of the input.
    210             }
    211         }
    212         close INFO;
    213 
    214         # It's possible (e.g. for developers of some ports) to have a WebKit
    215         # checkout in a subdirectory of another checkout.  So abort if the
    216         # repository root or the repository UUID suddenly changes.
    217         last if !$thisUUID;
    218         $repositoryUUID = $thisUUID if !$repositoryUUID;
    219         last if $thisUUID ne $repositoryUUID;
    220 
    221         last if !$thisRoot;
    222         $repositoryRoot = $thisRoot if !$repositoryRoot;
    223         last if $thisRoot ne $repositoryRoot;
    224 
    225         $last = $path;
    226         $path = File::Spec->catdir($parent, $path);
    227     }
    228 
    229     return File::Spec->rel2abs($last);
    230 }
    231 
    232 sub determineVCSRoot()
    233 {
    234     if (isGit()) {
    235         return determineGitRoot();
    236     }
    237 
    238     if (!isSVN()) {
    239         # Some users have a workflow where svn-create-patch, svn-apply and
    240         # svn-unapply are used outside of multiple svn working directores,
    241         # so warn the user and assume Subversion is being used in this case.
    242         warn "Unable to determine VCS root; assuming Subversion";
    243         $isSVN = 1;
    244     }
    245 
    246     return determineSVNRoot();
    247 }
    248 
    249 sub svnRevisionForDirectory($)
    250 {
    251     my ($dir) = @_;
    252     my $revision;
    253 
    254     if (isSVNDirectory($dir)) {
    255         my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`;
    256         ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
    257     } elsif (isGitDirectory($dir)) {
    258         my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`;
    259         ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
    260     }
    261     die "Unable to determine current SVN revision in $dir" unless (defined $revision);
    262     return $revision;
    263 }
    264 
    265 sub pathRelativeToSVNRepositoryRootForPath($)
    266 {
    267     my ($file) = @_;
    268     my $relativePath = File::Spec->abs2rel($file);
    269 
    270     my $svnInfo;
    271     if (isSVN()) {
    272         $svnInfo = `LC_ALL=C svn info $relativePath`;
    273     } elsif (isGit()) {
    274         $svnInfo = `LC_ALL=C git svn info $relativePath`;
    275     }
    276 
    277     $svnInfo =~ /.*^URL: (.*?)$/m;
    278     my $svnURL = $1;
    279 
    280     $svnInfo =~ /.*^Repository Root: (.*?)$/m;
    281     my $repositoryRoot = $1;
    282 
    283     $svnURL =~ s/$repositoryRoot\///;
    284     return $svnURL;
    285 }
    286 
    287 sub makeFilePathRelative($)
    288 {
    289     my ($path) = @_;
    290     return $path unless isGit();
    291 
    292     unless (defined $gitRoot) {
    293         chomp($gitRoot = `git rev-parse --show-cdup`);
    294     }
    295     return $gitRoot . $path;
    296 }
    297 
    298 sub normalizePath($)
    299 {
    300     my ($path) = @_;
    301     $path =~ s/\\/\//g;
    302     return $path;
    303 }
    304 
    305 sub canonicalizePath($)
    306 {
    307     my ($file) = @_;
    308 
    309     # Remove extra slashes and '.' directories in path
    310     $file = File::Spec->canonpath($file);
    311 
    312     # Remove '..' directories in path
    313     my @dirs = ();
    314     foreach my $dir (File::Spec->splitdir($file)) {
    315         if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
    316             pop(@dirs);
    317         } else {
    318             push(@dirs, $dir);
    319         }
    320     }
    321     return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
    322 }
    323 
    324 sub removeEOL($)
    325 {
    326     my ($line) = @_;
    327 
    328     $line =~ s/[\r\n]+$//g;
    329     return $line;
    330 }
    331 
    332 sub svnStatus($)
    333 {
    334     my ($fullPath) = @_;
    335     my $svnStatus;
    336     open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
    337     if (-d $fullPath) {
    338         # When running "svn stat" on a directory, we can't assume that only one
    339         # status will be returned (since any files with a status below the
    340         # directory will be returned), and we can't assume that the directory will
    341         # be first (since any files with unknown status will be listed first).
    342         my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
    343         while (<SVN>) {
    344             # Input may use a different EOL sequence than $/, so avoid chomp.
    345             $_ = removeEOL($_);
    346             my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
    347             if ($normalizedFullPath eq $normalizedStatPath) {
    348                 $svnStatus = "$_\n";
    349                 last;
    350             }
    351         }
    352         # Read the rest of the svn command output to avoid a broken pipe warning.
    353         local $/ = undef;
    354         <SVN>;
    355     }
    356     else {
    357         # Files will have only one status returned.
    358         $svnStatus = removeEOL(<SVN>) . "\n";
    359     }
    360     close SVN;
    361     return $svnStatus;
    362 }
    363 
    364 # Convert a line of a git-formatted patch to SVN format, while
    365 # preserving any end-of-line characters.
    366 sub gitdiff2svndiff($)
    367 {
    368     $_ = shift @_;
    369 
    370     if (m#^diff --git \w/(.+) \w/([^\r\n]+)#) {
    371         return "Index: $1$POSTMATCH";
    372     }
    373     if (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) {
    374         # FIXME: No need to return dividing line once parseDiffHeader() is used.
    375         return "===================================================================$POSTMATCH";
    376     }
    377     if (m#^--- \w/([^\r\n]+)#) {
    378         return "--- $1$POSTMATCH";
    379     }
    380     if (m#^\+\+\+ \w/([^\r\n]+)#) {
    381         return "+++ $1$POSTMATCH";
    382     }
    383     return $_;
    384 }
    385 
    386 # Parse the next diff header from the given file handle, and advance
    387 # the file handle so the last line read is the first line after the
    388 # parsed header block.
    389 #
    390 # This subroutine dies if given leading junk or if the end of the header
    391 # block could not be detected. The last line of a header block is a
    392 # line beginning with "+++".
    393 #
    394 # Args:
    395 #   $fileHandle: advanced so the last line read is the first line of the
    396 #                next diff header. For SVN-formatted diffs, this is the
    397 #                "Index:" line.
    398 #   $line: the line last read from $fileHandle
    399 #
    400 # Returns ($headerHashRef, $lastReadLine):
    401 #   $headerHashRef: a hash reference representing a diff header
    402 #     copiedFromPath: if a file copy, the path from which the file was
    403 #                     copied. Otherwise, undefined.
    404 #     indexPath: the path in the "Index:" line.
    405 #     sourceRevision: the revision number of the source. This is the same
    406 #                     as the revision number the file was copied from, in
    407 #                     the case of a file copy.
    408 #     svnConvertedText: the header text converted to SVN format.
    409 #                       Unrecognized lines are discarded.
    410 #   $lastReadLine: the line last read from $fileHandle. This is the first
    411 #                  line after the header ending.
    412 sub parseDiffHeader($$)
    413 {
    414     my ($fileHandle, $line) = @_;
    415 
    416     my $filter;
    417     if ($line =~ m#^diff --git #) {
    418         $filter = \&gitdiff2svndiff;
    419     }
    420     $line = &$filter($line) if $filter;
    421 
    422     my $indexPath;
    423     if ($line =~ /^Index: ([^\r\n]+)/) {
    424         $indexPath = $1;
    425     } else {
    426         die("Could not parse first line of diff header: \"$line\".");
    427     }
    428 
    429     my %header;
    430 
    431     my $foundHeaderEnding;
    432     my $lastReadLine; 
    433     my $sourceRevision;
    434     my $svnConvertedText = $line;
    435     while (<$fileHandle>) {
    436         # Temporarily strip off any end-of-line characters to simplify
    437         # regex matching below.
    438         s/([\n\r]+)$//;
    439         my $eol = $1;
    440 
    441         $_ = &$filter($_) if $filter;
    442 
    443         # Fix paths on ""---" and "+++" lines to match the leading
    444         # index line.
    445         if (s/^--- \S+/--- $indexPath/) {
    446             # ---
    447             if (/^--- .+\(revision (\d+)\)/) {
    448                 $sourceRevision = $1 if ($1 != 0);
    449                 if (/\(from (\S+):(\d+)\)$/) {
    450                     # The "from" clause is created by svn-create-patch, in
    451                     # which case there is always also a "revision" clause.
    452                     $header{copiedFromPath} = $1;
    453                     die("Revision number \"$2\" in \"from\" clause does not match " .
    454                         "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
    455                 }
    456             }
    457             $_ = "=" x 67 . "$eol$_"; # Prepend dividing line ===....
    458         } elsif (s/^\+\+\+ \S+/+++ $indexPath/) {
    459             # +++
    460             $foundHeaderEnding = 1;
    461         } else {
    462             # Skip unrecognized lines.
    463             next;
    464         }
    465 
    466         $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
    467         if ($foundHeaderEnding) {
    468             $lastReadLine = <$fileHandle>;
    469             last;
    470         }
    471     } # $lastReadLine is undef if while loop ran out.
    472 
    473     if (!$foundHeaderEnding) {
    474         die("Did not find end of header block corresponding to index path \"$indexPath\".");
    475     }
    476 
    477     $header{indexPath} = $indexPath;
    478     $header{sourceRevision} = $sourceRevision;
    479     $header{svnConvertedText} = $svnConvertedText;
    480 
    481     return (\%header, $lastReadLine);
    482 }
    483 
    484 # Parse one diff from a patch file created by svn-create-patch, and
    485 # advance the file handle so the last line read is the first line
    486 # of the next header block.
    487 #
    488 # This subroutine preserves any leading junk encountered before the header.
    489 #
    490 # Args:
    491 #   $fileHandle: a file handle advanced to the first line of the next
    492 #                header block. Leading junk is okay.
    493 #   $line: the line last read from $fileHandle.
    494 #
    495 # Returns ($diffHashRef, $lastReadLine):
    496 #   $diffHashRef:
    497 #     copiedFromPath: if a file copy, the path from which the file was
    498 #                     copied. Otherwise, undefined.
    499 #     indexPath: the path in the "Index:" line.
    500 #     sourceRevision: the revision number of the source. This is the same
    501 #                     as the revision number the file was copied from, in
    502 #                     the case of a file copy.
    503 #     svnConvertedText: the diff converted to SVN format.
    504 #   $lastReadLine: the line last read from $fileHandle
    505 sub parseDiff($$)
    506 {
    507     my ($fileHandle, $line) = @_;
    508 
    509     my $headerStartRegEx = qr#^Index: #; # SVN-style header for the default
    510     my $gitHeaderStartRegEx = qr#^diff --git \w/#;
    511 
    512     my $headerHashRef; # Last header found, as returned by parseDiffHeader().
    513     my $svnText;
    514     while (defined($line)) {
    515         if (!$headerHashRef && ($line =~ $gitHeaderStartRegEx)) {
    516             # Then assume all diffs in the patch are Git-formatted. This
    517             # block was made to be enterable at most once since we assume
    518             # all diffs in the patch are formatted the same (SVN or Git).
    519             $headerStartRegEx = $gitHeaderStartRegEx;
    520         }
    521 
    522         if ($line !~ $headerStartRegEx) {
    523             # Then we are in the body of the diff.
    524             $svnText .= $line;
    525             $line = <$fileHandle>;
    526             next;
    527         } # Otherwise, we found a diff header.
    528 
    529         if ($headerHashRef) {
    530             # Then this is the second diff header of this while loop.
    531             last;
    532         }
    533 
    534         ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
    535 
    536         $svnText .= $headerHashRef->{svnConvertedText};
    537     }
    538 
    539     my %diffHashRef;
    540     $diffHashRef{copiedFromPath} = $headerHashRef->{copiedFromPath};
    541     $diffHashRef{indexPath} = $headerHashRef->{indexPath};
    542     $diffHashRef{sourceRevision} = $headerHashRef->{sourceRevision};
    543     $diffHashRef{svnConvertedText} = $svnText;
    544 
    545     return (\%diffHashRef, $line);
    546 }
    547 
    548 # Parse a patch file created by svn-create-patch.
    549 #
    550 # Args:
    551 #   $fileHandle: A file handle to the patch file that has not yet been
    552 #                read from.
    553 #
    554 # Returns:
    555 #   @diffHashRefs: an array of diff hash references. See parseDiff() for
    556 #                  a description of each $diffHashRef.
    557 sub parsePatch($)
    558 {
    559     my ($fileHandle) = @_;
    560 
    561     my @diffHashRefs; # return value
    562 
    563     my $line = <$fileHandle>;
    564 
    565     while (defined($line)) { # Otherwise, at EOF.
    566 
    567         my $diffHashRef;
    568         ($diffHashRef, $line) = parseDiff($fileHandle, $line);
    569 
    570         push @diffHashRefs, $diffHashRef;
    571     }
    572 
    573     return @diffHashRefs;
    574 }
    575 
    576 # If possible, returns a ChangeLog patch equivalent to the given one,
    577 # but with the newest ChangeLog entry inserted at the top of the
    578 # file -- i.e. no leading context and all lines starting with "+".
    579 #
    580 # If given a patch string not representable as a patch with the above
    581 # properties, it returns the input back unchanged.
    582 #
    583 # WARNING: This subroutine can return an inequivalent patch string if
    584 # both the beginning of the new ChangeLog file matches the beginning
    585 # of the source ChangeLog, and the source beginning was modified.
    586 # Otherwise, it is guaranteed to return an equivalent patch string,
    587 # if it returns.
    588 #
    589 # Applying this subroutine to ChangeLog patches allows svn-apply to
    590 # insert new ChangeLog entries at the top of the ChangeLog file.
    591 # svn-apply uses patch with --fuzz=3 to do this. We need to apply
    592 # this subroutine because the diff(1) command is greedy when matching
    593 # lines. A new ChangeLog entry with the same date and author as the
    594 # previous will match and cause the diff to have lines of starting
    595 # context.
    596 #
    597 # This subroutine has unit tests in VCSUtils_unittest.pl.
    598 sub fixChangeLogPatch($)
    599 {
    600     my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
    601 
    602     $patch =~ /(\r?\n)/;
    603     my $lineEnding = $1;
    604     my @lines = split(/$lineEnding/, $patch);
    605 
    606     my $i = 0; # We reuse the same index throughout.
    607 
    608     # Skip to beginning of first chunk.
    609     for (; $i < @lines; ++$i) {
    610         if (substr($lines[$i], 0, 1) eq "@") {
    611             last;
    612         }
    613     }
    614     my $chunkStartIndex = ++$i;
    615 
    616     # Optimization: do not process if new lines already begin the chunk.
    617     if (substr($lines[$i], 0, 1) eq "+") {
    618         return $patch;
    619     }
    620 
    621     # Skip to first line of newly added ChangeLog entry.
    622     # For example, +2009-06-03  Eric Seidel  <eric (at] webkit.org>
    623     my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
    624                          . '\s+(.+)\s+' # name
    625                          . '<([^<>]+)>$'; # e-mail address
    626 
    627     for (; $i < @lines; ++$i) {
    628         my $line = $lines[$i];
    629         my $firstChar = substr($line, 0, 1);
    630         if ($line =~ /$dateStartRegEx/) {
    631             last;
    632         } elsif ($firstChar eq " " or $firstChar eq "+") {
    633             next;
    634         }
    635         return $patch; # Do not change if, for example, "-" or "@" found.
    636     }
    637     if ($i >= @lines) {
    638         return $patch; # Do not change if date not found.
    639     }
    640     my $dateStartIndex = $i;
    641 
    642     # Rewrite overlapping lines to lead with " ".
    643     my @overlappingLines = (); # These will include a leading "+".
    644     for (; $i < @lines; ++$i) {
    645         my $line = $lines[$i];
    646         if (substr($line, 0, 1) ne "+") {
    647           last;
    648         }
    649         push(@overlappingLines, $line);
    650         $lines[$i] = " " . substr($line, 1);
    651     }
    652 
    653     # Remove excess ending context, if necessary.
    654     my $shouldTrimContext = 1;
    655     for (; $i < @lines; ++$i) {
    656         my $firstChar = substr($lines[$i], 0, 1);
    657         if ($firstChar eq " ") {
    658             next;
    659         } elsif ($firstChar eq "@") {
    660             last;
    661         }
    662         $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
    663         last;
    664     }
    665     my $deletedLineCount = 0;
    666     if ($shouldTrimContext) { # Also occurs if end of file reached.
    667         splice(@lines, $i - @overlappingLines, @overlappingLines);
    668         $deletedLineCount = @overlappingLines;
    669     }
    670 
    671     # Work backwards, shifting overlapping lines towards front
    672     # while checking that patch stays equivalent.
    673     for ($i = $dateStartIndex - 1; $i >= $chunkStartIndex; --$i) {
    674         my $line = $lines[$i];
    675         if (substr($line, 0, 1) ne " ") {
    676             next;
    677         }
    678         my $text = substr($line, 1);
    679         my $newLine = pop(@overlappingLines);
    680         if ($text ne substr($newLine, 1)) {
    681             return $patch; # Unexpected difference.
    682         }
    683         $lines[$i] = "+$text";
    684     }
    685 
    686     # Finish moving whatever overlapping lines remain, and update
    687     # the initial chunk range.
    688     my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@
    689     if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) {
    690         # FIXME: Handle errors differently from ChangeLog files that
    691         # are okay but should not be altered. That way we can find out
    692         # if improvements to the script ever become necessary.
    693         return $patch; # Error: unexpected patch string format.
    694     }
    695     my $skippedFirstLineCount = $1 - 1;
    696     my $oldSourceLineCount = $2;
    697     my $oldTargetLineCount = $3;
    698 
    699     if (@overlappingLines != $skippedFirstLineCount) {
    700         # This can happen, for example, when deliberately inserting
    701         # a new ChangeLog entry earlier in the file.
    702         return $patch;
    703     }
    704     # If @overlappingLines > 0, this is where we make use of the
    705     # assumption that the beginning of the source file was not modified.
    706     splice(@lines, $chunkStartIndex, 0, @overlappingLines);
    707 
    708     my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
    709     my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
    710     $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
    711 
    712     return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
    713 }
    714 
    715 # This is a supporting method for runPatchCommand.
    716 #
    717 # Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
    718 #
    719 # Returns ($patchCommand, $isForcing).
    720 #
    721 # This subroutine has unit tests in VCSUtils_unittest.pl.
    722 sub generatePatchCommand($)
    723 {
    724     my ($passedArgsHashRef) = @_;
    725 
    726     my $argsHashRef = { # Defaults
    727         ensureForce => 0,
    728         shouldReverse => 0,
    729         options => []
    730     };
    731     
    732     # Merges hash references. It's okay here if passed hash reference is undefined.
    733     @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
    734     
    735     my $ensureForce = $argsHashRef->{ensureForce};
    736     my $shouldReverse = $argsHashRef->{shouldReverse};
    737     my $options = $argsHashRef->{options};
    738 
    739     if (! $options) {
    740         $options = [];
    741     } else {
    742         $options = [@{$options}]; # Copy to avoid side effects.
    743     }
    744 
    745     my $isForcing = 0;
    746     if (grep /^--force$/, @{$options}) {
    747         $isForcing = 1;
    748     } elsif ($ensureForce) {
    749         push @{$options}, "--force";
    750         $isForcing = 1;
    751     }
    752 
    753     if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
    754         push @{$options}, "--reverse";
    755     }
    756 
    757     @{$options} = sort(@{$options}); # For easier testing.
    758 
    759     my $patchCommand = join(" ", "patch -p0", @{$options});
    760 
    761     return ($patchCommand, $isForcing);
    762 }
    763 
    764 # Apply the given patch using the patch(1) command.
    765 #
    766 # On success, return the resulting exit status. Otherwise, exit with the
    767 # exit status. If "--force" is passed as an option, however, then never
    768 # exit and always return the exit status.
    769 #
    770 # Args:
    771 #   $patch: a patch string.
    772 #   $repositoryRootPath: an absolute path to the repository root.
    773 #   $pathRelativeToRoot: the path of the file to be patched, relative to the
    774 #                        repository root. This should normally be the path
    775 #                        found in the patch's "Index:" line. It is passed
    776 #                        explicitly rather than reparsed from the patch
    777 #                        string for optimization purposes.
    778 #                            This is used only for error reporting. The
    779 #                        patch command gleans the actual file to patch
    780 #                        from the patch string.
    781 #   $args: a reference to a hash of optional arguments. The possible
    782 #          keys are --
    783 #            ensureForce: whether to ensure --force is passed (defaults to 0).
    784 #            shouldReverse: whether to pass --reverse (defaults to 0).
    785 #            options: a reference to an array of options to pass to the
    786 #                     patch command. The subroutine passes the -p0 option
    787 #                     no matter what. This should not include --reverse.
    788 #
    789 # This subroutine has unit tests in VCSUtils_unittest.pl.
    790 sub runPatchCommand($$$;$)
    791 {
    792     my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
    793 
    794     my ($patchCommand, $isForcing) = generatePatchCommand($args);
    795 
    796     # Temporarily change the working directory since the path found
    797     # in the patch's "Index:" line is relative to the repository root
    798     # (i.e. the same as $pathRelativeToRoot).
    799     my $cwd = Cwd::getcwd();
    800     chdir $repositoryRootPath;
    801 
    802     open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
    803     print PATCH $patch;
    804     close PATCH;
    805     my $exitStatus = exitStatus($?);
    806 
    807     chdir $cwd;
    808 
    809     if ($exitStatus && !$isForcing) {
    810         print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
    811               "status $exitStatus.  Pass --force to ignore patch failures.\n";
    812         exit $exitStatus;
    813     }
    814 
    815     return $exitStatus;
    816 }
    817 
    818 sub gitConfig($)
    819 {
    820     return unless $isGit;
    821 
    822     my ($config) = @_;
    823 
    824     my $result = `git config $config`;
    825     if (($? >> 8)) {
    826         $result = `git repo-config $config`;
    827     }
    828     chomp $result;
    829     return $result;
    830 }
    831 
    832 sub changeLogNameError($)
    833 {
    834     my ($message) = @_;
    835     print STDERR "$message\nEither:\n";
    836     print STDERR "  set CHANGE_LOG_NAME in your environment\n";
    837     print STDERR "  OR pass --name= on the command line\n";
    838     print STDERR "  OR set REAL_NAME in your environment";
    839     print STDERR "  OR git users can set 'git config user.name'\n";
    840     exit(1);
    841 }
    842 
    843 sub changeLogName()
    844 {
    845     my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
    846 
    847     changeLogNameError("Failed to determine ChangeLog name.") unless $name;
    848     # getpwuid seems to always succeed on windows, returning the username instead of the full name.  This check will catch that case.
    849     changeLogNameError("'$name' does not contain a space!  ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
    850 
    851     return $name;
    852 }
    853 
    854 sub changeLogEmailAddressError($)
    855 {
    856     my ($message) = @_;
    857     print STDERR "$message\nEither:\n";
    858     print STDERR "  set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
    859     print STDERR "  OR pass --email= on the command line\n";
    860     print STDERR "  OR set EMAIL_ADDRESS in your environment\n";
    861     print STDERR "  OR git users can set 'git config user.email'\n";
    862     exit(1);
    863 }
    864 
    865 sub changeLogEmailAddress()
    866 {
    867     my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
    868 
    869     changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
    870     changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
    871 
    872     return $emailAddress;
    873 }
    874 
    875 # http://tools.ietf.org/html/rfc1924
    876 sub decodeBase85($)
    877 {
    878     my ($encoded) = @_;
    879     my %table;
    880     my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
    881     for (my $i = 0; $i < 85; $i++) {
    882         $table{$characters[$i]} = $i;
    883     }
    884 
    885     my $decoded = '';
    886     my @encodedChars = $encoded =~ /./g;
    887 
    888     for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
    889         my $digit = 0;
    890         for (my $i = 0; $i < 5; $i++) {
    891             $digit *= 85;
    892             my $char = $encodedChars[$encodedIter];
    893             $digit += $table{$char};
    894             $encodedIter++;
    895         }
    896 
    897         for (my $i = 0; $i < 4; $i++) {
    898             $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
    899         }
    900     }
    901 
    902     return $decoded;
    903 }
    904 
    905 sub decodeGitBinaryChunk($$)
    906 {
    907     my ($contents, $fullPath) = @_;
    908 
    909     # Load this module lazily in case the user don't have this module
    910     # and won't handle git binary patches.
    911     require Compress::Zlib;
    912 
    913     my $encoded = "";
    914     my $compressedSize = 0;
    915     while ($contents =~ /^([A-Za-z])(.*)$/gm) {
    916         my $line = $2;
    917         next if $line eq "";
    918         die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
    919         my $actualSize = length($2) / 5 * 4;
    920         my $encodedExpectedSize = ord($1);
    921         my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
    922 
    923         die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
    924         $compressedSize += $expectedSize;
    925         $encoded .= $line;
    926     }
    927 
    928     my $compressed = decodeBase85($encoded);
    929     $compressed = substr($compressed, 0, $compressedSize);
    930     return Compress::Zlib::uncompress($compressed);
    931 }
    932 
    933 sub decodeGitBinaryPatch($$)
    934 {
    935     my ($contents, $fullPath) = @_;
    936 
    937     # Git binary patch has two chunks. One is for the normal patching
    938     # and another is for the reverse patching.
    939     #
    940     # Each chunk a line which starts from either "literal" or "delta",
    941     # followed by a number which specifies decoded size of the chunk.
    942     # The "delta" type chunks aren't supported by this function yet.
    943     #
    944     # Then, content of the chunk comes. To decode the content, we
    945     # need decode it with base85 first, and then zlib.
    946     my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
    947     if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
    948         die "$fullPath: unknown git binary patch format"
    949     }
    950 
    951     my $binaryChunkType = $1;
    952     my $binaryChunkExpectedSize = $2;
    953     my $encodedChunk = $3;
    954     my $reverseBinaryChunkType = $4;
    955     my $reverseBinaryChunkExpectedSize = $5;
    956     my $encodedReverseChunk = $6;
    957 
    958     my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
    959     my $binaryChunkActualSize = length($binaryChunk);
    960     my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
    961     my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
    962 
    963     die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize);
    964     die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
    965 
    966     return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
    967 }
    968 
    969 1;
    970