# Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved.
# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
-# Copyright (C) Research In Motion Limited 2010. All rights reserved.
+# Copyright (C) 2010, 2011 Research In Motion Limited. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
$VERSION = 1.00;
@ISA = qw(Exporter);
@EXPORT = qw(
+ &applyGitBinaryPatchDelta
&callSilently
&canonicalizePath
&changeLogEmailAddress
&changeLogName
&chdirReturningRelativePath
+ &decodeGitBinaryChunk
&decodeGitBinaryPatch
&determineSVNRoot
&determineVCSRoot
&gitBranch
&gitdiff2svndiff
&isGit
+ &isGitSVN
&isGitBranchBuild
&isGitDirectory
&isSVN
&makeFilePathRelative
&mergeChangeLogs
&normalizePath
+ &parseFirstEOL
&parsePatch
&pathRelativeToSVNRepositoryRootForPath
&prepareParsedPatch
my $gitBranch;
my $gitRoot;
my $isGit;
+my $isGitSVN;
my $isGitBranchBuild;
my $isSVN;
my $svnVersion;
# Project time zone for Cupertino, CA, US
my $changeLogTimeZone = "PST8PDT";
+my $chunkRangeRegEx = qr#^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$#; # e.g. @@ -2,6 +2,18 @@
my $gitDiffStartRegEx = qr#^diff --git (\w/)?(.+) (\w/)?([^\r\n]+)#;
my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#;
my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path.
return $isGit;
}
+sub isGitSVN()
+{
+ return $isGitSVN if defined $isGitSVN;
+
+ # There doesn't seem to be an officially documented way to determine
+ # if you're in a git-svn checkout. The best suggestions seen so far
+ # all use something like the following:
+ my $output = `git config --get svn-remote.svn.fetch 2>& 1`;
+ $isGitSVN = $output ne '';
+ return $isGitSVN;
+}
+
sub gitBranch()
{
unless (defined $gitBranch) {
return $path;
}
+sub adjustPathForRecentRenamings($)
+{
+ my ($fullPath) = @_;
+
+ if ($fullPath =~ m|^WebCore/|
+ || $fullPath =~ m|^JavaScriptCore/|
+ || $fullPath =~ m|^WebKit/|
+ || $fullPath =~ m|^WebKit2/|) {
+ return "Source/$fullPath";
+ }
+ return $fullPath;
+}
+
sub canonicalizePath($)
{
my ($file) = @_;
return $line;
}
+sub parseFirstEOL($)
+{
+ my ($fileHandle) = @_;
+
+ # Make input record separator the new-line character to simplify regex matching below.
+ my $savedInputRecordSeparator = $INPUT_RECORD_SEPARATOR;
+ $INPUT_RECORD_SEPARATOR = "\n";
+ my $firstLine = <$fileHandle>;
+ $INPUT_RECORD_SEPARATOR = $savedInputRecordSeparator;
+
+ return unless defined($firstLine);
+
+ my $eol;
+ if ($firstLine =~ /\r\n/) {
+ $eol = "\r\n";
+ } elsif ($firstLine =~ /\r/) {
+ $eol = "\r";
+ } elsif ($firstLine =~ /\n/) {
+ $eol = "\n";
+ }
+ return $eol;
+}
+
+sub firstEOLInFile($)
+{
+ my ($file) = @_;
+ my $eol;
+ if (open(FILE, $file)) {
+ $eol = parseFirstEOL(*FILE);
+ close(FILE);
+ }
+ return $eol;
+}
+
sub svnStatus($)
{
my ($fullPath) = @_;
# The first and second paths can differ in the case of copies
# and renames. We use the second file path because it is the
# destination path.
- $indexPath = $4;
+ $indexPath = adjustPathForRecentRenamings($4);
# Use $POSTMATCH to preserve the end-of-line character.
$_ = "Index: $indexPath$POSTMATCH"; # Convert to SVN format.
} else {
my $indexPath;
if (/$svnDiffStartRegEx/) {
- $indexPath = $1;
+ $indexPath = adjustPathForRecentRenamings($1);
} else {
die("First line of SVN diff does not begin with \"Index \": \"$_\"");
}
# $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.
+# $optionsHashRef: a hash reference representing optional options to use
+# when processing a diff.
+# shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
+# instead of the line endings in the target file; the
+# value of 1 if svnConvertedText should use the line
+# endings in the diff.
#
# Returns ($diffHashRefs, $lastReadLine):
# $diffHashRefs: A reference to an array of references to %diffHash hashes.
# See the %diffHash documentation above.
# $lastReadLine: the line last read from $fileHandle
-sub parseDiff($$)
+sub parseDiff($$;$)
{
# FIXME: Adjust this method so that it dies if the first line does not
# match the start of a diff. This will require a change to
# parsePatch() so that parsePatch() skips over leading junk.
- my ($fileHandle, $line) = @_;
+ my ($fileHandle, $line, $optionsHashRef) = @_;
my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default
my $headerHashRef; # Last header found, as returned by parseDiffHeader().
my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties().
my $svnText;
+ my $indexPathEOL;
while (defined($line)) {
if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) {
# Then assume all diffs in the patch are Git-formatted. This
}
if ($line !~ $headerStartRegEx) {
# Then we are in the body of the diff.
+ if ($indexPathEOL && $line !~ /$chunkRangeRegEx/) {
+ # The chunk range is part of the body of the diff, but its line endings should't be
+ # modified or patch(1) will complain. So, we only modify non-chunk range lines.
+ $line =~ s/\r\n|\r|\n/$indexPathEOL/g;
+ }
$svnText .= $line;
$line = <$fileHandle>;
next;
}
($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
+ if (!$optionsHashRef || !$optionsHashRef->{shouldNotUseIndexPathEOL}) {
+ $indexPathEOL = firstEOLInFile($headerHashRef->{indexPath}) if !$headerHashRef->{isNew} && !$headerHashRef->{isBinary};
+ }
$svnText .= $headerHashRef->{svnConvertedText};
}
# Args:
# $fileHandle: A file handle to the patch file that has not yet been
# read from.
+# $optionsHashRef: a hash reference representing optional options to use
+# when processing a diff.
+# shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
+# instead of the line endings in the target file; the
+# value of 1 if svnConvertedText should use the line
+# endings in the diff.
#
# Returns:
# @diffHashRefs: an array of diff hash references.
# See the %diffHash documentation above.
-sub parsePatch($)
+sub parsePatch($;$)
{
- my ($fileHandle) = @_;
+ my ($fileHandle, $optionsHashRef) = @_;
my $newDiffHashRefs;
my @diffHashRefs; # return value
while (defined($line)) { # Otherwise, at EOF.
- ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line);
+ ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line, $optionsHashRef);
push @diffHashRefs, @$newDiffHashRefs;
}
# Returns $changeLogHashRef:
# $changeLogHashRef: a hash reference representing a change log patch.
# patch: a ChangeLog patch equivalent to the given one, but with the
-# newest ChangeLog entry inserted at the top of the file, if possible.
-# hasOverlappingLines: the value 1 if the change log entry overlaps
-# some lines of another change log entry. This can
-# happen when deliberately inserting a new ChangeLog
-# entry earlier in the file above an entry with
-# the same date and author.
+# newest ChangeLog entry inserted at the top of the file, if possible.
sub fixChangeLogPatch($)
{
my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
$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 @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);
+
+ # Update the date start index as it may have changed after shifting
+ # the overlapping lines towards the front.
+ for ($i = $chunkStartIndex; $i < $dateStartIndex; ++$i) {
+ $dateStartIndex = $i if $lines[$i] =~ /$dateStartRegEx/;
+ }
+ splice(@lines, $chunkStartIndex, $dateStartIndex - $chunkStartIndex); # Remove context of later entry.
+ $deletedLineCount += $dateStartIndex - $chunkStartIndex;
+
+ # Update the initial chunk range.
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
$changeLogHashRef{patch} = $patch; # Error: unexpected patch string format.
return \%changeLogHashRef;
}
- 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.
- $changeLogHashRef{hasOverlappingLines} = 1;
- $changeLogHashRef{patch} = $patch;
- return \%changeLogHashRef;
- }
- # 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 @@";
#
# 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 $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);
+ die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkType eq "literal" and $binaryChunkExpectedSize != $binaryChunkActualSize);
+ die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkType eq "literal" and $reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
}
+sub readByte($$)
+{
+ my ($data, $location) = @_;
+
+ # Return the byte at $location in $data as a numeric value.
+ return ord(substr($data, $location, 1));
+}
+
+# The git binary delta format is undocumented, except in code:
+# - https://github.com/git/git/blob/master/delta.h:get_delta_hdr_size is the source
+# of the algorithm in decodeGitBinaryPatchDeltaSize.
+# - https://github.com/git/git/blob/master/patch-delta.c:patch_delta is the source
+# of the algorithm in applyGitBinaryPatchDelta.
+sub decodeGitBinaryPatchDeltaSize($)
+{
+ my ($binaryChunk) = @_;
+
+ # Source and destination buffer sizes are stored in 7-bit chunks at the
+ # start of the binary delta patch data. The highest bit in each byte
+ # except the last is set; the remaining 7 bits provide the next
+ # chunk of the size. The chunks are stored in ascending significance
+ # order.
+ my $cmd;
+ my $size = 0;
+ my $shift = 0;
+ for (my $i = 0; $i < length($binaryChunk);) {
+ $cmd = readByte($binaryChunk, $i++);
+ $size |= ($cmd & 0x7f) << $shift;
+ $shift += 7;
+ if (!($cmd & 0x80)) {
+ return ($size, $i);
+ }
+ }
+}
+
+sub applyGitBinaryPatchDelta($$)
+{
+ my ($binaryChunk, $originalContents) = @_;
+
+ # Git delta format consists of two headers indicating source buffer size
+ # and result size, then a series of commands. Each command is either
+ # a copy-from-old-version (the 0x80 bit is set) or a copy-from-delta
+ # command. Commands are applied sequentially to generate the result.
+ #
+ # A copy-from-old-version command encodes an offset and size to copy
+ # from in subsequent bits, while a copy-from-delta command consists only
+ # of the number of bytes to copy from the delta.
+
+ # We don't use these values, but we need to know how big they are so that
+ # we can skip to the diff data.
+ my ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
+ $binaryChunk = substr($binaryChunk, $bytesUsed);
+ ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
+ $binaryChunk = substr($binaryChunk, $bytesUsed);
+
+ my $out = "";
+ for (my $i = 0; $i < length($binaryChunk); ) {
+ my $cmd = ord(substr($binaryChunk, $i++, 1));
+ if ($cmd & 0x80) {
+ # Extract an offset and size from the delta data, then copy
+ # $size bytes from $offset in the original data into the output.
+ my $offset = 0;
+ my $size = 0;
+ if ($cmd & 0x01) { $offset = readByte($binaryChunk, $i++); }
+ if ($cmd & 0x02) { $offset |= readByte($binaryChunk, $i++) << 8; }
+ if ($cmd & 0x04) { $offset |= readByte($binaryChunk, $i++) << 16; }
+ if ($cmd & 0x08) { $offset |= readByte($binaryChunk, $i++) << 24; }
+ if ($cmd & 0x10) { $size = readByte($binaryChunk, $i++); }
+ if ($cmd & 0x20) { $size |= readByte($binaryChunk, $i++) << 8; }
+ if ($cmd & 0x40) { $size |= readByte($binaryChunk, $i++) << 16; }
+ if ($size == 0) { $size = 0x10000; }
+ $out .= substr($originalContents, $offset, $size);
+ } elsif ($cmd) {
+ # Copy $cmd bytes from the delta data into the output.
+ $out .= substr($binaryChunk, $i, $cmd);
+ $i += $cmd;
+ } else {
+ die "unexpected delta opcode 0";
+ }
+ }
+
+ return $out;
+}
+
1;