blob: 022c72a11f6a9fe8c5356b8c063ae324dcb5fc9a [file] [log] [blame]
# Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved.
# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Module to share code to work with various version control systems.
package VCSUtils;
use strict;
use warnings;
use Cwd qw(); # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
use English; # for $POSTMATCH, etc.
use File::Basename;
use File::Spec;
use POSIX;
BEGIN {
use Exporter ();
our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
$VERSION = 1.00;
@ISA = qw(Exporter);
@EXPORT = qw(
&canonicalizePath
&changeLogEmailAddress
&changeLogName
&chdirReturningRelativePath
&decodeGitBinaryPatch
&determineSVNRoot
&determineVCSRoot
&exitStatus
&fixChangeLogPatch
&gitBranch
&gitdiff2svndiff
&isGit
&isGitBranchBuild
&isGitDirectory
&isSVN
&isSVNDirectory
&isSVNVersion16OrNewer
&makeFilePathRelative
&normalizePath
&parsePatch
&pathRelativeToSVNRepositoryRootForPath
&runPatchCommand
&svnRevisionForDirectory
&svnStatus
);
%EXPORT_TAGS = ( );
@EXPORT_OK = ();
}
our @EXPORT_OK;
my $gitBranch;
my $gitRoot;
my $isGit;
my $isGitBranchBuild;
my $isSVN;
my $svnVersion;
# This method is for portability. Return the system-appropriate exit
# status of a child process.
#
# Args: pass the child error status returned by the last pipe close,
# for example "$?".
sub exitStatus($)
{
my ($returnvalue) = @_;
if ($^O eq "MSWin32") {
return $returnvalue >> 8;
}
return WEXITSTATUS($returnvalue);
}
sub isGitDirectory($)
{
my ($dir) = @_;
return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
}
sub isGit()
{
return $isGit if defined $isGit;
$isGit = isGitDirectory(".");
return $isGit;
}
sub gitBranch()
{
unless (defined $gitBranch) {
chomp($gitBranch = `git symbolic-ref -q HEAD`);
$gitBranch = "" if exitStatus($?);
$gitBranch =~ s#^refs/heads/##;
$gitBranch = "" if $gitBranch eq "master";
}
return $gitBranch;
}
sub isGitBranchBuild()
{
my $branch = gitBranch();
chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
return 1 if $override eq "true";
return 0 if $override eq "false";
unless (defined $isGitBranchBuild) {
chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
$isGitBranchBuild = $gitBranchBuild eq "true";
}
return $isGitBranchBuild;
}
sub isSVNDirectory($)
{
my ($dir) = @_;
return -d File::Spec->catdir($dir, ".svn");
}
sub isSVN()
{
return $isSVN if defined $isSVN;
$isSVN = isSVNDirectory(".");
return $isSVN;
}
sub svnVersion()
{
return $svnVersion if defined $svnVersion;
if (!isSVN()) {
$svnVersion = 0;
} else {
chomp($svnVersion = `svn --version --quiet`);
}
return $svnVersion;
}
sub isSVNVersion16OrNewer()
{
my $version = svnVersion();
return eval "v$version" ge v1.6;
}
sub chdirReturningRelativePath($)
{
my ($directory) = @_;
my $previousDirectory = Cwd::getcwd();
chdir $directory;
my $newDirectory = Cwd::getcwd();
return "." if $newDirectory eq $previousDirectory;
return File::Spec->abs2rel($previousDirectory, $newDirectory);
}
sub determineGitRoot()
{
chomp(my $gitDir = `git rev-parse --git-dir`);
return dirname($gitDir);
}
sub determineSVNRoot()
{
my $last = '';
my $path = '.';
my $parent = '..';
my $repositoryRoot;
my $repositoryUUID;
while (1) {
my $thisRoot;
my $thisUUID;
# Ignore error messages in case we've run past the root of the checkout.
open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die;
while (<INFO>) {
if (/^Repository Root: (.+)/) {
$thisRoot = $1;
}
if (/^Repository UUID: (.+)/) {
$thisUUID = $1;
}
if ($thisRoot && $thisUUID) {
local $/ = undef;
<INFO>; # Consume the rest of the input.
}
}
close INFO;
# It's possible (e.g. for developers of some ports) to have a WebKit
# checkout in a subdirectory of another checkout. So abort if the
# repository root or the repository UUID suddenly changes.
last if !$thisUUID;
$repositoryUUID = $thisUUID if !$repositoryUUID;
last if $thisUUID ne $repositoryUUID;
last if !$thisRoot;
$repositoryRoot = $thisRoot if !$repositoryRoot;
last if $thisRoot ne $repositoryRoot;
$last = $path;
$path = File::Spec->catdir($parent, $path);
}
return File::Spec->rel2abs($last);
}
sub determineVCSRoot()
{
if (isGit()) {
return determineGitRoot();
}
if (!isSVN()) {
# Some users have a workflow where svn-create-patch, svn-apply and
# svn-unapply are used outside of multiple svn working directores,
# so warn the user and assume Subversion is being used in this case.
warn "Unable to determine VCS root; assuming Subversion";
$isSVN = 1;
}
return determineSVNRoot();
}
sub svnRevisionForDirectory($)
{
my ($dir) = @_;
my $revision;
if (isSVNDirectory($dir)) {
my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`;
($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
} elsif (isGitDirectory($dir)) {
my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`;
($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
}
die "Unable to determine current SVN revision in $dir" unless (defined $revision);
return $revision;
}
sub pathRelativeToSVNRepositoryRootForPath($)
{
my ($file) = @_;
my $relativePath = File::Spec->abs2rel($file);
my $svnInfo;
if (isSVN()) {
$svnInfo = `LC_ALL=C svn info $relativePath`;
} elsif (isGit()) {
$svnInfo = `LC_ALL=C git svn info $relativePath`;
}
$svnInfo =~ /.*^URL: (.*?)$/m;
my $svnURL = $1;
$svnInfo =~ /.*^Repository Root: (.*?)$/m;
my $repositoryRoot = $1;
$svnURL =~ s/$repositoryRoot\///;
return $svnURL;
}
sub makeFilePathRelative($)
{
my ($path) = @_;
return $path unless isGit();
unless (defined $gitRoot) {
chomp($gitRoot = `git rev-parse --show-cdup`);
}
return $gitRoot . $path;
}
sub normalizePath($)
{
my ($path) = @_;
$path =~ s/\\/\//g;
return $path;
}
sub canonicalizePath($)
{
my ($file) = @_;
# Remove extra slashes and '.' directories in path
$file = File::Spec->canonpath($file);
# Remove '..' directories in path
my @dirs = ();
foreach my $dir (File::Spec->splitdir($file)) {
if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
pop(@dirs);
} else {
push(@dirs, $dir);
}
}
return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
}
sub removeEOL($)
{
my ($line) = @_;
$line =~ s/[\r\n]+$//g;
return $line;
}
sub svnStatus($)
{
my ($fullPath) = @_;
my $svnStatus;
open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
if (-d $fullPath) {
# When running "svn stat" on a directory, we can't assume that only one
# status will be returned (since any files with a status below the
# directory will be returned), and we can't assume that the directory will
# be first (since any files with unknown status will be listed first).
my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
while (<SVN>) {
# Input may use a different EOL sequence than $/, so avoid chomp.
$_ = removeEOL($_);
my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
if ($normalizedFullPath eq $normalizedStatPath) {
$svnStatus = "$_\n";
last;
}
}
# Read the rest of the svn command output to avoid a broken pipe warning.
local $/ = undef;
<SVN>;
}
else {
# Files will have only one status returned.
$svnStatus = removeEOL(<SVN>) . "\n";
}
close SVN;
return $svnStatus;
}
# Convert a line of a git-formatted patch to SVN format, while
# preserving any end-of-line characters.
sub gitdiff2svndiff($)
{
$_ = shift @_;
if (m#^diff --git \w/(.+) \w/([^\r\n]+)#) {
return "Index: $1$POSTMATCH";
}
if (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) {
# FIXME: No need to return dividing line once parseDiffHeader() is used.
return "===================================================================$POSTMATCH";
}
if (m#^--- \w/([^\r\n]+)#) {
return "--- $1$POSTMATCH";
}
if (m#^\+\+\+ \w/([^\r\n]+)#) {
return "+++ $1$POSTMATCH";
}
return $_;
}
# Parse the next diff header from the given file handle, and advance
# the file handle so the last line read is the first line after the
# parsed header block.
#
# This subroutine dies if given leading junk or if the end of the header
# block could not be detected. The last line of a header block is a
# line beginning with "+++".
#
# Args:
# $fileHandle: advanced so the last line read is the first line of the
# next diff header. For SVN-formatted diffs, this is the
# "Index:" line.
# $line: the line last read from $fileHandle
#
# Returns ($headerHashRef, $lastReadLine):
# $headerHashRef: a hash reference representing a diff header
# copiedFromPath: if a file copy, the path from which the file was
# copied. Otherwise, undefined.
# indexPath: the path in the "Index:" line.
# sourceRevision: the revision number of the source. This is the same
# as the revision number the file was copied from, in
# the case of a file copy.
# svnConvertedText: the header text converted to SVN format.
# Unrecognized lines are discarded.
# $lastReadLine: the line last read from $fileHandle. This is the first
# line after the header ending.
sub parseDiffHeader($$)
{
my ($fileHandle, $line) = @_;
my $filter;
if ($line =~ m#^diff --git #) {
$filter = \&gitdiff2svndiff;
}
$line = &$filter($line) if $filter;
my $indexPath;
if ($line =~ /^Index: ([^\r\n]+)/) {
$indexPath = $1;
} else {
die("Could not parse first line of diff header: \"$line\".");
}
my %header;
my $foundHeaderEnding;
my $lastReadLine;
my $sourceRevision;
my $svnConvertedText = $line;
while (<$fileHandle>) {
# Temporarily strip off any end-of-line characters to simplify
# regex matching below.
s/([\n\r]+)$//;
my $eol = $1;
$_ = &$filter($_) if $filter;
# Fix paths on ""---" and "+++" lines to match the leading
# index line.
if (s/^--- \S+/--- $indexPath/) {
# ---
if (/^--- .+\(revision (\d+)\)/) {
$sourceRevision = $1 if ($1 != 0);
if (/\(from (\S+):(\d+)\)$/) {
# The "from" clause is created by svn-create-patch, in
# which case there is always also a "revision" clause.
$header{copiedFromPath} = $1;
die("Revision number \"$2\" in \"from\" clause does not match " .
"source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
}
}
$_ = "=" x 67 . "$eol$_"; # Prepend dividing line ===....
} elsif (s/^\+\+\+ \S+/+++ $indexPath/) {
# +++
$foundHeaderEnding = 1;
} else {
# Skip unrecognized lines.
next;
}
$svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
if ($foundHeaderEnding) {
$lastReadLine = <$fileHandle>;
last;
}
} # $lastReadLine is undef if while loop ran out.
if (!$foundHeaderEnding) {
die("Did not find end of header block corresponding to index path \"$indexPath\".");
}
$header{indexPath} = $indexPath;
$header{sourceRevision} = $sourceRevision;
$header{svnConvertedText} = $svnConvertedText;
return (\%header, $lastReadLine);
}
# Parse one diff from a patch file created by svn-create-patch, and
# advance the file handle so the last line read is the first line
# of the next header block.
#
# This subroutine preserves any leading junk encountered before the header.
#
# Args:
# $fileHandle: a file handle advanced to the first line of the next
# header block. Leading junk is okay.
# $line: the line last read from $fileHandle.
#
# Returns ($diffHashRef, $lastReadLine):
# $diffHashRef:
# copiedFromPath: if a file copy, the path from which the file was
# copied. Otherwise, undefined.
# indexPath: the path in the "Index:" line.
# sourceRevision: the revision number of the source. This is the same
# as the revision number the file was copied from, in
# the case of a file copy.
# svnConvertedText: the diff converted to SVN format.
# $lastReadLine: the line last read from $fileHandle
sub parseDiff($$)
{
my ($fileHandle, $line) = @_;
my $headerStartRegEx = qr#^Index: #; # SVN-style header for the default
my $gitHeaderStartRegEx = qr#^diff --git \w/#;
my $headerHashRef; # Last header found, as returned by parseDiffHeader().
my $svnText;
while (defined($line)) {
if (!$headerHashRef && ($line =~ $gitHeaderStartRegEx)) {
# Then assume all diffs in the patch are Git-formatted. This
# block was made to be enterable at most once since we assume
# all diffs in the patch are formatted the same (SVN or Git).
$headerStartRegEx = $gitHeaderStartRegEx;
}
if ($line !~ $headerStartRegEx) {
# Then we are in the body of the diff.
$svnText .= $line;
$line = <$fileHandle>;
next;
} # Otherwise, we found a diff header.
if ($headerHashRef) {
# Then this is the second diff header of this while loop.
last;
}
($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
$svnText .= $headerHashRef->{svnConvertedText};
}
my %diffHashRef;
$diffHashRef{copiedFromPath} = $headerHashRef->{copiedFromPath};
$diffHashRef{indexPath} = $headerHashRef->{indexPath};
$diffHashRef{sourceRevision} = $headerHashRef->{sourceRevision};
$diffHashRef{svnConvertedText} = $svnText;
return (\%diffHashRef, $line);
}
# Parse a patch file created by svn-create-patch.
#
# Args:
# $fileHandle: A file handle to the patch file that has not yet been
# read from.
#
# Returns:
# @diffHashRefs: an array of diff hash references. See parseDiff() for
# a description of each $diffHashRef.
sub parsePatch($)
{
my ($fileHandle) = @_;
my @diffHashRefs; # return value
my $line = <$fileHandle>;
while (defined($line)) { # Otherwise, at EOF.
my $diffHashRef;
($diffHashRef, $line) = parseDiff($fileHandle, $line);
push @diffHashRefs, $diffHashRef;
}
return @diffHashRefs;
}
# If possible, returns a ChangeLog patch equivalent to the given one,
# but with the newest ChangeLog entry inserted at the top of the
# file -- i.e. no leading context and all lines starting with "+".
#
# If given a patch string not representable as a patch with the above
# properties, it returns the input back unchanged.
#
# WARNING: This subroutine can return an inequivalent patch string if
# both the beginning of the new ChangeLog file matches the beginning
# of the source ChangeLog, and the source beginning was modified.
# Otherwise, it is guaranteed to return an equivalent patch string,
# if it returns.
#
# Applying this subroutine to ChangeLog patches allows svn-apply to
# insert new ChangeLog entries at the top of the ChangeLog file.
# svn-apply uses patch with --fuzz=3 to do this. We need to apply
# this subroutine because the diff(1) command is greedy when matching
# lines. A new ChangeLog entry with the same date and author as the
# previous will match and cause the diff to have lines of starting
# context.
#
# This subroutine has unit tests in VCSUtils_unittest.pl.
sub fixChangeLogPatch($)
{
my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
$patch =~ /(\r?\n)/;
my $lineEnding = $1;
my @lines = split(/$lineEnding/, $patch);
my $i = 0; # We reuse the same index throughout.
# Skip to beginning of first chunk.
for (; $i < @lines; ++$i) {
if (substr($lines[$i], 0, 1) eq "@") {
last;
}
}
my $chunkStartIndex = ++$i;
# Optimization: do not process if new lines already begin the chunk.
if (substr($lines[$i], 0, 1) eq "+") {
return $patch;
}
# Skip to first line of newly added ChangeLog entry.
# For example, +2009-06-03 Eric Seidel <eric@webkit.org>
my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
. '\s+(.+)\s+' # name
. '<([^<>]+)>$'; # e-mail address
for (; $i < @lines; ++$i) {
my $line = $lines[$i];
my $firstChar = substr($line, 0, 1);
if ($line =~ /$dateStartRegEx/) {
last;
} elsif ($firstChar eq " " or $firstChar eq "+") {
next;
}
return $patch; # Do not change if, for example, "-" or "@" found.
}
if ($i >= @lines) {
return $patch; # Do not change if date not found.
}
my $dateStartIndex = $i;
# Rewrite overlapping lines to lead with " ".
my @overlappingLines = (); # These will include a leading "+".
for (; $i < @lines; ++$i) {
my $line = $lines[$i];
if (substr($line, 0, 1) ne "+") {
last;
}
push(@overlappingLines, $line);
$lines[$i] = " " . substr($line, 1);
}
# Remove excess ending context, if necessary.
my $shouldTrimContext = 1;
for (; $i < @lines; ++$i) {
my $firstChar = substr($lines[$i], 0, 1);
if ($firstChar eq " ") {
next;
} elsif ($firstChar eq "@") {
last;
}
$shouldTrimContext = 0; # For example, if "+" or "-" encountered.
last;
}
my $deletedLineCount = 0;
if ($shouldTrimContext) { # Also occurs if end of file reached.
splice(@lines, $i - @overlappingLines, @overlappingLines);
$deletedLineCount = @overlappingLines;
}
# Work backwards, shifting overlapping lines towards front
# while checking that patch stays equivalent.
for ($i = $dateStartIndex - 1; $i >= $chunkStartIndex; --$i) {
my $line = $lines[$i];
if (substr($line, 0, 1) ne " ") {
next;
}
my $text = substr($line, 1);
my $newLine = pop(@overlappingLines);
if ($text ne substr($newLine, 1)) {
return $patch; # Unexpected difference.
}
$lines[$i] = "+$text";
}
# Finish moving whatever overlapping lines remain, and update
# the initial chunk range.
my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@
if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) {
# FIXME: Handle errors differently from ChangeLog files that
# are okay but should not be altered. That way we can find out
# if improvements to the script ever become necessary.
return $patch; # Error: unexpected patch string format.
}
my $skippedFirstLineCount = $1 - 1;
my $oldSourceLineCount = $2;
my $oldTargetLineCount = $3;
if (@overlappingLines != $skippedFirstLineCount) {
# This can happen, for example, when deliberately inserting
# a new ChangeLog entry earlier in the file.
return $patch;
}
# If @overlappingLines > 0, this is where we make use of the
# assumption that the beginning of the source file was not modified.
splice(@lines, $chunkStartIndex, 0, @overlappingLines);
my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
$lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
}
# This is a supporting method for runPatchCommand.
#
# Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
#
# Returns ($patchCommand, $isForcing).
#
# This subroutine has unit tests in VCSUtils_unittest.pl.
sub generatePatchCommand($)
{
my ($passedArgsHashRef) = @_;
my $argsHashRef = { # Defaults
ensureForce => 0,
shouldReverse => 0,
options => []
};
# Merges hash references. It's okay here if passed hash reference is undefined.
@{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
my $ensureForce = $argsHashRef->{ensureForce};
my $shouldReverse = $argsHashRef->{shouldReverse};
my $options = $argsHashRef->{options};
if (! $options) {
$options = [];
} else {
$options = [@{$options}]; # Copy to avoid side effects.
}
my $isForcing = 0;
if (grep /^--force$/, @{$options}) {
$isForcing = 1;
} elsif ($ensureForce) {
push @{$options}, "--force";
$isForcing = 1;
}
if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
push @{$options}, "--reverse";
}
@{$options} = sort(@{$options}); # For easier testing.
my $patchCommand = join(" ", "patch -p0", @{$options});
return ($patchCommand, $isForcing);
}
# Apply the given patch using the patch(1) command.
#
# On success, return the resulting exit status. Otherwise, exit with the
# exit status. If "--force" is passed as an option, however, then never
# exit and always return the exit status.
#
# Args:
# $patch: a patch string.
# $repositoryRootPath: an absolute path to the repository root.
# $pathRelativeToRoot: the path of the file to be patched, relative to the
# repository root. This should normally be the path
# found in the patch's "Index:" line. It is passed
# explicitly rather than reparsed from the patch
# string for optimization purposes.
# This is used only for error reporting. The
# patch command gleans the actual file to patch
# from the patch string.
# $args: a reference to a hash of optional arguments. The possible
# keys are --
# ensureForce: whether to ensure --force is passed (defaults to 0).
# shouldReverse: whether to pass --reverse (defaults to 0).
# options: a reference to an array of options to pass to the
# patch command. The subroutine passes the -p0 option
# no matter what. This should not include --reverse.
#
# This subroutine has unit tests in VCSUtils_unittest.pl.
sub runPatchCommand($$$;$)
{
my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
my ($patchCommand, $isForcing) = generatePatchCommand($args);
# Temporarily change the working directory since the path found
# in the patch's "Index:" line is relative to the repository root
# (i.e. the same as $pathRelativeToRoot).
my $cwd = Cwd::getcwd();
chdir $repositoryRootPath;
open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
print PATCH $patch;
close PATCH;
my $exitStatus = exitStatus($?);
chdir $cwd;
if ($exitStatus && !$isForcing) {
print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
"status $exitStatus. Pass --force to ignore patch failures.\n";
exit $exitStatus;
}
return $exitStatus;
}
sub gitConfig($)
{
return unless $isGit;
my ($config) = @_;
my $result = `git config $config`;
if (($? >> 8)) {
$result = `git repo-config $config`;
}
chomp $result;
return $result;
}
sub changeLogNameError($)
{
my ($message) = @_;
print STDERR "$message\nEither:\n";
print STDERR " set CHANGE_LOG_NAME in your environment\n";
print STDERR " OR pass --name= on the command line\n";
print STDERR " OR set REAL_NAME in your environment";
print STDERR " OR git users can set 'git config user.name'\n";
exit(1);
}
sub changeLogName()
{
my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
changeLogNameError("Failed to determine ChangeLog name.") unless $name;
# getpwuid seems to always succeed on windows, returning the username instead of the full name. This check will catch that case.
changeLogNameError("'$name' does not contain a space! ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
return $name;
}
sub changeLogEmailAddressError($)
{
my ($message) = @_;
print STDERR "$message\nEither:\n";
print STDERR " set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
print STDERR " OR pass --email= on the command line\n";
print STDERR " OR set EMAIL_ADDRESS in your environment\n";
print STDERR " OR git users can set 'git config user.email'\n";
exit(1);
}
sub changeLogEmailAddress()
{
my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
return $emailAddress;
}
# http://tools.ietf.org/html/rfc1924
sub decodeBase85($)
{
my ($encoded) = @_;
my %table;
my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
for (my $i = 0; $i < 85; $i++) {
$table{$characters[$i]} = $i;
}
my $decoded = '';
my @encodedChars = $encoded =~ /./g;
for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
my $digit = 0;
for (my $i = 0; $i < 5; $i++) {
$digit *= 85;
my $char = $encodedChars[$encodedIter];
$digit += $table{$char};
$encodedIter++;
}
for (my $i = 0; $i < 4; $i++) {
$decoded .= chr(($digit >> (3 - $i) * 8) & 255);
}
}
return $decoded;
}
sub decodeGitBinaryChunk($$)
{
my ($contents, $fullPath) = @_;
# Load this module lazily in case the user don't have this module
# and won't handle git binary patches.
require Compress::Zlib;
my $encoded = "";
my $compressedSize = 0;
while ($contents =~ /^([A-Za-z])(.*)$/gm) {
my $line = $2;
next if $line eq "";
die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
my $actualSize = length($2) / 5 * 4;
my $encodedExpectedSize = ord($1);
my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
$compressedSize += $expectedSize;
$encoded .= $line;
}
my $compressed = decodeBase85($encoded);
$compressed = substr($compressed, 0, $compressedSize);
return Compress::Zlib::uncompress($compressed);
}
sub decodeGitBinaryPatch($$)
{
my ($contents, $fullPath) = @_;
# Git binary patch has two chunks. One is for the normal patching
# and another is for the reverse patching.
#
# Each chunk a line which starts from either "literal" or "delta",
# followed by a number which specifies decoded size of the chunk.
# The "delta" type chunks aren't supported by this function yet.
#
# Then, content of the chunk comes. To decode the content, we
# need decode it with base85 first, and then zlib.
my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
die "$fullPath: unknown git binary patch format"
}
my $binaryChunkType = $1;
my $binaryChunkExpectedSize = $2;
my $encodedChunk = $3;
my $reverseBinaryChunkType = $4;
my $reverseBinaryChunkExpectedSize = $5;
my $encodedReverseChunk = $6;
my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
my $binaryChunkActualSize = length($binaryChunk);
my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize);
die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
}
1;