1 # Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved.
2 # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
3 # Copyright (C) Research In Motion Limited 2010. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
14 # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 # its contributors may be used to endorse or promote products derived
16 # from this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 # Module to share code to work with various version control systems.
35 use Cwd qw(); # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
36 use English; # for $POSTMATCH, etc.
43 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
49 &changeLogEmailAddress
51 &chdirReturningRelativePath
64 &isSVNVersion16OrNewer
69 &pathRelativeToSVNRepositoryRootForPath
74 &scmToggleExecutableBit
75 &setChangeLogDateAndReviewer
76 &svnRevisionForDirectory
93 # Project time zone for Cupertino, CA, US
94 my $changeLogTimeZone = "PST8PDT";
96 my $gitDiffStartRegEx = qr#^diff --git (\w/)?(.+) (\w/)?([^\r\n]+)#;
97 my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#;
98 my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path.
99 my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property.
100 my $svnPropertyValueStartRegEx = qr#^ (\+|-|Merged|Reverse-merged) ([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines).
102 # This method is for portability. Return the system-appropriate exit
103 # status of a child process.
105 # Args: pass the child error status returned by the last pipe close,
109 my ($returnvalue) = @_;
110 if ($^O eq "MSWin32") {
111 return $returnvalue >> 8;
113 return WEXITSTATUS($returnvalue);
116 # Call a function while suppressing STDERR, and return the return values
118 sub callSilently($@) {
119 my ($func, @args) = @_;
121 # The following pattern was taken from here:
122 # http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html
124 # Also see this Perl documentation (search for "open OLDERR"):
125 # http://perldoc.perl.org/functions/open.html
126 open(OLDERR, ">&STDERR");
128 my @returnValue = &$func(@args);
129 open(STDERR, ">&OLDERR");
135 sub toWindowsLineEndings
138 $text =~ s/\n/\r\n/g;
142 # Note, this method will not error if the file corresponding to the $source path does not exist.
143 sub scmMoveOrRenameFile
145 my ($source, $destination) = @_;
146 return if ! -e $source;
148 system("svn", "move", $source, $destination);
150 system("git", "mv", $source, $destination);
154 # Note, this method will not error if the file corresponding to the path does not exist.
155 sub scmToggleExecutableBit
157 my ($path, $executableBitDelta) = @_;
158 return if ! -e $path;
159 if ($executableBitDelta == 1) {
160 scmAddExecutableBit($path);
161 } elsif ($executableBitDelta == -1) {
162 scmRemoveExecutableBit($path);
166 sub scmAddExecutableBit($)
171 system("svn", "propset", "svn:executable", "on", $path) == 0 or die "Failed to run 'svn propset svn:executable on $path'.";
177 sub scmRemoveExecutableBit($)
182 system("svn", "propdel", "svn:executable", $path) == 0 or die "Failed to run 'svn propdel svn:executable $path'.";
188 sub isGitDirectory($)
191 return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
196 return $isGit if defined $isGit;
198 $isGit = isGitDirectory(".");
204 unless (defined $gitBranch) {
205 chomp($gitBranch = `git symbolic-ref -q HEAD`);
206 $gitBranch = "" if exitStatus($?);
207 $gitBranch =~ s#^refs/heads/##;
208 $gitBranch = "" if $gitBranch eq "master";
214 sub isGitBranchBuild()
216 my $branch = gitBranch();
217 chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
218 return 1 if $override eq "true";
219 return 0 if $override eq "false";
221 unless (defined $isGitBranchBuild) {
222 chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
223 $isGitBranchBuild = $gitBranchBuild eq "true";
226 return $isGitBranchBuild;
229 sub isSVNDirectory($)
233 return -d File::Spec->catdir($dir, ".svn");
238 return $isSVN if defined $isSVN;
240 $isSVN = isSVNDirectory(".");
246 return $svnVersion if defined $svnVersion;
251 chomp($svnVersion = `svn --version --quiet`);
256 sub isSVNVersion16OrNewer()
258 my $version = svnVersion();
259 return eval "v$version" ge v1.6;
262 sub chdirReturningRelativePath($)
264 my ($directory) = @_;
265 my $previousDirectory = Cwd::getcwd();
267 my $newDirectory = Cwd::getcwd();
268 return "." if $newDirectory eq $previousDirectory;
269 return File::Spec->abs2rel($previousDirectory, $newDirectory);
272 sub determineGitRoot()
274 chomp(my $gitDir = `git rev-parse --git-dir`);
275 return dirname($gitDir);
278 sub determineSVNRoot()
288 # Ignore error messages in case we've run past the root of the checkout.
289 open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die;
291 if (/^Repository Root: (.+)/) {
294 if (/^Repository UUID: (.+)/) {
297 if ($thisRoot && $thisUUID) {
299 <INFO>; # Consume the rest of the input.
304 # It's possible (e.g. for developers of some ports) to have a WebKit
305 # checkout in a subdirectory of another checkout. So abort if the
306 # repository root or the repository UUID suddenly changes.
308 $repositoryUUID = $thisUUID if !$repositoryUUID;
309 last if $thisUUID ne $repositoryUUID;
312 $repositoryRoot = $thisRoot if !$repositoryRoot;
313 last if $thisRoot ne $repositoryRoot;
316 $path = File::Spec->catdir($parent, $path);
319 return File::Spec->rel2abs($last);
322 sub determineVCSRoot()
325 return determineGitRoot();
329 # Some users have a workflow where svn-create-patch, svn-apply and
330 # svn-unapply are used outside of multiple svn working directores,
331 # so warn the user and assume Subversion is being used in this case.
332 warn "Unable to determine VCS root; assuming Subversion";
336 return determineSVNRoot();
339 sub svnRevisionForDirectory($)
344 if (isSVNDirectory($dir)) {
345 my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`;
346 ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
347 } elsif (isGitDirectory($dir)) {
348 my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`;
349 ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
351 die "Unable to determine current SVN revision in $dir" unless (defined $revision);
355 sub pathRelativeToSVNRepositoryRootForPath($)
358 my $relativePath = File::Spec->abs2rel($file);
362 $svnInfo = `LC_ALL=C svn info $relativePath`;
364 $svnInfo = `LC_ALL=C git svn info $relativePath`;
367 $svnInfo =~ /.*^URL: (.*?)$/m;
370 $svnInfo =~ /.*^Repository Root: (.*?)$/m;
371 my $repositoryRoot = $1;
373 $svnURL =~ s/$repositoryRoot\///;
377 sub makeFilePathRelative($)
380 return $path unless isGit();
382 unless (defined $gitRoot) {
383 chomp($gitRoot = `git rev-parse --show-cdup`);
385 return $gitRoot . $path;
395 sub canonicalizePath($)
399 # Remove extra slashes and '.' directories in path
400 $file = File::Spec->canonpath($file);
402 # Remove '..' directories in path
404 foreach my $dir (File::Spec->splitdir($file)) {
405 if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
411 return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
417 return "" unless $line;
419 $line =~ s/[\r\n]+$//g;
427 open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
429 # When running "svn stat" on a directory, we can't assume that only one
430 # status will be returned (since any files with a status below the
431 # directory will be returned), and we can't assume that the directory will
432 # be first (since any files with unknown status will be listed first).
433 my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
435 # Input may use a different EOL sequence than $/, so avoid chomp.
437 my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
438 if ($normalizedFullPath eq $normalizedStatPath) {
443 # Read the rest of the svn command output to avoid a broken pipe warning.
448 # Files will have only one status returned.
449 $svnStatus = removeEOL(<SVN>) . "\n";
455 # Return whether the given file mode is executable in the source control
456 # sense. We make this determination based on whether the executable bit
457 # is set for "others" rather than the stronger condition that it be set
458 # for the user, group, and others. This is sufficient for distinguishing
459 # the default behavior in Git and SVN.
462 # $fileMode: A number or string representing a file mode in octal notation.
465 my $fileMode = shift;
467 return $fileMode % 2;
470 # Parse the next Git diff header from the given file handle, and advance
471 # the handle so the last line read is the first line after the header.
473 # This subroutine dies if given leading junk.
476 # $fileHandle: advanced so the last line read from the handle is the first
477 # line of the header to parse. This should be a line
478 # beginning with "diff --git".
479 # $line: the line last read from $fileHandle
481 # Returns ($headerHashRef, $lastReadLine):
482 # $headerHashRef: a hash reference representing a diff header, as follows--
483 # copiedFromPath: the path from which the file was copied or moved if
484 # the diff is a copy or move.
485 # executableBitDelta: the value 1 or -1 if the executable bit was added or
486 # removed, respectively. New and deleted files have
487 # this value only if the file is executable, in which
488 # case the value is 1 and -1, respectively.
489 # indexPath: the path of the target file.
490 # isBinary: the value 1 if the diff is for a binary file.
491 # isDeletion: the value 1 if the diff is a file deletion.
492 # isCopyWithChanges: the value 1 if the file was copied or moved and
493 # the target file was changed in some way after being
494 # copied or moved (e.g. if its contents or executable
496 # isNew: the value 1 if the diff is for a new file.
497 # shouldDeleteSource: the value 1 if the file was copied or moved and
498 # the source file was deleted -- i.e. if the copy
499 # was actually a move.
500 # svnConvertedText: the header text with some lines converted to SVN
501 # format. Git-specific lines are preserved.
502 # $lastReadLine: the line last read from $fileHandle.
503 sub parseGitDiffHeader($$)
505 my ($fileHandle, $line) = @_;
510 if (/$gitDiffStartRegEx/) {
511 # The first and second paths can differ in the case of copies
512 # and renames. We use the second file path because it is the
515 # Use $POSTMATCH to preserve the end-of-line character.
516 $_ = "Index: $indexPath$POSTMATCH"; # Convert to SVN format.
518 die("Could not parse leading \"diff --git\" line: \"$line\".");
522 my $foundHeaderEnding;
526 my $newExecutableBit = 0;
527 my $oldExecutableBit = 0;
528 my $shouldDeleteSource = 0;
529 my $similarityIndex = 0;
530 my $svnConvertedText;
532 # Temporarily strip off any end-of-line characters to simplify
533 # regex matching below.
537 if (/^(deleted file|old) mode (\d+)/) {
538 $oldExecutableBit = (isExecutable($2) ? 1 : 0);
539 $isDeletion = 1 if $1 eq "deleted file";
540 } elsif (/^new( file)? mode (\d+)/) {
541 $newExecutableBit = (isExecutable($2) ? 1 : 0);
543 } elsif (/^similarity index (\d+)%/) {
544 $similarityIndex = $1;
545 } elsif (/^copy from (\S+)/) {
546 $copiedFromPath = $1;
547 } elsif (/^rename from (\S+)/) {
548 # FIXME: Record this as a move rather than as a copy-and-delete.
549 # This will simplify adding rename support to svn-unapply.
550 # Otherwise, the hash for a deletion would have to know
551 # everything about the file being deleted in order to
552 # support undoing itself. Recording as a move will also
553 # permit us to use "svn move" and "git move".
554 $copiedFromPath = $1;
555 $shouldDeleteSource = 1;
556 } elsif (/^--- \S+/) {
557 $_ = "--- $indexPath"; # Convert to SVN format.
558 } elsif (/^\+\+\+ \S+/) {
559 $_ = "+++ $indexPath"; # Convert to SVN format.
560 $foundHeaderEnding = 1;
561 } elsif (/^GIT binary patch$/ ) {
563 $foundHeaderEnding = 1;
564 # The "git diff" command includes a line of the form "Binary files
565 # <path1> and <path2> differ" if the --binary flag is not used.
566 } elsif (/^Binary files / ) {
567 die("Error: the Git diff contains a binary file without the binary data in ".
568 "line: \"$_\". Be sure to use the --binary flag when invoking \"git diff\" ".
569 "with diffs containing binary files.");
572 $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
574 $_ = <$fileHandle>; # Not defined if end-of-file reached.
576 last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding);
579 my $executableBitDelta = $newExecutableBit - $oldExecutableBit;
583 $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
584 $header{executableBitDelta} = $executableBitDelta if $executableBitDelta;
585 $header{indexPath} = $indexPath;
586 $header{isBinary} = $isBinary if $isBinary;
587 $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta));
588 $header{isDeletion} = $isDeletion if $isDeletion;
589 $header{isNew} = $isNew if $isNew;
590 $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource;
591 $header{svnConvertedText} = $svnConvertedText;
593 return (\%header, $_);
596 # Parse the next SVN diff header from the given file handle, and advance
597 # the handle so the last line read is the first line after the header.
599 # This subroutine dies if given leading junk or if it could not detect
600 # the end of the header block.
603 # $fileHandle: advanced so the last line read from the handle is the first
604 # line of the header to parse. This should be a line
605 # beginning with "Index:".
606 # $line: the line last read from $fileHandle
608 # Returns ($headerHashRef, $lastReadLine):
609 # $headerHashRef: a hash reference representing a diff header, as follows--
610 # copiedFromPath: the path from which the file was copied if the diff
612 # indexPath: the path of the target file, which is the path found in
614 # isBinary: the value 1 if the diff is for a binary file.
615 # isNew: the value 1 if the diff is for a new file.
616 # sourceRevision: the revision number of the source, if it exists. This
617 # is the same as the revision number the file was copied
618 # from, in the case of a file copy.
619 # svnConvertedText: the header text converted to a header with the paths
620 # in some lines corrected.
621 # $lastReadLine: the line last read from $fileHandle.
622 sub parseSvnDiffHeader($$)
624 my ($fileHandle, $line) = @_;
629 if (/$svnDiffStartRegEx/) {
632 die("First line of SVN diff does not begin with \"Index \": \"$_\"");
636 my $foundHeaderEnding;
640 my $svnConvertedText;
642 # Temporarily strip off any end-of-line characters to simplify
643 # regex matching below.
647 # Fix paths on ""---" and "+++" lines to match the leading
649 if (s/^--- \S+/--- $indexPath/) {
651 if (/^--- .+\(revision (\d+)\)/) {
652 $sourceRevision = $1;
653 $isNew = 1 if !$sourceRevision; # if revision 0.
654 if (/\(from (\S+):(\d+)\)$/) {
655 # The "from" clause is created by svn-create-patch, in
656 # which case there is always also a "revision" clause.
657 $copiedFromPath = $1;
658 die("Revision number \"$2\" in \"from\" clause does not match " .
659 "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
662 } elsif (s/^\+\+\+ \S+/+++ $indexPath/) {
663 $foundHeaderEnding = 1;
664 } elsif (/^Cannot display: file marked as a binary type.$/) {
666 $foundHeaderEnding = 1;
669 $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
671 $_ = <$fileHandle>; # Not defined if end-of-file reached.
673 last if (!defined($_) || /$svnDiffStartRegEx/ || $foundHeaderEnding);
676 if (!$foundHeaderEnding) {
677 die("Did not find end of header block corresponding to index path \"$indexPath\".");
682 $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
683 $header{indexPath} = $indexPath;
684 $header{isBinary} = $isBinary if $isBinary;
685 $header{isNew} = $isNew if $isNew;
686 $header{sourceRevision} = $sourceRevision if $sourceRevision;
687 $header{svnConvertedText} = $svnConvertedText;
689 return (\%header, $_);
692 # Parse the next diff header from the given file handle, and advance
693 # the handle so the last line read is the first line after the header.
695 # This subroutine dies if given leading junk or if it could not detect
696 # the end of the header block.
699 # $fileHandle: advanced so the last line read from the handle is the first
700 # line of the header to parse. For SVN-formatted diffs, this
701 # is a line beginning with "Index:". For Git, this is a line
702 # beginning with "diff --git".
703 # $line: the line last read from $fileHandle
705 # Returns ($headerHashRef, $lastReadLine):
706 # $headerHashRef: a hash reference representing a diff header
707 # copiedFromPath: the path from which the file was copied if the diff
709 # executableBitDelta: the value 1 or -1 if the executable bit was added or
710 # removed, respectively. New and deleted files have
711 # this value only if the file is executable, in which
712 # case the value is 1 and -1, respectively.
713 # indexPath: the path of the target file.
714 # isBinary: the value 1 if the diff is for a binary file.
715 # isGit: the value 1 if the diff is Git-formatted.
716 # isSvn: the value 1 if the diff is SVN-formatted.
717 # sourceRevision: the revision number of the source, if it exists. This
718 # is the same as the revision number the file was copied
719 # from, in the case of a file copy.
720 # svnConvertedText: the header text with some lines converted to SVN
721 # format. Git-specific lines are preserved.
722 # $lastReadLine: the line last read from $fileHandle.
723 sub parseDiffHeader($$)
725 my ($fileHandle, $line) = @_;
727 my $header; # This is a hash ref.
732 if ($line =~ $svnDiffStartRegEx) {
734 ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line);
735 } elsif ($line =~ $gitDiffStartRegEx) {
737 ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line);
739 die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\"");
742 $header->{isGit} = $isGit if $isGit;
743 $header->{isSvn} = $isSvn if $isSvn;
745 return ($header, $lastReadLine);
748 # FIXME: The %diffHash "object" should not have an svnConvertedText property.
749 # Instead, the hash object should store its information in a
750 # structured way as properties. This should be done in a way so
751 # that, if necessary, the text of an SVN or Git patch can be
752 # reconstructed from the information in those hash properties.
754 # A %diffHash is a hash representing a source control diff of a single
755 # file operation (e.g. a file modification, copy, or delete).
757 # These hashes appear, for example, in the parseDiff(), parsePatch(),
758 # and prepareParsedPatch() subroutines of this package.
760 # The corresponding values are--
762 # copiedFromPath: the path from which the file was copied if the diff
764 # executableBitDelta: the value 1 or -1 if the executable bit was added or
765 # removed from the target file, respectively.
766 # indexPath: the path of the target file. For SVN-formatted diffs,
767 # this is the same as the path in the "Index:" line.
768 # isBinary: the value 1 if the diff is for a binary file.
769 # isDeletion: the value 1 if the diff is known from the header to be a deletion.
770 # isGit: the value 1 if the diff is Git-formatted.
771 # isNew: the value 1 if the dif is known from the header to be a new file.
772 # isSvn: the value 1 if the diff is SVN-formatted.
773 # sourceRevision: the revision number of the source, if it exists. This
774 # is the same as the revision number the file was copied
775 # from, in the case of a file copy.
776 # svnConvertedText: the diff with some lines converted to SVN format.
777 # Git-specific lines are preserved.
779 # Parse one diff from a patch file created by svn-create-patch, and
780 # advance the file handle so the last line read is the first line
781 # of the next header block.
783 # This subroutine preserves any leading junk encountered before the header.
785 # Composition of an SVN diff
787 # There are three parts to an SVN diff: the header, the property change, and
788 # the binary contents, in that order. Either the header or the property change
789 # may be ommitted, but not both. If there are binary changes, then you always
793 # $fileHandle: a file handle advanced to the first line of the next
794 # header block. Leading junk is okay.
795 # $line: the line last read from $fileHandle.
797 # Returns ($diffHashRefs, $lastReadLine):
798 # $diffHashRefs: A reference to an array of references to %diffHash hashes.
799 # See the %diffHash documentation above.
800 # $lastReadLine: the line last read from $fileHandle
803 # FIXME: Adjust this method so that it dies if the first line does not
804 # match the start of a diff. This will require a change to
805 # parsePatch() so that parsePatch() skips over leading junk.
806 my ($fileHandle, $line) = @_;
808 my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default
810 my $headerHashRef; # Last header found, as returned by parseDiffHeader().
811 my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties().
813 while (defined($line)) {
814 if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) {
815 # Then assume all diffs in the patch are Git-formatted. This
816 # block was made to be enterable at most once since we assume
817 # all diffs in the patch are formatted the same (SVN or Git).
818 $headerStartRegEx = $gitDiffStartRegEx;
821 if ($line =~ $svnPropertiesStartRegEx) {
822 my $propertyPath = $1;
823 if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) {
824 # This is the start of the second diff in the while loop, which happens to
825 # be a property diff. If $svnPropertiesHasRef is defined, then this is the
826 # second consecutive property diff, otherwise it's the start of a property
827 # diff for a file that only has property changes.
830 ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line);
833 if ($line !~ $headerStartRegEx) {
834 # Then we are in the body of the diff.
836 $line = <$fileHandle>;
838 } # Otherwise, we found a diff header.
840 if ($svnPropertiesHashRef || $headerHashRef) {
841 # Then either we just processed an SVN property change or this
842 # is the start of the second diff header of this while loop.
846 ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
848 $svnText .= $headerHashRef->{svnConvertedText};
853 if ($headerHashRef->{shouldDeleteSource}) {
855 $deletionHash{indexPath} = $headerHashRef->{copiedFromPath};
856 $deletionHash{isDeletion} = 1;
857 push @diffHashRefs, \%deletionHash;
859 if ($headerHashRef->{copiedFromPath}) {
861 $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath};
862 $copyHash{indexPath} = $headerHashRef->{indexPath};
863 $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
864 if ($headerHashRef->{isSvn}) {
865 $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
867 push @diffHashRefs, \%copyHash;
870 # Note, the order of evaluation for the following if conditional has been explicitly chosen so that
871 # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that
872 # only has property changes).
873 if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) {
874 # Then add the usual file modification.
876 # FIXME: We should expand this code to support other properties. In the future,
877 # parseSvnDiffProperties may return a hash whose keys are the properties.
878 if ($headerHashRef->{isSvn}) {
879 # SVN records the change to the executable bit in a separate property change diff
880 # that follows the contents of the diff, except for binary diffs. For binary
881 # diffs, the property change diff follows the diff header.
882 $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
883 } elsif ($headerHashRef->{isGit}) {
884 # Git records the change to the executable bit in the header of a diff.
885 $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta};
887 $diffHash{indexPath} = $headerHashRef->{indexPath};
888 $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary};
889 $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion};
890 $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit};
891 $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew};
892 $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn};
893 if (!$headerHashRef->{copiedFromPath}) {
894 # If the file was copied, then we have already incorporated the
895 # sourceRevision information into the change.
896 $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
898 # FIXME: Remove the need for svnConvertedText. See the %diffHash
899 # code comments above for more information.
901 # Note, we may not always have SVN converted text since we intend
902 # to deprecate it in the future. For example, a property change
903 # diff for a file that only has property changes will not return
904 # any SVN converted text.
905 $diffHash{svnConvertedText} = $svnText if $svnText;
906 push @diffHashRefs, \%diffHash;
909 if (!%$headerHashRef && $svnPropertiesHashRef) {
910 # A property change diff for a file that only has property changes.
911 my %propertyChangeHash;
912 $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
913 $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath};
914 $propertyChangeHash{isSvn} = 1;
915 push @diffHashRefs, \%propertyChangeHash;
918 return (\@diffHashRefs, $line);
921 # Parse an SVN property change diff from the given file handle, and advance
922 # the handle so the last line read is the first line after this diff.
924 # For the case of an SVN binary diff, the binary contents will follow the
925 # the property changes.
927 # This subroutine dies if the first line does not begin with "Property changes on"
928 # or if the separator line that follows this line is missing.
931 # $fileHandle: advanced so the last line read from the handle is the first
932 # line of the footer to parse. This line begins with
933 # "Property changes on".
934 # $line: the line last read from $fileHandle.
936 # Returns ($propertyHashRef, $lastReadLine):
937 # $propertyHashRef: a hash reference representing an SVN diff footer.
938 # propertyPath: the path of the target file.
939 # executableBitDelta: the value 1 or -1 if the executable bit was added or
940 # removed from the target file, respectively.
941 # $lastReadLine: the line last read from $fileHandle.
942 sub parseSvnDiffProperties($$)
944 my ($fileHandle, $line) = @_;
949 if (/$svnPropertiesStartRegEx/) {
950 $footer{propertyPath} = $1;
952 die("Failed to find start of SVN property change, \"Property changes on \": \"$_\"");
955 # We advance $fileHandle two lines so that the next line that
956 # we process is $svnPropertyStartRegEx in a well-formed footer.
957 # A well-formed footer has the form:
958 # Property changes on: FileA
959 # ___________________________________________________________________
960 # Added: svn:executable
962 $_ = <$fileHandle>; # Not defined if end-of-file reached.
963 my $separator = "_" x 67;
964 if (defined($_) && /^$separator[\r\n]+$/) {
967 die("Failed to find separator line: \"$_\".");
970 # FIXME: We should expand this to support other SVN properties
971 # (e.g. return a hash of property key-values that represents
974 # Notice, we keep processing until we hit end-of-file or some
975 # line that does not resemble $svnPropertyStartRegEx, such as
976 # the empty line that precedes the start of the binary contents
977 # of a patch, or the start of the next diff (e.g. "Index:").
979 while (defined($_) && /$svnPropertyStartRegEx/) {
980 ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_);
981 if ($propertyHashRef->{name} eq "svn:executable") {
982 # Notice, for SVN properties, propertyChangeDelta is always non-zero
983 # because a property can only be added or removed.
984 $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta};
988 return(\%footer, $_);
991 # Parse the next SVN property from the given file handle, and advance the handle so the last
992 # line read is the first line after the property.
994 # This subroutine dies if the first line is not a valid start of an SVN property,
995 # or the property is missing a value, or the property change type (e.g. "Added")
996 # does not correspond to the property value type (e.g. "+").
999 # $fileHandle: advanced so the last line read from the handle is the first
1000 # line of the property to parse. This should be a line
1001 # that matches $svnPropertyStartRegEx.
1002 # $line: the line last read from $fileHandle.
1004 # Returns ($propertyHashRef, $lastReadLine):
1005 # $propertyHashRef: a hash reference representing a SVN property.
1006 # name: the name of the property.
1007 # value: the last property value. For instance, suppose the property is "Modified".
1008 # Then it has both a '-' and '+' property value in that order. Therefore,
1009 # the value of this key is the value of the '+' property by ordering (since
1010 # it is the last value).
1011 # propertyChangeDelta: the value 1 or -1 if the property was added or
1012 # removed, respectively.
1013 # $lastReadLine: the line last read from $fileHandle.
1014 sub parseSvnProperty($$)
1016 my ($fileHandle, $line) = @_;
1021 my $propertyChangeType;
1022 if (/$svnPropertyStartRegEx/) {
1023 $propertyChangeType = $1;
1026 die("Failed to find SVN property: \"$_\".");
1029 $_ = <$fileHandle>; # Not defined if end-of-file reached.
1031 # The "svn diff" command neither inserts newline characters between property values
1032 # nor between successive properties.
1034 # FIXME: We do not support property values that contain tailing newline characters
1035 # as it is difficult to disambiguate these trailing newlines from the empty
1036 # line that precedes the contents of a binary patch.
1038 my $propertyValueType;
1039 while (defined($_) && /$svnPropertyValueStartRegEx/) {
1040 # Note, a '-' property may be followed by a '+' property in the case of a "Modified"
1041 # or "Name" property. We only care about the ending value (i.e. the '+' property)
1042 # in such circumstances. So, we take the property value for the property to be its
1043 # last parsed property value.
1045 # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or
1046 # add error checking to prevent '+', '+', ..., '+' and other invalid combinations.
1047 $propertyValueType = $1;
1048 ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_);
1051 if (!$propertyValue) {
1052 die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\".");
1055 my $propertyChangeDelta;
1056 if ($propertyValueType eq "+" || $propertyValueType eq "Merged") {
1057 $propertyChangeDelta = 1;
1058 } elsif ($propertyValueType eq "-" || $propertyValueType eq "Reverse-merged") {
1059 $propertyChangeDelta = -1;
1061 die("Not reached.");
1064 # We perform a simple validation that an "Added" or "Deleted" property
1065 # change type corresponds with a "+" and "-" value type, respectively.
1066 my $expectedChangeDelta;
1067 if ($propertyChangeType eq "Added") {
1068 $expectedChangeDelta = 1;
1069 } elsif ($propertyChangeType eq "Deleted") {
1070 $expectedChangeDelta = -1;
1073 if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) {
1074 die("The final property value type found \"$propertyValueType\" does not " .
1075 "correspond to the property change type found \"$propertyChangeType\".");
1079 $propertyHash{name} = $propertyName;
1080 $propertyHash{propertyChangeDelta} = $propertyChangeDelta;
1081 $propertyHash{value} = $propertyValue;
1082 return (\%propertyHash, $_);
1085 # Parse the value of an SVN property from the given file handle, and advance
1086 # the handle so the last line read is the first line after the property value.
1088 # This subroutine dies if the first line is an invalid SVN property value line
1089 # (i.e. a line that does not begin with " +" or " -").
1092 # $fileHandle: advanced so the last line read from the handle is the first
1093 # line of the property value to parse. This should be a line
1094 # beginning with " +" or " -".
1095 # $line: the line last read from $fileHandle.
1097 # Returns ($propertyValue, $lastReadLine):
1098 # $propertyValue: the value of the property.
1099 # $lastReadLine: the line last read from $fileHandle.
1100 sub parseSvnPropertyValue($$)
1102 my ($fileHandle, $line) = @_;
1108 if (/$svnPropertyValueStartRegEx/) {
1109 $propertyValue = $2; # Does not include the end-of-line character(s).
1112 die("Failed to find property value beginning with '+', '-', 'Merged', or 'Reverse-merged': \"$_\".");
1115 while (<$fileHandle>) {
1116 if (/^[\r\n]+$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/) {
1117 # Note, we may encounter an empty line before the contents of a binary patch.
1118 # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be
1119 # followed by a '+' property in the case of a "Modified" or "Name" property.
1120 # We check for $svnPropertyStartRegEx because it indicates the start of the
1121 # next property to parse.
1125 # Temporarily strip off any end-of-line characters. We add the end-of-line characters
1126 # from the previously processed line to the start of this line so that the last line
1127 # of the property value does not end in end-of-line characters.
1129 $propertyValue .= "$eol$_";
1133 return ($propertyValue, $_);
1136 # Parse a patch file created by svn-create-patch.
1139 # $fileHandle: A file handle to the patch file that has not yet been
1143 # @diffHashRefs: an array of diff hash references.
1144 # See the %diffHash documentation above.
1147 my ($fileHandle) = @_;
1149 my $newDiffHashRefs;
1150 my @diffHashRefs; # return value
1152 my $line = <$fileHandle>;
1154 while (defined($line)) { # Otherwise, at EOF.
1156 ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line);
1158 push @diffHashRefs, @$newDiffHashRefs;
1161 return @diffHashRefs;
1164 # Prepare the results of parsePatch() for use in svn-apply and svn-unapply.
1167 # $shouldForce: Whether to continue processing if an unexpected
1169 # @diffHashRefs: An array of references to %diffHashes.
1170 # See the %diffHash documentation above.
1172 # Returns $preparedPatchHashRef:
1173 # copyDiffHashRefs: A reference to an array of the $diffHashRefs in
1174 # @diffHashRefs that represent file copies. The original
1175 # ordering is preserved.
1176 # nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in
1177 # @diffHashRefs that do not represent file copies.
1178 # The original ordering is preserved.
1179 # sourceRevisionHash: A reference to a hash of source path to source
1181 sub prepareParsedPatch($@)
1183 my ($shouldForce, @diffHashRefs) = @_;
1188 my @copyDiffHashRefs = ();
1189 my @nonCopyDiffHashRefs = ();
1190 my %sourceRevisionHash = ();
1191 for my $diffHashRef (@diffHashRefs) {
1192 my $copiedFromPath = $diffHashRef->{copiedFromPath};
1193 my $indexPath = $diffHashRef->{indexPath};
1194 my $sourceRevision = $diffHashRef->{sourceRevision};
1197 if (defined($copiedFromPath)) {
1198 # Then the diff is a copy operation.
1199 $sourcePath = $copiedFromPath;
1201 # FIXME: Consider printing a warning or exiting if
1202 # exists($copiedFiles{$indexPath}) is true -- i.e. if
1203 # $indexPath appears twice as a copy target.
1204 $copiedFiles{$indexPath} = $sourcePath;
1206 push @copyDiffHashRefs, $diffHashRef;
1208 # Then the diff is not a copy operation.
1209 $sourcePath = $indexPath;
1211 push @nonCopyDiffHashRefs, $diffHashRef;
1214 if (defined($sourceRevision)) {
1215 if (exists($sourceRevisionHash{$sourcePath}) &&
1216 ($sourceRevisionHash{$sourcePath} != $sourceRevision)) {
1217 if (!$shouldForce) {
1218 die "Two revisions of the same file required as a source:\n".
1219 " $sourcePath:$sourceRevisionHash{$sourcePath}\n".
1220 " $sourcePath:$sourceRevision";
1223 $sourceRevisionHash{$sourcePath} = $sourceRevision;
1227 my %preparedPatchHash;
1229 $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs;
1230 $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs;
1231 $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash;
1233 return \%preparedPatchHash;
1236 # Return localtime() for the project's time zone, given an integer time as
1237 # returned by Perl's time() function.
1238 sub localTimeInProjectTimeZone($)
1240 my $epochTime = shift;
1242 # Change the time zone temporarily for the localtime() call.
1243 my $savedTimeZone = $ENV{'TZ'};
1244 $ENV{'TZ'} = $changeLogTimeZone;
1245 my @localTime = localtime($epochTime);
1246 if (defined $savedTimeZone) {
1247 $ENV{'TZ'} = $savedTimeZone;
1255 # Set the reviewer and date in a ChangeLog patch, and return the new patch.
1258 # $patch: a ChangeLog patch as a string.
1259 # $reviewer: the name of the reviewer, or undef if the reviewer should not be set.
1260 # $epochTime: an integer time as returned by Perl's time() function.
1261 sub setChangeLogDateAndReviewer($$$)
1263 my ($patch, $reviewer, $epochTime) = @_;
1265 my @localTime = localTimeInProjectTimeZone($epochTime);
1266 my $newDate = strftime("%Y-%m-%d", @localTime);
1268 my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}( )#;
1269 $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/;
1271 if (defined($reviewer)) {
1272 # We include a leading plus ("+") in the regular expression to make
1273 # the regular expression less likely to match text in the leading junk
1274 # for the patch, if the patch has leading junk.
1275 $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/;
1281 # If possible, returns a ChangeLog patch equivalent to the given one,
1282 # but with the newest ChangeLog entry inserted at the top of the
1283 # file -- i.e. no leading context and all lines starting with "+".
1285 # If given a patch string not representable as a patch with the above
1286 # properties, it returns the input back unchanged.
1288 # WARNING: This subroutine can return an inequivalent patch string if
1289 # both the beginning of the new ChangeLog file matches the beginning
1290 # of the source ChangeLog, and the source beginning was modified.
1291 # Otherwise, it is guaranteed to return an equivalent patch string,
1294 # Applying this subroutine to ChangeLog patches allows svn-apply to
1295 # insert new ChangeLog entries at the top of the ChangeLog file.
1296 # svn-apply uses patch with --fuzz=3 to do this. We need to apply
1297 # this subroutine because the diff(1) command is greedy when matching
1298 # lines. A new ChangeLog entry with the same date and author as the
1299 # previous will match and cause the diff to have lines of starting
1302 # This subroutine has unit tests in VCSUtils_unittest.pl.
1304 # Returns $changeLogHashRef:
1305 # $changeLogHashRef: a hash reference representing a change log patch.
1306 # patch: a ChangeLog patch equivalent to the given one, but with the
1307 # newest ChangeLog entry inserted at the top of the file, if possible.
1308 # hasOverlappingLines: the value 1 if the change log entry overlaps
1309 # some lines of another change log entry. This can
1310 # happen when deliberately inserting a new ChangeLog
1311 # entry earlier in the file above an entry with
1312 # the same date and author.
1313 sub fixChangeLogPatch($)
1315 my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
1317 $patch =~ /(\r?\n)/;
1318 my $lineEnding = $1;
1319 my @lines = split(/$lineEnding/, $patch);
1321 my $i = 0; # We reuse the same index throughout.
1323 # Skip to beginning of first chunk.
1324 for (; $i < @lines; ++$i) {
1325 if (substr($lines[$i], 0, 1) eq "@") {
1329 my $chunkStartIndex = ++$i;
1330 my %changeLogHashRef;
1332 # Optimization: do not process if new lines already begin the chunk.
1333 if (substr($lines[$i], 0, 1) eq "+") {
1334 $changeLogHashRef{patch} = $patch;
1335 return \%changeLogHashRef;
1338 # Skip to first line of newly added ChangeLog entry.
1339 # For example, +2009-06-03 Eric Seidel <eric@webkit.org>
1340 my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
1341 . '\s+(.+)\s+' # name
1342 . '<([^<>]+)>$'; # e-mail address
1344 for (; $i < @lines; ++$i) {
1345 my $line = $lines[$i];
1346 my $firstChar = substr($line, 0, 1);
1347 if ($line =~ /$dateStartRegEx/) {
1349 } elsif ($firstChar eq " " or $firstChar eq "+") {
1352 $changeLogHashRef{patch} = $patch; # Do not change if, for example, "-" or "@" found.
1353 return \%changeLogHashRef;
1356 $changeLogHashRef{patch} = $patch; # Do not change if date not found.
1357 return \%changeLogHashRef;
1359 my $dateStartIndex = $i;
1361 # Rewrite overlapping lines to lead with " ".
1362 my @overlappingLines = (); # These will include a leading "+".
1363 for (; $i < @lines; ++$i) {
1364 my $line = $lines[$i];
1365 if (substr($line, 0, 1) ne "+") {
1368 push(@overlappingLines, $line);
1369 $lines[$i] = " " . substr($line, 1);
1372 # Remove excess ending context, if necessary.
1373 my $shouldTrimContext = 1;
1374 for (; $i < @lines; ++$i) {
1375 my $firstChar = substr($lines[$i], 0, 1);
1376 if ($firstChar eq " ") {
1378 } elsif ($firstChar eq "@") {
1381 $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
1384 my $deletedLineCount = 0;
1385 if ($shouldTrimContext) { # Also occurs if end of file reached.
1386 splice(@lines, $i - @overlappingLines, @overlappingLines);
1387 $deletedLineCount = @overlappingLines;
1390 # Work backwards, shifting overlapping lines towards front
1391 # while checking that patch stays equivalent.
1392 for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) {
1393 my $line = $lines[$i];
1394 if (substr($line, 0, 1) ne " ") {
1397 my $text = substr($line, 1);
1398 my $newLine = pop(@overlappingLines);
1399 if ($text ne substr($newLine, 1)) {
1400 $changeLogHashRef{patch} = $patch; # Unexpected difference.
1401 return \%changeLogHashRef;
1403 $lines[$i] = "+$text";
1406 # Finish moving whatever overlapping lines remain, and update
1407 # the initial chunk range.
1408 my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@
1409 if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) {
1410 # FIXME: Handle errors differently from ChangeLog files that
1411 # are okay but should not be altered. That way we can find out
1412 # if improvements to the script ever become necessary.
1413 $changeLogHashRef{patch} = $patch; # Error: unexpected patch string format.
1414 return \%changeLogHashRef;
1416 my $skippedFirstLineCount = $1 - 1;
1417 my $oldSourceLineCount = $2;
1418 my $oldTargetLineCount = $3;
1420 if (@overlappingLines != $skippedFirstLineCount) {
1421 # This can happen, for example, when deliberately inserting
1422 # a new ChangeLog entry earlier in the file.
1423 $changeLogHashRef{hasOverlappingLines} = 1;
1424 $changeLogHashRef{patch} = $patch;
1425 return \%changeLogHashRef;
1427 # If @overlappingLines > 0, this is where we make use of the
1428 # assumption that the beginning of the source file was not modified.
1429 splice(@lines, $chunkStartIndex, 0, @overlappingLines);
1431 my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
1432 my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
1433 $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
1435 $changeLogHashRef{patch} = join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
1436 return \%changeLogHashRef;
1439 # This is a supporting method for runPatchCommand.
1441 # Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
1443 # Returns ($patchCommand, $isForcing).
1445 # This subroutine has unit tests in VCSUtils_unittest.pl.
1446 sub generatePatchCommand($)
1448 my ($passedArgsHashRef) = @_;
1450 my $argsHashRef = { # Defaults
1456 # Merges hash references. It's okay here if passed hash reference is undefined.
1457 @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
1459 my $ensureForce = $argsHashRef->{ensureForce};
1460 my $shouldReverse = $argsHashRef->{shouldReverse};
1461 my $options = $argsHashRef->{options};
1466 $options = [@{$options}]; # Copy to avoid side effects.
1470 if (grep /^--force$/, @{$options}) {
1472 } elsif ($ensureForce) {
1473 push @{$options}, "--force";
1477 if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
1478 push @{$options}, "--reverse";
1481 @{$options} = sort(@{$options}); # For easier testing.
1483 my $patchCommand = join(" ", "patch -p0", @{$options});
1485 return ($patchCommand, $isForcing);
1488 # Apply the given patch using the patch(1) command.
1490 # On success, return the resulting exit status. Otherwise, exit with the
1491 # exit status. If "--force" is passed as an option, however, then never
1492 # exit and always return the exit status.
1495 # $patch: a patch string.
1496 # $repositoryRootPath: an absolute path to the repository root.
1497 # $pathRelativeToRoot: the path of the file to be patched, relative to the
1498 # repository root. This should normally be the path
1499 # found in the patch's "Index:" line. It is passed
1500 # explicitly rather than reparsed from the patch
1501 # string for optimization purposes.
1502 # This is used only for error reporting. The
1503 # patch command gleans the actual file to patch
1504 # from the patch string.
1505 # $args: a reference to a hash of optional arguments. The possible
1507 # ensureForce: whether to ensure --force is passed (defaults to 0).
1508 # shouldReverse: whether to pass --reverse (defaults to 0).
1509 # options: a reference to an array of options to pass to the
1510 # patch command. The subroutine passes the -p0 option
1511 # no matter what. This should not include --reverse.
1513 # This subroutine has unit tests in VCSUtils_unittest.pl.
1514 sub runPatchCommand($$$;$)
1516 my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
1518 my ($patchCommand, $isForcing) = generatePatchCommand($args);
1520 # Temporarily change the working directory since the path found
1521 # in the patch's "Index:" line is relative to the repository root
1522 # (i.e. the same as $pathRelativeToRoot).
1523 my $cwd = Cwd::getcwd();
1524 chdir $repositoryRootPath;
1526 open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
1529 my $exitStatus = exitStatus($?);
1533 if ($exitStatus && !$isForcing) {
1534 print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
1535 "status $exitStatus. Pass --force to ignore patch failures.\n";
1542 # Merge ChangeLog patches using a three-file approach.
1544 # This is used by resolve-ChangeLogs when it's operated as a merge driver
1545 # and when it's used to merge conflicts after a patch is applied or after
1548 # It's also used for traditional rejected patches.
1551 # $fileMine: The merged version of the file. Also known in git as the
1552 # other branch's version (%B) or "ours".
1553 # For traditional patch rejects, this is the *.rej file.
1554 # $fileOlder: The base version of the file. Also known in git as the
1555 # ancestor version (%O) or "base".
1556 # For traditional patch rejects, this is the *.orig file.
1557 # $fileNewer: The current version of the file. Also known in git as the
1558 # current version (%A) or "theirs".
1559 # For traditional patch rejects, this is the original-named
1562 # Returns 1 if merge was successful, else 0.
1563 sub mergeChangeLogs($$$)
1565 my ($fileMine, $fileOlder, $fileNewer) = @_;
1567 my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0;
1572 if ($traditionalReject) {
1573 open(DIFF, "<", $fileMine) or die $!;
1576 rename($fileMine, "$fileMine.save");
1577 rename($fileOlder, "$fileOlder.save");
1579 open(DIFF, "-|", qw(diff -u -a --binary), $fileOlder, $fileMine) or die $!;
1584 unlink("${fileNewer}.orig");
1585 unlink("${fileNewer}.rej");
1587 open(PATCH, "| patch --force --fuzz=3 --binary $fileNewer > " . File::Spec->devnull()) or die $!;
1588 if ($traditionalReject) {
1591 my $changeLogHash = fixChangeLogPatch($patch);
1592 print PATCH $changeLogHash->{patch};
1596 my $result = !exitStatus($?);
1598 # Refuse to merge the patch if it did not apply cleanly
1599 if (-e "${fileNewer}.rej") {
1600 unlink("${fileNewer}.rej");
1601 if (-f "${fileNewer}.orig") {
1603 rename("${fileNewer}.orig", $fileNewer);
1606 unlink("${fileNewer}.orig");
1609 if ($traditionalReject) {
1610 rename("$fileMine.save", $fileMine);
1611 rename("$fileOlder.save", $fileOlder);
1619 return unless $isGit;
1623 my $result = `git config $config`;
1625 $result = `git repo-config $config`;
1631 sub changeLogNameError($)
1634 print STDERR "$message\nEither:\n";
1635 print STDERR " set CHANGE_LOG_NAME in your environment\n";
1636 print STDERR " OR pass --name= on the command line\n";
1637 print STDERR " OR set REAL_NAME in your environment";
1638 print STDERR " OR git users can set 'git config user.name'\n";
1644 my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
1646 changeLogNameError("Failed to determine ChangeLog name.") unless $name;
1647 # getpwuid seems to always succeed on windows, returning the username instead of the full name. This check will catch that case.
1648 changeLogNameError("'$name' does not contain a space! ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
1653 sub changeLogEmailAddressError($)
1656 print STDERR "$message\nEither:\n";
1657 print STDERR " set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
1658 print STDERR " OR pass --email= on the command line\n";
1659 print STDERR " OR set EMAIL_ADDRESS in your environment\n";
1660 print STDERR " OR git users can set 'git config user.email'\n";
1664 sub changeLogEmailAddress()
1666 my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
1668 changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
1669 changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
1671 return $emailAddress;
1674 # http://tools.ietf.org/html/rfc1924
1679 my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
1680 for (my $i = 0; $i < 85; $i++) {
1681 $table{$characters[$i]} = $i;
1685 my @encodedChars = $encoded =~ /./g;
1687 for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
1689 for (my $i = 0; $i < 5; $i++) {
1691 my $char = $encodedChars[$encodedIter];
1692 $digit += $table{$char};
1696 for (my $i = 0; $i < 4; $i++) {
1697 $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
1704 sub decodeGitBinaryChunk($$)
1706 my ($contents, $fullPath) = @_;
1708 # Load this module lazily in case the user don't have this module
1709 # and won't handle git binary patches.
1710 require Compress::Zlib;
1713 my $compressedSize = 0;
1714 while ($contents =~ /^([A-Za-z])(.*)$/gm) {
1716 next if $line eq "";
1717 die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
1718 my $actualSize = length($2) / 5 * 4;
1719 my $encodedExpectedSize = ord($1);
1720 my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
1722 die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
1723 $compressedSize += $expectedSize;
1727 my $compressed = decodeBase85($encoded);
1728 $compressed = substr($compressed, 0, $compressedSize);
1729 return Compress::Zlib::uncompress($compressed);
1732 sub decodeGitBinaryPatch($$)
1734 my ($contents, $fullPath) = @_;
1736 # Git binary patch has two chunks. One is for the normal patching
1737 # and another is for the reverse patching.
1739 # Each chunk a line which starts from either "literal" or "delta",
1740 # followed by a number which specifies decoded size of the chunk.
1741 # The "delta" type chunks aren't supported by this function yet.
1743 # Then, content of the chunk comes. To decode the content, we
1744 # need decode it with base85 first, and then zlib.
1745 my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
1746 if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
1747 die "$fullPath: unknown git binary patch format"
1750 my $binaryChunkType = $1;
1751 my $binaryChunkExpectedSize = $2;
1752 my $encodedChunk = $3;
1753 my $reverseBinaryChunkType = $4;
1754 my $reverseBinaryChunkExpectedSize = $5;
1755 my $encodedReverseChunk = $6;
1757 my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
1758 my $binaryChunkActualSize = length($binaryChunk);
1759 my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
1760 my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
1762 die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize);
1763 die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
1765 return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);