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