|
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. |
|
29 package VCSUtils; |
|
30 |
|
31 use strict; |
|
32 use warnings; |
|
33 |
|
34 use Cwd qw(); # "qw()" prevents warnings about redefining getcwd() with "use POSIX;" |
|
35 use English; # for $POSTMATCH, etc. |
|
36 use File::Basename; |
|
37 use File::Spec; |
|
38 use POSIX; |
|
39 |
|
40 BEGIN { |
|
41 use Exporter (); |
|
42 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); |
|
43 $VERSION = 1.00; |
|
44 @ISA = qw(Exporter); |
|
45 @EXPORT = qw( |
|
46 &callSilently |
|
47 &canonicalizePath |
|
48 &changeLogEmailAddress |
|
49 &changeLogName |
|
50 &chdirReturningRelativePath |
|
51 &decodeGitBinaryPatch |
|
52 &determineSVNRoot |
|
53 &determineVCSRoot |
|
54 &exitStatus |
|
55 &fixChangeLogPatch |
|
56 &gitBranch |
|
57 &gitdiff2svndiff |
|
58 &isGit |
|
59 &isGitBranchBuild |
|
60 &isGitDirectory |
|
61 &isSVN |
|
62 &isSVNDirectory |
|
63 &isSVNVersion16OrNewer |
|
64 &makeFilePathRelative |
|
65 &mergeChangeLogs |
|
66 &normalizePath |
|
67 &parsePatch |
|
68 &pathRelativeToSVNRepositoryRootForPath |
|
69 &prepareParsedPatch |
|
70 &runPatchCommand |
|
71 &scmToggleExecutableBit |
|
72 &setChangeLogDateAndReviewer |
|
73 &svnRevisionForDirectory |
|
74 &svnStatus |
|
75 ); |
|
76 %EXPORT_TAGS = ( ); |
|
77 @EXPORT_OK = (); |
|
78 } |
|
79 |
|
80 our @EXPORT_OK; |
|
81 |
|
82 my $gitBranch; |
|
83 my $gitRoot; |
|
84 my $isGit; |
|
85 my $isGitBranchBuild; |
|
86 my $isSVN; |
|
87 my $svnVersion; |
|
88 |
|
89 # Project time zone for Cupertino, CA, US |
|
90 my $changeLogTimeZone = "PST8PDT"; |
|
91 |
|
92 my $gitDiffStartRegEx = qr#^diff --git (\w/)?(.+) (\w/)?([^\r\n]+)#; |
|
93 my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#; |
|
94 my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path. |
|
95 my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property. |
|
96 my $svnPropertyValueStartRegEx = qr#^ (\+|-) ([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines). |
|
97 |
|
98 # This method is for portability. Return the system-appropriate exit |
|
99 # status of a child process. |
|
100 # |
|
101 # Args: pass the child error status returned by the last pipe close, |
|
102 # for example "$?". |
|
103 sub exitStatus($) |
|
104 { |
|
105 my ($returnvalue) = @_; |
|
106 if ($^O eq "MSWin32") { |
|
107 return $returnvalue >> 8; |
|
108 } |
|
109 return WEXITSTATUS($returnvalue); |
|
110 } |
|
111 |
|
112 # Call a function while suppressing STDERR, and return the return values |
|
113 # as an array. |
|
114 sub callSilently($@) { |
|
115 my ($func, @args) = @_; |
|
116 |
|
117 # The following pattern was taken from here: |
|
118 # http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html |
|
119 # |
|
120 # Also see this Perl documentation (search for "open OLDERR"): |
|
121 # http://perldoc.perl.org/functions/open.html |
|
122 open(OLDERR, ">&STDERR"); |
|
123 close(STDERR); |
|
124 my @returnValue = &$func(@args); |
|
125 open(STDERR, ">&OLDERR"); |
|
126 close(OLDERR); |
|
127 |
|
128 return @returnValue; |
|
129 } |
|
130 |
|
131 # Note, this method will not error if the file corresponding to the path does not exist. |
|
132 sub scmToggleExecutableBit |
|
133 { |
|
134 my ($path, $executableBitDelta) = @_; |
|
135 return if ! -e $path; |
|
136 if ($executableBitDelta == 1) { |
|
137 scmAddExecutableBit($path); |
|
138 } elsif ($executableBitDelta == -1) { |
|
139 scmRemoveExecutableBit($path); |
|
140 } |
|
141 } |
|
142 |
|
143 sub scmAddExecutableBit($) |
|
144 { |
|
145 my ($path) = @_; |
|
146 |
|
147 if (isSVN()) { |
|
148 system("svn", "propset", "svn:executable", "on", $path) == 0 or die "Failed to run 'svn propset svn:executable on $path'."; |
|
149 } elsif (isGit()) { |
|
150 chmod(0755, $path); |
|
151 } |
|
152 } |
|
153 |
|
154 sub scmRemoveExecutableBit($) |
|
155 { |
|
156 my ($path) = @_; |
|
157 |
|
158 if (isSVN()) { |
|
159 system("svn", "propdel", "svn:executable", $path) == 0 or die "Failed to run 'svn propdel svn:executable $path'."; |
|
160 } elsif (isGit()) { |
|
161 chmod(0664, $path); |
|
162 } |
|
163 } |
|
164 |
|
165 sub isGitDirectory($) |
|
166 { |
|
167 my ($dir) = @_; |
|
168 return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0; |
|
169 } |
|
170 |
|
171 sub isGit() |
|
172 { |
|
173 return $isGit if defined $isGit; |
|
174 |
|
175 $isGit = isGitDirectory("."); |
|
176 return $isGit; |
|
177 } |
|
178 |
|
179 sub gitBranch() |
|
180 { |
|
181 unless (defined $gitBranch) { |
|
182 chomp($gitBranch = `git symbolic-ref -q HEAD`); |
|
183 $gitBranch = "" if exitStatus($?); |
|
184 $gitBranch =~ s#^refs/heads/##; |
|
185 $gitBranch = "" if $gitBranch eq "master"; |
|
186 } |
|
187 |
|
188 return $gitBranch; |
|
189 } |
|
190 |
|
191 sub isGitBranchBuild() |
|
192 { |
|
193 my $branch = gitBranch(); |
|
194 chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`); |
|
195 return 1 if $override eq "true"; |
|
196 return 0 if $override eq "false"; |
|
197 |
|
198 unless (defined $isGitBranchBuild) { |
|
199 chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`); |
|
200 $isGitBranchBuild = $gitBranchBuild eq "true"; |
|
201 } |
|
202 |
|
203 return $isGitBranchBuild; |
|
204 } |
|
205 |
|
206 sub isSVNDirectory($) |
|
207 { |
|
208 my ($dir) = @_; |
|
209 |
|
210 return -d File::Spec->catdir($dir, ".svn"); |
|
211 } |
|
212 |
|
213 sub isSVN() |
|
214 { |
|
215 return $isSVN if defined $isSVN; |
|
216 |
|
217 $isSVN = isSVNDirectory("."); |
|
218 return $isSVN; |
|
219 } |
|
220 |
|
221 sub svnVersion() |
|
222 { |
|
223 return $svnVersion if defined $svnVersion; |
|
224 |
|
225 if (!isSVN()) { |
|
226 $svnVersion = 0; |
|
227 } else { |
|
228 chomp($svnVersion = `svn --version --quiet`); |
|
229 } |
|
230 return $svnVersion; |
|
231 } |
|
232 |
|
233 sub isSVNVersion16OrNewer() |
|
234 { |
|
235 my $version = svnVersion(); |
|
236 return eval "v$version" ge v1.6; |
|
237 } |
|
238 |
|
239 sub chdirReturningRelativePath($) |
|
240 { |
|
241 my ($directory) = @_; |
|
242 my $previousDirectory = Cwd::getcwd(); |
|
243 chdir $directory; |
|
244 my $newDirectory = Cwd::getcwd(); |
|
245 return "." if $newDirectory eq $previousDirectory; |
|
246 return File::Spec->abs2rel($previousDirectory, $newDirectory); |
|
247 } |
|
248 |
|
249 sub determineGitRoot() |
|
250 { |
|
251 chomp(my $gitDir = `git rev-parse --git-dir`); |
|
252 return dirname($gitDir); |
|
253 } |
|
254 |
|
255 sub determineSVNRoot() |
|
256 { |
|
257 my $last = ''; |
|
258 my $path = '.'; |
|
259 my $parent = '..'; |
|
260 my $repositoryRoot; |
|
261 my $repositoryUUID; |
|
262 while (1) { |
|
263 my $thisRoot; |
|
264 my $thisUUID; |
|
265 # Ignore error messages in case we've run past the root of the checkout. |
|
266 open INFO, "svn info '$path' 2> " . File::Spec->devnull() . " |" or die; |
|
267 while (<INFO>) { |
|
268 if (/^Repository Root: (.+)/) { |
|
269 $thisRoot = $1; |
|
270 } |
|
271 if (/^Repository UUID: (.+)/) { |
|
272 $thisUUID = $1; |
|
273 } |
|
274 if ($thisRoot && $thisUUID) { |
|
275 local $/ = undef; |
|
276 <INFO>; # Consume the rest of the input. |
|
277 } |
|
278 } |
|
279 close INFO; |
|
280 |
|
281 # It's possible (e.g. for developers of some ports) to have a WebKit |
|
282 # checkout in a subdirectory of another checkout. So abort if the |
|
283 # repository root or the repository UUID suddenly changes. |
|
284 last if !$thisUUID; |
|
285 $repositoryUUID = $thisUUID if !$repositoryUUID; |
|
286 last if $thisUUID ne $repositoryUUID; |
|
287 |
|
288 last if !$thisRoot; |
|
289 $repositoryRoot = $thisRoot if !$repositoryRoot; |
|
290 last if $thisRoot ne $repositoryRoot; |
|
291 |
|
292 $last = $path; |
|
293 $path = File::Spec->catdir($parent, $path); |
|
294 } |
|
295 |
|
296 return File::Spec->rel2abs($last); |
|
297 } |
|
298 |
|
299 sub determineVCSRoot() |
|
300 { |
|
301 if (isGit()) { |
|
302 return determineGitRoot(); |
|
303 } |
|
304 |
|
305 if (!isSVN()) { |
|
306 # Some users have a workflow where svn-create-patch, svn-apply and |
|
307 # svn-unapply are used outside of multiple svn working directores, |
|
308 # so warn the user and assume Subversion is being used in this case. |
|
309 warn "Unable to determine VCS root; assuming Subversion"; |
|
310 $isSVN = 1; |
|
311 } |
|
312 |
|
313 return determineSVNRoot(); |
|
314 } |
|
315 |
|
316 sub svnRevisionForDirectory($) |
|
317 { |
|
318 my ($dir) = @_; |
|
319 my $revision; |
|
320 |
|
321 if (isSVNDirectory($dir)) { |
|
322 my $svnInfo = `LC_ALL=C svn info $dir | grep Revision:`; |
|
323 ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g); |
|
324 } elsif (isGitDirectory($dir)) { |
|
325 my $gitLog = `cd $dir && LC_ALL=C git log --grep='git-svn-id: ' -n 1 | grep git-svn-id:`; |
|
326 ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g); |
|
327 } |
|
328 die "Unable to determine current SVN revision in $dir" unless (defined $revision); |
|
329 return $revision; |
|
330 } |
|
331 |
|
332 sub pathRelativeToSVNRepositoryRootForPath($) |
|
333 { |
|
334 my ($file) = @_; |
|
335 my $relativePath = File::Spec->abs2rel($file); |
|
336 |
|
337 my $svnInfo; |
|
338 if (isSVN()) { |
|
339 $svnInfo = `LC_ALL=C svn info $relativePath`; |
|
340 } elsif (isGit()) { |
|
341 $svnInfo = `LC_ALL=C git svn info $relativePath`; |
|
342 } |
|
343 |
|
344 $svnInfo =~ /.*^URL: (.*?)$/m; |
|
345 my $svnURL = $1; |
|
346 |
|
347 $svnInfo =~ /.*^Repository Root: (.*?)$/m; |
|
348 my $repositoryRoot = $1; |
|
349 |
|
350 $svnURL =~ s/$repositoryRoot\///; |
|
351 return $svnURL; |
|
352 } |
|
353 |
|
354 sub makeFilePathRelative($) |
|
355 { |
|
356 my ($path) = @_; |
|
357 return $path unless isGit(); |
|
358 |
|
359 unless (defined $gitRoot) { |
|
360 chomp($gitRoot = `git rev-parse --show-cdup`); |
|
361 } |
|
362 return $gitRoot . $path; |
|
363 } |
|
364 |
|
365 sub normalizePath($) |
|
366 { |
|
367 my ($path) = @_; |
|
368 $path =~ s/\\/\//g; |
|
369 return $path; |
|
370 } |
|
371 |
|
372 sub canonicalizePath($) |
|
373 { |
|
374 my ($file) = @_; |
|
375 |
|
376 # Remove extra slashes and '.' directories in path |
|
377 $file = File::Spec->canonpath($file); |
|
378 |
|
379 # Remove '..' directories in path |
|
380 my @dirs = (); |
|
381 foreach my $dir (File::Spec->splitdir($file)) { |
|
382 if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') { |
|
383 pop(@dirs); |
|
384 } else { |
|
385 push(@dirs, $dir); |
|
386 } |
|
387 } |
|
388 return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : "."; |
|
389 } |
|
390 |
|
391 sub removeEOL($) |
|
392 { |
|
393 my ($line) = @_; |
|
394 |
|
395 $line =~ s/[\r\n]+$//g; |
|
396 return $line; |
|
397 } |
|
398 |
|
399 sub svnStatus($) |
|
400 { |
|
401 my ($fullPath) = @_; |
|
402 my $svnStatus; |
|
403 open SVN, "svn status --non-interactive --non-recursive '$fullPath' |" or die; |
|
404 if (-d $fullPath) { |
|
405 # When running "svn stat" on a directory, we can't assume that only one |
|
406 # status will be returned (since any files with a status below the |
|
407 # directory will be returned), and we can't assume that the directory will |
|
408 # be first (since any files with unknown status will be listed first). |
|
409 my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath)); |
|
410 while (<SVN>) { |
|
411 # Input may use a different EOL sequence than $/, so avoid chomp. |
|
412 $_ = removeEOL($_); |
|
413 my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7))); |
|
414 if ($normalizedFullPath eq $normalizedStatPath) { |
|
415 $svnStatus = "$_\n"; |
|
416 last; |
|
417 } |
|
418 } |
|
419 # Read the rest of the svn command output to avoid a broken pipe warning. |
|
420 local $/ = undef; |
|
421 <SVN>; |
|
422 } |
|
423 else { |
|
424 # Files will have only one status returned. |
|
425 $svnStatus = removeEOL(<SVN>) . "\n"; |
|
426 } |
|
427 close SVN; |
|
428 return $svnStatus; |
|
429 } |
|
430 |
|
431 # Return whether the given file mode is executable in the source control |
|
432 # sense. We make this determination based on whether the executable bit |
|
433 # is set for "others" rather than the stronger condition that it be set |
|
434 # for the user, group, and others. This is sufficient for distinguishing |
|
435 # the default behavior in Git and SVN. |
|
436 # |
|
437 # Args: |
|
438 # $fileMode: A number or string representing a file mode in octal notation. |
|
439 sub isExecutable($) |
|
440 { |
|
441 my $fileMode = shift; |
|
442 |
|
443 return $fileMode % 2; |
|
444 } |
|
445 |
|
446 # Parse the next Git diff header from the given file handle, and advance |
|
447 # the handle so the last line read is the first line after the header. |
|
448 # |
|
449 # This subroutine dies if given leading junk. |
|
450 # |
|
451 # Args: |
|
452 # $fileHandle: advanced so the last line read from the handle is the first |
|
453 # line of the header to parse. This should be a line |
|
454 # beginning with "diff --git". |
|
455 # $line: the line last read from $fileHandle |
|
456 # |
|
457 # Returns ($headerHashRef, $lastReadLine): |
|
458 # $headerHashRef: a hash reference representing a diff header, as follows-- |
|
459 # copiedFromPath: the path from which the file was copied or moved if |
|
460 # the diff is a copy or move. |
|
461 # executableBitDelta: the value 1 or -1 if the executable bit was added or |
|
462 # removed, respectively. New and deleted files have |
|
463 # this value only if the file is executable, in which |
|
464 # case the value is 1 and -1, respectively. |
|
465 # indexPath: the path of the target file. |
|
466 # isBinary: the value 1 if the diff is for a binary file. |
|
467 # isDeletion: the value 1 if the diff is a file deletion. |
|
468 # isCopyWithChanges: the value 1 if the file was copied or moved and |
|
469 # the target file was changed in some way after being |
|
470 # copied or moved (e.g. if its contents or executable |
|
471 # bit were changed). |
|
472 # isNew: the value 1 if the diff is for a new file. |
|
473 # shouldDeleteSource: the value 1 if the file was copied or moved and |
|
474 # the source file was deleted -- i.e. if the copy |
|
475 # was actually a move. |
|
476 # svnConvertedText: the header text with some lines converted to SVN |
|
477 # format. Git-specific lines are preserved. |
|
478 # $lastReadLine: the line last read from $fileHandle. |
|
479 sub parseGitDiffHeader($$) |
|
480 { |
|
481 my ($fileHandle, $line) = @_; |
|
482 |
|
483 $_ = $line; |
|
484 |
|
485 my $indexPath; |
|
486 if (/$gitDiffStartRegEx/) { |
|
487 # The first and second paths can differ in the case of copies |
|
488 # and renames. We use the second file path because it is the |
|
489 # destination path. |
|
490 $indexPath = $4; |
|
491 # Use $POSTMATCH to preserve the end-of-line character. |
|
492 $_ = "Index: $indexPath$POSTMATCH"; # Convert to SVN format. |
|
493 } else { |
|
494 die("Could not parse leading \"diff --git\" line: \"$line\"."); |
|
495 } |
|
496 |
|
497 my $copiedFromPath; |
|
498 my $foundHeaderEnding; |
|
499 my $isBinary; |
|
500 my $isDeletion; |
|
501 my $isNew; |
|
502 my $newExecutableBit = 0; |
|
503 my $oldExecutableBit = 0; |
|
504 my $shouldDeleteSource = 0; |
|
505 my $similarityIndex = 0; |
|
506 my $svnConvertedText; |
|
507 while (1) { |
|
508 # Temporarily strip off any end-of-line characters to simplify |
|
509 # regex matching below. |
|
510 s/([\n\r]+)$//; |
|
511 my $eol = $1; |
|
512 |
|
513 if (/^(deleted file|old) mode (\d+)/) { |
|
514 $oldExecutableBit = (isExecutable($2) ? 1 : 0); |
|
515 $isDeletion = 1 if $1 eq "deleted file"; |
|
516 } elsif (/^new( file)? mode (\d+)/) { |
|
517 $newExecutableBit = (isExecutable($2) ? 1 : 0); |
|
518 $isNew = 1 if $1; |
|
519 } elsif (/^similarity index (\d+)%/) { |
|
520 $similarityIndex = $1; |
|
521 } elsif (/^copy from (\S+)/) { |
|
522 $copiedFromPath = $1; |
|
523 } elsif (/^rename from (\S+)/) { |
|
524 # FIXME: Record this as a move rather than as a copy-and-delete. |
|
525 # This will simplify adding rename support to svn-unapply. |
|
526 # Otherwise, the hash for a deletion would have to know |
|
527 # everything about the file being deleted in order to |
|
528 # support undoing itself. Recording as a move will also |
|
529 # permit us to use "svn move" and "git move". |
|
530 $copiedFromPath = $1; |
|
531 $shouldDeleteSource = 1; |
|
532 } elsif (/^--- \S+/) { |
|
533 $_ = "--- $indexPath"; # Convert to SVN format. |
|
534 } elsif (/^\+\+\+ \S+/) { |
|
535 $_ = "+++ $indexPath"; # Convert to SVN format. |
|
536 $foundHeaderEnding = 1; |
|
537 } elsif (/^GIT binary patch$/ ) { |
|
538 $isBinary = 1; |
|
539 $foundHeaderEnding = 1; |
|
540 # The "git diff" command includes a line of the form "Binary files |
|
541 # <path1> and <path2> differ" if the --binary flag is not used. |
|
542 } elsif (/^Binary files / ) { |
|
543 die("Error: the Git diff contains a binary file without the binary data in ". |
|
544 "line: \"$_\". Be sure to use the --binary flag when invoking \"git diff\" ". |
|
545 "with diffs containing binary files."); |
|
546 } |
|
547 |
|
548 $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters. |
|
549 |
|
550 $_ = <$fileHandle>; # Not defined if end-of-file reached. |
|
551 |
|
552 last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding); |
|
553 } |
|
554 |
|
555 my $executableBitDelta = $newExecutableBit - $oldExecutableBit; |
|
556 |
|
557 my %header; |
|
558 |
|
559 $header{copiedFromPath} = $copiedFromPath if $copiedFromPath; |
|
560 $header{executableBitDelta} = $executableBitDelta if $executableBitDelta; |
|
561 $header{indexPath} = $indexPath; |
|
562 $header{isBinary} = $isBinary if $isBinary; |
|
563 $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta)); |
|
564 $header{isDeletion} = $isDeletion if $isDeletion; |
|
565 $header{isNew} = $isNew if $isNew; |
|
566 $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource; |
|
567 $header{svnConvertedText} = $svnConvertedText; |
|
568 |
|
569 return (\%header, $_); |
|
570 } |
|
571 |
|
572 # Parse the next SVN diff header from the given file handle, and advance |
|
573 # the handle so the last line read is the first line after the header. |
|
574 # |
|
575 # This subroutine dies if given leading junk or if it could not detect |
|
576 # the end of the header block. |
|
577 # |
|
578 # Args: |
|
579 # $fileHandle: advanced so the last line read from the handle is the first |
|
580 # line of the header to parse. This should be a line |
|
581 # beginning with "Index:". |
|
582 # $line: the line last read from $fileHandle |
|
583 # |
|
584 # Returns ($headerHashRef, $lastReadLine): |
|
585 # $headerHashRef: a hash reference representing a diff header, as follows-- |
|
586 # copiedFromPath: the path from which the file was copied if the diff |
|
587 # is a copy. |
|
588 # indexPath: the path of the target file, which is the path found in |
|
589 # the "Index:" line. |
|
590 # isBinary: the value 1 if the diff is for a binary file. |
|
591 # isNew: the value 1 if the diff is for a new file. |
|
592 # sourceRevision: the revision number of the source, if it exists. This |
|
593 # is the same as the revision number the file was copied |
|
594 # from, in the case of a file copy. |
|
595 # svnConvertedText: the header text converted to a header with the paths |
|
596 # in some lines corrected. |
|
597 # $lastReadLine: the line last read from $fileHandle. |
|
598 sub parseSvnDiffHeader($$) |
|
599 { |
|
600 my ($fileHandle, $line) = @_; |
|
601 |
|
602 $_ = $line; |
|
603 |
|
604 my $indexPath; |
|
605 if (/$svnDiffStartRegEx/) { |
|
606 $indexPath = $1; |
|
607 } else { |
|
608 die("First line of SVN diff does not begin with \"Index \": \"$_\""); |
|
609 } |
|
610 |
|
611 my $copiedFromPath; |
|
612 my $foundHeaderEnding; |
|
613 my $isBinary; |
|
614 my $isNew; |
|
615 my $sourceRevision; |
|
616 my $svnConvertedText; |
|
617 while (1) { |
|
618 # Temporarily strip off any end-of-line characters to simplify |
|
619 # regex matching below. |
|
620 s/([\n\r]+)$//; |
|
621 my $eol = $1; |
|
622 |
|
623 # Fix paths on ""---" and "+++" lines to match the leading |
|
624 # index line. |
|
625 if (s/^--- \S+/--- $indexPath/) { |
|
626 # --- |
|
627 if (/^--- .+\(revision (\d+)\)/) { |
|
628 $sourceRevision = $1; |
|
629 $isNew = 1 if !$sourceRevision; # if revision 0. |
|
630 if (/\(from (\S+):(\d+)\)$/) { |
|
631 # The "from" clause is created by svn-create-patch, in |
|
632 # which case there is always also a "revision" clause. |
|
633 $copiedFromPath = $1; |
|
634 die("Revision number \"$2\" in \"from\" clause does not match " . |
|
635 "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision); |
|
636 } |
|
637 } |
|
638 } elsif (s/^\+\+\+ \S+/+++ $indexPath/) { |
|
639 $foundHeaderEnding = 1; |
|
640 } elsif (/^Cannot display: file marked as a binary type.$/) { |
|
641 $isBinary = 1; |
|
642 $foundHeaderEnding = 1; |
|
643 } |
|
644 |
|
645 $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters. |
|
646 |
|
647 $_ = <$fileHandle>; # Not defined if end-of-file reached. |
|
648 |
|
649 last if (!defined($_) || /$svnDiffStartRegEx/ || $foundHeaderEnding); |
|
650 } |
|
651 |
|
652 if (!$foundHeaderEnding) { |
|
653 die("Did not find end of header block corresponding to index path \"$indexPath\"."); |
|
654 } |
|
655 |
|
656 my %header; |
|
657 |
|
658 $header{copiedFromPath} = $copiedFromPath if $copiedFromPath; |
|
659 $header{indexPath} = $indexPath; |
|
660 $header{isBinary} = $isBinary if $isBinary; |
|
661 $header{isNew} = $isNew if $isNew; |
|
662 $header{sourceRevision} = $sourceRevision if $sourceRevision; |
|
663 $header{svnConvertedText} = $svnConvertedText; |
|
664 |
|
665 return (\%header, $_); |
|
666 } |
|
667 |
|
668 # Parse the next diff header from the given file handle, and advance |
|
669 # the handle so the last line read is the first line after the header. |
|
670 # |
|
671 # This subroutine dies if given leading junk or if it could not detect |
|
672 # the end of the header block. |
|
673 # |
|
674 # Args: |
|
675 # $fileHandle: advanced so the last line read from the handle is the first |
|
676 # line of the header to parse. For SVN-formatted diffs, this |
|
677 # is a line beginning with "Index:". For Git, this is a line |
|
678 # beginning with "diff --git". |
|
679 # $line: the line last read from $fileHandle |
|
680 # |
|
681 # Returns ($headerHashRef, $lastReadLine): |
|
682 # $headerHashRef: a hash reference representing a diff header |
|
683 # copiedFromPath: the path from which the file was copied if the diff |
|
684 # is a copy. |
|
685 # executableBitDelta: the value 1 or -1 if the executable bit was added or |
|
686 # removed, respectively. New and deleted files have |
|
687 # this value only if the file is executable, in which |
|
688 # case the value is 1 and -1, respectively. |
|
689 # indexPath: the path of the target file. |
|
690 # isBinary: the value 1 if the diff is for a binary file. |
|
691 # isGit: the value 1 if the diff is Git-formatted. |
|
692 # isSvn: the value 1 if the diff is SVN-formatted. |
|
693 # sourceRevision: the revision number of the source, if it exists. This |
|
694 # is the same as the revision number the file was copied |
|
695 # from, in the case of a file copy. |
|
696 # svnConvertedText: the header text with some lines converted to SVN |
|
697 # format. Git-specific lines are preserved. |
|
698 # $lastReadLine: the line last read from $fileHandle. |
|
699 sub parseDiffHeader($$) |
|
700 { |
|
701 my ($fileHandle, $line) = @_; |
|
702 |
|
703 my $header; # This is a hash ref. |
|
704 my $isGit; |
|
705 my $isSvn; |
|
706 my $lastReadLine; |
|
707 |
|
708 if ($line =~ $svnDiffStartRegEx) { |
|
709 $isSvn = 1; |
|
710 ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line); |
|
711 } elsif ($line =~ $gitDiffStartRegEx) { |
|
712 $isGit = 1; |
|
713 ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line); |
|
714 } else { |
|
715 die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\""); |
|
716 } |
|
717 |
|
718 $header->{isGit} = $isGit if $isGit; |
|
719 $header->{isSvn} = $isSvn if $isSvn; |
|
720 |
|
721 return ($header, $lastReadLine); |
|
722 } |
|
723 |
|
724 # FIXME: The %diffHash "object" should not have an svnConvertedText property. |
|
725 # Instead, the hash object should store its information in a |
|
726 # structured way as properties. This should be done in a way so |
|
727 # that, if necessary, the text of an SVN or Git patch can be |
|
728 # reconstructed from the information in those hash properties. |
|
729 # |
|
730 # A %diffHash is a hash representing a source control diff of a single |
|
731 # file operation (e.g. a file modification, copy, or delete). |
|
732 # |
|
733 # These hashes appear, for example, in the parseDiff(), parsePatch(), |
|
734 # and prepareParsedPatch() subroutines of this package. |
|
735 # |
|
736 # The corresponding values are-- |
|
737 # |
|
738 # copiedFromPath: the path from which the file was copied if the diff |
|
739 # is a copy. |
|
740 # executableBitDelta: the value 1 or -1 if the executable bit was added or |
|
741 # removed from the target file, respectively. |
|
742 # indexPath: the path of the target file. For SVN-formatted diffs, |
|
743 # this is the same as the path in the "Index:" line. |
|
744 # isBinary: the value 1 if the diff is for a binary file. |
|
745 # isDeletion: the value 1 if the diff is known from the header to be a deletion. |
|
746 # isGit: the value 1 if the diff is Git-formatted. |
|
747 # isNew: the value 1 if the dif is known from the header to be a new file. |
|
748 # isSvn: the value 1 if the diff is SVN-formatted. |
|
749 # sourceRevision: the revision number of the source, if it exists. This |
|
750 # is the same as the revision number the file was copied |
|
751 # from, in the case of a file copy. |
|
752 # svnConvertedText: the diff with some lines converted to SVN format. |
|
753 # Git-specific lines are preserved. |
|
754 |
|
755 # Parse one diff from a patch file created by svn-create-patch, and |
|
756 # advance the file handle so the last line read is the first line |
|
757 # of the next header block. |
|
758 # |
|
759 # This subroutine preserves any leading junk encountered before the header. |
|
760 # |
|
761 # Composition of an SVN diff |
|
762 # |
|
763 # There are three parts to an SVN diff: the header, the property change, and |
|
764 # the binary contents, in that order. Either the header or the property change |
|
765 # may be ommitted, but not both. If there are binary changes, then you always |
|
766 # have all three. |
|
767 # |
|
768 # Args: |
|
769 # $fileHandle: a file handle advanced to the first line of the next |
|
770 # header block. Leading junk is okay. |
|
771 # $line: the line last read from $fileHandle. |
|
772 # |
|
773 # Returns ($diffHashRefs, $lastReadLine): |
|
774 # $diffHashRefs: A reference to an array of references to %diffHash hashes. |
|
775 # See the %diffHash documentation above. |
|
776 # $lastReadLine: the line last read from $fileHandle |
|
777 sub parseDiff($$) |
|
778 { |
|
779 # FIXME: Adjust this method so that it dies if the first line does not |
|
780 # match the start of a diff. This will require a change to |
|
781 # parsePatch() so that parsePatch() skips over leading junk. |
|
782 my ($fileHandle, $line) = @_; |
|
783 |
|
784 my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default |
|
785 |
|
786 my $headerHashRef; # Last header found, as returned by parseDiffHeader(). |
|
787 my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties(). |
|
788 my $svnText; |
|
789 while (defined($line)) { |
|
790 if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) { |
|
791 # Then assume all diffs in the patch are Git-formatted. This |
|
792 # block was made to be enterable at most once since we assume |
|
793 # all diffs in the patch are formatted the same (SVN or Git). |
|
794 $headerStartRegEx = $gitDiffStartRegEx; |
|
795 } |
|
796 |
|
797 if ($line =~ $svnPropertiesStartRegEx) { |
|
798 my $propertyPath = $1; |
|
799 if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) { |
|
800 # This is the start of the second diff in the while loop, which happens to |
|
801 # be a property diff. If $svnPropertiesHasRef is defined, then this is the |
|
802 # second consecutive property diff, otherwise it's the start of a property |
|
803 # diff for a file that only has property changes. |
|
804 last; |
|
805 } |
|
806 ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line); |
|
807 next; |
|
808 } |
|
809 if ($line !~ $headerStartRegEx) { |
|
810 # Then we are in the body of the diff. |
|
811 $svnText .= $line; |
|
812 $line = <$fileHandle>; |
|
813 next; |
|
814 } # Otherwise, we found a diff header. |
|
815 |
|
816 if ($svnPropertiesHashRef || $headerHashRef) { |
|
817 # Then either we just processed an SVN property change or this |
|
818 # is the start of the second diff header of this while loop. |
|
819 last; |
|
820 } |
|
821 |
|
822 ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line); |
|
823 |
|
824 $svnText .= $headerHashRef->{svnConvertedText}; |
|
825 } |
|
826 |
|
827 my @diffHashRefs; |
|
828 |
|
829 if ($headerHashRef->{shouldDeleteSource}) { |
|
830 my %deletionHash; |
|
831 $deletionHash{indexPath} = $headerHashRef->{copiedFromPath}; |
|
832 $deletionHash{isDeletion} = 1; |
|
833 push @diffHashRefs, \%deletionHash; |
|
834 } |
|
835 if ($headerHashRef->{copiedFromPath}) { |
|
836 my %copyHash; |
|
837 $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath}; |
|
838 $copyHash{indexPath} = $headerHashRef->{indexPath}; |
|
839 $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision}; |
|
840 if ($headerHashRef->{isSvn}) { |
|
841 $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; |
|
842 } |
|
843 push @diffHashRefs, \%copyHash; |
|
844 } |
|
845 |
|
846 # Note, the order of evaluation for the following if conditional has been explicitly chosen so that |
|
847 # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that |
|
848 # only has property changes). |
|
849 if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) { |
|
850 # Then add the usual file modification. |
|
851 my %diffHash; |
|
852 # FIXME: We should expand this code to support other properties. In the future, |
|
853 # parseSvnDiffProperties may return a hash whose keys are the properties. |
|
854 if ($headerHashRef->{isSvn}) { |
|
855 # SVN records the change to the executable bit in a separate property change diff |
|
856 # that follows the contents of the diff, except for binary diffs. For binary |
|
857 # diffs, the property change diff follows the diff header. |
|
858 $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; |
|
859 } elsif ($headerHashRef->{isGit}) { |
|
860 # Git records the change to the executable bit in the header of a diff. |
|
861 $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta}; |
|
862 } |
|
863 $diffHash{indexPath} = $headerHashRef->{indexPath}; |
|
864 $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary}; |
|
865 $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion}; |
|
866 $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit}; |
|
867 $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew}; |
|
868 $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn}; |
|
869 if (!$headerHashRef->{copiedFromPath}) { |
|
870 # If the file was copied, then we have already incorporated the |
|
871 # sourceRevision information into the change. |
|
872 $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision}; |
|
873 } |
|
874 # FIXME: Remove the need for svnConvertedText. See the %diffHash |
|
875 # code comments above for more information. |
|
876 # |
|
877 # Note, we may not always have SVN converted text since we intend |
|
878 # to deprecate it in the future. For example, a property change |
|
879 # diff for a file that only has property changes will not return |
|
880 # any SVN converted text. |
|
881 $diffHash{svnConvertedText} = $svnText if $svnText; |
|
882 push @diffHashRefs, \%diffHash; |
|
883 } |
|
884 |
|
885 if (!%$headerHashRef && $svnPropertiesHashRef) { |
|
886 # A property change diff for a file that only has property changes. |
|
887 my %propertyChangeHash; |
|
888 $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; |
|
889 $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath}; |
|
890 $propertyChangeHash{isSvn} = 1; |
|
891 push @diffHashRefs, \%propertyChangeHash; |
|
892 } |
|
893 |
|
894 return (\@diffHashRefs, $line); |
|
895 } |
|
896 |
|
897 # Parse an SVN property change diff from the given file handle, and advance |
|
898 # the handle so the last line read is the first line after this diff. |
|
899 # |
|
900 # For the case of an SVN binary diff, the binary contents will follow the |
|
901 # the property changes. |
|
902 # |
|
903 # This subroutine dies if the first line does not begin with "Property changes on" |
|
904 # or if the separator line that follows this line is missing. |
|
905 # |
|
906 # Args: |
|
907 # $fileHandle: advanced so the last line read from the handle is the first |
|
908 # line of the footer to parse. This line begins with |
|
909 # "Property changes on". |
|
910 # $line: the line last read from $fileHandle. |
|
911 # |
|
912 # Returns ($propertyHashRef, $lastReadLine): |
|
913 # $propertyHashRef: a hash reference representing an SVN diff footer. |
|
914 # propertyPath: the path of the target file. |
|
915 # executableBitDelta: the value 1 or -1 if the executable bit was added or |
|
916 # removed from the target file, respectively. |
|
917 # $lastReadLine: the line last read from $fileHandle. |
|
918 sub parseSvnDiffProperties($$) |
|
919 { |
|
920 my ($fileHandle, $line) = @_; |
|
921 |
|
922 $_ = $line; |
|
923 |
|
924 my %footer; |
|
925 if (/$svnPropertiesStartRegEx/) { |
|
926 $footer{propertyPath} = $1; |
|
927 } else { |
|
928 die("Failed to find start of SVN property change, \"Property changes on \": \"$_\""); |
|
929 } |
|
930 |
|
931 # We advance $fileHandle two lines so that the next line that |
|
932 # we process is $svnPropertyStartRegEx in a well-formed footer. |
|
933 # A well-formed footer has the form: |
|
934 # Property changes on: FileA |
|
935 # ___________________________________________________________________ |
|
936 # Added: svn:executable |
|
937 # + * |
|
938 $_ = <$fileHandle>; # Not defined if end-of-file reached. |
|
939 my $separator = "_" x 67; |
|
940 if (defined($_) && /^$separator[\r\n]+$/) { |
|
941 $_ = <$fileHandle>; |
|
942 } else { |
|
943 die("Failed to find separator line: \"$_\"."); |
|
944 } |
|
945 |
|
946 # FIXME: We should expand this to support other SVN properties |
|
947 # (e.g. return a hash of property key-values that represents |
|
948 # all properties). |
|
949 # |
|
950 # Notice, we keep processing until we hit end-of-file or some |
|
951 # line that does not resemble $svnPropertyStartRegEx, such as |
|
952 # the empty line that precedes the start of the binary contents |
|
953 # of a patch, or the start of the next diff (e.g. "Index:"). |
|
954 my $propertyHashRef; |
|
955 while (defined($_) && /$svnPropertyStartRegEx/) { |
|
956 ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_); |
|
957 if ($propertyHashRef->{name} eq "svn:executable") { |
|
958 # Notice, for SVN properties, propertyChangeDelta is always non-zero |
|
959 # because a property can only be added or removed. |
|
960 $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta}; |
|
961 } |
|
962 } |
|
963 |
|
964 return(\%footer, $_); |
|
965 } |
|
966 |
|
967 # Parse the next SVN property from the given file handle, and advance the handle so the last |
|
968 # line read is the first line after the property. |
|
969 # |
|
970 # This subroutine dies if the first line is not a valid start of an SVN property, |
|
971 # or the property is missing a value, or the property change type (e.g. "Added") |
|
972 # does not correspond to the property value type (e.g. "+"). |
|
973 # |
|
974 # Args: |
|
975 # $fileHandle: advanced so the last line read from the handle is the first |
|
976 # line of the property to parse. This should be a line |
|
977 # that matches $svnPropertyStartRegEx. |
|
978 # $line: the line last read from $fileHandle. |
|
979 # |
|
980 # Returns ($propertyHashRef, $lastReadLine): |
|
981 # $propertyHashRef: a hash reference representing a SVN property. |
|
982 # name: the name of the property. |
|
983 # value: the last property value. For instance, suppose the property is "Modified". |
|
984 # Then it has both a '-' and '+' property value in that order. Therefore, |
|
985 # the value of this key is the value of the '+' property by ordering (since |
|
986 # it is the last value). |
|
987 # propertyChangeDelta: the value 1 or -1 if the property was added or |
|
988 # removed, respectively. |
|
989 # $lastReadLine: the line last read from $fileHandle. |
|
990 sub parseSvnProperty($$) |
|
991 { |
|
992 my ($fileHandle, $line) = @_; |
|
993 |
|
994 $_ = $line; |
|
995 |
|
996 my $propertyName; |
|
997 my $propertyChangeType; |
|
998 if (/$svnPropertyStartRegEx/) { |
|
999 $propertyChangeType = $1; |
|
1000 $propertyName = $2; |
|
1001 } else { |
|
1002 die("Failed to find SVN property: \"$_\"."); |
|
1003 } |
|
1004 |
|
1005 $_ = <$fileHandle>; # Not defined if end-of-file reached. |
|
1006 |
|
1007 # The "svn diff" command neither inserts newline characters between property values |
|
1008 # nor between successive properties. |
|
1009 # |
|
1010 # FIXME: We do not support property values that contain tailing newline characters |
|
1011 # as it is difficult to disambiguate these trailing newlines from the empty |
|
1012 # line that precedes the contents of a binary patch. |
|
1013 my $propertyValue; |
|
1014 my $propertyValueType; |
|
1015 while (defined($_) && /$svnPropertyValueStartRegEx/) { |
|
1016 # Note, a '-' property may be followed by a '+' property in the case of a "Modified" |
|
1017 # or "Name" property. We only care about the ending value (i.e. the '+' property) |
|
1018 # in such circumstances. So, we take the property value for the property to be its |
|
1019 # last parsed property value. |
|
1020 # |
|
1021 # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or |
|
1022 # add error checking to prevent '+', '+', ..., '+' and other invalid combinations. |
|
1023 $propertyValueType = $1; |
|
1024 ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_); |
|
1025 } |
|
1026 |
|
1027 if (!$propertyValue) { |
|
1028 die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\"."); |
|
1029 } |
|
1030 |
|
1031 my $propertyChangeDelta; |
|
1032 if ($propertyValueType eq '+') { |
|
1033 $propertyChangeDelta = 1; |
|
1034 } elsif ($propertyValueType eq '-') { |
|
1035 $propertyChangeDelta = -1; |
|
1036 } else { |
|
1037 die("Not reached."); |
|
1038 } |
|
1039 |
|
1040 # We perform a simple validation that an "Added" or "Deleted" property |
|
1041 # change type corresponds with a "+" and "-" value type, respectively. |
|
1042 my $expectedChangeDelta; |
|
1043 if ($propertyChangeType eq "Added") { |
|
1044 $expectedChangeDelta = 1; |
|
1045 } elsif ($propertyChangeType eq "Deleted") { |
|
1046 $expectedChangeDelta = -1; |
|
1047 } |
|
1048 |
|
1049 if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) { |
|
1050 die("The final property value type found \"$propertyValueType\" does not " . |
|
1051 "correspond to the property change type found \"$propertyChangeType\"."); |
|
1052 } |
|
1053 |
|
1054 my %propertyHash; |
|
1055 $propertyHash{name} = $propertyName; |
|
1056 $propertyHash{propertyChangeDelta} = $propertyChangeDelta; |
|
1057 $propertyHash{value} = $propertyValue; |
|
1058 return (\%propertyHash, $_); |
|
1059 } |
|
1060 |
|
1061 # Parse the value of an SVN property from the given file handle, and advance |
|
1062 # the handle so the last line read is the first line after the property value. |
|
1063 # |
|
1064 # This subroutine dies if the first line is an invalid SVN property value line |
|
1065 # (i.e. a line that does not begin with " +" or " -"). |
|
1066 # |
|
1067 # Args: |
|
1068 # $fileHandle: advanced so the last line read from the handle is the first |
|
1069 # line of the property value to parse. This should be a line |
|
1070 # beginning with " +" or " -". |
|
1071 # $line: the line last read from $fileHandle. |
|
1072 # |
|
1073 # Returns ($propertyValue, $lastReadLine): |
|
1074 # $propertyValue: the value of the property. |
|
1075 # $lastReadLine: the line last read from $fileHandle. |
|
1076 sub parseSvnPropertyValue($$) |
|
1077 { |
|
1078 my ($fileHandle, $line) = @_; |
|
1079 |
|
1080 $_ = $line; |
|
1081 |
|
1082 my $propertyValue; |
|
1083 my $eol; |
|
1084 if (/$svnPropertyValueStartRegEx/) { |
|
1085 $propertyValue = $2; # Does not include the end-of-line character(s). |
|
1086 $eol = $POSTMATCH; |
|
1087 } else { |
|
1088 die("Failed to find property value beginning with '+' or '-': \"$_\"."); |
|
1089 } |
|
1090 |
|
1091 while (<$fileHandle>) { |
|
1092 if (/^$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/) { |
|
1093 # Note, we may encounter an empty line before the contents of a binary patch. |
|
1094 # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be |
|
1095 # followed by a '+' property in the case of a "Modified" or "Name" property. |
|
1096 # We check for $svnPropertyStartRegEx because it indicates the start of the |
|
1097 # next property to parse. |
|
1098 last; |
|
1099 } |
|
1100 |
|
1101 # Temporarily strip off any end-of-line characters. We add the end-of-line characters |
|
1102 # from the previously processed line to the start of this line so that the last line |
|
1103 # of the property value does not end in end-of-line characters. |
|
1104 s/([\n\r]+)$//; |
|
1105 $propertyValue .= "$eol$_"; |
|
1106 $eol = $1; |
|
1107 } |
|
1108 |
|
1109 return ($propertyValue, $_); |
|
1110 } |
|
1111 |
|
1112 # Parse a patch file created by svn-create-patch. |
|
1113 # |
|
1114 # Args: |
|
1115 # $fileHandle: A file handle to the patch file that has not yet been |
|
1116 # read from. |
|
1117 # |
|
1118 # Returns: |
|
1119 # @diffHashRefs: an array of diff hash references. |
|
1120 # See the %diffHash documentation above. |
|
1121 sub parsePatch($) |
|
1122 { |
|
1123 my ($fileHandle) = @_; |
|
1124 |
|
1125 my $newDiffHashRefs; |
|
1126 my @diffHashRefs; # return value |
|
1127 |
|
1128 my $line = <$fileHandle>; |
|
1129 |
|
1130 while (defined($line)) { # Otherwise, at EOF. |
|
1131 |
|
1132 ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line); |
|
1133 |
|
1134 push @diffHashRefs, @$newDiffHashRefs; |
|
1135 } |
|
1136 |
|
1137 return @diffHashRefs; |
|
1138 } |
|
1139 |
|
1140 # Prepare the results of parsePatch() for use in svn-apply and svn-unapply. |
|
1141 # |
|
1142 # Args: |
|
1143 # $shouldForce: Whether to continue processing if an unexpected |
|
1144 # state occurs. |
|
1145 # @diffHashRefs: An array of references to %diffHashes. |
|
1146 # See the %diffHash documentation above. |
|
1147 # |
|
1148 # Returns $preparedPatchHashRef: |
|
1149 # copyDiffHashRefs: A reference to an array of the $diffHashRefs in |
|
1150 # @diffHashRefs that represent file copies. The original |
|
1151 # ordering is preserved. |
|
1152 # nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in |
|
1153 # @diffHashRefs that do not represent file copies. |
|
1154 # The original ordering is preserved. |
|
1155 # sourceRevisionHash: A reference to a hash of source path to source |
|
1156 # revision number. |
|
1157 sub prepareParsedPatch($@) |
|
1158 { |
|
1159 my ($shouldForce, @diffHashRefs) = @_; |
|
1160 |
|
1161 my %copiedFiles; |
|
1162 |
|
1163 # Return values |
|
1164 my @copyDiffHashRefs = (); |
|
1165 my @nonCopyDiffHashRefs = (); |
|
1166 my %sourceRevisionHash = (); |
|
1167 for my $diffHashRef (@diffHashRefs) { |
|
1168 my $copiedFromPath = $diffHashRef->{copiedFromPath}; |
|
1169 my $indexPath = $diffHashRef->{indexPath}; |
|
1170 my $sourceRevision = $diffHashRef->{sourceRevision}; |
|
1171 my $sourcePath; |
|
1172 |
|
1173 if (defined($copiedFromPath)) { |
|
1174 # Then the diff is a copy operation. |
|
1175 $sourcePath = $copiedFromPath; |
|
1176 |
|
1177 # FIXME: Consider printing a warning or exiting if |
|
1178 # exists($copiedFiles{$indexPath}) is true -- i.e. if |
|
1179 # $indexPath appears twice as a copy target. |
|
1180 $copiedFiles{$indexPath} = $sourcePath; |
|
1181 |
|
1182 push @copyDiffHashRefs, $diffHashRef; |
|
1183 } else { |
|
1184 # Then the diff is not a copy operation. |
|
1185 $sourcePath = $indexPath; |
|
1186 |
|
1187 push @nonCopyDiffHashRefs, $diffHashRef; |
|
1188 } |
|
1189 |
|
1190 if (defined($sourceRevision)) { |
|
1191 if (exists($sourceRevisionHash{$sourcePath}) && |
|
1192 ($sourceRevisionHash{$sourcePath} != $sourceRevision)) { |
|
1193 if (!$shouldForce) { |
|
1194 die "Two revisions of the same file required as a source:\n". |
|
1195 " $sourcePath:$sourceRevisionHash{$sourcePath}\n". |
|
1196 " $sourcePath:$sourceRevision"; |
|
1197 } |
|
1198 } |
|
1199 $sourceRevisionHash{$sourcePath} = $sourceRevision; |
|
1200 } |
|
1201 } |
|
1202 |
|
1203 my %preparedPatchHash; |
|
1204 |
|
1205 $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs; |
|
1206 $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs; |
|
1207 $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash; |
|
1208 |
|
1209 return \%preparedPatchHash; |
|
1210 } |
|
1211 |
|
1212 # Return localtime() for the project's time zone, given an integer time as |
|
1213 # returned by Perl's time() function. |
|
1214 sub localTimeInProjectTimeZone($) |
|
1215 { |
|
1216 my $epochTime = shift; |
|
1217 |
|
1218 # Change the time zone temporarily for the localtime() call. |
|
1219 my $savedTimeZone = $ENV{'TZ'}; |
|
1220 $ENV{'TZ'} = $changeLogTimeZone; |
|
1221 my @localTime = localtime($epochTime); |
|
1222 if (defined $savedTimeZone) { |
|
1223 $ENV{'TZ'} = $savedTimeZone; |
|
1224 } else { |
|
1225 delete $ENV{'TZ'}; |
|
1226 } |
|
1227 |
|
1228 return @localTime; |
|
1229 } |
|
1230 |
|
1231 # Set the reviewer and date in a ChangeLog patch, and return the new patch. |
|
1232 # |
|
1233 # Args: |
|
1234 # $patch: a ChangeLog patch as a string. |
|
1235 # $reviewer: the name of the reviewer, or undef if the reviewer should not be set. |
|
1236 # $epochTime: an integer time as returned by Perl's time() function. |
|
1237 sub setChangeLogDateAndReviewer($$$) |
|
1238 { |
|
1239 my ($patch, $reviewer, $epochTime) = @_; |
|
1240 |
|
1241 my @localTime = localTimeInProjectTimeZone($epochTime); |
|
1242 my $newDate = strftime("%Y-%m-%d", @localTime); |
|
1243 |
|
1244 my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}( )#; |
|
1245 $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/; |
|
1246 |
|
1247 if (defined($reviewer)) { |
|
1248 # We include a leading plus ("+") in the regular expression to make |
|
1249 # the regular expression less likely to match text in the leading junk |
|
1250 # for the patch, if the patch has leading junk. |
|
1251 $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/; |
|
1252 } |
|
1253 |
|
1254 return $patch; |
|
1255 } |
|
1256 |
|
1257 # If possible, returns a ChangeLog patch equivalent to the given one, |
|
1258 # but with the newest ChangeLog entry inserted at the top of the |
|
1259 # file -- i.e. no leading context and all lines starting with "+". |
|
1260 # |
|
1261 # If given a patch string not representable as a patch with the above |
|
1262 # properties, it returns the input back unchanged. |
|
1263 # |
|
1264 # WARNING: This subroutine can return an inequivalent patch string if |
|
1265 # both the beginning of the new ChangeLog file matches the beginning |
|
1266 # of the source ChangeLog, and the source beginning was modified. |
|
1267 # Otherwise, it is guaranteed to return an equivalent patch string, |
|
1268 # if it returns. |
|
1269 # |
|
1270 # Applying this subroutine to ChangeLog patches allows svn-apply to |
|
1271 # insert new ChangeLog entries at the top of the ChangeLog file. |
|
1272 # svn-apply uses patch with --fuzz=3 to do this. We need to apply |
|
1273 # this subroutine because the diff(1) command is greedy when matching |
|
1274 # lines. A new ChangeLog entry with the same date and author as the |
|
1275 # previous will match and cause the diff to have lines of starting |
|
1276 # context. |
|
1277 # |
|
1278 # This subroutine has unit tests in VCSUtils_unittest.pl. |
|
1279 sub fixChangeLogPatch($) |
|
1280 { |
|
1281 my $patch = shift; # $patch will only contain patch fragments for ChangeLog. |
|
1282 |
|
1283 $patch =~ /(\r?\n)/; |
|
1284 my $lineEnding = $1; |
|
1285 my @lines = split(/$lineEnding/, $patch); |
|
1286 |
|
1287 my $i = 0; # We reuse the same index throughout. |
|
1288 |
|
1289 # Skip to beginning of first chunk. |
|
1290 for (; $i < @lines; ++$i) { |
|
1291 if (substr($lines[$i], 0, 1) eq "@") { |
|
1292 last; |
|
1293 } |
|
1294 } |
|
1295 my $chunkStartIndex = ++$i; |
|
1296 |
|
1297 # Optimization: do not process if new lines already begin the chunk. |
|
1298 if (substr($lines[$i], 0, 1) eq "+") { |
|
1299 return $patch; |
|
1300 } |
|
1301 |
|
1302 # Skip to first line of newly added ChangeLog entry. |
|
1303 # For example, +2009-06-03 Eric Seidel <eric@webkit.org> |
|
1304 my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date |
|
1305 . '\s+(.+)\s+' # name |
|
1306 . '<([^<>]+)>$'; # e-mail address |
|
1307 |
|
1308 for (; $i < @lines; ++$i) { |
|
1309 my $line = $lines[$i]; |
|
1310 my $firstChar = substr($line, 0, 1); |
|
1311 if ($line =~ /$dateStartRegEx/) { |
|
1312 last; |
|
1313 } elsif ($firstChar eq " " or $firstChar eq "+") { |
|
1314 next; |
|
1315 } |
|
1316 return $patch; # Do not change if, for example, "-" or "@" found. |
|
1317 } |
|
1318 if ($i >= @lines) { |
|
1319 return $patch; # Do not change if date not found. |
|
1320 } |
|
1321 my $dateStartIndex = $i; |
|
1322 |
|
1323 # Rewrite overlapping lines to lead with " ". |
|
1324 my @overlappingLines = (); # These will include a leading "+". |
|
1325 for (; $i < @lines; ++$i) { |
|
1326 my $line = $lines[$i]; |
|
1327 if (substr($line, 0, 1) ne "+") { |
|
1328 last; |
|
1329 } |
|
1330 push(@overlappingLines, $line); |
|
1331 $lines[$i] = " " . substr($line, 1); |
|
1332 } |
|
1333 |
|
1334 # Remove excess ending context, if necessary. |
|
1335 my $shouldTrimContext = 1; |
|
1336 for (; $i < @lines; ++$i) { |
|
1337 my $firstChar = substr($lines[$i], 0, 1); |
|
1338 if ($firstChar eq " ") { |
|
1339 next; |
|
1340 } elsif ($firstChar eq "@") { |
|
1341 last; |
|
1342 } |
|
1343 $shouldTrimContext = 0; # For example, if "+" or "-" encountered. |
|
1344 last; |
|
1345 } |
|
1346 my $deletedLineCount = 0; |
|
1347 if ($shouldTrimContext) { # Also occurs if end of file reached. |
|
1348 splice(@lines, $i - @overlappingLines, @overlappingLines); |
|
1349 $deletedLineCount = @overlappingLines; |
|
1350 } |
|
1351 |
|
1352 # Work backwards, shifting overlapping lines towards front |
|
1353 # while checking that patch stays equivalent. |
|
1354 for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) { |
|
1355 my $line = $lines[$i]; |
|
1356 if (substr($line, 0, 1) ne " ") { |
|
1357 next; |
|
1358 } |
|
1359 my $text = substr($line, 1); |
|
1360 my $newLine = pop(@overlappingLines); |
|
1361 if ($text ne substr($newLine, 1)) { |
|
1362 return $patch; # Unexpected difference. |
|
1363 } |
|
1364 $lines[$i] = "+$text"; |
|
1365 } |
|
1366 |
|
1367 # Finish moving whatever overlapping lines remain, and update |
|
1368 # the initial chunk range. |
|
1369 my $chunkRangeRegEx = '^\@\@ -(\d+),(\d+) \+\d+,(\d+) \@\@$'; # e.g. @@ -2,6 +2,18 @@ |
|
1370 if ($lines[$chunkStartIndex - 1] !~ /$chunkRangeRegEx/) { |
|
1371 # FIXME: Handle errors differently from ChangeLog files that |
|
1372 # are okay but should not be altered. That way we can find out |
|
1373 # if improvements to the script ever become necessary. |
|
1374 return $patch; # Error: unexpected patch string format. |
|
1375 } |
|
1376 my $skippedFirstLineCount = $1 - 1; |
|
1377 my $oldSourceLineCount = $2; |
|
1378 my $oldTargetLineCount = $3; |
|
1379 |
|
1380 if (@overlappingLines != $skippedFirstLineCount) { |
|
1381 # This can happen, for example, when deliberately inserting |
|
1382 # a new ChangeLog entry earlier in the file. |
|
1383 return $patch; |
|
1384 } |
|
1385 # If @overlappingLines > 0, this is where we make use of the |
|
1386 # assumption that the beginning of the source file was not modified. |
|
1387 splice(@lines, $chunkStartIndex, 0, @overlappingLines); |
|
1388 |
|
1389 my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount; |
|
1390 my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount; |
|
1391 $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@"; |
|
1392 |
|
1393 return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline. |
|
1394 } |
|
1395 |
|
1396 # This is a supporting method for runPatchCommand. |
|
1397 # |
|
1398 # Arg: the optional $args parameter passed to runPatchCommand (can be undefined). |
|
1399 # |
|
1400 # Returns ($patchCommand, $isForcing). |
|
1401 # |
|
1402 # This subroutine has unit tests in VCSUtils_unittest.pl. |
|
1403 sub generatePatchCommand($) |
|
1404 { |
|
1405 my ($passedArgsHashRef) = @_; |
|
1406 |
|
1407 my $argsHashRef = { # Defaults |
|
1408 ensureForce => 0, |
|
1409 shouldReverse => 0, |
|
1410 options => [] |
|
1411 }; |
|
1412 |
|
1413 # Merges hash references. It's okay here if passed hash reference is undefined. |
|
1414 @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef}; |
|
1415 |
|
1416 my $ensureForce = $argsHashRef->{ensureForce}; |
|
1417 my $shouldReverse = $argsHashRef->{shouldReverse}; |
|
1418 my $options = $argsHashRef->{options}; |
|
1419 |
|
1420 if (! $options) { |
|
1421 $options = []; |
|
1422 } else { |
|
1423 $options = [@{$options}]; # Copy to avoid side effects. |
|
1424 } |
|
1425 |
|
1426 my $isForcing = 0; |
|
1427 if (grep /^--force$/, @{$options}) { |
|
1428 $isForcing = 1; |
|
1429 } elsif ($ensureForce) { |
|
1430 push @{$options}, "--force"; |
|
1431 $isForcing = 1; |
|
1432 } |
|
1433 |
|
1434 if ($shouldReverse) { # No check: --reverse should never be passed explicitly. |
|
1435 push @{$options}, "--reverse"; |
|
1436 } |
|
1437 |
|
1438 @{$options} = sort(@{$options}); # For easier testing. |
|
1439 |
|
1440 my $patchCommand = join(" ", "patch -p0", @{$options}); |
|
1441 |
|
1442 return ($patchCommand, $isForcing); |
|
1443 } |
|
1444 |
|
1445 # Apply the given patch using the patch(1) command. |
|
1446 # |
|
1447 # On success, return the resulting exit status. Otherwise, exit with the |
|
1448 # exit status. If "--force" is passed as an option, however, then never |
|
1449 # exit and always return the exit status. |
|
1450 # |
|
1451 # Args: |
|
1452 # $patch: a patch string. |
|
1453 # $repositoryRootPath: an absolute path to the repository root. |
|
1454 # $pathRelativeToRoot: the path of the file to be patched, relative to the |
|
1455 # repository root. This should normally be the path |
|
1456 # found in the patch's "Index:" line. It is passed |
|
1457 # explicitly rather than reparsed from the patch |
|
1458 # string for optimization purposes. |
|
1459 # This is used only for error reporting. The |
|
1460 # patch command gleans the actual file to patch |
|
1461 # from the patch string. |
|
1462 # $args: a reference to a hash of optional arguments. The possible |
|
1463 # keys are -- |
|
1464 # ensureForce: whether to ensure --force is passed (defaults to 0). |
|
1465 # shouldReverse: whether to pass --reverse (defaults to 0). |
|
1466 # options: a reference to an array of options to pass to the |
|
1467 # patch command. The subroutine passes the -p0 option |
|
1468 # no matter what. This should not include --reverse. |
|
1469 # |
|
1470 # This subroutine has unit tests in VCSUtils_unittest.pl. |
|
1471 sub runPatchCommand($$$;$) |
|
1472 { |
|
1473 my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_; |
|
1474 |
|
1475 my ($patchCommand, $isForcing) = generatePatchCommand($args); |
|
1476 |
|
1477 # Temporarily change the working directory since the path found |
|
1478 # in the patch's "Index:" line is relative to the repository root |
|
1479 # (i.e. the same as $pathRelativeToRoot). |
|
1480 my $cwd = Cwd::getcwd(); |
|
1481 chdir $repositoryRootPath; |
|
1482 |
|
1483 open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!"; |
|
1484 print PATCH $patch; |
|
1485 close PATCH; |
|
1486 my $exitStatus = exitStatus($?); |
|
1487 |
|
1488 chdir $cwd; |
|
1489 |
|
1490 if ($exitStatus && !$isForcing) { |
|
1491 print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " . |
|
1492 "status $exitStatus. Pass --force to ignore patch failures.\n"; |
|
1493 exit $exitStatus; |
|
1494 } |
|
1495 |
|
1496 return $exitStatus; |
|
1497 } |
|
1498 |
|
1499 # Merge ChangeLog patches using a three-file approach. |
|
1500 # |
|
1501 # This is used by resolve-ChangeLogs when it's operated as a merge driver |
|
1502 # and when it's used to merge conflicts after a patch is applied or after |
|
1503 # an svn update. |
|
1504 # |
|
1505 # It's also used for traditional rejected patches. |
|
1506 # |
|
1507 # Args: |
|
1508 # $fileMine: The merged version of the file. Also known in git as the |
|
1509 # other branch's version (%B) or "ours". |
|
1510 # For traditional patch rejects, this is the *.rej file. |
|
1511 # $fileOlder: The base version of the file. Also known in git as the |
|
1512 # ancestor version (%O) or "base". |
|
1513 # For traditional patch rejects, this is the *.orig file. |
|
1514 # $fileNewer: The current version of the file. Also known in git as the |
|
1515 # current version (%A) or "theirs". |
|
1516 # For traditional patch rejects, this is the original-named |
|
1517 # file. |
|
1518 # |
|
1519 # Returns 1 if merge was successful, else 0. |
|
1520 sub mergeChangeLogs($$$) |
|
1521 { |
|
1522 my ($fileMine, $fileOlder, $fileNewer) = @_; |
|
1523 |
|
1524 my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0; |
|
1525 |
|
1526 local $/ = undef; |
|
1527 |
|
1528 my $patch; |
|
1529 if ($traditionalReject) { |
|
1530 open(DIFF, "<", $fileMine) or die $!; |
|
1531 $patch = <DIFF>; |
|
1532 close(DIFF); |
|
1533 rename($fileMine, "$fileMine.save"); |
|
1534 rename($fileOlder, "$fileOlder.save"); |
|
1535 } else { |
|
1536 open(DIFF, "-|", qw(diff -u -a --binary), $fileOlder, $fileMine) or die $!; |
|
1537 $patch = <DIFF>; |
|
1538 close(DIFF); |
|
1539 } |
|
1540 |
|
1541 unlink("${fileNewer}.orig"); |
|
1542 unlink("${fileNewer}.rej"); |
|
1543 |
|
1544 open(PATCH, "| patch --force --fuzz=3 --binary $fileNewer > " . File::Spec->devnull()) or die $!; |
|
1545 print PATCH ($traditionalReject ? $patch : fixChangeLogPatch($patch)); |
|
1546 close(PATCH); |
|
1547 |
|
1548 my $result = !exitStatus($?); |
|
1549 |
|
1550 # Refuse to merge the patch if it did not apply cleanly |
|
1551 if (-e "${fileNewer}.rej") { |
|
1552 unlink("${fileNewer}.rej"); |
|
1553 if (-f "${fileNewer}.orig") { |
|
1554 unlink($fileNewer); |
|
1555 rename("${fileNewer}.orig", $fileNewer); |
|
1556 } |
|
1557 } else { |
|
1558 unlink("${fileNewer}.orig"); |
|
1559 } |
|
1560 |
|
1561 if ($traditionalReject) { |
|
1562 rename("$fileMine.save", $fileMine); |
|
1563 rename("$fileOlder.save", $fileOlder); |
|
1564 } |
|
1565 |
|
1566 return $result; |
|
1567 } |
|
1568 |
|
1569 sub gitConfig($) |
|
1570 { |
|
1571 return unless $isGit; |
|
1572 |
|
1573 my ($config) = @_; |
|
1574 |
|
1575 my $result = `git config $config`; |
|
1576 if (($? >> 8)) { |
|
1577 $result = `git repo-config $config`; |
|
1578 } |
|
1579 chomp $result; |
|
1580 return $result; |
|
1581 } |
|
1582 |
|
1583 sub changeLogNameError($) |
|
1584 { |
|
1585 my ($message) = @_; |
|
1586 print STDERR "$message\nEither:\n"; |
|
1587 print STDERR " set CHANGE_LOG_NAME in your environment\n"; |
|
1588 print STDERR " OR pass --name= on the command line\n"; |
|
1589 print STDERR " OR set REAL_NAME in your environment"; |
|
1590 print STDERR " OR git users can set 'git config user.name'\n"; |
|
1591 exit(1); |
|
1592 } |
|
1593 |
|
1594 sub changeLogName() |
|
1595 { |
|
1596 my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0]; |
|
1597 |
|
1598 changeLogNameError("Failed to determine ChangeLog name.") unless $name; |
|
1599 # getpwuid seems to always succeed on windows, returning the username instead of the full name. This check will catch that case. |
|
1600 changeLogNameError("'$name' does not contain a space! ChangeLogs should contain your full name.") unless ($name =~ /\w \w/); |
|
1601 |
|
1602 return $name; |
|
1603 } |
|
1604 |
|
1605 sub changeLogEmailAddressError($) |
|
1606 { |
|
1607 my ($message) = @_; |
|
1608 print STDERR "$message\nEither:\n"; |
|
1609 print STDERR " set CHANGE_LOG_EMAIL_ADDRESS in your environment\n"; |
|
1610 print STDERR " OR pass --email= on the command line\n"; |
|
1611 print STDERR " OR set EMAIL_ADDRESS in your environment\n"; |
|
1612 print STDERR " OR git users can set 'git config user.email'\n"; |
|
1613 exit(1); |
|
1614 } |
|
1615 |
|
1616 sub changeLogEmailAddress() |
|
1617 { |
|
1618 my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email"); |
|
1619 |
|
1620 changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress; |
|
1621 changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/); |
|
1622 |
|
1623 return $emailAddress; |
|
1624 } |
|
1625 |
|
1626 # http://tools.ietf.org/html/rfc1924 |
|
1627 sub decodeBase85($) |
|
1628 { |
|
1629 my ($encoded) = @_; |
|
1630 my %table; |
|
1631 my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~'); |
|
1632 for (my $i = 0; $i < 85; $i++) { |
|
1633 $table{$characters[$i]} = $i; |
|
1634 } |
|
1635 |
|
1636 my $decoded = ''; |
|
1637 my @encodedChars = $encoded =~ /./g; |
|
1638 |
|
1639 for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) { |
|
1640 my $digit = 0; |
|
1641 for (my $i = 0; $i < 5; $i++) { |
|
1642 $digit *= 85; |
|
1643 my $char = $encodedChars[$encodedIter]; |
|
1644 $digit += $table{$char}; |
|
1645 $encodedIter++; |
|
1646 } |
|
1647 |
|
1648 for (my $i = 0; $i < 4; $i++) { |
|
1649 $decoded .= chr(($digit >> (3 - $i) * 8) & 255); |
|
1650 } |
|
1651 } |
|
1652 |
|
1653 return $decoded; |
|
1654 } |
|
1655 |
|
1656 sub decodeGitBinaryChunk($$) |
|
1657 { |
|
1658 my ($contents, $fullPath) = @_; |
|
1659 |
|
1660 # Load this module lazily in case the user don't have this module |
|
1661 # and won't handle git binary patches. |
|
1662 require Compress::Zlib; |
|
1663 |
|
1664 my $encoded = ""; |
|
1665 my $compressedSize = 0; |
|
1666 while ($contents =~ /^([A-Za-z])(.*)$/gm) { |
|
1667 my $line = $2; |
|
1668 next if $line eq ""; |
|
1669 die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0; |
|
1670 my $actualSize = length($2) / 5 * 4; |
|
1671 my $encodedExpectedSize = ord($1); |
|
1672 my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27; |
|
1673 |
|
1674 die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize; |
|
1675 $compressedSize += $expectedSize; |
|
1676 $encoded .= $line; |
|
1677 } |
|
1678 |
|
1679 my $compressed = decodeBase85($encoded); |
|
1680 $compressed = substr($compressed, 0, $compressedSize); |
|
1681 return Compress::Zlib::uncompress($compressed); |
|
1682 } |
|
1683 |
|
1684 sub decodeGitBinaryPatch($$) |
|
1685 { |
|
1686 my ($contents, $fullPath) = @_; |
|
1687 |
|
1688 # Git binary patch has two chunks. One is for the normal patching |
|
1689 # and another is for the reverse patching. |
|
1690 # |
|
1691 # Each chunk a line which starts from either "literal" or "delta", |
|
1692 # followed by a number which specifies decoded size of the chunk. |
|
1693 # The "delta" type chunks aren't supported by this function yet. |
|
1694 # |
|
1695 # Then, content of the chunk comes. To decode the content, we |
|
1696 # need decode it with base85 first, and then zlib. |
|
1697 my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n'; |
|
1698 if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") { |
|
1699 die "$fullPath: unknown git binary patch format" |
|
1700 } |
|
1701 |
|
1702 my $binaryChunkType = $1; |
|
1703 my $binaryChunkExpectedSize = $2; |
|
1704 my $encodedChunk = $3; |
|
1705 my $reverseBinaryChunkType = $4; |
|
1706 my $reverseBinaryChunkExpectedSize = $5; |
|
1707 my $encodedReverseChunk = $6; |
|
1708 |
|
1709 my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath); |
|
1710 my $binaryChunkActualSize = length($binaryChunk); |
|
1711 my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath); |
|
1712 my $reverseBinaryChunkActualSize = length($reverseBinaryChunk); |
|
1713 |
|
1714 die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkExpectedSize != $binaryChunkActualSize); |
|
1715 die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize); |
|
1716 |
|
1717 return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk); |
|
1718 } |
|
1719 |
|
1720 1; |