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