• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (C) 2007, 2008, 2009 Apple Inc.  All rights reserved.
2# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions
6# are met:
7#
8# 1.  Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10# 2.  Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14#     its contributors may be used to endorse or promote products derived
15#     from this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28# Module to share code to work with various version control systems.
29package VCSUtils;
30
31use strict;
32use warnings;
33
34use Cwd qw();  # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
35use English; # for $POSTMATCH, etc.
36use File::Basename;
37use File::Spec;
38use POSIX;
39
40BEGIN {
41    use Exporter   ();
42    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
43    $VERSION     = 1.00;
44    @ISA         = qw(Exporter);
45    @EXPORT      = qw(
46        &canonicalizePath
47        &changeLogEmailAddress
48        &changeLogName
49        &chdirReturningRelativePath
50        &decodeGitBinaryPatch
51        &determineSVNRoot
52        &determineVCSRoot
53        &exitStatus
54        &fixChangeLogPatch
55        &gitBranch
56        &gitdiff2svndiff
57        &isGit
58        &isGitBranchBuild
59        &isGitDirectory
60        &isSVN
61        &isSVNDirectory
62        &isSVNVersion16OrNewer
63        &makeFilePathRelative
64        &normalizePath
65        &parsePatch
66        &pathRelativeToSVNRepositoryRootForPath
67        &runPatchCommand
68        &svnRevisionForDirectory
69        &svnStatus
70    );
71    %EXPORT_TAGS = ( );
72    @EXPORT_OK   = ();
73}
74
75our @EXPORT_OK;
76
77my $gitBranch;
78my $gitRoot;
79my $isGit;
80my $isGitBranchBuild;
81my $isSVN;
82my $svnVersion;
83
84# This method is for portability. Return the system-appropriate exit
85# status of a child process.
86#
87# Args: pass the child error status returned by the last pipe close,
88#       for example "$?".
89sub exitStatus($)
90{
91    my ($returnvalue) = @_;
92    if ($^O eq "MSWin32") {
93        return $returnvalue >> 8;
94    }
95    return WEXITSTATUS($returnvalue);
96}
97
98sub isGitDirectory($)
99{
100    my ($dir) = @_;
101    return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
102}
103
104sub isGit()
105{
106    return $isGit if defined $isGit;
107
108    $isGit = isGitDirectory(".");
109    return $isGit;
110}
111
112sub gitBranch()
113{
114    unless (defined $gitBranch) {
115        chomp($gitBranch = `git symbolic-ref -q HEAD`);
116        $gitBranch = "" if exitStatus($?);
117        $gitBranch =~ s#^refs/heads/##;
118        $gitBranch = "" if $gitBranch eq "master";
119    }
120
121    return $gitBranch;
122}
123
124sub isGitBranchBuild()
125{
126    my $branch = gitBranch();
127    chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
128    return 1 if $override eq "true";
129    return 0 if $override eq "false";
130
131    unless (defined $isGitBranchBuild) {
132        chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
133        $isGitBranchBuild = $gitBranchBuild eq "true";
134    }
135
136    return $isGitBranchBuild;
137}
138
139sub isSVNDirectory($)
140{
141    my ($dir) = @_;
142
143    return -d File::Spec->catdir($dir, ".svn");
144}
145
146sub isSVN()
147{
148    return $isSVN if defined $isSVN;
149
150    $isSVN = isSVNDirectory(".");
151    return $isSVN;
152}
153
154sub svnVersion()
155{
156    return $svnVersion if defined $svnVersion;
157
158    if (!isSVN()) {
159        $svnVersion = 0;
160    } else {
161        chomp($svnVersion = `svn --version --quiet`);
162    }
163    return $svnVersion;
164}
165
166sub isSVNVersion16OrNewer()
167{
168    my $version = svnVersion();
169    return eval "v$version" ge v1.6;
170}
171
172sub chdirReturningRelativePath($)
173{
174    my ($directory) = @_;
175    my $previousDirectory = Cwd::getcwd();
176    chdir $directory;
177    my $newDirectory = Cwd::getcwd();
178    return "." if $newDirectory eq $previousDirectory;
179    return File::Spec->abs2rel($previousDirectory, $newDirectory);
180}
181
182sub determineGitRoot()
183{
184    chomp(my $gitDir = `git rev-parse --git-dir`);
185    return dirname($gitDir);
186}
187
188sub determineSVNRoot()
189{
190    my $last = '';
191    my $path = '.';
192    my $parent = '..';
193    my $repositoryRoot;
194    my $repositoryUUID;
195    while (1) {
196        my $thisRoot;
197        my $thisUUID;
198        # Ignore error messages in case we've run past the root of the checkout.
199        open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die;
200        while (<INFO>) {
201            if (/^Repository Root: (.+)/) {
202                $thisRoot = $1;
203            }
204            if (/^Repository UUID: (.+)/) {
205                $thisUUID = $1;
206            }
207            if ($thisRoot && $thisUUID) {
208                local $/ = undef;
209                <INFO>; # Consume the rest of the input.
210            }
211        }
212        close INFO;
213
214        # It's possible (e.g. for developers of some ports) to have a WebKit
215        # checkout in a subdirectory of another checkout.  So abort if the
216        # repository root or the repository UUID suddenly changes.
217        last if !$thisUUID;
218        $repositoryUUID = $thisUUID if !$repositoryUUID;
219        last if $thisUUID ne $repositoryUUID;
220
221        last if !$thisRoot;
222        $repositoryRoot = $thisRoot if !$repositoryRoot;
223        last if $thisRoot ne $repositoryRoot;
224
225        $last = $path;
226        $path = File::Spec->catdir($parent, $path);
227    }
228
229    return File::Spec->rel2abs($last);
230}
231
232sub determineVCSRoot()
233{
234    if (isGit()) {
235        return determineGitRoot();
236    }
237
238    if (!isSVN()) {
239        # Some users have a workflow where svn-create-patch, svn-apply and
240        # svn-unapply are used outside of multiple svn working directores,
241        # so warn the user and assume Subversion is being used in this case.
242        warn "Unable to determine VCS root; assuming Subversion";
243        $isSVN = 1;
244    }
245
246    return determineSVNRoot();
247}
248
249sub svnRevisionForDirectory($)
250{
251    my ($dir) = @_;
252    my $revision;
253
254    if (isSVNDirectory($dir)) {
255        my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`;
256        ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
257    } elsif (isGitDirectory($dir)) {
258        my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`;
259        ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
260    }
261    die "Unable to determine current SVN revision in $dir" unless (defined $revision);
262    return $revision;
263}
264
265sub pathRelativeToSVNRepositoryRootForPath($)
266{
267    my ($file) = @_;
268    my $relativePath = File::Spec->abs2rel($file);
269
270    my $svnInfo;
271    if (isSVN()) {
272        $svnInfo = `LC_ALL=C svn info $relativePath`;
273    } elsif (isGit()) {
274        $svnInfo = `LC_ALL=C git svn info $relativePath`;
275    }
276
277    $svnInfo =~ /.*^URL: (.*?)$/m;
278    my $svnURL = $1;
279
280    $svnInfo =~ /.*^Repository Root: (.*?)$/m;
281    my $repositoryRoot = $1;
282
283    $svnURL =~ s/$repositoryRoot\///;
284    return $svnURL;
285}
286
287sub makeFilePathRelative($)
288{
289    my ($path) = @_;
290    return $path unless isGit();
291
292    unless (defined $gitRoot) {
293        chomp($gitRoot = `git rev-parse --show-cdup`);
294    }
295    return $gitRoot . $path;
296}
297
298sub normalizePath($)
299{
300    my ($path) = @_;
301    $path =~ s/\\/\//g;
302    return $path;
303}
304
305sub canonicalizePath($)
306{
307    my ($file) = @_;
308
309    # Remove extra slashes and '.' directories in path
310    $file = File::Spec->canonpath($file);
311
312    # Remove '..' directories in path
313    my @dirs = ();
314    foreach my $dir (File::Spec->splitdir($file)) {
315        if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
316            pop(@dirs);
317        } else {
318            push(@dirs, $dir);
319        }
320    }
321    return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
322}
323
324sub removeEOL($)
325{
326    my ($line) = @_;
327
328    $line =~ s/[\r\n]+$//g;
329    return $line;
330}
331
332sub svnStatus($)
333{
334    my ($fullPath) = @_;
335    my $svnStatus;
336    open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die;
337    if (-d $fullPath) {
338        # When running "svn stat" on a directory, we can't assume that only one
339        # status will be returned (since any files with a status below the
340        # directory will be returned), and we can't assume that the directory will
341        # be first (since any files with unknown status will be listed first).
342        my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
343        while (<SVN>) {
344            # Input may use a different EOL sequence than $/, so avoid chomp.
345            $_ = removeEOL($_);
346            my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
347            if ($normalizedFullPath eq $normalizedStatPath) {
348                $svnStatus = "$_\n";
349                last;
350            }
351        }
352        # Read the rest of the svn command output to avoid a broken pipe warning.
353        local $/ = undef;
354        <SVN>;
355    }
356    else {
357        # Files will have only one status returned.
358        $svnStatus = removeEOL(<SVN>) . "\n";
359    }
360    close SVN;
361    return $svnStatus;
362}
363
364# Convert a line of a git-formatted patch to SVN format, while
365# preserving any end-of-line characters.
366sub gitdiff2svndiff($)
367{
368    $_ = shift @_;
369
370    if (m#^diff --git \w/(.+) \w/([^\r\n]+)#) {
371        return "Index: $1$POSTMATCH";
372    }
373    if (m#^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}#) {
374        # FIXME: No need to return dividing line once parseDiffHeader() is used.
375        return "===================================================================$POSTMATCH";
376    }
377    if (m#^--- \w/([^\r\n]+)#) {
378        return "--- $1$POSTMATCH";
379    }
380    if (m#^\+\+\+ \w/([^\r\n]+)#) {
381        return "+++ $1$POSTMATCH";
382    }
383    return $_;
384}
385
386# Parse the next diff header from the given file handle, and advance
387# the file handle so the last line read is the first line after the
388# parsed header block.
389#
390# This subroutine dies if given leading junk or if the end of the header
391# block could not be detected. The last line of a header block is a
392# line beginning with "+++".
393#
394# Args:
395#   $fileHandle: advanced so the last line read is the first line of the
396#                next diff header. For SVN-formatted diffs, this is the
397#                "Index:" line.
398#   $line: the line last read from $fileHandle
399#
400# Returns ($headerHashRef, $lastReadLine):
401#   $headerHashRef: a hash reference representing a diff header
402#     copiedFromPath: if a file copy, the path from which the file was
403#                     copied. Otherwise, undefined.
404#     indexPath: the path in the "Index:" line.
405#     sourceRevision: the revision number of the source. This is the same
406#                     as the revision number the file was copied from, in
407#                     the case of a file copy.
408#     svnConvertedText: the header text converted to SVN format.
409#                       Unrecognized lines are discarded.
410#   $lastReadLine: the line last read from $fileHandle. This is the first
411#                  line after the header ending.
412sub parseDiffHeader($$)
413{
414    my ($fileHandle, $line) = @_;
415
416    my $filter;
417    if ($line =~ m#^diff --git #) {
418        $filter = \&gitdiff2svndiff;
419    }
420    $line = &$filter($line) if $filter;
421
422    my $indexPath;
423    if ($line =~ /^Index: ([^\r\n]+)/) {
424        $indexPath = $1;
425    } else {
426        die("Could not parse first line of diff header: \"$line\".");
427    }
428
429    my %header;
430
431    my $foundHeaderEnding;
432    my $lastReadLine;
433    my $sourceRevision;
434    my $svnConvertedText = $line;
435    while (<$fileHandle>) {
436        # Temporarily strip off any end-of-line characters to simplify
437        # regex matching below.
438        s/([\n\r]+)$//;
439        my $eol = $1;
440
441        $_ = &$filter($_) if $filter;
442
443        # Fix paths on ""---" and "+++" lines to match the leading
444        # index line.
445        if (s/^--- \S+/--- $indexPath/) {
446            # ---
447            if (/^--- .+\(revision (\d+)\)/) {
448                $sourceRevision = $1 if ($1 != 0);
449                if (/\(from (\S+):(\d+)\)$/) {
450                    # The "from" clause is created by svn-create-patch, in
451                    # which case there is always also a "revision" clause.
452                    $header{copiedFromPath} = $1;
453                    die("Revision number \"$2\" in \"from\" clause does not match " .
454                        "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
455                }
456            }
457            $_ = "=" x 67 . "$eol$_"; # Prepend dividing line ===....
458        } elsif (s/^\+\+\+ \S+/+++ $indexPath/) {
459            # +++
460            $foundHeaderEnding = 1;
461        } else {
462            # Skip unrecognized lines.
463            next;
464        }
465
466        $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
467        if ($foundHeaderEnding) {
468            $lastReadLine = <$fileHandle>;
469            last;
470        }
471    } # $lastReadLine is undef if while loop ran out.
472
473    if (!$foundHeaderEnding) {
474        die("Did not find end of header block corresponding to index path \"$indexPath\".");
475    }
476
477    $header{indexPath} = $indexPath;
478    $header{sourceRevision} = $sourceRevision;
479    $header{svnConvertedText} = $svnConvertedText;
480
481    return (\%header, $lastReadLine);
482}
483
484# Parse one diff from a patch file created by svn-create-patch, and
485# advance the file handle so the last line read is the first line
486# of the next header block.
487#
488# This subroutine preserves any leading junk encountered before the header.
489#
490# Args:
491#   $fileHandle: a file handle advanced to the first line of the next
492#                header block. Leading junk is okay.
493#   $line: the line last read from $fileHandle.
494#
495# Returns ($diffHashRef, $lastReadLine):
496#   $diffHashRef:
497#     copiedFromPath: if a file copy, the path from which the file was
498#                     copied. Otherwise, undefined.
499#     indexPath: the path in the "Index:" line.
500#     sourceRevision: the revision number of the source. This is the same
501#                     as the revision number the file was copied from, in
502#                     the case of a file copy.
503#     svnConvertedText: the diff converted to SVN format.
504#   $lastReadLine: the line last read from $fileHandle
505sub parseDiff($$)
506{
507    my ($fileHandle, $line) = @_;
508
509    my $headerStartRegEx = qr#^Index: #; # SVN-style header for the default
510    my $gitHeaderStartRegEx = qr#^diff --git \w/#;
511
512    my $headerHashRef; # Last header found, as returned by parseDiffHeader().
513    my $svnText;
514    while (defined($line)) {
515        if (!$headerHashRef && ($line =~ $gitHeaderStartRegEx)) {
516            # Then assume all diffs in the patch are Git-formatted. This
517            # block was made to be enterable at most once since we assume
518            # all diffs in the patch are formatted the same (SVN or Git).
519            $headerStartRegEx = $gitHeaderStartRegEx;
520        }
521
522        if ($line !~ $headerStartRegEx) {
523            # Then we are in the body of the diff.
524            $svnText .= $line;
525            $line = <$fileHandle>;
526            next;
527        } # Otherwise, we found a diff header.
528
529        if ($headerHashRef) {
530            # Then this is the second diff header of this while loop.
531            last;
532        }
533
534        ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
535
536        $svnText .= $headerHashRef->{svnConvertedText};
537    }
538
539    my %diffHashRef;
540    $diffHashRef{copiedFromPath} = $headerHashRef->{copiedFromPath};
541    $diffHashRef{indexPath} = $headerHashRef->{indexPath};
542    $diffHashRef{sourceRevision} = $headerHashRef->{sourceRevision};
543    $diffHashRef{svnConvertedText} = $svnText;
544
545    return (\%diffHashRef, $line);
546}
547
548# Parse a patch file created by svn-create-patch.
549#
550# Args:
551#   $fileHandle: A file handle to the patch file that has not yet been
552#                read from.
553#
554# Returns:
555#   @diffHashRefs: an array of diff hash references. See parseDiff() for
556#                  a description of each $diffHashRef.
557sub parsePatch($)
558{
559    my ($fileHandle) = @_;
560
561    my @diffHashRefs; # return value
562
563    my $line = <$fileHandle>;
564
565    while (defined($line)) { # Otherwise, at EOF.
566
567        my $diffHashRef;
568        ($diffHashRef, $line) = parseDiff($fileHandle, $line);
569
570        push @diffHashRefs, $diffHashRef;
571    }
572
573    return @diffHashRefs;
574}
575
576# If possible, returns a ChangeLog patch equivalent to the given one,
577# but with the newest ChangeLog entry inserted at the top of the
578# file -- i.e. no leading context and all lines starting with "+".
579#
580# If given a patch string not representable as a patch with the above
581# properties, it returns the input back unchanged.
582#
583# WARNING: This subroutine can return an inequivalent patch string if
584# both the beginning of the new ChangeLog file matches the beginning
585# of the source ChangeLog, and the source beginning was modified.
586# Otherwise, it is guaranteed to return an equivalent patch string,
587# if it returns.
588#
589# Applying this subroutine to ChangeLog patches allows svn-apply to
590# insert new ChangeLog entries at the top of the ChangeLog file.
591# svn-apply uses patch with --fuzz=3 to do this. We need to apply
592# this subroutine because the diff(1) command is greedy when matching
593# lines. A new ChangeLog entry with the same date and author as the
594# previous will match and cause the diff to have lines of starting
595# context.
596#
597# This subroutine has unit tests in VCSUtils_unittest.pl.
598sub fixChangeLogPatch($)
599{
600    my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
601
602    $patch =~ /(\r?\n)/;
603    my $lineEnding = $1;
604    my @lines = split(/$lineEnding/, $patch);
605
606    my $i = 0; # We reuse the same index throughout.
607
608    # Skip to beginning of first chunk.
609    for (; $i < @lines; ++$i) {
610        if (substr($lines[$i], 0, 1) eq "@") {
611            last;
612        }
613    }
614    my $chunkStartIndex = ++$i;
615
616    # Optimization: do not process if new lines already begin the chunk.
617    if (substr($lines[$i], 0, 1) eq "+") {
618        return $patch;
619    }
620
621    # Skip to first line of newly added ChangeLog entry.
622    # For example, +2009-06-03  Eric Seidel  <eric@webkit.org>
623    my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
624                         . '\s+(.+)\s+' # name
625                         . '<([^<>]+)>$'; # e-mail address
626
627    for (; $i < @lines; ++$i) {
628        my $line = $lines[$i];
629        my $firstChar = substr($line, 0, 1);
630        if ($line =~ /$dateStartRegEx/) {
631            last;
632        } elsif ($firstChar eq " " or $firstChar eq "+") {
633            next;
634        }
635        return $patch; # Do not change if, for example, "-" or "@" found.
636    }
637    if ($i >= @lines) {
638        return $patch; # Do not change if date not found.
639    }
640    my $dateStartIndex = $i;
641
642    # Rewrite overlapping lines to lead with " ".
643    my @overlappingLines = (); # These will include a leading "+".
644    for (; $i < @lines; ++$i) {
645        my $line = $lines[$i];
646        if (substr($line, 0, 1) ne "+") {
647          last;
648        }
649        push(@overlappingLines, $line);
650        $lines[$i] = " " . substr($line, 1);
651    }
652
653    # Remove excess ending context, if necessary.
654    my $shouldTrimContext = 1;
655    for (; $i < @lines; ++$i) {
656        my $firstChar = substr($lines[$i], 0, 1);
657        if ($firstChar eq " ") {
658            next;
659        } elsif ($firstChar eq "@") {
660            last;
661        }
662        $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
663        last;
664    }
665    my $deletedLineCount = 0;
666    if ($shouldTrimContext) { # Also occurs if end of file reached.
667        splice(@lines, $i - @overlappingLines, @overlappingLines);
668        $deletedLineCount = @overlappingLines;
669    }
670
671    # Work backwards, shifting overlapping lines towards front
672    # while checking that patch stays equivalent.
673    for ($i = $dateStartIndex - 1; $i >= $chunkStartIndex; --$i) {
674        my $line = $lines[$i];
675        if (substr($line, 0, 1) ne " ") {
676            next;
677        }
678        my $text = substr($line, 1);
679        my $newLine = pop(@overlappingLines);
680        if ($text ne substr($newLine, 1)) {
681            return $patch; # Unexpected difference.
682        }
683        $lines[$i] = "+$text";
684    }
685
686    # Finish moving whatever overlapping lines remain, and update
687    # the initial chunk range.
688    my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@
689    if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) {
690        # FIXME: Handle errors differently from ChangeLog files that
691        # are okay but should not be altered. That way we can find out
692        # if improvements to the script ever become necessary.
693        return $patch; # Error: unexpected patch string format.
694    }
695    my $skippedFirstLineCount = $1 - 1;
696    my $oldSourceLineCount = $2;
697    my $oldTargetLineCount = $3;
698
699    if (@overlappingLines != $skippedFirstLineCount) {
700        # This can happen, for example, when deliberately inserting
701        # a new ChangeLog entry earlier in the file.
702        return $patch;
703    }
704    # If @overlappingLines > 0, this is where we make use of the
705    # assumption that the beginning of the source file was not modified.
706    splice(@lines, $chunkStartIndex, 0, @overlappingLines);
707
708    my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
709    my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
710    $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
711
712    return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
713}
714
715# This is a supporting method for runPatchCommand.
716#
717# Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
718#
719# Returns ($patchCommand, $isForcing).
720#
721# This subroutine has unit tests in VCSUtils_unittest.pl.
722sub generatePatchCommand($)
723{
724    my ($passedArgsHashRef) = @_;
725
726    my $argsHashRef = { # Defaults
727        ensureForce => 0,
728        shouldReverse => 0,
729        options => []
730    };
731
732    # Merges hash references. It's okay here if passed hash reference is undefined.
733    @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
734
735    my $ensureForce = $argsHashRef->{ensureForce};
736    my $shouldReverse = $argsHashRef->{shouldReverse};
737    my $options = $argsHashRef->{options};
738
739    if (! $options) {
740        $options = [];
741    } else {
742        $options = [@{$options}]; # Copy to avoid side effects.
743    }
744
745    my $isForcing = 0;
746    if (grep /^--force$/, @{$options}) {
747        $isForcing = 1;
748    } elsif ($ensureForce) {
749        push @{$options}, "--force";
750        $isForcing = 1;
751    }
752
753    if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
754        push @{$options}, "--reverse";
755    }
756
757    @{$options} = sort(@{$options}); # For easier testing.
758
759    my $patchCommand = join(" ", "patch -p0", @{$options});
760
761    return ($patchCommand, $isForcing);
762}
763
764# Apply the given patch using the patch(1) command.
765#
766# On success, return the resulting exit status. Otherwise, exit with the
767# exit status. If "--force" is passed as an option, however, then never
768# exit and always return the exit status.
769#
770# Args:
771#   $patch: a patch string.
772#   $repositoryRootPath: an absolute path to the repository root.
773#   $pathRelativeToRoot: the path of the file to be patched, relative to the
774#                        repository root. This should normally be the path
775#                        found in the patch's "Index:" line. It is passed
776#                        explicitly rather than reparsed from the patch
777#                        string for optimization purposes.
778#                            This is used only for error reporting. The
779#                        patch command gleans the actual file to patch
780#                        from the patch string.
781#   $args: a reference to a hash of optional arguments. The possible
782#          keys are --
783#            ensureForce: whether to ensure --force is passed (defaults to 0).
784#            shouldReverse: whether to pass --reverse (defaults to 0).
785#            options: a reference to an array of options to pass to the
786#                     patch command. The subroutine passes the -p0 option
787#                     no matter what. This should not include --reverse.
788#
789# This subroutine has unit tests in VCSUtils_unittest.pl.
790sub runPatchCommand($$$;$)
791{
792    my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
793
794    my ($patchCommand, $isForcing) = generatePatchCommand($args);
795
796    # Temporarily change the working directory since the path found
797    # in the patch's "Index:" line is relative to the repository root
798    # (i.e. the same as $pathRelativeToRoot).
799    my $cwd = Cwd::getcwd();
800    chdir $repositoryRootPath;
801
802    open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
803    print PATCH $patch;
804    close PATCH;
805    my $exitStatus = exitStatus($?);
806
807    chdir $cwd;
808
809    if ($exitStatus && !$isForcing) {
810        print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
811              "status $exitStatus.  Pass --force to ignore patch failures.\n";
812        exit $exitStatus;
813    }
814
815    return $exitStatus;
816}
817
818sub gitConfig($)
819{
820    return unless $isGit;
821
822    my ($config) = @_;
823
824    my $result = `git config $config`;
825    if (($? >> 8)) {
826        $result = `git repo-config $config`;
827    }
828    chomp $result;
829    return $result;
830}
831
832sub changeLogNameError($)
833{
834    my ($message) = @_;
835    print STDERR "$message\nEither:\n";
836    print STDERR "  set CHANGE_LOG_NAME in your environment\n";
837    print STDERR "  OR pass --name= on the command line\n";
838    print STDERR "  OR set REAL_NAME in your environment";
839    print STDERR "  OR git users can set 'git config user.name'\n";
840    exit(1);
841}
842
843sub changeLogName()
844{
845    my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
846
847    changeLogNameError("Failed to determine ChangeLog name.") unless $name;
848    # getpwuid seems to always succeed on windows, returning the username instead of the full name.  This check will catch that case.
849    changeLogNameError("'$name' does not contain a space!  ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
850
851    return $name;
852}
853
854sub changeLogEmailAddressError($)
855{
856    my ($message) = @_;
857    print STDERR "$message\nEither:\n";
858    print STDERR "  set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
859    print STDERR "  OR pass --email= on the command line\n";
860    print STDERR "  OR set EMAIL_ADDRESS in your environment\n";
861    print STDERR "  OR git users can set 'git config user.email'\n";
862    exit(1);
863}
864
865sub changeLogEmailAddress()
866{
867    my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
868
869    changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
870    changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
871
872    return $emailAddress;
873}
874
875# http://tools.ietf.org/html/rfc1924
876sub decodeBase85($)
877{
878    my ($encoded) = @_;
879    my %table;
880    my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
881    for (my $i = 0; $i < 85; $i++) {
882        $table{$characters[$i]} = $i;
883    }
884
885    my $decoded = '';
886    my @encodedChars = $encoded =~ /./g;
887
888    for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
889        my $digit = 0;
890        for (my $i = 0; $i < 5; $i++) {
891            $digit *= 85;
892            my $char = $encodedChars[$encodedIter];
893            $digit += $table{$char};
894            $encodedIter++;
895        }
896
897        for (my $i = 0; $i < 4; $i++) {
898            $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
899        }
900    }
901
902    return $decoded;
903}
904
905sub decodeGitBinaryChunk($$)
906{
907    my ($contents, $fullPath) = @_;
908
909    # Load this module lazily in case the user don't have this module
910    # and won't handle git binary patches.
911    require Compress::Zlib;
912
913    my $encoded = "";
914    my $compressedSize = 0;
915    while ($contents =~ /^([A-Za-z])(.*)$/gm) {
916        my $line = $2;
917        next if $line eq "";
918        die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
919        my $actualSize = length($2) / 5 * 4;
920        my $encodedExpectedSize = ord($1);
921        my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
922
923        die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
924        $compressedSize += $expectedSize;
925        $encoded .= $line;
926    }
927
928    my $compressed = decodeBase85($encoded);
929    $compressed = substr($compressed, 0, $compressedSize);
930    return Compress::Zlib::uncompress($compressed);
931}
932
933sub decodeGitBinaryPatch($$)
934{
935    my ($contents, $fullPath) = @_;
936
937    # Git binary patch has two chunks. One is for the normal patching
938    # and another is for the reverse patching.
939    #
940    # Each chunk a line which starts from either "literal" or "delta",
941    # followed by a number which specifies decoded size of the chunk.
942    # The "delta" type chunks aren't supported by this function yet.
943    #
944    # Then, content of the chunk comes. To decode the content, we
945    # need decode it with base85 first, and then zlib.
946    my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
947    if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
948        die "$fullPath: unknown git binary patch format"
949    }
950
951    my $binaryChunkType = $1;
952    my $binaryChunkExpectedSize = $2;
953    my $encodedChunk = $3;
954    my $reverseBinaryChunkType = $4;
955    my $reverseBinaryChunkExpectedSize = $5;
956    my $encodedReverseChunk = $6;
957
958    my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
959    my $binaryChunkActualSize = length($binaryChunk);
960    my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
961    my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
962
963    die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize);
964    die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
965
966    return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
967}
968
9691;
970