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