webengine/osswebengine/WebKitTools/Scripts/prepare-ChangeLog
changeset 0 dd21522fd290
equal deleted inserted replaced
-1:000000000000 0:dd21522fd290
       
     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.
       
     7 #
       
     8 #  prepare-ChangeLog is free software; you can redistribute it and/or
       
     9 #  modify it under the terms of the GNU General Public
       
    10 #  License as published by the Free Software Foundation; either
       
    11 #  version 2 of the License, or (at your option) any later version.
       
    12 #
       
    13 #  prepare-ChangeLog is distributed in the hope that it will be useful,
       
    14 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    15 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
       
    16 #  General Public License for more details.
       
    17 #
       
    18 #  You should have received a copy of the GNU General Public
       
    19 #  License along with this program; if not, write to the Free
       
    20 #  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
       
    21 #
       
    22 
       
    23 
       
    24 # Perl script to create a ChangeLog entry with names of files
       
    25 # and functions from a diff.
       
    26 #
       
    27 # Darin Adler <darin@bentspoon.com>, started 20 April 2000
       
    28 # Java support added by Maciej Stachowiak <mjs@eazel.com>
       
    29 # Objective-C, C++ and Objective-C++ support added by Maciej Stachowiak <mjs@apple.com>
       
    30 # Git support added by Adam Roben <aroben@apple.com>
       
    31 
       
    32 
       
    33 #
       
    34 # TODO:
       
    35 #   List functions that have been removed too.
       
    36 #   Decide what a good logical order is for the changed files
       
    37 #     other than a normal text "sort" (top level first?)
       
    38 #     (group directories?) (.h before .c?)
       
    39 #   Handle yacc source files too (other languages?).
       
    40 #   Help merge when there are ChangeLog conflicts or if there's
       
    41 #     already a partly written ChangeLog entry.
       
    42 #   Add command line option to put the ChangeLog into a separate
       
    43 #     file or just spew it out stdout.
       
    44 #   Add SVN version numbers for commit (can't do that until
       
    45 #     the changes are checked in, though).
       
    46 #   Work around diff stupidity where deleting a function that starts
       
    47 #     with a comment makes diff think that the following function
       
    48 #     has been changed (if the following function starts with a comment
       
    49 #     with the same first line, such as /**)
       
    50 #   Work around diff stupidity where deleting an entire function and
       
    51 #     the blank lines before it makes diff think you've changed the
       
    52 #     previous function.
       
    53 
       
    54 use strict;
       
    55 use warnings;
       
    56 
       
    57 use File::Basename;
       
    58 use File::Spec;
       
    59 use FindBin;
       
    60 use Getopt::Long;
       
    61 use lib $FindBin::Bin;
       
    62 use POSIX qw(strftime);
       
    63 use VCSUtils;
       
    64 
       
    65 sub changeLogDate($);
       
    66 sub firstDirectoryOrCwd();
       
    67 sub diffFromToString();
       
    68 sub diffCommand(@);
       
    69 sub statusCommand(@);
       
    70 sub createPatchCommand($);
       
    71 sub diffHeaderFormat();
       
    72 sub findOriginalFileFromSvn($);
       
    73 sub generateFileList(\@\@\%);
       
    74 sub gitConfig($);
       
    75 sub isModifiedStatus($);
       
    76 sub isAddedStatus($);
       
    77 sub isConflictStatus($);
       
    78 sub statusDescription($$);
       
    79 sub extractLineRange($);
       
    80 sub canonicalizePath($);
       
    81 sub testListForChangeLog(@);
       
    82 sub get_function_line_ranges($$);
       
    83 sub get_function_line_ranges_for_c($$);
       
    84 sub get_function_line_ranges_for_java($$);
       
    85 sub method_decl_to_selector($);
       
    86 sub processPaths(\@);
       
    87 sub reviewerAndDescriptionForGitCommit($);
       
    88 
       
    89 # Project time zone for Cupertino, CA, US
       
    90 my $changeLogTimeZone = "PST8PDT";
       
    91 
       
    92 my $gitCommit = 0;
       
    93 my $gitReviewer = "";
       
    94 my $openChangeLogs = 0;
       
    95 my $showHelp = 0;
       
    96 my $spewDiff = $ENV{"PREPARE_CHANGELOG_DIFF"};
       
    97 my $updateChangeLogs = 1;
       
    98 my $parseOptionsResult =
       
    99     GetOptions("diff|d!" => \$spewDiff,
       
   100                "git-commit:s" => \$gitCommit,
       
   101                "git-reviewer:s" => \$gitReviewer,
       
   102                "help|h!" => \$showHelp,
       
   103                "open|o!" => \$openChangeLogs,
       
   104                "update!" => \$updateChangeLogs);
       
   105 if (!$parseOptionsResult || $showHelp) {
       
   106     print STDERR basename($0) . " [-d|--diff] [-h|--help] [-o|--open] [--git-commit=<committish>] [--git-reviewer=<name>] [svndir1 [svndir2 ...]]\n";
       
   107     print STDERR "  -d|--diff      Spew diff to stdout when running\n";
       
   108     print STDERR "  --git-commit   Populate the ChangeLogs from the specified git commit\n";
       
   109     print STDERR "  --git-reviewer When populating the ChangeLogs from a git commit claim that the spcified name reviewed the change.\n";
       
   110     print STDERR "                 This option is useful when the git commit lacks a Signed-Off-By: line\n";
       
   111     print STDERR "  -h|--help      Show this help message\n";
       
   112     print STDERR "  -o|--open      Open ChangeLogs in an editor when done\n";
       
   113     print STDERR "  --[no-]update  Update ChangeLogs from svn before adding entry (default: update)\n";
       
   114     exit 1;
       
   115 }
       
   116 
       
   117 my %paths = processPaths(@ARGV);
       
   118 
       
   119 my $isGit = isGitDirectory(firstDirectoryOrCwd());
       
   120 my $isSVN = isSVNDirectory(firstDirectoryOrCwd());
       
   121 
       
   122 $isSVN || $isGit || die "Couldn't determine your version control system.";
       
   123 
       
   124 # Find the list of modified files
       
   125 my @changed_files;
       
   126 my $changed_files_string;
       
   127 my %changed_line_ranges;
       
   128 my %function_lists;
       
   129 my @conflict_files;
       
   130 
       
   131 my $SVN = "svn";
       
   132 my $GIT = "git";
       
   133 
       
   134 my %supportedTestExtensions = map { $_ => 1 } qw(html shtml svg xml xhtml pl php);
       
   135 my @addedRegressionTests = ();
       
   136 my $didChangeRegressionTests = 0;
       
   137 
       
   138 generateFileList(@changed_files, @conflict_files, %function_lists);
       
   139 
       
   140 if (!@changed_files && !@conflict_files || !%function_lists) {
       
   141     print STDERR "  No changes found.\n";
       
   142     exit 1;
       
   143 }
       
   144 
       
   145 if (@conflict_files) {
       
   146     print STDERR "  The following files have conflicts. Run prepare-ChangeLog again after fixing the conflicts:\n";
       
   147     print STDERR join("\n", @conflict_files), "\n";
       
   148     exit 1;
       
   149 }
       
   150 
       
   151 if (@changed_files) {
       
   152     $changed_files_string = "'" . join ("' '", @changed_files) . "'";
       
   153 
       
   154     # For each file, build a list of modified lines.
       
   155     # Use line numbers from the "after" side of each diff.
       
   156     print STDERR "  Reviewing diff to determine which lines changed.\n";
       
   157     my $file;
       
   158     open DIFF, "-|", diffCommand(@changed_files) or die "The diff failed: $!.\n";
       
   159     while (<DIFF>) {
       
   160         $file = makeFilePathRelative($1) if $_ =~ diffHeaderFormat();
       
   161         if (defined $file) {
       
   162             my ($start, $end) = extractLineRange($_);
       
   163             if ($start >= 0 && $end >= 0) {
       
   164                 push @{$changed_line_ranges{$file}}, [ $start, $end ];
       
   165             } elsif (/DO_NOT_COMMIT/) {
       
   166                 print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
       
   167             }
       
   168         }
       
   169     }
       
   170     close DIFF;
       
   171 }
       
   172 
       
   173 # For each source file, convert line range to function list.
       
   174 if (%changed_line_ranges) {
       
   175     print STDERR "  Extracting affected function names from source files.\n";
       
   176     foreach my $file (keys %changed_line_ranges) {
       
   177         # Only look for function names in .c files.
       
   178         next unless $file =~ /\.(c|cpp|m|mm|h|java)/;
       
   179     
       
   180         # Find all the functions in the file.
       
   181         open SOURCE, $file or next;
       
   182         my @function_ranges = get_function_line_ranges(\*SOURCE, $file);
       
   183         close SOURCE;
       
   184     
       
   185         # Find all the modified functions.
       
   186         my @functions;
       
   187         my %saw_function;
       
   188         my @change_ranges = (@{$changed_line_ranges{$file}}, []);
       
   189         my @change_range = (0, 0);
       
   190         FUNCTION: foreach my $function_range_ref (@function_ranges) {
       
   191             my @function_range = @$function_range_ref;
       
   192     
       
   193             # Advance to successive change ranges.
       
   194             for (;; @change_range = @{shift @change_ranges}) {
       
   195                 last FUNCTION unless @change_range;
       
   196     
       
   197                 # If past this function, move on to the next one.
       
   198                 next FUNCTION if $change_range[0] > $function_range[1];
       
   199     
       
   200                 # If an overlap with this function range, record the function name.
       
   201                 if ($change_range[1] >= $function_range[0]
       
   202                     and $change_range[0] <= $function_range[1]) {
       
   203                     if (!$saw_function{$function_range[2]}) {
       
   204                         $saw_function{$function_range[2]} = 1;
       
   205                         push @functions, $function_range[2];
       
   206                     }
       
   207                     next FUNCTION;
       
   208                 }
       
   209             }
       
   210         }
       
   211     
       
   212         # Format the list of functions now.
       
   213 
       
   214         if (@functions) {
       
   215             $function_lists{$file} = "" if !defined $function_lists{$file};
       
   216             $function_lists{$file} .= "\n        (" . join("):\n        (", @functions) . "):";
       
   217         }
       
   218     }
       
   219 }
       
   220 
       
   221 # Get some parameters for the ChangeLog we are about to write.
       
   222 my $date = changeLogDate($changeLogTimeZone);
       
   223 my $name = $ENV{CHANGE_LOG_NAME}
       
   224   || $ENV{REAL_NAME}
       
   225   || gitConfig("user.name")
       
   226   || (split /\s*,\s*/, (getpwuid $<)[6])[0]
       
   227   || "set REAL_NAME environment variable";
       
   228 my $email_address = $ENV{CHANGE_LOG_EMAIL_ADDRESS}
       
   229   || $ENV{EMAIL_ADDRESS}
       
   230   || gitConfig("user.email")
       
   231   || "set EMAIL_ADDRESS environment variable";
       
   232 
       
   233 if ($gitCommit) {
       
   234     $name = `$GIT log --max-count=1 --pretty=\"format:%an\" \"$gitCommit\"`;
       
   235     $email_address = `$GIT log --max-count=1 --pretty=\"format:%ae\" \"$gitCommit\"`;
       
   236 }
       
   237 
       
   238 # Remove trailing parenthesized notes from user name (bit of hack).
       
   239 $name =~ s/\(.*?\)\s*$//g;
       
   240 
       
   241 # Find the change logs.
       
   242 my %has_log;
       
   243 my %files;
       
   244 foreach my $file (sort keys %function_lists) {
       
   245     my $prefix = $file;
       
   246     my $has_log = 0;
       
   247     while ($prefix) {
       
   248         $prefix =~ s-/[^/]+/?$-/- or $prefix = "";
       
   249         $has_log = $has_log{$prefix};
       
   250         if (!defined $has_log) {
       
   251             $has_log = -f "${prefix}ChangeLog";
       
   252             $has_log{$prefix} = $has_log;
       
   253         }
       
   254         last if $has_log;
       
   255     }
       
   256     if (!$has_log) {
       
   257         print STDERR "No ChangeLog found for $file.\n";
       
   258     } else {
       
   259         push @{$files{$prefix}}, $file;
       
   260     }
       
   261 }
       
   262 
       
   263 # Get the latest ChangeLog files from svn.
       
   264 my $logs = "";
       
   265 foreach my $prefix (sort keys %files) {
       
   266     $logs .= " ${prefix}ChangeLog";
       
   267 }
       
   268 
       
   269 if ($logs && $updateChangeLogs && $isSVN) {
       
   270     print STDERR "  Running 'svn update' to update ChangeLog files.\n";
       
   271     open ERRORS, "$SVN update -q$logs |" or die "The svn update of ChangeLog files failed: $!.\n";
       
   272     print STDERR "    $_" while <ERRORS>;
       
   273     close ERRORS;
       
   274 }
       
   275 
       
   276 # Write out a new ChangeLog file.
       
   277 foreach my $prefix (sort keys %files) {
       
   278     print STDERR "  Editing the ${prefix}ChangeLog file.\n";
       
   279     open OLD_CHANGE_LOG, "${prefix}ChangeLog" or die "Could not open ${prefix}ChangeLog file: $!.\n";
       
   280     # It's less efficient to read the whole thing into memory than it would be
       
   281     # to read it while we prepend to it later, but I like doing this part first.
       
   282     my @old_change_log = <OLD_CHANGE_LOG>;
       
   283     close OLD_CHANGE_LOG;
       
   284     open CHANGE_LOG, "> ${prefix}ChangeLog" or die "Could not write ${prefix}ChangeLog\n.";
       
   285     print CHANGE_LOG "$date  $name  <$email_address>\n\n";
       
   286 
       
   287     my ($reviewer, $description) = reviewerAndDescriptionForGitCommit($gitCommit) if $gitCommit;
       
   288     $reviewer = "NOBODY (OO" . "PS!)" if !$reviewer;
       
   289 
       
   290     print CHANGE_LOG "        Reviewed by $reviewer.\n\n";
       
   291     print CHANGE_LOG $description . "\n" if $description;
       
   292 
       
   293     if ($prefix =~ m/WebCore/ || `pwd` =~ m/WebCore/) {
       
   294         if ($didChangeRegressionTests) {
       
   295             print CHANGE_LOG testListForChangeLog(sort @addedRegressionTests);
       
   296         } else {
       
   297             print CHANGE_LOG "        WARNING: NO TEST CASES ADDED OR CHANGED\n\n";
       
   298         }
       
   299     }
       
   300 
       
   301     foreach my $file (sort @{$files{$prefix}}) {
       
   302         my $file_stem = substr $file, length $prefix;
       
   303         print CHANGE_LOG "        * $file_stem:$function_lists{$file}\n";
       
   304     }
       
   305     print CHANGE_LOG "\n", @old_change_log;
       
   306     close CHANGE_LOG;
       
   307 }
       
   308 
       
   309 # Write out another diff.
       
   310 if ($spewDiff && @changed_files) {
       
   311     print STDERR "  Running diff to help you write the ChangeLog entries.\n";
       
   312     local $/ = undef; # local slurp mode
       
   313     open DIFF, "-|", createPatchCommand($changed_files_string) or die "The diff failed: $!.\n";
       
   314     print <DIFF>;
       
   315     close DIFF;
       
   316 }
       
   317 
       
   318 # Open ChangeLogs.
       
   319 if ($openChangeLogs && $logs) {
       
   320     print STDERR "  Opening the edited ChangeLog files.\n";
       
   321     my $editor = $ENV{"CHANGE_LOG_EDIT_APPLICATION"};
       
   322     if ($editor) {
       
   323         system "open -a '$editor'$logs";
       
   324     } else {
       
   325         system "open -e$logs";
       
   326     }
       
   327 }
       
   328 
       
   329 # Done.
       
   330 exit;
       
   331 
       
   332 sub canonicalizePath($)
       
   333 {
       
   334     my ($file) = @_;
       
   335 
       
   336     # Remove extra slashes and '.' directories in path
       
   337     $file = File::Spec->canonpath($file);
       
   338 
       
   339     # Remove '..' directories in path
       
   340     my @dirs = ();
       
   341     foreach my $dir (File::Spec->splitdir($file)) {
       
   342         if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
       
   343             pop(@dirs);
       
   344         } else {
       
   345             push(@dirs, $dir);
       
   346         }
       
   347     }
       
   348     return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
       
   349 }
       
   350 
       
   351 sub changeLogDate($)
       
   352 {
       
   353     my ($timeZone) = @_;
       
   354     my $savedTimeZone = $ENV{'TZ'};
       
   355     # Set TZ temporarily so that localtime() is in that time zone
       
   356     $ENV{'TZ'} = $timeZone;
       
   357     my $date = strftime("%Y-%m-%d", localtime());
       
   358     if (defined $savedTimeZone) {
       
   359          $ENV{'TZ'} = $savedTimeZone;
       
   360     } else {
       
   361          delete $ENV{'TZ'};
       
   362     }
       
   363     return $date;
       
   364 }
       
   365 
       
   366 sub get_function_line_ranges($$)
       
   367 {
       
   368     my ($file_handle, $file_name) = @_;
       
   369 
       
   370     if ($file_name =~ /\.(c|cpp|m|mm|h)$/) {
       
   371         return get_function_line_ranges_for_c ($file_handle, $file_name);
       
   372     } elsif ($file_name =~ /\.java$/) {
       
   373         return get_function_line_ranges_for_java ($file_handle, $file_name);
       
   374     }
       
   375     return ();
       
   376 }
       
   377 
       
   378 
       
   379 sub method_decl_to_selector($)
       
   380 {
       
   381     (my $method_decl) = @_;
       
   382 
       
   383     $_ = $method_decl;
       
   384 
       
   385     if ((my $comment_stripped) = m-([^/]*)(//|/*).*-) {
       
   386         $_ = $comment_stripped;
       
   387     }
       
   388 
       
   389     s/,\s*...//;
       
   390 
       
   391     if (/:/) {
       
   392         my @components = split /:/;
       
   393         pop @components if (scalar @components > 1);
       
   394         $_ = (join ':', map {s/.*[^[:word:]]//; scalar $_;} @components) . ':';
       
   395     } else {
       
   396         s/\s*$//;
       
   397         s/.*[^[:word:]]//;
       
   398     }
       
   399 
       
   400     return $_;
       
   401 }
       
   402 
       
   403 
       
   404 
       
   405 # Read a file and get all the line ranges of the things that look like C functions.
       
   406 # A function name is the last word before an open parenthesis before the outer
       
   407 # level open brace. A function starts at the first character after the last close
       
   408 # brace or semicolon before the function name and ends at the close brace.
       
   409 # Comment handling is simple-minded but will work for all but pathological cases.
       
   410 #
       
   411 # Result is a list of triples: [ start_line, end_line, function_name ].
       
   412 
       
   413 sub get_function_line_ranges_for_c($$)
       
   414 {
       
   415     my ($file_handle, $file_name) = @_;
       
   416 
       
   417     my @ranges;
       
   418 
       
   419     my $in_comment = 0;
       
   420     my $in_macro = 0;
       
   421     my $in_method_declaration = 0;
       
   422     my $in_parentheses = 0;
       
   423     my $in_braces = 0;
       
   424     my $brace_start = 0;
       
   425     my $brace_end = 0;
       
   426     my $skip_til_brace_or_semicolon = 0;
       
   427 
       
   428     my $word = "";
       
   429     my $interface_name = "";
       
   430 
       
   431     my $potential_method_char = "";
       
   432     my $potential_method_spec = "";
       
   433 
       
   434     my $potential_start = 0;
       
   435     my $potential_name = "";
       
   436 
       
   437     my $start = 0;
       
   438     my $name = "";
       
   439 
       
   440     my $next_word_could_be_namespace = 0;
       
   441     my $potential_namespace = "";
       
   442     my @namespaces;
       
   443 
       
   444     while (<$file_handle>) {
       
   445         # Handle continued multi-line comment.
       
   446         if ($in_comment) {
       
   447             next unless s-.*\*/--;
       
   448             $in_comment = 0;
       
   449         }
       
   450 
       
   451         # Handle continued macro.
       
   452         if ($in_macro) {
       
   453             $in_macro = 0 unless /\\$/;
       
   454             next;
       
   455         }
       
   456 
       
   457         # Handle start of macro (or any preprocessor directive).
       
   458         if (/^\s*\#/) {
       
   459             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
       
   460             next;
       
   461         }
       
   462 
       
   463         # Handle comments and quoted text.
       
   464         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
       
   465             my $match = $1;
       
   466             if ($match eq "/*") {
       
   467                 if (!s-/\*.*?\*/--) {
       
   468                     s-/\*.*--;
       
   469                     $in_comment = 1;
       
   470                 }
       
   471             } elsif ($match eq "//") {
       
   472                 s-//.*--;
       
   473             } else { # ' or "
       
   474                 if (!s-$match([^\\]|\\.)*?$match--) {
       
   475                     warn "mismatched quotes at line $. in $file_name\n";
       
   476                     s-$match.*--;
       
   477                 }
       
   478             }
       
   479         }
       
   480 
       
   481 
       
   482         # continued method declaration
       
   483         if ($in_method_declaration) {
       
   484               my $original = $_;
       
   485               my $method_cont = $_;
       
   486 
       
   487               chomp $method_cont;
       
   488               $method_cont =~ s/[;\{].*//;
       
   489               $potential_method_spec = "${potential_method_spec} ${method_cont}";
       
   490 
       
   491               $_ = $original;
       
   492               if (/;/) {
       
   493                   $potential_start = 0;
       
   494                   $potential_method_spec = "";
       
   495                   $potential_method_char = "";
       
   496                   $in_method_declaration = 0;
       
   497                   s/^[^;\{]*//;
       
   498               } elsif (/{/) {
       
   499                   my $selector = method_decl_to_selector ($potential_method_spec);
       
   500                   $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
       
   501                   
       
   502                   $potential_method_spec = "";
       
   503                   $potential_method_char = "";
       
   504                   $in_method_declaration = 0;
       
   505   
       
   506                   $_ = $original;
       
   507                   s/^[^;{]*//;
       
   508               } elsif (/\@end/) {
       
   509                   $in_method_declaration = 0;
       
   510                   $interface_name = "";
       
   511                   $_ = $original;
       
   512               } else {
       
   513                   next;
       
   514               }
       
   515         }
       
   516 
       
   517         
       
   518         # start of method declaration
       
   519         if ((my $method_char, my $method_spec) = m&^([-+])([^0-9;][^;]*);?$&) {
       
   520             my $original = $_;
       
   521 
       
   522             if ($interface_name) {
       
   523                 chomp $method_spec;
       
   524                 $method_spec =~ s/\{.*//;
       
   525 
       
   526                 $potential_method_char = $method_char;
       
   527                 $potential_method_spec = $method_spec;
       
   528                 $potential_start = $.;
       
   529                 $in_method_declaration = 1;
       
   530             } else { 
       
   531                 warn "declaring a method but don't have interface on line $. in $file_name\n";
       
   532             }
       
   533             $_ = $original;
       
   534             if (/\{/) {
       
   535               my $selector = method_decl_to_selector ($potential_method_spec);
       
   536               $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
       
   537               
       
   538               $potential_method_spec = "";
       
   539               $potential_method_char = "";
       
   540               $in_method_declaration = 0;
       
   541               $_ = $original;
       
   542               s/^[^{]*//;
       
   543             } elsif (/\@end/) {
       
   544               $in_method_declaration = 0;
       
   545               $interface_name = "";
       
   546               $_ = $original;
       
   547             } else {
       
   548               next;
       
   549             }
       
   550         }
       
   551 
       
   552 
       
   553         # Find function, interface and method names.
       
   554         while (m&((?:[[:word:]]+::)*operator(?:[ \t]*\(\)|[^()]*)|[[:word:]:~]+|[(){}:;])|\@(?:implementation|interface|protocol)\s+(\w+)[^{]*&g) {
       
   555             # interface name
       
   556             if ($2) {
       
   557                 $interface_name = $2;
       
   558                 next;
       
   559             }
       
   560 
       
   561             # Open parenthesis.
       
   562             if ($1 eq "(") {
       
   563                 $potential_name = $word unless $in_parentheses || $skip_til_brace_or_semicolon;
       
   564                 $in_parentheses++;
       
   565                 next;
       
   566             }
       
   567 
       
   568             # Close parenthesis.
       
   569             if ($1 eq ")") {
       
   570                 $in_parentheses--;
       
   571                 next;
       
   572             }
       
   573 
       
   574             # C++ constructor initializers
       
   575             if ($1 eq ":") {
       
   576                   $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
       
   577             }
       
   578 
       
   579             # Open brace.
       
   580             if ($1 eq "{") {
       
   581                 $skip_til_brace_or_semicolon = 0;
       
   582 
       
   583                 if ($potential_namespace) {
       
   584                     push @namespaces, $potential_namespace;
       
   585                     $potential_namespace = "";
       
   586                     next;
       
   587                 }
       
   588 
       
   589                 # Promote potential name to real function name at the
       
   590                 # start of the outer level set of braces (function body?).
       
   591                 if (!$in_braces and $potential_start) {
       
   592                     $start = $potential_start;
       
   593                     $name = $potential_name;
       
   594                     if (@namespaces && (length($name) < 2 || substr($name,1,1) ne "[")) {
       
   595                         $name = join ('::', @namespaces, $name);
       
   596                     }
       
   597                 }
       
   598 
       
   599                 $in_method_declaration = 0;
       
   600 
       
   601                 $brace_start = $. if (!$in_braces);
       
   602                 $in_braces++;
       
   603                 next;
       
   604             }
       
   605 
       
   606             # Close brace.
       
   607             if ($1 eq "}") {
       
   608                 if (!$in_braces && @namespaces) {
       
   609                     pop @namespaces;
       
   610                     next;
       
   611                 }
       
   612 
       
   613                 $in_braces--;
       
   614                 $brace_end = $. if (!$in_braces);
       
   615 
       
   616                 # End of an outer level set of braces.
       
   617                 # This could be a function body.
       
   618                 if (!$in_braces and $name) {
       
   619                     push @ranges, [ $start, $., $name ];
       
   620                     $name = "";
       
   621                 }
       
   622 
       
   623                 $potential_start = 0;
       
   624                 $potential_name = "";
       
   625                 next;
       
   626             }
       
   627 
       
   628             # Semicolon.
       
   629             if ($1 eq ";") {
       
   630                 $skip_til_brace_or_semicolon = 0;
       
   631                 $potential_start = 0;
       
   632                 $potential_name = "";
       
   633                 $in_method_declaration = 0;
       
   634                 next;
       
   635             }
       
   636 
       
   637             # Ignore "const" method qualifier.
       
   638             if ($1 eq "const") {
       
   639                 next;
       
   640             }
       
   641 
       
   642             if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
       
   643                 $next_word_could_be_namespace = 1;
       
   644                 next;
       
   645             }
       
   646 
       
   647             # Word.
       
   648             $word = $1;
       
   649             if (!$skip_til_brace_or_semicolon) {
       
   650                 if ($next_word_could_be_namespace) {
       
   651                     $potential_namespace = $word;
       
   652                     $next_word_could_be_namespace = 0;
       
   653                 } elsif ($potential_namespace) {
       
   654                     $potential_namespace = "";
       
   655                 }
       
   656 
       
   657                 if (!$in_parentheses) {
       
   658                     $potential_start = 0;
       
   659                     $potential_name = "";
       
   660                 }
       
   661                 if (!$potential_start) {
       
   662                     $potential_start = $.;
       
   663                     $potential_name = "";
       
   664                 }
       
   665             }
       
   666         }
       
   667     }
       
   668 
       
   669     warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
       
   670     warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
       
   671 
       
   672     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
       
   673 
       
   674     return @ranges;
       
   675 }
       
   676 
       
   677 
       
   678 
       
   679 # Read a file and get all the line ranges of the things that look like Java
       
   680 # classes, interfaces and methods.
       
   681 #
       
   682 # A class or interface name is the word that immediately follows
       
   683 # `class' or `interface' when followed by an open curly brace and not
       
   684 # a semicolon. It can appear at the top level, or inside another class
       
   685 # or interface block, but not inside a function block
       
   686 #
       
   687 # A class or interface starts at the first character after the first close
       
   688 # brace or after the function name and ends at the close brace.
       
   689 #
       
   690 # A function name is the last word before an open parenthesis before
       
   691 # an open brace rather than a semicolon. It can appear at top level or
       
   692 # inside a class or interface block, but not inside a function block.
       
   693 #
       
   694 # A function starts at the first character after the first close
       
   695 # brace or after the function name and ends at the close brace.
       
   696 #
       
   697 # Comment handling is simple-minded but will work for all but pathological cases.
       
   698 #
       
   699 # Result is a list of triples: [ start_line, end_line, function_name ].
       
   700 
       
   701 sub get_function_line_ranges_for_java($$)
       
   702 {
       
   703     my ($file_handle, $file_name) = @_;
       
   704 
       
   705     my @current_scopes;
       
   706 
       
   707     my @ranges;
       
   708 
       
   709     my $in_comment = 0;
       
   710     my $in_macro = 0;
       
   711     my $in_parentheses = 0;
       
   712     my $in_braces = 0;
       
   713     my $in_non_block_braces = 0;
       
   714     my $class_or_interface_just_seen = 0;
       
   715 
       
   716     my $word = "";
       
   717 
       
   718     my $potential_start = 0;
       
   719     my $potential_name = "";
       
   720     my $potential_name_is_class_or_interface = 0;
       
   721 
       
   722     my $start = 0;
       
   723     my $name = "";
       
   724     my $current_name_is_class_or_interface = 0;
       
   725 
       
   726     while (<$file_handle>) {
       
   727         # Handle continued multi-line comment.
       
   728         if ($in_comment) {
       
   729             next unless s-.*\*/--;
       
   730             $in_comment = 0;
       
   731         }
       
   732 
       
   733         # Handle continued macro.
       
   734         if ($in_macro) {
       
   735             $in_macro = 0 unless /\\$/;
       
   736             next;
       
   737         }
       
   738 
       
   739         # Handle start of macro (or any preprocessor directive).
       
   740         if (/^\s*\#/) {
       
   741             $in_macro = 1 if /^([^\\]|\\.)*\\$/;
       
   742             next;
       
   743         }
       
   744 
       
   745         # Handle comments and quoted text.
       
   746         while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
       
   747             my $match = $1;
       
   748             if ($match eq "/*") {
       
   749                 if (!s-/\*.*?\*/--) {
       
   750                     s-/\*.*--;
       
   751                     $in_comment = 1;
       
   752                 }
       
   753             } elsif ($match eq "//") {
       
   754                 s-//.*--;
       
   755             } else { # ' or "
       
   756                 if (!s-$match([^\\]|\\.)*?$match--) {
       
   757                     warn "mismatched quotes at line $. in $file_name\n";
       
   758                     s-$match.*--;
       
   759                 }
       
   760             }
       
   761         }
       
   762 
       
   763         # Find function names.
       
   764         while (m-(\w+|[(){};])-g) {
       
   765             # Open parenthesis.
       
   766             if ($1 eq "(") {
       
   767                 if (!$in_parentheses) {
       
   768                     $potential_name = $word;
       
   769                     $potential_name_is_class_or_interface = 0;
       
   770                 }
       
   771                 $in_parentheses++;
       
   772                 next;
       
   773             }
       
   774 
       
   775             # Close parenthesis.
       
   776             if ($1 eq ")") {
       
   777                 $in_parentheses--;
       
   778                 next;
       
   779             }
       
   780 
       
   781             # Open brace.
       
   782             if ($1 eq "{") {
       
   783                 # Promote potential name to real function name at the
       
   784                 # start of the outer level set of braces (function/class/interface body?).
       
   785                 if (!$in_non_block_braces
       
   786                     and (!$in_braces or $current_name_is_class_or_interface)
       
   787                     and $potential_start) {
       
   788                     if ($name) {
       
   789                           push @ranges, [ $start, ($. - 1),
       
   790                                           join ('.', @current_scopes) ];
       
   791                     }
       
   792 
       
   793 
       
   794                     $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
       
   795 
       
   796                     $start = $potential_start;
       
   797                     $name = $potential_name;
       
   798 
       
   799                     push (@current_scopes, $name);
       
   800                 } else {
       
   801                     $in_non_block_braces++;
       
   802                 }
       
   803 
       
   804                 $potential_name = "";
       
   805                 $potential_start = 0;
       
   806 
       
   807                 $in_braces++;
       
   808                 next;
       
   809             }
       
   810 
       
   811             # Close brace.
       
   812             if ($1 eq "}") {
       
   813                 $in_braces--;
       
   814 
       
   815                 # End of an outer level set of braces.
       
   816                 # This could be a function body.
       
   817                 if (!$in_non_block_braces) {
       
   818                     if ($name) {
       
   819                         push @ranges, [ $start, $.,
       
   820                                         join ('.', @current_scopes) ];
       
   821 
       
   822                         pop (@current_scopes);
       
   823 
       
   824                         if (@current_scopes) {
       
   825                             $current_name_is_class_or_interface = 1;
       
   826 
       
   827                             $start = $. + 1;
       
   828                             $name =  $current_scopes[$#current_scopes-1];
       
   829                         } else {
       
   830                             $current_name_is_class_or_interface = 0;
       
   831                             $start = 0;
       
   832                             $name =  "";
       
   833                         }
       
   834                     }
       
   835                 } else {
       
   836                     $in_non_block_braces-- if $in_non_block_braces;
       
   837                 }
       
   838 
       
   839                 $potential_start = 0;
       
   840                 $potential_name = "";
       
   841                 next;
       
   842             }
       
   843 
       
   844             # Semicolon.
       
   845             if ($1 eq ";") {
       
   846                 $potential_start = 0;
       
   847                 $potential_name = "";
       
   848                 next;
       
   849             }
       
   850 
       
   851             if ($1 eq "class" or $1 eq "interface") {
       
   852                 $class_or_interface_just_seen = 1;
       
   853                 next;
       
   854             }
       
   855 
       
   856             # Word.
       
   857             $word = $1;
       
   858             if (!$in_parentheses) {
       
   859                 if ($class_or_interface_just_seen) {
       
   860                     $potential_name = $word;
       
   861                     $potential_start = $.;
       
   862                     $class_or_interface_just_seen = 0;
       
   863                     $potential_name_is_class_or_interface = 1;
       
   864                     next;
       
   865                 }
       
   866             }
       
   867             if (!$potential_start) {
       
   868                 $potential_start = $.;
       
   869                 $potential_name = "";
       
   870             }
       
   871             $class_or_interface_just_seen = 0;
       
   872         }
       
   873     }
       
   874 
       
   875     warn "mismatched braces in $file_name\n" if $in_braces;
       
   876     warn "mismatched parentheses in $file_name\n" if $in_parentheses;
       
   877 
       
   878     return @ranges;
       
   879 }
       
   880 
       
   881 sub processPaths(\@)
       
   882 {
       
   883     my ($paths) = @_;
       
   884     return ("." => 1) if (!@{$paths});
       
   885 
       
   886     my %result = ();
       
   887 
       
   888     for my $file (@{$paths}) {
       
   889         die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
       
   890         die "can't handle empty string path\n" if $file eq "";
       
   891         die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
       
   892 
       
   893         my $untouchedFile = $file;
       
   894 
       
   895         $file = canonicalizePath($file);
       
   896 
       
   897         die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
       
   898 
       
   899         $result{$file} = 1;
       
   900     }
       
   901 
       
   902     return ("." => 1) if ($result{"."});
       
   903 
       
   904     # Remove any paths that also have a parent listed.
       
   905     for my $path (keys %result) {
       
   906         for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
       
   907             if ($result{$parent}) {
       
   908                 delete $result{$path};
       
   909                 last;
       
   910             }
       
   911         }
       
   912     }
       
   913 
       
   914     return %result;
       
   915 }
       
   916 
       
   917 sub diffFromToString()
       
   918 {
       
   919     return "" if $isSVN;
       
   920     return $gitCommit if $gitCommit =~ m/.+\.\..+/;
       
   921     return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
       
   922     return "HEAD" if $isGit;
       
   923 }
       
   924 
       
   925 sub diffCommand(@)
       
   926 {
       
   927     my @paths = @_;
       
   928 
       
   929     my $pathsString = "'" . join("' '", @paths) . "'"; 
       
   930 
       
   931     my $command;
       
   932     if ($isSVN) {
       
   933         $command = "$SVN diff --diff-cmd diff -x -N $pathsString";
       
   934     } elsif ($isGit) {
       
   935         $command = "$GIT diff " . diffFromToString();
       
   936         $command .= " -- $pathsString" unless $gitCommit;
       
   937     }
       
   938 
       
   939     return $command;
       
   940 }
       
   941 
       
   942 sub statusCommand(@)
       
   943 {
       
   944     my @files = @_;
       
   945 
       
   946     my $filesString = "'" . join ("' '", @files) . "'";
       
   947     my $command;
       
   948     if ($isSVN) {
       
   949         $command = "$SVN stat $filesString";
       
   950     } elsif ($isGit) {
       
   951         $command = "$GIT diff -r --name-status -C -C -M " . diffFromToString();
       
   952         $command .= " -- $filesString" unless $gitCommit;
       
   953     }
       
   954 
       
   955     return "$command 2> /dev/stdout";
       
   956 }
       
   957 
       
   958 sub createPatchCommand($)
       
   959 {
       
   960     my ($changedFilesString) = @_;
       
   961 
       
   962     my $command;
       
   963     if ($isSVN) {
       
   964         $command = "'$FindBin::Bin/svn-create-patch' $changedFilesString";
       
   965     } elsif ($isGit) {
       
   966         $command = "$GIT diff -C -C -M " . diffFromToString();
       
   967         $command .= " -- $changedFilesString" unless $gitCommit;
       
   968     }
       
   969 
       
   970     return $command;
       
   971 }
       
   972 
       
   973 sub diffHeaderFormat()
       
   974 {
       
   975     return qr/^Index: (\S+)$/ if $isSVN;
       
   976     return qr/^diff --git a\/.+ b\/(.+)$/ if $isGit;
       
   977 }
       
   978 
       
   979 sub findOriginalFileFromSvn($)
       
   980 {
       
   981     my ($file) = @_;
       
   982     my $baseUrl;
       
   983     open INFO, "$SVN info . |" or die;
       
   984     while (<INFO>) {
       
   985         if (/^URL: (.+)/) {
       
   986             $baseUrl = $1;
       
   987             last;
       
   988         }
       
   989     }
       
   990     close INFO;
       
   991     my $sourceFile;
       
   992     open INFO, "$SVN info '$file' |" or die;
       
   993     while (<INFO>) {
       
   994         if (/^Copied From URL: (.+)/) {
       
   995             $sourceFile = File::Spec->abs2rel($1, $baseUrl);
       
   996             last;
       
   997         }
       
   998     }
       
   999     close INFO;
       
  1000     return $sourceFile;
       
  1001 }
       
  1002 
       
  1003 sub generateFileList(\@\@\%)
       
  1004 {
       
  1005     my ($changedFiles, $conflictFiles, $functionLists) = @_;
       
  1006     print STDERR "  Running status to find changed, added, or removed files.\n";
       
  1007     open STAT, "-|", statusCommand(keys %paths) or die "The status failed: $!.\n";
       
  1008     my $inGitCommitSection = 0;
       
  1009     while (<STAT>) {
       
  1010         my $status;
       
  1011         my $original;
       
  1012         my $file;
       
  1013 
       
  1014         if ($isSVN) {
       
  1015             if (/^([ACDMR]).{5} (.+)$/) {
       
  1016                 $status = $1;
       
  1017                 $file = $2;
       
  1018                 $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
       
  1019             } else {
       
  1020                 print;  # error output from svn stat
       
  1021             }
       
  1022         } elsif ($isGit) {
       
  1023             if (/^([ADM])\t(.+)$/) {
       
  1024                 $status = $1;
       
  1025                 $file = $2;
       
  1026             } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90%    newfile    oldfile
       
  1027                 $status = $1;
       
  1028                 $original = $2;
       
  1029                 $file = $3;
       
  1030             } else {
       
  1031                 print;  # error output from git diff
       
  1032             }
       
  1033         }
       
  1034 
       
  1035         next unless $status;
       
  1036 
       
  1037         $file = makeFilePathRelative($file);
       
  1038 
       
  1039         if (isModifiedStatus($status) || isAddedStatus($status)) {
       
  1040             my @components = File::Spec->splitdir($file);
       
  1041             if ($components[0] eq "LayoutTests") {
       
  1042                 $didChangeRegressionTests = 1;
       
  1043                 push @addedRegressionTests, $file
       
  1044                     if isAddedStatus($status)
       
  1045                        && $file =~ /\.([a-zA-Z]+)$/
       
  1046                        && $supportedTestExtensions{lc($1)}
       
  1047                        && !scalar(grep(/^resources$/i, @components));
       
  1048             }
       
  1049             push @{$changedFiles}, $file if $components[$#components] ne "ChangeLog";
       
  1050         } elsif (isConflictStatus($status)) {
       
  1051             push @{$conflictFiles}, $file;
       
  1052         }
       
  1053         my $description = statusDescription($status, $original);
       
  1054         $functionLists->{$file} = $description if defined $description;
       
  1055     }
       
  1056     close STAT;
       
  1057 }
       
  1058 
       
  1059 sub gitConfig($)
       
  1060 {
       
  1061     return unless $isGit;
       
  1062 
       
  1063     my ($config) = @_;
       
  1064 
       
  1065     my $result = `$GIT config $config`;
       
  1066     chomp $result;
       
  1067     return $result;
       
  1068 }
       
  1069 
       
  1070 sub isModifiedStatus($)
       
  1071 {
       
  1072     my ($status) = @_;
       
  1073 
       
  1074     my %statusCodes = (
       
  1075         "M" => 1,
       
  1076     );
       
  1077 
       
  1078     return $statusCodes{$status};
       
  1079 }
       
  1080 
       
  1081 sub isAddedStatus($)
       
  1082 {
       
  1083     my ($status) = @_;
       
  1084 
       
  1085     my %statusCodes = (
       
  1086         "A" => 1,
       
  1087         "C" => $isGit,
       
  1088         "R" => 1,
       
  1089     );
       
  1090 
       
  1091     return $statusCodes{$status};
       
  1092 }
       
  1093 
       
  1094 sub isConflictStatus($)
       
  1095 {
       
  1096     my ($status) = @_;
       
  1097 
       
  1098     my %svn = (
       
  1099         "C" => 1,
       
  1100     );
       
  1101 
       
  1102     my %git = (
       
  1103         "U" => 1,
       
  1104     );
       
  1105 
       
  1106     return 0 if $gitCommit; # an existing commit cannot have conflicts
       
  1107     return $svn{$status} if $isSVN;
       
  1108     return $git{$status} if $isGit;
       
  1109 }
       
  1110 
       
  1111 sub statusDescription($$)
       
  1112 {
       
  1113     my ($status, $original) = @_;
       
  1114 
       
  1115     my %svn = (
       
  1116         "A" => defined $original ? " Copied from \%s." : " Added.",
       
  1117         "D" => " Removed.",
       
  1118         "M" => "",
       
  1119         "R" => defined $original ? " Replaced with \%s." : " Replaced.",
       
  1120     );
       
  1121 
       
  1122     my %git = %svn;
       
  1123     $git{"A"} = " Added.";
       
  1124     $git{"C"} = " Copied from \%s.";
       
  1125     $git{"R"} = " Renamed from \%s.";
       
  1126 
       
  1127     return sprintf($svn{$status}, $original) if $isSVN && exists $svn{$status};
       
  1128     return sprintf($git{$status}, $original) if $isGit && exists $git{$status};
       
  1129     return undef;
       
  1130 }
       
  1131 
       
  1132 sub extractLineRange($)
       
  1133 {
       
  1134     my ($string) = @_;
       
  1135 
       
  1136     my ($start, $end) = (-1, -1);
       
  1137 
       
  1138     if ($isSVN && $string =~ /^\d+(,\d+)?[acd](\d+)(,(\d+))?/) {
       
  1139         $start = $2;
       
  1140         $end = $4 || $2;
       
  1141     } elsif ($isGit && $string =~ /^@@ -\d+,\d+ \+(\d+),(\d+) @@/) {
       
  1142         $start = $1;
       
  1143         $end = $1 + $2 - 1;
       
  1144 
       
  1145         # git-diff shows 3 lines of context above and below the actual changes,
       
  1146         # so we need to subtract that context to find the actual changed range.
       
  1147 
       
  1148         # FIXME: This won't work if there's a change at the very beginning or
       
  1149         # very end of a file.
       
  1150 
       
  1151         $start += 3;
       
  1152         $end -= 6;
       
  1153     }
       
  1154 
       
  1155     return ($start, $end);
       
  1156 }
       
  1157 
       
  1158 sub firstDirectoryOrCwd()
       
  1159 {
       
  1160     my $dir = ".";
       
  1161     my @dirs = keys(%paths);
       
  1162 
       
  1163     $dir = -d $dirs[0] ? $dirs[0] : dirname($dirs[0]) if @dirs;
       
  1164 
       
  1165     return $dir;
       
  1166 }
       
  1167 
       
  1168 sub testListForChangeLog(@)
       
  1169 {
       
  1170     my (@tests) = @_;
       
  1171 
       
  1172     return "" unless @tests;
       
  1173 
       
  1174     my $leadString = "        Test" . (@tests == 1 ? "" : "s") . ": ";
       
  1175     my $list = $leadString;
       
  1176     foreach my $i (0..$#tests) {
       
  1177         $list .= " " x length($leadString) if $i;
       
  1178         my $test = $tests[$i];
       
  1179         $test =~ s/^LayoutTests\///;
       
  1180         $list .= "$test\n";
       
  1181     }
       
  1182     $list .= "\n";
       
  1183 
       
  1184     return $list;
       
  1185 }
       
  1186 
       
  1187 sub reviewerAndDescriptionForGitCommit($)
       
  1188 {
       
  1189     my ($commit) = @_;
       
  1190 
       
  1191     my $description = '';
       
  1192     my $reviewer;
       
  1193 
       
  1194     my @args = qw(rev-list --pretty);
       
  1195     push @args, '-1' if $commit !~ m/.+\.\..+/;
       
  1196     my $gitLog;
       
  1197     {
       
  1198         local $/ = undef;
       
  1199         open(GIT, "-|", $GIT, @args, $commit) || die;
       
  1200         $gitLog = <GIT>;
       
  1201         close(GIT);
       
  1202     }
       
  1203 
       
  1204     my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
       
  1205     shift @commitLogs; # Remove initial blank commit log
       
  1206     my $commitLogCount = 0;
       
  1207     foreach my $commitLog (@commitLogs) {
       
  1208         $description .= "\n" if $commitLogCount;
       
  1209         $commitLogCount++;
       
  1210         my $inHeader = 1;
       
  1211         my @lines = split(/\n/, $commitLog);
       
  1212         shift @lines; # Remove initial blank line
       
  1213         foreach my $line (@lines) {
       
  1214             if ($inHeader) {
       
  1215                 if (!$line) {
       
  1216                     $inHeader = 0;
       
  1217                 } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
       
  1218                     if (!$reviewer) {
       
  1219                         $reviewer = $1;
       
  1220                     } else {
       
  1221                         $reviewer .= ", " . $1;
       
  1222                     }
       
  1223                 }
       
  1224                 next;
       
  1225             } elsif (length $line == 0) {
       
  1226                 $description = $description . "\n";
       
  1227             } else {
       
  1228                 $line =~ s/^\s*//;
       
  1229                 $description = $description . "        " . $line . "\n";
       
  1230             }
       
  1231         }
       
  1232     }
       
  1233     if (!$reviewer) {
       
  1234       $reviewer = $gitReviewer;
       
  1235     }
       
  1236 
       
  1237     return ($reviewer, $description);
       
  1238 }