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