2009-07-08 Maciej Stachowiak <mjs@apple.com>
[WebKit.git] / WebKitTools / Scripts / prepare-ChangeLog
1 #!/usr/bin/perl -w
2 # -*- Mode: perl; indent-tabs-mode: nil; c-basic-offset: 2  -*-
3
4 #
5 #  Copyright (C) 2000, 2001 Eazel, Inc.
6 #  Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Apple Inc.  All rights reserved.
7 #  Copyright (C) 2009 Torchmobile, Inc.
8 #
9 #  prepare-ChangeLog is free software; you can redistribute it and/or
10 #  modify it under the terms of the GNU General Public
11 #  License as published by the Free Software Foundation; either
12 #  version 2 of the License, or (at your option) any later version.
13 #
14 #  prepare-ChangeLog is distributed in the hope that it will be useful,
15 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 #  General Public License for more details.
18 #
19 #  You should have received a copy of the GNU General Public
20 #  License along with this program; if not, write to the Free
21 #  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22 #
23
24
25 # Perl script to create a ChangeLog entry with names of files
26 # and functions from a diff.
27 #
28 # Darin Adler <darin@bentspoon.com>, started 20 April 2000
29 # Java support added by Maciej Stachowiak <mjs@eazel.com>
30 # Objective-C, C++ and Objective-C++ support added by Maciej Stachowiak <mjs@apple.com>
31 # Git support added by Adam Roben <aroben@apple.com>
32 # --git-index flag added by Joe Mason <joe.mason@torchmobile.com>
33
34
35 #
36 # TODO:
37 #   List functions that have been removed too.
38 #   Decide what a good logical order is for the changed files
39 #     other than a normal text "sort" (top level first?)
40 #     (group directories?) (.h before .c?)
41 #   Handle yacc source files too (other languages?).
42 #   Help merge when there are ChangeLog conflicts or if there's
43 #     already a partly written ChangeLog entry.
44 #   Add command line option to put the ChangeLog into a separate
45 #     file or just spew it out stdout.
46 #   Add SVN version numbers for commit (can't do that until
47 #     the changes are checked in, though).
48 #   Work around diff stupidity where deleting a function that starts
49 #     with a comment makes diff think that the following function
50 #     has been changed (if the following function starts with a comment
51 #     with the same first line, such as /**)
52 #   Work around diff stupidity where deleting an entire function and
53 #     the blank lines before it makes diff think you've changed the
54 #     previous function.
55
56 use strict;
57 use warnings;
58
59 use File::Basename;
60 use File::Spec;
61 use FindBin;
62 use Getopt::Long;
63 use lib $FindBin::Bin;
64 use POSIX qw(strftime);
65 use VCSUtils;
66
67 sub changeLogDate($);
68 sub changeLogEmailAddress($);
69 sub changeLogName($);
70 sub firstDirectoryOrCwd();
71 sub diffFromToString();
72 sub diffCommand(@);
73 sub statusCommand(@);
74 sub createPatchCommand($);
75 sub diffHeaderFormat();
76 sub findOriginalFileFromSvn($);
77 sub generateFileList(\@\@\%);
78 sub gitConfig($);
79 sub isModifiedStatus($);
80 sub isAddedStatus($);
81 sub isConflictStatus($);
82 sub statusDescription($$);
83 sub extractLineRange($);
84 sub canonicalizePath($);
85 sub testListForChangeLog(@);
86 sub get_function_line_ranges($$);
87 sub get_function_line_ranges_for_c($$);
88 sub get_function_line_ranges_for_java($$);
89 sub get_function_line_ranges_for_javascript($$);
90 sub method_decl_to_selector($);
91 sub processPaths(\@);
92 sub reviewerAndDescriptionForGitCommit($);
93 sub normalizeLineEndings($$);
94 sub normalizePath($);
95
96 # Project time zone for Cupertino, CA, US
97 my $changeLogTimeZone = "PST8PDT";
98
99 my $bugNumber;
100 my $name;
101 my $emailAddress;
102 my $gitCommit = 0;
103 my $gitIndex = "";
104 my $gitReviewer = "";
105 my $openChangeLogs = 0;
106 my $showHelp = 0;
107 my $spewDiff = $ENV{"PREPARE_CHANGELOG_DIFF"};
108 my $updateChangeLogs = 1;
109 my $parseOptionsResult =
110     GetOptions("diff|d!" => \$spewDiff,
111                "bug:i" => \$bugNumber,
112                "name:s" => \$name,
113                "email:s" => \$emailAddress,
114                "git-commit:s" => \$gitCommit,
115                "git-index" => \$gitIndex,
116                "git-reviewer:s" => \$gitReviewer,
117                "help|h!" => \$showHelp,
118                "open|o!" => \$openChangeLogs,
119                "update!" => \$updateChangeLogs);
120 if (!$parseOptionsResult || $showHelp) {
121     print STDERR basename($0) . " [-d|--diff] [-h|--help] [-o|--open] [--git-commit=<committish>] [--git-reviewer=<name>] [svndir1 [svndir2 ...]]\n";
122     print STDERR "  -d|--diff      Spew diff to stdout when running\n";
123     print STDERR "  --git-commit   Populate the ChangeLogs from the specified git commit\n";
124     print STDERR "  --git-index    Populate the ChangeLogs from the git index only\n";
125     print STDERR "  --git-reviewer When populating the ChangeLogs from a git commit claim that the spcified name reviewed the change.\n";
126     print STDERR "                 This option is useful when the git commit lacks a Signed-Off-By: line\n";
127     print STDERR "  -h|--help      Show this help message\n";
128     print STDERR "  -o|--open      Open ChangeLogs in an editor when done\n";
129     print STDERR "  --[no-]update  Update ChangeLogs from svn before adding entry (default: update)\n";
130     exit 1;
131 }
132
133 die "--git-commit and --git-index are incompatible." if ($gitIndex && $gitCommit);
134
135 my %paths = processPaths(@ARGV);
136
137 my $isGit = isGitDirectory(firstDirectoryOrCwd());
138 my $isSVN = isSVNDirectory(firstDirectoryOrCwd());
139
140 $isSVN || $isGit || die "Couldn't determine your version control system.";
141
142 my $SVN = "svn";
143 my $GIT = "git";
144
145 my $svnVersion = `svn --version --quiet` if $isSVN;
146
147 # Find the list of modified files
148 my @changed_files;
149 my $changed_files_string;
150 my %changed_line_ranges;
151 my %function_lists;
152 my @conflict_files;
153
154
155 my %supportedTestExtensions = map { $_ => 1 } qw(html shtml svg xml xhtml pl php);
156 my @addedRegressionTests = ();
157 my $didChangeRegressionTests = 0;
158
159 generateFileList(@changed_files, @conflict_files, %function_lists);
160
161 if (!@changed_files && !@conflict_files && !keys %function_lists) {
162     print STDERR "  No changes found.\n";
163     exit 1;
164 }
165
166 if (@conflict_files) {
167     print STDERR "  The following files have conflicts. Run prepare-ChangeLog again after fixing the conflicts:\n";
168     print STDERR join("\n", @conflict_files), "\n";
169     exit 1;
170 }
171
172 if (@changed_files) {
173     $changed_files_string = "'" . join ("' '", @changed_files) . "'";
174
175     # For each file, build a list of modified lines.
176     # Use line numbers from the "after" side of each diff.
177     print STDERR "  Reviewing diff to determine which lines changed.\n";
178     my $file;
179     open DIFF, "-|", diffCommand(@changed_files) or die "The diff failed: $!.\n";
180     while (<DIFF>) {
181         $file = makeFilePathRelative($1) if $_ =~ diffHeaderFormat();
182         if (defined $file) {
183             my ($start, $end) = extractLineRange($_);
184             if ($start >= 0 && $end >= 0) {
185                 push @{$changed_line_ranges{$file}}, [ $start, $end ];
186             } elsif (/DO_NOT_COMMIT/) {
187                 print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
188             }
189         }
190     }
191     close DIFF;
192 }
193
194 # For each source file, convert line range to function list.
195 if (%changed_line_ranges) {
196     print STDERR "  Extracting affected function names from source files.\n";
197     foreach my $file (keys %changed_line_ranges) {
198         # Only look for function names in certain source files.
199         next unless $file =~ /\.(c|cpp|m|mm|h|java|js)/;
200     
201         # Find all the functions in the file.
202         open SOURCE, $file or next;
203         my @function_ranges = get_function_line_ranges(\*SOURCE, $file);
204         close SOURCE;
205     
206         # Find all the modified functions.
207         my @functions;
208         my %saw_function;
209         my @change_ranges = (@{$changed_line_ranges{$file}}, []);
210         my @change_range = (0, 0);
211         FUNCTION: foreach my $function_range_ref (@function_ranges) {
212             my @function_range = @$function_range_ref;
213     
214             # Advance to successive change ranges.
215             for (;; @change_range = @{shift @change_ranges}) {
216                 last FUNCTION unless @change_range;
217     
218                 # If past this function, move on to the next one.
219                 next FUNCTION if $change_range[0] > $function_range[1];
220     
221                 # If an overlap with this function range, record the function name.
222                 if ($change_range[1] >= $function_range[0]
223                     and $change_range[0] <= $function_range[1]) {
224                     if (!$saw_function{$function_range[2]}) {
225                         $saw_function{$function_range[2]} = 1;
226                         push @functions, $function_range[2];
227                     }
228                     next FUNCTION;
229                 }
230             }
231         }
232     
233         # Format the list of functions now.
234
235         if (@functions) {
236             $function_lists{$file} = "" if !defined $function_lists{$file};
237             $function_lists{$file} .= "\n        (" . join("):\n        (", @functions) . "):";
238         }
239     }
240 }
241
242 # Get some parameters for the ChangeLog we are about to write.
243 my $date = changeLogDate($changeLogTimeZone);
244 $name = changeLogName($name);
245 $emailAddress = changeLogEmailAddress($emailAddress);
246
247 print STDERR "  Change author: $name <$emailAddress>.\n";
248
249 my $bugDescription;
250 my $bugURL;
251 if ($bugNumber) {
252     $bugURL = "https://bugs.webkit.org/show_bug.cgi?id=$bugNumber";
253     my $bugXMLURL = "$bugURL&ctype=xml";
254     # Perl has no built in XML processing, so we'll fetch and parse with curl and grep
255     my $descriptionLine = `curl --silent "$bugXMLURL" | grep short_desc`;
256     $descriptionLine =~ /<short_desc>(.*)<\/short_desc>/;
257     $bugDescription = $1;
258     print STDERR "  Description from bug $bugNumber:\n    \"$bugDescription\".\n";
259 }
260
261 # Remove trailing parenthesized notes from user name (bit of hack).
262 $name =~ s/\(.*?\)\s*$//g;
263
264 # Find the change logs.
265 my %has_log;
266 my %files;
267 foreach my $file (sort keys %function_lists) {
268     my $prefix = $file;
269     my $has_log = 0;
270     while ($prefix) {
271         $prefix =~ s-/[^/]+/?$-/- or $prefix = "";
272         $has_log = $has_log{$prefix};
273         if (!defined $has_log) {
274             $has_log = -f "${prefix}ChangeLog";
275             $has_log{$prefix} = $has_log;
276         }
277         last if $has_log;
278     }
279     if (!$has_log) {
280         print STDERR "No ChangeLog found for $file.\n";
281     } else {
282         push @{$files{$prefix}}, $file;
283     }
284 }
285
286 # Get the latest ChangeLog files from svn.
287 my @logs = ();
288 foreach my $prefix (sort keys %files) {
289     push @logs, File::Spec->catfile($prefix || ".", "ChangeLog");
290 }
291
292 if (@logs && $updateChangeLogs && $isSVN) {
293     print STDERR "  Running 'svn update' to update ChangeLog files.\n";
294     open ERRORS, "-|", $SVN, "update", @logs
295         or die "The svn update of ChangeLog files failed: $!.\n";
296     my @conflictedChangeLogs;
297     while (my $line = <ERRORS>) {
298         print STDERR "    ", $line;
299         push @conflictedChangeLogs, $1 if $line =~ m/^C\s+(.+)\s*$/;
300     }
301     close ERRORS;
302
303     if (@conflictedChangeLogs) {
304         print STDERR "  Attempting to merge conflicted ChangeLogs.\n";
305         my $resolveChangeLogsPath = File::Spec->catfile(dirname($0), "resolve-ChangeLogs");
306         open RESOLVE, "-|", $resolveChangeLogsPath, "--no-warnings", @conflictedChangeLogs
307             or die "Could not open resolve-ChangeLogs script: $!.\n";
308         print STDERR "    $_" while <RESOLVE>;
309         close RESOLVE;
310     }
311 }
312
313 # Write out a new ChangeLog file.
314 foreach my $prefix (sort keys %files) {
315     my $changeLogPath = File::Spec->catfile($prefix || ".", "ChangeLog");
316     print STDERR "  Editing the ${changeLogPath} file.\n";
317     open OLD_CHANGE_LOG, ${changeLogPath} or die "Could not open ${changeLogPath} file: $!.\n";
318     # It's less efficient to read the whole thing into memory than it would be
319     # to read it while we prepend to it later, but I like doing this part first.
320     my @old_change_log = <OLD_CHANGE_LOG>;
321     close OLD_CHANGE_LOG;
322     # We want to match the ChangeLog's line endings in case it doesn't match
323     # the native line endings for this version of perl.
324     my $endl = "\n";
325     if ($old_change_log[0] =~ /(\r?\n)/g) {
326         $endl = "$1";
327     }
328     open CHANGE_LOG, "> ${changeLogPath}" or die "Could not write ${changeLogPath}\n.";
329     print CHANGE_LOG normalizeLineEndings("$date  $name  <$emailAddress>\n\n", $endl);
330
331     my ($reviewer, $description) = reviewerAndDescriptionForGitCommit($gitCommit) if $gitCommit;
332     $reviewer = "NOBODY (OO" . "PS!)" if !$reviewer;
333
334     print CHANGE_LOG normalizeLineEndings("        Reviewed by $reviewer.\n\n", $endl);
335     print CHANGE_LOG normalizeLineEndings($description . "\n", $endl) if $description;
336
337     $bugDescription = "Need a short description and bug URL (OOPS!)" unless $bugDescription;
338     print CHANGE_LOG normalizeLineEndings("        $bugDescription\n", $endl) if $bugDescription;
339     print CHANGE_LOG normalizeLineEndings("        $bugURL\n", $endl) if $bugURL;
340     print CHANGE_LOG "\n";
341
342     # We could pre-populate this with the git commit log or with the bug description.
343     print ("-- Please remember to include a detailed description in your ChangeLog entry. --\n-- See <http://webkit.org/coding/contributing.html> for more info --\n");
344
345     if ($prefix =~ m/WebCore/ || `pwd` =~ m/WebCore/) {
346         if ($didChangeRegressionTests) {
347             print CHANGE_LOG normalizeLineEndings(testListForChangeLog(sort @addedRegressionTests), $endl);
348         } else {
349             print CHANGE_LOG normalizeLineEndings("        No new tests. (OOPS!)\n\n", $endl);
350         }
351     }
352
353     foreach my $file (sort @{$files{$prefix}}) {
354         my $file_stem = normalizePath(substr $file, length $prefix);
355         print CHANGE_LOG normalizeLineEndings("        * $file_stem:$function_lists{$file}\n", $endl);;
356     }
357     print CHANGE_LOG normalizeLineEndings("\n", $endl), @old_change_log;
358     close CHANGE_LOG;
359 }
360
361 # Write out another diff.
362 if ($spewDiff && @changed_files) {
363     print STDERR "  Running diff to help you write the ChangeLog entries.\n";
364     local $/ = undef; # local slurp mode
365     open DIFF, "-|", createPatchCommand($changed_files_string) or die "The diff failed: $!.\n";
366     print <DIFF>;
367     close DIFF;
368 }
369
370 # Open ChangeLogs.
371 if ($openChangeLogs && @logs) {
372     print STDERR "  Opening the edited ChangeLog files.\n";
373     my $editor = $ENV{"CHANGE_LOG_EDIT_APPLICATION"};
374     if ($editor) {
375         system "open", "-a", $editor, @logs;
376     } else {
377         system "open", "-e", @logs;
378     }
379 }
380
381 # Done.
382 exit;
383
384 sub canonicalizePath($)
385 {
386     my ($file) = @_;
387
388     # Remove extra slashes and '.' directories in path
389     $file = File::Spec->canonpath($file);
390
391     # Remove '..' directories in path
392     my @dirs = ();
393     foreach my $dir (File::Spec->splitdir($file)) {
394         if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
395             pop(@dirs);
396         } else {
397             push(@dirs, $dir);
398         }
399     }
400     return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
401 }
402
403 sub changeLogDate($)
404 {
405     my ($timeZone) = @_;
406     my $savedTimeZone = $ENV{'TZ'};
407     # Set TZ temporarily so that localtime() is in that time zone
408     $ENV{'TZ'} = $timeZone;
409     my $date = strftime("%Y-%m-%d", localtime());
410     if (defined $savedTimeZone) {
411          $ENV{'TZ'} = $savedTimeZone;
412     } else {
413          delete $ENV{'TZ'};
414     }
415     return $date;
416 }
417
418 sub changeLogNameError($)
419 {
420     my ($message) = @_;
421     print STDERR "$message\nEither:\n";
422     print STDERR "  set CHANGE_LOG_NAME in your environment\n";
423     print STDERR "  OR pass --name= on the command line\n";
424     print STDERR "  OR set REAL_NAME in your environment";
425     print STDERR "  OR git users can set 'git config user.name'\n";
426     exit(1);
427 }
428
429 sub changeLogName($)
430 {
431     my ($nameFromArgs) = @_;
432     # Silently allow --git-commit to win, we could warn if $emailAddressFromArgs is defined.
433     return `$GIT log --max-count=1 --pretty=\"format:%an\" \"$gitCommit\"` if $gitCommit;
434
435     my $name = $nameFromArgs
436         || $ENV{CHANGE_LOG_NAME}
437         || $ENV{REAL_NAME}
438         || gitConfig("user.name")
439         || (split /\s*,\s*/, (getpwuid $<)[6])[0];
440
441     changeLogNameError("Failed to determine ChangeLog name.") unless $name;
442     # getpwuid seems to always succeed on windows, returning the username instead of the full name.  This check will catch that case.
443     changeLogNameError("'$name' does not contain a space!  ChangeLogs should contain your full name.") unless ($name =~ /\w \w/);
444
445     return $name;
446 }
447
448 sub changeLogEmailAddressError($)
449 {
450     my ($message) = @_;
451     print STDERR "$message\nEither:\n";
452     print STDERR "  set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
453     print STDERR "  OR pass --email= on the command line\n";
454     print STDERR "  OR set EMAIL_ADDRESS in your environment\n";
455     print STDERR "  OR git users can set 'git config user.email'\n";
456     exit(1);
457 }
458
459 sub changeLogEmailAddress($)
460 {
461     my ($emailAddressFromArgs) = @_;
462     # Silently allow --git-commit to win, we could warn if $emailAddressFromArgs is defined.
463     return `$GIT log --max-count=1 --pretty=\"format:%ae\" \"$gitCommit\"` if $gitCommit;
464
465     my $emailAddress = $emailAddressFromArgs
466         || $ENV{CHANGE_LOG_EMAIL_ADDRESS}
467         || $ENV{EMAIL_ADDRESS}
468         || gitConfig("user.email");
469
470     changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
471     changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
472
473     return $emailAddress;
474 }
475
476 sub get_function_line_ranges($$)
477 {
478     my ($file_handle, $file_name) = @_;
479
480     if ($file_name =~ /\.(c|cpp|m|mm|h)$/) {
481         return get_function_line_ranges_for_c ($file_handle, $file_name);
482     } elsif ($file_name =~ /\.java$/) {
483         return get_function_line_ranges_for_java ($file_handle, $file_name);
484     } elsif ($file_name =~ /\.js$/) {
485         return get_function_line_ranges_for_javascript ($file_handle, $file_name);
486     }
487     return ();
488 }
489
490
491 sub method_decl_to_selector($)
492 {
493     (my $method_decl) = @_;
494
495     $_ = $method_decl;
496
497     if ((my $comment_stripped) = m-([^/]*)(//|/*).*-) {
498         $_ = $comment_stripped;
499     }
500
501     s/,\s*...//;
502
503     if (/:/) {
504         my @components = split /:/;
505         pop @components if (scalar @components > 1);
506         $_ = (join ':', map {s/.*[^[:word:]]//; scalar $_;} @components) . ':';
507     } else {
508         s/\s*$//;
509         s/.*[^[:word:]]//;
510     }
511
512     return $_;
513 }
514
515
516
517 # Read a file and get all the line ranges of the things that look like C functions.
518 # A function name is the last word before an open parenthesis before the outer
519 # level open brace. A function starts at the first character after the last close
520 # brace or semicolon before the function name and ends at the close brace.
521 # Comment handling is simple-minded but will work for all but pathological cases.
522 #
523 # Result is a list of triples: [ start_line, end_line, function_name ].
524
525 sub get_function_line_ranges_for_c($$)
526 {
527     my ($file_handle, $file_name) = @_;
528
529     my @ranges;
530
531     my $in_comment = 0;
532     my $in_macro = 0;
533     my $in_method_declaration = 0;
534     my $in_parentheses = 0;
535     my $in_braces = 0;
536     my $brace_start = 0;
537     my $brace_end = 0;
538     my $skip_til_brace_or_semicolon = 0;
539
540     my $word = "";
541     my $interface_name = "";
542
543     my $potential_method_char = "";
544     my $potential_method_spec = "";
545
546     my $potential_start = 0;
547     my $potential_name = "";
548
549     my $start = 0;
550     my $name = "";
551
552     my $next_word_could_be_namespace = 0;
553     my $potential_namespace = "";
554     my @namespaces;
555
556     while (<$file_handle>) {
557         # Handle continued multi-line comment.
558         if ($in_comment) {
559             next unless s-.*\*/--;
560             $in_comment = 0;
561         }
562
563         # Handle continued macro.
564         if ($in_macro) {
565             $in_macro = 0 unless /\\$/;
566             next;
567         }
568
569         # Handle start of macro (or any preprocessor directive).
570         if (/^\s*\#/) {
571             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
572             next;
573         }
574
575         # Handle comments and quoted text.
576         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
577             my $match = $1;
578             if ($match eq "/*") {
579                 if (!s-/\*.*?\*/--) {
580                     s-/\*.*--;
581                     $in_comment = 1;
582                 }
583             } elsif ($match eq "//") {
584                 s-//.*--;
585             } else { # ' or "
586                 if (!s-$match([^\\]|\\.)*?$match--) {
587                     warn "mismatched quotes at line $. in $file_name\n";
588                     s-$match.*--;
589                 }
590             }
591         }
592
593
594         # continued method declaration
595         if ($in_method_declaration) {
596               my $original = $_;
597               my $method_cont = $_;
598
599               chomp $method_cont;
600               $method_cont =~ s/[;\{].*//;
601               $potential_method_spec = "${potential_method_spec} ${method_cont}";
602
603               $_ = $original;
604               if (/;/) {
605                   $potential_start = 0;
606                   $potential_method_spec = "";
607                   $potential_method_char = "";
608                   $in_method_declaration = 0;
609                   s/^[^;\{]*//;
610               } elsif (/{/) {
611                   my $selector = method_decl_to_selector ($potential_method_spec);
612                   $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
613                   
614                   $potential_method_spec = "";
615                   $potential_method_char = "";
616                   $in_method_declaration = 0;
617   
618                   $_ = $original;
619                   s/^[^;{]*//;
620               } elsif (/\@end/) {
621                   $in_method_declaration = 0;
622                   $interface_name = "";
623                   $_ = $original;
624               } else {
625                   next;
626               }
627         }
628
629         
630         # start of method declaration
631         if ((my $method_char, my $method_spec) = m&^([-+])([^0-9;][^;]*);?$&) {
632             my $original = $_;
633
634             if ($interface_name) {
635                 chomp $method_spec;
636                 $method_spec =~ s/\{.*//;
637
638                 $potential_method_char = $method_char;
639                 $potential_method_spec = $method_spec;
640                 $potential_start = $.;
641                 $in_method_declaration = 1;
642             } else { 
643                 warn "declaring a method but don't have interface on line $. in $file_name\n";
644             }
645             $_ = $original;
646             if (/\{/) {
647               my $selector = method_decl_to_selector ($potential_method_spec);
648               $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
649               
650               $potential_method_spec = "";
651               $potential_method_char = "";
652               $in_method_declaration = 0;
653               $_ = $original;
654               s/^[^{]*//;
655             } elsif (/\@end/) {
656               $in_method_declaration = 0;
657               $interface_name = "";
658               $_ = $original;
659             } else {
660               next;
661             }
662         }
663
664
665         # Find function, interface and method names.
666         while (m&((?:[[:word:]]+::)*operator(?:[ \t]*\(\)|[^()]*)|[[:word:]:~]+|[(){}:;])|\@(?:implementation|interface|protocol)\s+(\w+)[^{]*&g) {
667             # interface name
668             if ($2) {
669                 $interface_name = $2;
670                 next;
671             }
672
673             # Open parenthesis.
674             if ($1 eq "(") {
675                 $potential_name = $word unless $in_parentheses || $skip_til_brace_or_semicolon;
676                 $in_parentheses++;
677                 next;
678             }
679
680             # Close parenthesis.
681             if ($1 eq ")") {
682                 $in_parentheses--;
683                 next;
684             }
685
686             # C++ constructor initializers
687             if ($1 eq ":") {
688                   $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
689             }
690
691             # Open brace.
692             if ($1 eq "{") {
693                 $skip_til_brace_or_semicolon = 0;
694
695                 if ($potential_namespace) {
696                     push @namespaces, $potential_namespace;
697                     $potential_namespace = "";
698                     next;
699                 }
700
701                 # Promote potential name to real function name at the
702                 # start of the outer level set of braces (function body?).
703                 if (!$in_braces and $potential_start) {
704                     $start = $potential_start;
705                     $name = $potential_name;
706                     if (@namespaces && (length($name) < 2 || substr($name,1,1) ne "[")) {
707                         $name = join ('::', @namespaces, $name);
708                     }
709                 }
710
711                 $in_method_declaration = 0;
712
713                 $brace_start = $. if (!$in_braces);
714                 $in_braces++;
715                 next;
716             }
717
718             # Close brace.
719             if ($1 eq "}") {
720                 if (!$in_braces && @namespaces) {
721                     pop @namespaces;
722                     next;
723                 }
724
725                 $in_braces--;
726                 $brace_end = $. if (!$in_braces);
727
728                 # End of an outer level set of braces.
729                 # This could be a function body.
730                 if (!$in_braces and $name) {
731                     push @ranges, [ $start, $., $name ];
732                     $name = "";
733                 }
734
735                 $potential_start = 0;
736                 $potential_name = "";
737                 next;
738             }
739
740             # Semicolon.
741             if ($1 eq ";") {
742                 $skip_til_brace_or_semicolon = 0;
743                 $potential_start = 0;
744                 $potential_name = "";
745                 $in_method_declaration = 0;
746                 next;
747             }
748
749             # Ignore "const" method qualifier.
750             if ($1 eq "const") {
751                 next;
752             }
753
754             if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
755                 $next_word_could_be_namespace = 1;
756                 next;
757             }
758
759             # Word.
760             $word = $1;
761             if (!$skip_til_brace_or_semicolon) {
762                 if ($next_word_could_be_namespace) {
763                     $potential_namespace = $word;
764                     $next_word_could_be_namespace = 0;
765                 } elsif ($potential_namespace) {
766                     $potential_namespace = "";
767                 }
768
769                 if (!$in_parentheses) {
770                     $potential_start = 0;
771                     $potential_name = "";
772                 }
773                 if (!$potential_start) {
774                     $potential_start = $.;
775                     $potential_name = "";
776                 }
777             }
778         }
779     }
780
781     warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
782     warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
783
784     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
785
786     return @ranges;
787 }
788
789
790
791 # Read a file and get all the line ranges of the things that look like Java
792 # classes, interfaces and methods.
793 #
794 # A class or interface name is the word that immediately follows
795 # `class' or `interface' when followed by an open curly brace and not
796 # a semicolon. It can appear at the top level, or inside another class
797 # or interface block, but not inside a function block
798 #
799 # A class or interface starts at the first character after the first close
800 # brace or after the function name and ends at the close brace.
801 #
802 # A function name is the last word before an open parenthesis before
803 # an open brace rather than a semicolon. It can appear at top level or
804 # inside a class or interface block, but not inside a function block.
805 #
806 # A function starts at the first character after the first close
807 # brace or after the function name and ends at the close brace.
808 #
809 # Comment handling is simple-minded but will work for all but pathological cases.
810 #
811 # Result is a list of triples: [ start_line, end_line, function_name ].
812
813 sub get_function_line_ranges_for_java($$)
814 {
815     my ($file_handle, $file_name) = @_;
816
817     my @current_scopes;
818
819     my @ranges;
820
821     my $in_comment = 0;
822     my $in_macro = 0;
823     my $in_parentheses = 0;
824     my $in_braces = 0;
825     my $in_non_block_braces = 0;
826     my $class_or_interface_just_seen = 0;
827
828     my $word = "";
829
830     my $potential_start = 0;
831     my $potential_name = "";
832     my $potential_name_is_class_or_interface = 0;
833
834     my $start = 0;
835     my $name = "";
836     my $current_name_is_class_or_interface = 0;
837
838     while (<$file_handle>) {
839         # Handle continued multi-line comment.
840         if ($in_comment) {
841             next unless s-.*\*/--;
842             $in_comment = 0;
843         }
844
845         # Handle continued macro.
846         if ($in_macro) {
847             $in_macro = 0 unless /\\$/;
848             next;
849         }
850
851         # Handle start of macro (or any preprocessor directive).
852         if (/^\s*\#/) {
853             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
854             next;
855         }
856
857         # Handle comments and quoted text.
858         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
859             my $match = $1;
860             if ($match eq "/*") {
861                 if (!s-/\*.*?\*/--) {
862                     s-/\*.*--;
863                     $in_comment = 1;
864                 }
865             } elsif ($match eq "//") {
866                 s-//.*--;
867             } else { # ' or "
868                 if (!s-$match([^\\]|\\.)*?$match--) {
869                     warn "mismatched quotes at line $. in $file_name\n";
870                     s-$match.*--;
871                 }
872             }
873         }
874
875         # Find function names.
876         while (m-(\w+|[(){};])-g) {
877             # Open parenthesis.
878             if ($1 eq "(") {
879                 if (!$in_parentheses) {
880                     $potential_name = $word;
881                     $potential_name_is_class_or_interface = 0;
882                 }
883                 $in_parentheses++;
884                 next;
885             }
886
887             # Close parenthesis.
888             if ($1 eq ")") {
889                 $in_parentheses--;
890                 next;
891             }
892
893             # Open brace.
894             if ($1 eq "{") {
895                 # Promote potential name to real function name at the
896                 # start of the outer level set of braces (function/class/interface body?).
897                 if (!$in_non_block_braces
898                     and (!$in_braces or $current_name_is_class_or_interface)
899                     and $potential_start) {
900                     if ($name) {
901                           push @ranges, [ $start, ($. - 1),
902                                           join ('.', @current_scopes) ];
903                     }
904
905
906                     $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
907
908                     $start = $potential_start;
909                     $name = $potential_name;
910
911                     push (@current_scopes, $name);
912                 } else {
913                     $in_non_block_braces++;
914                 }
915
916                 $potential_name = "";
917                 $potential_start = 0;
918
919                 $in_braces++;
920                 next;
921             }
922
923             # Close brace.
924             if ($1 eq "}") {
925                 $in_braces--;
926
927                 # End of an outer level set of braces.
928                 # This could be a function body.
929                 if (!$in_non_block_braces) {
930                     if ($name) {
931                         push @ranges, [ $start, $.,
932                                         join ('.', @current_scopes) ];
933
934                         pop (@current_scopes);
935
936                         if (@current_scopes) {
937                             $current_name_is_class_or_interface = 1;
938
939                             $start = $. + 1;
940                             $name =  $current_scopes[$#current_scopes-1];
941                         } else {
942                             $current_name_is_class_or_interface = 0;
943                             $start = 0;
944                             $name =  "";
945                         }
946                     }
947                 } else {
948                     $in_non_block_braces-- if $in_non_block_braces;
949                 }
950
951                 $potential_start = 0;
952                 $potential_name = "";
953                 next;
954             }
955
956             # Semicolon.
957             if ($1 eq ";") {
958                 $potential_start = 0;
959                 $potential_name = "";
960                 next;
961             }
962
963             if ($1 eq "class" or $1 eq "interface") {
964                 $class_or_interface_just_seen = 1;
965                 next;
966             }
967
968             # Word.
969             $word = $1;
970             if (!$in_parentheses) {
971                 if ($class_or_interface_just_seen) {
972                     $potential_name = $word;
973                     $potential_start = $.;
974                     $class_or_interface_just_seen = 0;
975                     $potential_name_is_class_or_interface = 1;
976                     next;
977                 }
978             }
979             if (!$potential_start) {
980                 $potential_start = $.;
981                 $potential_name = "";
982             }
983             $class_or_interface_just_seen = 0;
984         }
985     }
986
987     warn "mismatched braces in $file_name\n" if $in_braces;
988     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
989
990     return @ranges;
991 }
992
993
994
995 # Read a file and get all the line ranges of the things that look like
996 # JavaScript functions.
997 #
998 # A function name is the word that immediately follows `function' when
999 # followed by an open curly brace. It can appear at the top level, or
1000 # inside other functions.
1001 #
1002 # An anonymous function name is the identifier chain immediately before
1003 # an assignment with the equals operator or object notation that has a
1004 # value starting with `function' followed by an open curly brace.
1005 #
1006 # A getter or setter name is the word that immediately follows `get' or
1007 # `set' when followed by an open curly brace .
1008 #
1009 # Comment handling is simple-minded but will work for all but pathological cases.
1010 #
1011 # Result is a list of triples: [ start_line, end_line, function_name ].
1012
1013 sub get_function_line_ranges_for_javascript($$)
1014 {
1015     my ($fileHandle, $fileName) = @_;
1016
1017     my @currentScopes;
1018     my @currentIdentifiers;
1019     my @currentFunctionNames;
1020     my @currentFunctionDepths;
1021     my @currentFunctionStartLines;
1022
1023     my @ranges;
1024
1025     my $inComment = 0;
1026     my $inQuotedText = "";
1027     my $parenthesesDepth = 0;
1028     my $bracesDepth = 0;
1029
1030     my $functionJustSeen = 0;
1031     my $getterJustSeen = 0;
1032     my $setterJustSeen = 0;
1033     my $assignmentJustSeen = 0;
1034
1035     my $word = "";
1036
1037     while (<$fileHandle>) {
1038         # Handle continued multi-line comment.
1039         if ($inComment) {
1040             next unless s-.*\*/--;
1041             $inComment = 0;
1042         }
1043
1044         # Handle continued quoted text.
1045         if ($inQuotedText ne "") {
1046             next if /\\$/;
1047             s-([^\\]|\\.)*?$inQuotedText--;
1048             $inQuotedText = "";
1049         }
1050
1051         # Handle comments and quoted text.
1052         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1053             my $match = $1;
1054             if ($match eq '/*') {
1055                 if (!s-/\*.*?\*/--) {
1056                     s-/\*.*--;
1057                     $inComment = 1;
1058                 }
1059             } elsif ($match eq '//') {
1060                 s-//.*--;
1061             } else { # ' or "
1062                 if (!s-$match([^\\]|\\.)*?$match--) {
1063                     $inQuotedText = $match if /\\$/;
1064                     warn "mismatched quotes at line $. in $fileName\n" if $inQuotedText eq "";
1065                     s-$match.*--;
1066                 }
1067             }
1068         }
1069
1070         # Find function names.
1071         while (m-(\w+|[(){}=:;])-g) {
1072             # Open parenthesis.
1073             if ($1 eq '(') {
1074                 $parenthesesDepth++;
1075                 next;
1076             }
1077
1078             # Close parenthesis.
1079             if ($1 eq ')') {
1080                 $parenthesesDepth--;
1081                 next;
1082             }
1083
1084             # Open brace.
1085             if ($1 eq '{') {
1086                 push(@currentScopes, join(".", @currentIdentifiers));
1087                 @currentIdentifiers = ();
1088
1089                 $bracesDepth++;
1090                 next;
1091             }
1092
1093             # Close brace.
1094             if ($1 eq '}') {
1095                 $bracesDepth--;
1096
1097                 if (@currentFunctionDepths and $bracesDepth == $currentFunctionDepths[$#currentFunctionDepths]) {
1098                     pop(@currentFunctionDepths);
1099
1100                     my $currentFunction = pop(@currentFunctionNames);
1101                     my $start = pop(@currentFunctionStartLines);
1102
1103                     push(@ranges, [$start, $., $currentFunction]);
1104                 }
1105
1106                 pop(@currentScopes);
1107                 @currentIdentifiers = ();
1108
1109                 next;
1110             }
1111
1112             # Semicolon.
1113             if ($1 eq ';') {
1114                 @currentIdentifiers = ();
1115                 next;
1116             }
1117
1118             # Function.
1119             if ($1 eq 'function') {
1120                 $functionJustSeen = 1;
1121
1122                 if ($assignmentJustSeen) {
1123                     my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1124                     $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1125
1126                     push(@currentFunctionNames, $currentFunction);
1127                     push(@currentFunctionDepths, $bracesDepth);
1128                     push(@currentFunctionStartLines, $.);
1129                 }
1130
1131                 next;
1132             }
1133
1134             # Getter prefix.
1135             if ($1 eq 'get') {
1136                 $getterJustSeen = 1;
1137                 next;
1138             }
1139
1140             # Setter prefix.
1141             if ($1 eq 'set') {
1142                 $setterJustSeen = 1;
1143                 next;
1144             }
1145
1146             # Assignment operator.
1147             if ($1 eq '=' or $1 eq ':') {
1148                 $assignmentJustSeen = 1;
1149                 next;
1150             }
1151
1152             next if $parenthesesDepth;
1153
1154             # Word.
1155             $word = $1;
1156             $word = "get $word" if $getterJustSeen;
1157             $word = "set $word" if $setterJustSeen;
1158
1159             if (($functionJustSeen and !$assignmentJustSeen) or $getterJustSeen or $setterJustSeen) {
1160                 push(@currentIdentifiers, $word);
1161
1162                 my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1163                 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1164
1165                 push(@currentFunctionNames, $currentFunction);
1166                 push(@currentFunctionDepths, $bracesDepth);
1167                 push(@currentFunctionStartLines, $.);
1168             } elsif ($word ne 'if' and $word ne 'for' and $word ne 'do' and $word ne 'while' and $word ne 'which' and $word ne 'var') {
1169                 push(@currentIdentifiers, $word);
1170             }
1171
1172             $functionJustSeen = 0;
1173             $getterJustSeen = 0;
1174             $setterJustSeen = 0;
1175             $assignmentJustSeen = 0;
1176         }
1177     }
1178
1179     warn "mismatched braces in $fileName\n" if $bracesDepth;
1180     warn "mismatched parentheses in $fileName\n" if $parenthesesDepth;
1181
1182     return @ranges;
1183 }
1184
1185
1186 sub processPaths(\@)
1187 {
1188     my ($paths) = @_;
1189     return ("." => 1) if (!@{$paths});
1190
1191     my %result = ();
1192
1193     for my $file (@{$paths}) {
1194         die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
1195         die "can't handle empty string path\n" if $file eq "";
1196         die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
1197
1198         my $untouchedFile = $file;
1199
1200         $file = canonicalizePath($file);
1201
1202         die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
1203
1204         $result{$file} = 1;
1205     }
1206
1207     return ("." => 1) if ($result{"."});
1208
1209     # Remove any paths that also have a parent listed.
1210     for my $path (keys %result) {
1211         for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
1212             if ($result{$parent}) {
1213                 delete $result{$path};
1214                 last;
1215             }
1216         }
1217     }
1218
1219     return %result;
1220 }
1221
1222 sub diffFromToString()
1223 {
1224     return "" if $isSVN;
1225     return $gitCommit if $gitCommit =~ m/.+\.\..+/;
1226     return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
1227     return "--cached" if $gitIndex;
1228     return "HEAD" if $isGit;
1229 }
1230
1231 sub diffCommand(@)
1232 {
1233     my @paths = @_;
1234
1235     my $pathsString = "'" . join("' '", @paths) . "'"; 
1236
1237     my $command;
1238     if ($isSVN) {
1239         $command = "$SVN diff --diff-cmd diff -x -N $pathsString";
1240     } elsif ($isGit) {
1241         $command = "$GIT diff --no-ext-diff -U0 " . diffFromToString();
1242         $command .= " -- $pathsString" unless $gitCommit;
1243     }
1244
1245     return $command;
1246 }
1247
1248 sub statusCommand(@)
1249 {
1250     my @files = @_;
1251
1252     my $filesString = "'" . join ("' '", @files) . "'";
1253     my $command;
1254     if ($isSVN) {
1255         $command = "$SVN stat $filesString";
1256     } elsif ($isGit) {
1257         $command = "$GIT diff -r --name-status -C -C -M " . diffFromToString();
1258         $command .= " -- $filesString" unless $gitCommit;
1259     }
1260
1261     return "$command 2>&1";
1262 }
1263
1264 sub createPatchCommand($)
1265 {
1266     my ($changedFilesString) = @_;
1267
1268     my $command;
1269     if ($isSVN) {
1270         $command = "'$FindBin::Bin/svn-create-patch' $changedFilesString";
1271     } elsif ($isGit) {
1272         $command = "$GIT diff -C -C -M " . diffFromToString();
1273         $command .= " -- $changedFilesString" unless $gitCommit;
1274     }
1275
1276     return $command;
1277 }
1278
1279 sub diffHeaderFormat()
1280 {
1281     return qr/^Index: (\S+)$/ if $isSVN;
1282     return qr/^diff --git a\/.+ b\/(.+)$/ if $isGit;
1283 }
1284
1285 sub findOriginalFileFromSvn($)
1286 {
1287     my ($file) = @_;
1288     my $baseUrl;
1289     open INFO, "$SVN info . |" or die;
1290     while (<INFO>) {
1291         if (/^URL: (.+)/) {
1292             $baseUrl = $1;
1293         }
1294     }
1295     close INFO;
1296     my $sourceFile;
1297     open INFO, "$SVN info '$file' |" or die;
1298     while (<INFO>) {
1299         if (/^Copied From URL: (.+)/) {
1300             $sourceFile = File::Spec->abs2rel($1, $baseUrl);
1301         }
1302     }
1303     close INFO;
1304     return $sourceFile;
1305 }
1306
1307 sub generateFileList(\@\@\%)
1308 {
1309     my ($changedFiles, $conflictFiles, $functionLists) = @_;
1310     print STDERR "  Running status to find changed, added, or removed files.\n";
1311     open STAT, "-|", statusCommand(keys %paths) or die "The status failed: $!.\n";
1312     while (<STAT>) {
1313         my $status;
1314         my $original;
1315         my $file;
1316
1317         if ($isSVN) {
1318             my $matches;
1319             if (eval "v$svnVersion" ge v1.6) {
1320                 $matches = /^([ACDMR]).{6} (.+)$/;
1321                 $status = $1;
1322                 $file = $2;
1323             } else {
1324                 $matches = /^([ACDMR]).{5} (.+)$/;
1325                 $status = $1;
1326                 $file = $2;
1327             }
1328             if ($matches) {
1329                 $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
1330             } else {
1331                 print;  # error output from svn stat
1332             }
1333         } elsif ($isGit) {
1334             if (/^([ADM])\t(.+)$/) {
1335                 $status = $1;
1336                 $file = $2;
1337             } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90%    newfile    oldfile
1338                 $status = $1;
1339                 $original = $2;
1340                 $file = $3;
1341             } else {
1342                 print;  # error output from git diff
1343             }
1344         }
1345
1346         next unless $status;
1347
1348         $file = makeFilePathRelative($file);
1349
1350         if (isModifiedStatus($status) || isAddedStatus($status)) {
1351             my @components = File::Spec->splitdir($file);
1352             if ($components[0] eq "LayoutTests") {
1353                 $didChangeRegressionTests = 1;
1354                 push @addedRegressionTests, $file
1355                     if isAddedStatus($status)
1356                        && $file =~ /\.([a-zA-Z]+)$/
1357                        && $supportedTestExtensions{lc($1)}
1358                        && !scalar(grep(/^resources$/i, @components));
1359             }
1360             push @{$changedFiles}, $file if $components[$#components] ne "ChangeLog";
1361         } elsif (isConflictStatus($status)) {
1362             push @{$conflictFiles}, $file;
1363         }
1364         if (basename($file) ne "ChangeLog") {
1365             my $description = statusDescription($status, $original);
1366             $functionLists->{$file} = $description if defined $description;
1367         }
1368     }
1369     close STAT;
1370 }
1371
1372 sub gitConfig($)
1373 {
1374     return unless $isGit;
1375
1376     my ($config) = @_;
1377
1378     my $result = `$GIT config $config`;
1379     if (($? >> 8) != 0) {
1380         $result = `$GIT repo-config $config`;
1381     }
1382     chomp $result;
1383     return $result;
1384 }
1385
1386 sub isModifiedStatus($)
1387 {
1388     my ($status) = @_;
1389
1390     my %statusCodes = (
1391         "M" => 1,
1392     );
1393
1394     return $statusCodes{$status};
1395 }
1396
1397 sub isAddedStatus($)
1398 {
1399     my ($status) = @_;
1400
1401     my %statusCodes = (
1402         "A" => 1,
1403         "C" => $isGit,
1404         "R" => 1,
1405     );
1406
1407     return $statusCodes{$status};
1408 }
1409
1410 sub isConflictStatus($)
1411 {
1412     my ($status) = @_;
1413
1414     my %svn = (
1415         "C" => 1,
1416     );
1417
1418     my %git = (
1419         "U" => 1,
1420     );
1421
1422     return 0 if ($gitCommit || $gitIndex); # an existing commit or staged change cannot have conflicts
1423     return $svn{$status} if $isSVN;
1424     return $git{$status} if $isGit;
1425 }
1426
1427 sub statusDescription($$)
1428 {
1429     my ($status, $original) = @_;
1430
1431     my %svn = (
1432         "A" => defined $original ? " Copied from \%s." : " Added.",
1433         "D" => " Removed.",
1434         "M" => "",
1435         "R" => defined $original ? " Replaced with \%s." : " Replaced.",
1436     );
1437
1438     my %git = %svn;
1439     $git{"A"} = " Added.";
1440     $git{"C"} = " Copied from \%s.";
1441     $git{"R"} = " Renamed from \%s.";
1442
1443     return sprintf($svn{$status}, $original) if $isSVN && exists $svn{$status};
1444     return sprintf($git{$status}, $original) if $isGit && exists $git{$status};
1445     return undef;
1446 }
1447
1448 sub extractLineRange($)
1449 {
1450     my ($string) = @_;
1451
1452     my ($start, $end) = (-1, -1);
1453
1454     if ($isSVN && $string =~ /^\d+(,\d+)?[acd](\d+)(,(\d+))?/) {
1455         $start = $2;
1456         $end = $4 || $2;
1457     } elsif ($isGit && $string =~ /^@@ -\d+(,\d+)? \+(\d+)(,(\d+))? @@/) {
1458         $start = $2;
1459         $end = defined($4) ? $4 + $2 - 1 : $2;
1460     }
1461
1462     return ($start, $end);
1463 }
1464
1465 sub firstDirectoryOrCwd()
1466 {
1467     my $dir = ".";
1468     my @dirs = keys(%paths);
1469
1470     $dir = -d $dirs[0] ? $dirs[0] : dirname($dirs[0]) if @dirs;
1471
1472     return $dir;
1473 }
1474
1475 sub testListForChangeLog(@)
1476 {
1477     my (@tests) = @_;
1478
1479     return "" unless @tests;
1480
1481     my $leadString = "        Test" . (@tests == 1 ? "" : "s") . ": ";
1482     my $list = $leadString;
1483     foreach my $i (0..$#tests) {
1484         $list .= " " x length($leadString) if $i;
1485         my $test = $tests[$i];
1486         $test =~ s/^LayoutTests\///;
1487         $list .= "$test\n";
1488     }
1489     $list .= "\n";
1490
1491     return $list;
1492 }
1493
1494 sub reviewerAndDescriptionForGitCommit($)
1495 {
1496     my ($commit) = @_;
1497
1498     my $description = '';
1499     my $reviewer;
1500
1501     my @args = qw(rev-list --pretty);
1502     push @args, '-1' if $commit !~ m/.+\.\..+/;
1503     my $gitLog;
1504     {
1505         local $/ = undef;
1506         open(GIT, "-|", $GIT, @args, $commit) || die;
1507         $gitLog = <GIT>;
1508         close(GIT);
1509     }
1510
1511     my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
1512     shift @commitLogs; # Remove initial blank commit log
1513     my $commitLogCount = 0;
1514     foreach my $commitLog (@commitLogs) {
1515         $description .= "\n" if $commitLogCount;
1516         $commitLogCount++;
1517         my $inHeader = 1;
1518         my @lines = split(/\n/, $commitLog);
1519         shift @lines; # Remove initial blank line
1520         foreach my $line (@lines) {
1521             if ($inHeader) {
1522                 if (!$line) {
1523                     $inHeader = 0;
1524                 }
1525                 next;
1526             } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
1527                 if (!$reviewer) {
1528                     $reviewer = $1;
1529                 } else {
1530                     $reviewer .= ", " . $1;
1531                 }
1532             } elsif (length $line == 0) {
1533                 $description = $description . "\n";
1534             } else {
1535                 $line =~ s/^\s*//;
1536                 $description = $description . "        " . $line . "\n";
1537             }
1538         }
1539     }
1540     if (!$reviewer) {
1541       $reviewer = $gitReviewer;
1542     }
1543
1544     return ($reviewer, $description);
1545 }
1546
1547 sub normalizeLineEndings($$)
1548 {
1549     my ($string, $endl) = @_;
1550     $string =~ s/\r?\n/$endl/g;
1551     return $string;
1552 }
1553
1554 sub normalizePath($)
1555 {
1556     my ($path) = @_;
1557     $path =~ s/\\/\//g;
1558     return $path;
1559 }