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