blob: 41063c0b1970f7a095d78e037b3f57c0d2b7a4f9 [file] [log] [blame]
commit-queue@webkit.org9437c022014-10-10 10:52:41 +00001#!/usr/bin/env perl
mjsb85f64e2005-06-17 07:49:31 +00002# -*- Mode: perl; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3
4#
5# Copyright (C) 2000, 2001 Eazel, Inc.
commit-queue@webkit.org2e904ac2021-03-01 16:38:39 +00006# Copyright (C) 2002-2021 Apple Inc. All rights reserved.
treat@webkit.org1c86c8e2009-07-23 16:32:44 +00007# Copyright (C) 2009 Torch Mobile, Inc.
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00008# Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
mjsb85f64e2005-06-17 07:49:31 +00009#
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
timothy_horton@apple.com730200b2013-08-21 20:30:40 +000025
mjsb85f64e2005-06-17 07:49:31 +000026# Perl script to create a ChangeLog entry with names of files
thatcherbefc69c2006-01-08 04:38:04 +000027# and functions from a diff.
mjsb85f64e2005-06-17 07:49:31 +000028#
29# Darin Adler <darin@bentspoon.com>, started 20 April 2000
30# Java support added by Maciej Stachowiak <mjs@eazel.com>
31# Objective-C, C++ and Objective-C++ support added by Maciej Stachowiak <mjs@apple.com>
aroben363e7842007-05-15 23:35:59 +000032# Git support added by Adam Roben <aroben@apple.com>
treat@webkit.orgcaf90532009-06-23 23:10:55 +000033# --git-index flag added by Joe Mason <joe.mason@torchmobile.com>
mjsb85f64e2005-06-17 07:49:31 +000034
35
36#
37# TODO:
mjsb85f64e2005-06-17 07:49:31 +000038# Decide what a good logical order is for the changed files
39# other than a normal text "sort" (top level first?)
40# (group directories?) (.h before .c?)
41# Handle yacc source files too (other languages?).
42# Help merge when there are ChangeLog conflicts or if there's
43# already a partly written ChangeLog entry.
treat@webkit.org1c86c8e2009-07-23 16:32:44 +000044# Add command line option to put the ChangeLog into a separate file.
thatcherbefc69c2006-01-08 04:38:04 +000045# Add SVN version numbers for commit (can't do that until
mjsb85f64e2005-06-17 07:49:31 +000046# the changes are checked in, though).
47# Work around diff stupidity where deleting a function that starts
48# with a comment makes diff think that the following function
49# has been changed (if the following function starts with a comment
50# with the same first line, such as /**)
51# Work around diff stupidity where deleting an entire function and
52# the blank lines before it makes diff think you've changed the
53# previous function.
54
55use strict;
ddkilzer0507c8f2006-06-11 23:49:28 +000056use warnings;
mjsb85f64e2005-06-17 07:49:31 +000057
ddkilzer0507c8f2006-06-11 23:49:28 +000058use File::Basename;
59use File::Spec;
aroben8cc9bc62007-04-20 22:33:36 +000060use FindBin;
mjsb85f64e2005-06-17 07:49:31 +000061use Getopt::Long;
arobenc3e1ec22007-07-19 17:23:54 +000062use lib $FindBin::Bin;
matthew_hanson@apple.com4b8ac1c2015-04-21 01:48:18 +000063use List::Util qw/max/;
ddkilzer1f0d4f02007-08-26 13:39:57 +000064use POSIX qw(strftime);
arobenc3e1ec22007-07-19 17:23:54 +000065use VCSUtils;
mjsb85f64e2005-06-17 07:49:31 +000066
jbedard@apple.com49f09da62021-10-19 00:48:12 +000067sub actuallyGenerateFunctionLists($$$$$$);
68sub attributeCommand($$$);
ddkilzer1f0d4f02007-08-26 13:39:57 +000069sub changeLogDate($);
haraken@chromium.org5711f062011-12-16 09:21:02 +000070sub changeLogEmailAddressFromArgs($$);
71sub changeLogNameFromArgs($$);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000072sub computeModifiedFunctions($$$);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +000073sub createPatchCommand($$$$);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000074sub decodeEntities($);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +000075sub determinePropertyChanges($$$);
76sub diffCommand($$$$);
77sub diffFromToString($$$);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000078sub extractLineRangeAfterChange($);
79sub extractLineRangeBeforeChange($);
commit-queue@webkit.org83402582016-01-08 02:18:01 +000080sub fetchBugXMLData($$);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000081sub fetchBugDescriptionFromBugXMLData($$$);
82sub fetchRadarURLFromBugXMLData($$);
mitz@apple.comdd8efa82014-08-02 17:34:34 +000083sub findChangeLogs($$);
ddkilzer33ab5032007-05-28 20:48:56 +000084sub findOriginalFileFromSvn($);
ddkilzer@apple.com62990322020-12-10 18:05:39 +000085sub generateFileList(\%$$$\%);
haraken@chromium.org5711f062011-12-16 09:21:02 +000086sub generateFunctionLists($$$$$);
commit-queue@webkit.org545bf102021-09-14 15:56:51 +000087sub generateNewChangeLogs($$$$$$$$$$$$$$$);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +000088sub getLatestChangeLogs($);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000089sub get_function_line_ranges($$);
haraken@chromium.org849a8ed2011-12-25 08:24:07 +000090sub get_function_line_ranges_for_cpp($$);
commit-queue@webkit.orgd30e55d2013-05-30 01:07:22 +000091sub delete_namespaces_from_ranges_for_cpp(\@\@);
92sub is_function_in_namespace($$);
ddkilzerb7b3b912006-06-25 06:01:41 +000093sub get_function_line_ranges_for_java($$);
timothy@apple.com31f57e72008-10-14 19:17:08 +000094sub get_function_line_ranges_for_javascript($$);
aroben@apple.comd4acad02011-07-05 22:20:53 +000095sub get_function_line_ranges_for_perl($$);
aroben@apple.comec7621d2010-03-12 19:06:01 +000096sub get_selector_line_ranges_for_css($$);
commit-queue@webkit.org408ede92015-09-04 17:43:19 +000097sub get_function_line_ranges_for_swift($$);
jbedard@apple.com49f09da62021-10-19 00:48:12 +000098sub parseSwiftFunctionArgs($);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +000099sub isAddedStatus($);
100sub isConflictStatus($$$);
101sub isModifiedStatus($);
102sub isUnmodifiedStatus($);
103sub main();
ddkilzerb7b3b912006-06-25 06:01:41 +0000104sub method_decl_to_selector($);
abarth@webkit.orgc93f2152009-06-16 08:06:58 +0000105sub normalizeLineEndings($$);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +0000106sub openChangeLogs($);
llango.u-szeged@partner.samsung.comd8b2d3c2014-03-24 09:27:38 +0000107sub originalFile($$$$);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +0000108sub pluralizeAndList($$@);
109sub printDiff($$$$);
110sub processPaths(\@);
111sub propertyChangeDescription($);
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000112sub resolveChangeLogsPath($@);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +0000113sub resolveConflictedChangeLogs($);
114sub reviewerAndDescriptionForGitCommit($$);
115sub statusCommand($$$$);
116sub statusDescription($$$$);
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000117sub svnUpdateCommand(@);
haraken@chromium.orgaf2d77f2011-12-18 14:43:14 +0000118sub testListForChangeLog(@);
ddkilzerb7b3b912006-06-25 06:01:41 +0000119
haraken@chromium.org71adf4d2011-12-14 05:13:07 +0000120### Constant variables.
ddkilzer1f0d4f02007-08-26 13:39:57 +0000121# Project time zone for Cupertino, CA, US
haraken@chromium.org577bbab2011-12-21 08:54:22 +0000122use constant ChangeLogTimeZone => "PST8PDT";
123use constant SVN => "svn";
124use constant GIT => "git";
125use constant SupportedTestExtensions => {map { $_ => 1 } qw(html shtml svg xml xhtml pl php)};
haraken@chromium.org71adf4d2011-12-14 05:13:07 +0000126
haraken@chromium.org35216512011-12-17 18:15:48 +0000127exit(main());
haraken@chromium.org71adf4d2011-12-14 05:13:07 +0000128
haraken@chromium.org35216512011-12-17 18:15:48 +0000129sub main()
130{
ddkilzer@apple.com62990322020-12-10 18:05:39 +0000131 my %attributeCache;
haraken@chromium.org35216512011-12-17 18:15:48 +0000132 my $bugDescription;
commit-queue@webkit.org83402582016-01-08 02:18:01 +0000133 my $bugRadarURL;
haraken@chromium.org35216512011-12-17 18:15:48 +0000134 my $bugNumber;
135 my $name;
136 my $emailAddress;
137 my $mergeBase = 0;
138 my $gitCommit = 0;
139 my $gitIndex = "";
140 my $gitReviewer = "";
commit-queue@webkit.org22105de2017-01-09 17:41:25 +0000141 my $checkWebKitStyle = 0;
haraken@chromium.org35216512011-12-17 18:15:48 +0000142 my $openChangeLogs = 0;
143 my $writeChangeLogs = 1;
mitz@apple.com7db6fe12014-08-01 23:10:50 +0000144 my $delimiters = 0;
haraken@chromium.org35216512011-12-17 18:15:48 +0000145 my $showHelp = 0;
146 my $spewDiff = $ENV{"PREPARE_CHANGELOG_DIFF"};
147 my $updateChangeLogs = 1;
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000148 my $onlyFiles = 0;
haraken@chromium.org35216512011-12-17 18:15:48 +0000149 my $parseOptionsResult =
150 GetOptions("diff|d!" => \$spewDiff,
151 "bug|b:i" => \$bugNumber,
mitz@apple.com7db6fe12014-08-01 23:10:50 +0000152 "delimiters" => \$delimiters,
haraken@chromium.org35216512011-12-17 18:15:48 +0000153 "description:s" => \$bugDescription,
154 "name:s" => \$name,
155 "email:s" => \$emailAddress,
156 "merge-base:s" => \$mergeBase,
157 "git-commit|g:s" => \$gitCommit,
158 "git-index" => \$gitIndex,
159 "git-reviewer:s" => \$gitReviewer,
160 "help|h!" => \$showHelp,
jcraig@apple.com159d8fd2014-04-24 07:07:06 +0000161 "style!" => \$checkWebKitStyle,
haraken@chromium.org35216512011-12-17 18:15:48 +0000162 "open|o!" => \$openChangeLogs,
163 "write!" => \$writeChangeLogs,
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000164 "update!" => \$updateChangeLogs,
165 "only-files" => \$onlyFiles);
haraken@chromium.org35216512011-12-17 18:15:48 +0000166 if (!$parseOptionsResult || $showHelp) {
167 print STDERR basename($0) . " [-b|--bug=<bugid>] [-d|--diff] [-h|--help] [-o|--open] [-g|--git-commit=<committish>] [--git-reviewer=<name>] [svndir1 [svndir2 ...]]\n";
168 print STDERR " -b|--bug Fill in the ChangeLog bug information from the given bug.\n";
169 print STDERR " --description One-line description that matches the bug title.\n";
170 print STDERR " -d|--diff Spew diff to stdout when running\n";
171 print STDERR " --merge-base Populate the ChangeLogs with the diff to this branch\n";
172 print STDERR " -g|--git-commit Populate the ChangeLogs from the specified git commit\n";
173 print STDERR " --git-index Populate the ChangeLogs from the git index only\n";
174 print STDERR " --git-reviewer When populating the ChangeLogs from a git commit claim that the spcified name reviewed the change.\n";
175 print STDERR " This option is useful when the git commit lacks a Signed-Off-By: line\n";
176 print STDERR " -h|--help Show this help message\n";
commit-queue@webkit.org22105de2017-01-09 17:41:25 +0000177 print STDERR " --[no-]style Run check-webkit-style script when done (default: no-style)\n";
haraken@chromium.org35216512011-12-17 18:15:48 +0000178 print STDERR " -o|--open Open ChangeLogs in an editor when done\n";
179 print STDERR " --[no-]update Update ChangeLogs from svn before adding entry (default: update)\n";
180 print STDERR " --[no-]write Write ChangeLogs to disk (otherwise send new entries to stdout) (default: write)\n";
mitz@apple.com7db6fe12014-08-01 23:10:50 +0000181 print STDERR " --delimiters When writing to stdout, label and print a \"~\" after each entry\n";
haraken@chromium.org35216512011-12-17 18:15:48 +0000182 print STDERR " --email= Specify the email address to be used in the patch\n";
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000183 print STDERR " --only-files Exclude the standard changelog header and only include references to files.\n";
haraken@chromium.org35216512011-12-17 18:15:48 +0000184 return 1;
185 }
186
jcraig@apple.com8a8167b2014-04-25 23:48:10 +0000187 if ($checkWebKitStyle) {
188 print STDERR " Running check-webkit-style.\n ";
189 system "$FindBin::Bin/check-webkit-style";
190 }
191
commit-queue@webkit.org29cb7772021-09-16 18:08:14 +0000192 die "--git-commit and --git-index are incompatible when not used with --only-files." if ($gitIndex && $gitCommit && !$onlyFiles);
haraken@chromium.org35216512011-12-17 18:15:48 +0000193
194 isSVN() || isGit() || die "Couldn't determine your version control system.";
195
196 my %paths = processPaths(@ARGV);
197
198 # Find the list of modified files
ddkilzer@apple.com62990322020-12-10 18:05:39 +0000199 my ($changedFiles, $conflictFiles, $functionLists, $addedRegressionTests, $requiresTests) = generateFileList(%paths, $gitCommit, $gitIndex, $mergeBase, %attributeCache);
haraken@chromium.org35216512011-12-17 18:15:48 +0000200
201 if (!@$changedFiles && !@$conflictFiles && !keys %$functionLists) {
202 print STDERR " No changes found.\n";
203 return 1;
204 }
205
206 if (@$conflictFiles) {
207 print STDERR " The following files have conflicts. Run prepare-ChangeLog again after fixing the conflicts:\n";
208 print STDERR join("\n", @$conflictFiles), "\n";
209 return 1;
210 }
211
212 generateFunctionLists($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase);
213
214 # Get some parameters for the ChangeLog we are about to write.
215 $name = changeLogNameFromArgs($name, $gitCommit);
216 $emailAddress = changeLogEmailAddressFromArgs($emailAddress, $gitCommit);
217
218 print STDERR " Change author: $name <$emailAddress>.\n";
219
220 # Remove trailing parenthesized notes from user name (bit of hack).
221 $name =~ s/\(.*?\)\s*$//g;
222
223 my $bugURL;
224 if ($bugNumber) {
225 $bugURL = "https://bugs.webkit.org/show_bug.cgi?id=$bugNumber";
226 }
227
228 if ($bugNumber && !$bugDescription) {
commit-queue@webkit.org83402582016-01-08 02:18:01 +0000229 my $bugXMLData = fetchBugXMLData($bugURL, $bugNumber);
230 $bugDescription = fetchBugDescriptionFromBugXMLData($bugURL, $bugNumber, $bugXMLData);
231 $bugRadarURL = fetchRadarURLFromBugXMLData($bugNumber, $bugXMLData);
haraken@chromium.org35216512011-12-17 18:15:48 +0000232 }
233
mitz@apple.comdd8efa82014-08-02 17:34:34 +0000234 my ($filesInChangeLog, $prefixes) = findChangeLogs($functionLists, $writeChangeLogs);
haraken@chromium.org35216512011-12-17 18:15:48 +0000235
236 # Get the latest ChangeLog files from svn.
237 my $changeLogs = getLatestChangeLogs($prefixes);
238
239 if (@$changeLogs && $updateChangeLogs && isSVN()) {
240 resolveConflictedChangeLogs($changeLogs);
241 }
242
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000243 generateNewChangeLogs($prefixes, $filesInChangeLog, $addedRegressionTests, $requiresTests, $functionLists, $bugURL, $bugDescription, $bugRadarURL, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs, $delimiters, $onlyFiles);
haraken@chromium.org35216512011-12-17 18:15:48 +0000244
245 if ($writeChangeLogs) {
246 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";
247 }
248
249 # Write out another diff.
250 if ($spewDiff && @$changedFiles) {
251 printDiff($changedFiles, $gitCommit, $gitIndex, $mergeBase);
252 }
253
254 # Open ChangeLogs.
255 if ($openChangeLogs && @$changeLogs) {
256 openChangeLogs($changeLogs);
257 }
258 return 0;
aroben38e341c2007-04-28 06:57:49 +0000259}
mjsb85f64e2005-06-17 07:49:31 +0000260
llango.u-szeged@partner.samsung.comd8b2d3c2014-03-24 09:27:38 +0000261sub originalFile($$$$)
262{
263 my ($file, $gitCommit, $gitIndex, $mergeBase) = @_;
264
265 my $command;
266 if (isSVN()) {
267 my $escapedPathsString = escapeSubversionPath($file);
268 $command = SVN . " cat $escapedPathsString";
269 } elsif (isGit()) {
llango.u-szeged@partner.samsung.com8a173e52014-03-25 15:53:39 +0000270 $command = GIT . " show ";
commit-queue@webkit.org1c3668f2021-09-27 15:50:48 +0000271 if ($gitCommit) {
272 $command .= "$gitCommit^";
273 } elsif ($mergeBase) {
llango.u-szeged@partner.samsung.com41967212014-04-24 08:55:18 +0000274 $command .= "$mergeBase";
275 } else {
matthew_hanson@apple.coma1240e72015-04-20 22:05:52 +0000276 $command .= "HEAD";
llango.u-szeged@partner.samsung.com41967212014-04-24 08:55:18 +0000277 }
commit-queue@webkit.org7fa4cbd2021-04-08 18:30:02 +0000278 $command .= ":'$file'";
llango.u-szeged@partner.samsung.comd8b2d3c2014-03-24 09:27:38 +0000279 }
280
281 return $command;
282}
283
haraken@chromium.org5711f062011-12-16 09:21:02 +0000284sub generateFunctionLists($$$$$)
haraken@chromium.orge22e05e2011-12-09 23:50:58 +0000285{
haraken@chromium.org5711f062011-12-16 09:21:02 +0000286 my ($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase) = @_;
dbates@webkit.org4532eae2016-08-17 02:30:27 +0000287 my %delegateHash = (
288 openDiff => sub ($$$$) {
289 my ($changedFiles, $gitCommit, $gitIndex, $mergeBase) = @_;
290 return unless open(DIFF, "-|", diffCommand($changedFiles, $gitCommit, $gitIndex, $mergeBase));
291 return \*DIFF;
292 },
293 openFile => sub ($) {
294 my ($file) = @_;
commit-queue@webkit.org1c3668f2021-09-27 15:50:48 +0000295 if (isGit() && $gitCommit && !$gitIndex) {
296 my $command = GIT . " show " . "$gitCommit" . ":'$file'";
297 return unless open(SOURCE, "-|", $command);
298 } else {
299 return unless open(SOURCE, "<", $file);
300 }
dbates@webkit.org4532eae2016-08-17 02:30:27 +0000301 return \*SOURCE;
302 },
303 openOriginalFile => sub ($) {
304 my ($file, $gitCommit, $gitIndex, $mergeBase) = @_;
305 return unless open(SOURCE, "-|", originalFile($file, $gitCommit, $gitIndex, $mergeBase));
306 return \*SOURCE;
307 },
308 normalizePath => sub ($) {
309 my ($path) = @_;
310 return normalizePath(makeFilePathRelative($path));
311 },
312 );
313 actuallyGenerateFunctionLists($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase, \%delegateHash);
314}
315
jbedard@apple.com49f09da62021-10-19 00:48:12 +0000316sub actuallyGenerateFunctionLists($$$$$$)
317{
318 my ($changedFiles, $functionLists, $gitCommit, $gitIndex, $mergeBase, $delegateHashRef) = @_;
319
320 my %line_ranges_after_changed;
321 my %line_ranges_before_changed;
322 if (@$changedFiles) {
323 # For each file, build a list of modified lines.
324 # Use line numbers from the "after" side of each diff.
325 print STDERR " Reviewing diff to determine which lines changed.\n";
326 my $file;
327 my $diffFileHandle = $delegateHashRef->{openDiff}($changedFiles, $gitCommit, $gitIndex, $mergeBase);
328 if (!$diffFileHandle) {
329 die "The diff failed: $!.\n";
330 }
331 while (<$diffFileHandle>) {
332 my $filePath = parseDiffStartLine($_);
333 $file = $delegateHashRef->{normalizePath}($filePath) if $filePath;
334 if (defined $file) {
335 my ($before_start, $before_end) = extractLineRangeBeforeChange($_);
336 if ($before_start >= 1 && $before_end >= 1) {
337 push @{$line_ranges_before_changed{$file}}, [ $before_start, $before_end ];
338 } elsif (/DO_NOT_COMMIT/) {
339 print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
340 }
341 my ($after_start, $after_end) = extractLineRangeAfterChange($_);
342 if ($after_start >= 1 && $after_end >= 1) {
343 push @{$line_ranges_after_changed{$file}}, [ $after_start, $after_end ];
344 } elsif (/DO_NOT_COMMIT/) {
345 print STDERR "WARNING: file $file contains the string DO_NOT_COMMIT, line $.\n";
346 }
347 }
348 }
349 close($diffFileHandle);
350 }
351
352 # For each source file, convert line range to function list.
353 print STDERR " Extracting affected function names from source files.\n";
354 my %filesToExamine = map { $_ => 1 } (keys(%line_ranges_before_changed), keys(%line_ranges_after_changed));
355 foreach my $file (keys %filesToExamine) {
356 my %saw_function;
357
358 # Find all the functions in the file.
359 my $sourceFileHandle = $delegateHashRef->{openFile}($file);
360 next unless $sourceFileHandle;
361 my @afterChangeFunctionRanges = get_function_line_ranges($sourceFileHandle, $file);
362 close($sourceFileHandle);
363
364 # Find modified functions in the file.
365 if ($line_ranges_after_changed{$file}) {
366 my @change_ranges = (@{$line_ranges_after_changed{$file}}, []);
367 my @functions = computeModifiedFunctions($file, \@change_ranges, \@afterChangeFunctionRanges);
368
369 # Format the list of functions.
370 if (@functions) {
371 $functionLists->{$file} = "" if !defined $functionLists->{$file};
372 $functionLists->{$file} .= "\n (" . join("):\n (", @functions) . "):";
373 }
374 }
375 # Find the deleted functions in the original file.
376 if ($line_ranges_before_changed{$file}) {
377 my $originalFileHandle = $delegateHashRef->{openOriginalFile}($file, $gitCommit, $gitIndex, $mergeBase);
378 next unless $originalFileHandle;
379 my @beforeChangeFunctionRanges = get_function_line_ranges($originalFileHandle, $file);
380 close($originalFileHandle);
381
382 my %existsAfterChange = map { $_->[2] => 1 } @afterChangeFunctionRanges;
383
384 my @functions;
385 my %sawFunctions;
386 for my $functionRange (@beforeChangeFunctionRanges) {
387 my $functionName = $functionRange->[2];
388 if (!$existsAfterChange{$functionName} && !$sawFunctions{$functionName}) {
389 push @functions, $functionName;
390 $sawFunctions{$functionName} = 1;
391 }
392 }
393
394 # Format the list of deleted functions.
395 if (@functions) {
396 $functionLists->{$file} = "" if !defined $functionLists->{$file};
397 $functionLists->{$file} .= "\n (" . join("): Deleted.\n (", @functions) . "): Deleted.";
398 }
399 }
400 }
401}
402
403sub computeModifiedFunctions($$$)
404{
405 my ($file, $changedLineRanges, $functionRanges) = @_;
406
407 my %sawFunction;
408
409 # Find all the modified functions.
410 my @functions;
411 my @change_ranges = @{$changedLineRanges};
412 my @change_range = (0, 0);
413 FUNCTION: foreach my $function_range_ref (@{$functionRanges}) {
414 my @function_range = @{$function_range_ref};
415
416 # FIXME: This is a hack. If the function name is empty, skip it.
417 # The cpp, python, javascript, perl, css and java parsers
418 # are not perfectly implemented and sometimes function names cannot be retrieved
419 # correctly. As you can see in get_function_line_ranges_XXXX(), those parsers
420 # are not intended to implement real parsers but intended to just retrieve function names
421 # for most practical syntaxes.
422 next unless $function_range[2];
423
424 # Advance to successive change ranges.
425 for (;; @change_range = @{shift @change_ranges}) {
426 last FUNCTION unless @change_range;
427
428 # If past this function, move on to the next one.
429 next FUNCTION if $change_range[0] > $function_range[1];
430
431 # If an overlap with this function range, record the function name.
432 if ($change_range[1] >= $function_range[0]
433 and $change_range[0] <= $function_range[1]) {
434 if (!$sawFunction{$function_range[2]}) {
435 $sawFunction{$function_range[2]} = 1;
436 push @functions, $function_range[2];
437 }
438 next FUNCTION;
439 }
440 }
441 }
442
443 return @functions;
444}
445
ddkilzer1f0d4f02007-08-26 13:39:57 +0000446sub changeLogDate($)
447{
448 my ($timeZone) = @_;
449 my $savedTimeZone = $ENV{'TZ'};
450 # Set TZ temporarily so that localtime() is in that time zone
451 $ENV{'TZ'} = $timeZone;
452 my $date = strftime("%Y-%m-%d", localtime());
453 if (defined $savedTimeZone) {
454 $ENV{'TZ'} = $savedTimeZone;
455 } else {
456 delete $ENV{'TZ'};
457 }
458 return $date;
459}
460
haraken@chromium.org5711f062011-12-16 09:21:02 +0000461sub changeLogNameFromArgs($$)
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000462{
haraken@chromium.org5711f062011-12-16 09:21:02 +0000463 my ($nameFromArgs, $gitCommit) = @_;
mrowe@apple.com264efc42009-11-08 03:59:28 +0000464 # Silently allow --git-commit to win, we could warn if $nameFromArgs is defined.
haraken@chromium.org577bbab2011-12-21 08:54:22 +0000465 my $command = GIT . ' log --max-count=1 --pretty="format:%an" "' . $gitCommit . '"';
466 return `$command` if $gitCommit;
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000467
mrowe@apple.com264efc42009-11-08 03:59:28 +0000468 return $nameFromArgs || changeLogName();
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000469}
470
haraken@chromium.org5711f062011-12-16 09:21:02 +0000471sub changeLogEmailAddressFromArgs($$)
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000472{
haraken@chromium.org5711f062011-12-16 09:21:02 +0000473 my ($emailAddressFromArgs, $gitCommit) = @_;
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000474 # Silently allow --git-commit to win, we could warn if $emailAddressFromArgs is defined.
haraken@chromium.org577bbab2011-12-21 08:54:22 +0000475 my $command = GIT . ' log --max-count=1 --pretty="format:%ae" "' . $gitCommit . '"';
476 return `$command` if $gitCommit;
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000477
mrowe@apple.com264efc42009-11-08 03:59:28 +0000478 return $emailAddressFromArgs || changeLogEmailAddress();
eric@webkit.org2c1cc582009-07-01 23:31:37 +0000479}
480
commit-queue@webkit.org83402582016-01-08 02:18:01 +0000481sub fetchBugXMLData($$)
haraken@chromium.org7daa6722011-12-09 23:26:42 +0000482{
haraken@chromium.orge25b42a2011-12-15 23:13:40 +0000483 my ($bugURL, $bugNumber) = @_;
haraken@chromium.org7daa6722011-12-09 23:26:42 +0000484
ddkilzer@apple.com948169b2012-05-14 12:08:16 +0000485 my $bugXMLURL = "$bugURL&ctype=xml&excludefield=attachmentdata";
haraken@chromium.org7daa6722011-12-09 23:26:42 +0000486 # Perl has no built in XML processing, so we'll fetch and parse with curl and grep
487 # Pass --insecure because some cygwin installs have no certs we don't
488 # care about validating that bugs.webkit.org is who it says it is here.
commit-queue@webkit.org83402582016-01-08 02:18:01 +0000489 my $xmlData = `curl --insecure --silent "$bugXMLURL"`;
490 if ($xmlData !~ /<\?xml/) {
haraken@chromium.org7daa6722011-12-09 23:26:42 +0000491 # Maybe the reason the above did not work is because the curl that is installed doesn't
492 # support ssl at all.
493 if (`curl --version | grep ^Protocols` !~ /\bhttps\b/) {
494 print STDERR " Could not get description for bug $bugNumber.\n";
495 print STDERR " It looks like your version of curl does not support ssl.\n";
496 print STDERR " If you are using macports, this can be fixed with sudo port install curl +ssl.\n";
haraken@chromium.org7daa6722011-12-09 23:26:42 +0000497 }
498 exit 1;
499 }
commit-queue@webkit.org83402582016-01-08 02:18:01 +0000500 return $xmlData;
501}
502
jbedard@apple.com49f09da62021-10-19 00:48:12 +0000503sub fetchBugDescriptionFromBugXMLData($$$)
504{
505 my ($bugURL, $bugNumber, $bugXMLData) = @_;
506
507 if ($bugXMLData !~ /<short_desc>(.*)<\/short_desc>/) {
508 print STDERR " Bug $bugNumber has no bug description. Maybe you set wrong bug ID?\n";
509 print STDERR " The bug URL: $bugURL\n";
510 exit 1;
511 }
512
513 my $bugDescription = decodeEntities($1);
514 print STDERR " Description from bug $bugNumber:\n \"$bugDescription\".\n";
515 return $bugDescription;
516}
517
518sub fetchRadarURLFromBugXMLData($$)
519{
520 my ($bugNumber, $bugXMLData) = @_;
521
522 return "" if $bugXMLData !~ m|<thetext>\s*((&lt;)?rdar://(problem/)?\d+(&gt;)?)|;
523
524 my $bugRadarURL = decodeEntities($1);
525 print STDERR " Radar URL from bug $bugNumber:\n \"$bugRadarURL\".\n";
526 return $bugRadarURL;
527}
528
mitz@apple.comdd8efa82014-08-02 17:34:34 +0000529sub findChangeLogs($$)
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000530{
mitz@apple.comdd8efa82014-08-02 17:34:34 +0000531 my ($functionLists, $requireChangeLogToExist) = @_;
haraken@chromium.org71adf4d2011-12-14 05:13:07 +0000532
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000533 # Find the change logs.
534 my %has_log;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000535 my %filesInChangeLog;
haraken@chromium.org71adf4d2011-12-14 05:13:07 +0000536 foreach my $file (sort keys %$functionLists) {
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000537 my $prefix = $file;
538 my $has_log = 0;
539 while ($prefix) {
commit-queue@webkit.orgf9efd1e2016-04-04 17:44:01 +0000540 if ($^O eq "MSWin32") {
541 $prefix =~ s-\\[^\\]+\\?$-\\- or $prefix = "";
542 } else {
543 $prefix =~ s-/[^/]+/?$-/- or $prefix = "";
544 }
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000545 $has_log = $has_log{$prefix};
546 if (!defined $has_log) {
547 $has_log = -f "${prefix}ChangeLog";
548 $has_log{$prefix} = $has_log;
549 }
550 last if $has_log;
551 }
mitz@apple.comdd8efa82014-08-02 17:34:34 +0000552 if (!$has_log && $requireChangeLogToExist) {
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000553 print STDERR "No ChangeLog found for $file.\n";
554 } else {
haraken@chromium.org407dc832011-12-11 18:04:50 +0000555 push @{$filesInChangeLog{$prefix}}, $file;
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000556 }
557 }
558
559 # Build the list of ChangeLog prefixes in the correct project order
560 my @prefixes;
561 my %prefixesSort;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000562 foreach my $prefix (keys %filesInChangeLog) {
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000563 my $prefixDir = substr($prefix, 0, length($prefix) - 1); # strip trailing /
564 my $sortKey = lc $prefix;
565 $sortKey = "top level" unless length $sortKey;
566
567 if ($prefixDir eq "top level") {
568 $sortKey = "";
569 } elsif ($prefixDir eq "Tools") {
570 $sortKey = "-, just after top level";
571 } elsif ($prefixDir eq "WebBrowser") {
572 $sortKey = lc "WebKit, WebBrowser after";
573 } elsif ($prefixDir eq "Source/WebCore") {
574 $sortKey = lc "WebFoundation, WebCore after";
575 } elsif ($prefixDir eq "LayoutTests") {
576 $sortKey = lc "~, LayoutTests last";
577 }
578
579 $prefixesSort{$sortKey} = $prefix;
580 }
581 foreach my $prefixSort (sort keys %prefixesSort) {
582 push @prefixes, $prefixesSort{$prefixSort};
583 }
haraken@chromium.org407dc832011-12-11 18:04:50 +0000584 return (\%filesInChangeLog, \@prefixes);
585}
586
haraken@chromium.org7ecd43a2011-12-12 07:07:56 +0000587sub getLatestChangeLogs($)
588{
589 my ($prefixes) = @_;
590
591 my @changeLogs = ();
592 foreach my $prefix (@$prefixes) {
benjamin@webkit.org04476b22015-01-20 02:06:03 +0000593 push @changeLogs, File::Spec->catfile($prefix || ".", "ChangeLog");
haraken@chromium.org7ecd43a2011-12-12 07:07:56 +0000594 }
595 return \@changeLogs;
596}
597
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000598sub svnUpdateCommand(@)
599{
600 my @changeLogs = shift;
601
602 my @escapedChangeLogPaths = map(escapeSubversionPath($_), @changeLogs);
603 my $escapedChangeLogPathsString = qq(") . join(qq(" "), @escapedChangeLogPaths) . qq(");
604 my $command = SVN . " update $escapedChangeLogPathsString";
605
606 return $command;
607}
608
609sub resolveChangeLogsPath($@)
610{
611 my ($resolveChangeLogsPath, @conflictedChangeLogs) = @_;
612
613 my @escapedConflictedChangeLogs = map(escapeSubversionPath($_), @conflictedChangeLogs);
614 my $escapedConflictedChangeLogsString = qq(") . join(qq(" "), @escapedConflictedChangeLogs) . qq(");
615 my $command = "$resolveChangeLogsPath --no-warnings $escapedConflictedChangeLogsString";
616
617 return $command;
618}
619
haraken@chromium.org7ecd43a2011-12-12 07:07:56 +0000620sub resolveConflictedChangeLogs($)
621{
622 my ($changeLogs) = @_;
623
624 print STDERR " Running 'svn update' to update ChangeLog files.\n";
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000625 open ERRORS, "-|", svnUpdateCommand(@$changeLogs)
haraken@chromium.org7ecd43a2011-12-12 07:07:56 +0000626 or die "The svn update of ChangeLog files failed: $!.\n";
627 my @conflictedChangeLogs;
628 while (my $line = <ERRORS>) {
629 print STDERR " ", $line;
630 push @conflictedChangeLogs, $1 if $line =~ m/^C\s+(.+?)[\r\n]*$/;
631 }
632 close ERRORS;
633
634 return if !@conflictedChangeLogs;
635
636 print STDERR " Attempting to merge conflicted ChangeLogs.\n";
637 my $resolveChangeLogsPath = File::Spec->catfile(dirname($0), "resolve-ChangeLogs");
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000638 open RESOLVE, "-|", resolveChangeLogsPath($resolveChangeLogsPath, @conflictedChangeLogs)
haraken@chromium.org7ecd43a2011-12-12 07:07:56 +0000639 or die "Could not open resolve-ChangeLogs script: $!.\n";
640 print STDERR " $_" while <RESOLVE>;
641 close RESOLVE;
642}
643
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000644sub generateNewChangeLogs($$$$$$$$$$$$$$$)
haraken@chromium.org407dc832011-12-11 18:04:50 +0000645{
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000646 my ($prefixes, $filesInChangeLog, $addedRegressionTests, $requiresTests, $functionLists, $bugURL, $bugDescription, $bugRadarURL, $name, $emailAddress, $gitReviewer, $gitCommit, $writeChangeLogs, $delimiters, $onlyFiles) = @_;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000647
timothy_horton@apple.com60116d02019-10-23 07:31:05 +0000648 my $hasWebCoreChange = 0;
649 foreach my $prefix (@$prefixes) {
650 if (unixPath($prefix) =~ m|/WebCore/$|) {
651 $hasWebCoreChange = 1;
652 }
653 }
654
haraken@chromium.org407dc832011-12-11 18:04:50 +0000655 # Generate new ChangeLog entries and (optionally) write out new ChangeLog files.
656 foreach my $prefix (@$prefixes) {
657 my $endl = "\n";
658 my @old_change_log;
659
660 if ($writeChangeLogs) {
benjamin@webkit.org04476b22015-01-20 02:06:03 +0000661 my $changeLogPath = File::Spec->catfile($prefix || ".", "ChangeLog");
haraken@chromium.org407dc832011-12-11 18:04:50 +0000662 print STDERR " Editing the ${changeLogPath} file.\n";
663 open OLD_CHANGE_LOG, ${changeLogPath} or die "Could not open ${changeLogPath} file: $!.\n";
664 # It's less efficient to read the whole thing into memory than it would be
665 # to read it while we prepend to it later, but I like doing this part first.
666 @old_change_log = <OLD_CHANGE_LOG>;
667 close OLD_CHANGE_LOG;
668 # We want to match the ChangeLog's line endings in case it doesn't match
669 # the native line endings for this version of perl.
670 if ($old_change_log[0] =~ /(\r?\n)$/g) {
671 $endl = "$1";
672 }
673 open CHANGE_LOG, "> ${changeLogPath}" or die "Could not write ${changeLogPath}\n.";
commit-queue@webkit.orgb8e3fd12016-04-04 22:54:31 +0000674 binmode(CHANGE_LOG);
haraken@chromium.org407dc832011-12-11 18:04:50 +0000675 } else {
676 open CHANGE_LOG, ">-" or die "Could not write to STDOUT\n.";
mitz@apple.com7db6fe12014-08-01 23:10:50 +0000677 print substr($prefix, 0, length($prefix) - 1) . ":\n\n" unless (scalar @$prefixes) == 1 && !$delimiters;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000678 }
679
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000680 if (!$onlyFiles) {
681 my $date = changeLogDate(ChangeLogTimeZone);
682 print CHANGE_LOG normalizeLineEndings("$date $name <$emailAddress>\n\n", $endl);
683 }
haraken@chromium.org407dc832011-12-11 18:04:50 +0000684
haraken@chromium.orge25b42a2011-12-15 23:13:40 +0000685 my ($reviewer, $description) = reviewerAndDescriptionForGitCommit($gitCommit, $gitReviewer) if $gitCommit;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000686 $reviewer = "NOBODY (OO" . "PS!)" if !$reviewer;
687
commit-queue@webkit.orgb2866a82017-07-12 17:16:48 +0000688 ($bugDescription, $description) =
689 ($description =~ /^(?:\s*(.*)\n)?(?:\s*\n)*((?:\n|.)*)/)
690 if !$bugDescription && $description;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000691
commit-queue@webkit.orgb2866a82017-07-12 17:16:48 +0000692 $bugDescription = "Need a short description (OOPS!)." unless $bugDescription;
693 $bugURL = "Need the bug URL (OOPS!)." unless $bugURL;
694
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000695 if (!$onlyFiles) {
696 print CHANGE_LOG normalizeLineEndings(" $bugDescription\n", $endl) if $bugDescription;
697 print CHANGE_LOG normalizeLineEndings(" $bugURL\n", $endl) if $bugURL;
698 print CHANGE_LOG normalizeLineEndings(" $bugRadarURL\n", $endl) if $bugRadarURL;
699 print CHANGE_LOG normalizeLineEndings("\n", $endl);
haraken@chromium.org407dc832011-12-11 18:04:50 +0000700
commit-queue@webkit.org545bf102021-09-14 15:56:51 +0000701 print CHANGE_LOG normalizeLineEndings(" Reviewed by $reviewer.\n\n", $endl);
702 print CHANGE_LOG normalizeLineEndings($description . "\n", $endl) if $description;
703 }
haraken@chromium.org407dc832011-12-11 18:04:50 +0000704
timothy_horton@apple.com60116d02019-10-23 07:31:05 +0000705 my $shouldMentionTests = @$requiresTests;
706 $shouldMentionTests |= !$hasWebCoreChange && unixPath($prefix) =~ m|/WebKit/$|;
707 $shouldMentionTests |= unixPath($prefix) =~ m|/WebCore/$|;
708
709 if ($shouldMentionTests) {
haraken@chromium.org407dc832011-12-11 18:04:50 +0000710 if (@$addedRegressionTests) {
711 print CHANGE_LOG normalizeLineEndings(testListForChangeLog(sort @$addedRegressionTests), $endl);
712 } else {
tkent@chromium.org499cdde2012-07-10 04:35:05 +0000713 print CHANGE_LOG normalizeLineEndings(" No new tests (OOPS!).\n\n", $endl);
haraken@chromium.org407dc832011-12-11 18:04:50 +0000714 }
715 }
716
717 foreach my $file (sort @{$filesInChangeLog->{$prefix}}) {
718 my $file_stem = substr $file, length $prefix;
bfulgham@apple.com3d033d42015-09-29 21:16:41 +0000719 $file_stem = unixPath($file_stem);
haraken@chromium.org407dc832011-12-11 18:04:50 +0000720 print CHANGE_LOG normalizeLineEndings(" * $file_stem:$functionLists->{$file}\n", $endl);
721 }
722
723 if ($writeChangeLogs) {
724 print CHANGE_LOG normalizeLineEndings("\n", $endl), @old_change_log;
725 } else {
726 print CHANGE_LOG "\n";
mitz@apple.com7db6fe12014-08-01 23:10:50 +0000727 print "~\n" if $delimiters;
haraken@chromium.org407dc832011-12-11 18:04:50 +0000728 }
729
730 close CHANGE_LOG;
731 }
haraken@chromium.orgd5ffa752011-12-11 06:32:04 +0000732}
733
haraken@chromium.org5711f062011-12-16 09:21:02 +0000734sub printDiff($$$$)
haraken@chromium.orgff13ca52011-12-12 08:58:58 +0000735{
haraken@chromium.org5711f062011-12-16 09:21:02 +0000736 my ($changedFiles, $gitCommit, $gitIndex, $mergeBase) = @_;
haraken@chromium.orgff13ca52011-12-12 08:58:58 +0000737
738 print STDERR " Running diff to help you write the ChangeLog entries.\n";
739 local $/ = undef; # local slurp mode
haraken@chromium.org5711f062011-12-16 09:21:02 +0000740 my $changedFilesString = "'" . join("' '", @$changedFiles) . "'";
741 open DIFF, "-|", createPatchCommand($changedFilesString, $gitCommit, $gitIndex, $mergeBase) or die "The diff failed: $!.\n";
haraken@chromium.orgff13ca52011-12-12 08:58:58 +0000742 print <DIFF>;
743 close DIFF;
744}
745
746sub openChangeLogs($)
747{
748 my ($changeLogs) = @_;
749
750 print STDERR " Opening the edited ChangeLog files.\n";
kubo@profusion.mobi7bb214c2012-01-13 19:05:57 +0000751 my $editor = $ENV{CHANGE_LOG_EDITOR} || $ENV{VISUAL} || $ENV{EDITOR};
haraken@chromium.orgff13ca52011-12-12 08:58:58 +0000752 if ($editor) {
753 system ((split ' ', $editor), @$changeLogs);
754 } else {
755 $editor = $ENV{CHANGE_LOG_EDIT_APPLICATION};
756 if ($editor) {
757 system "open", "-a", $editor, @$changeLogs;
758 } else {
759 system "open", "-e", @$changeLogs;
760 }
761 }
762}
763
jbedard@apple.com49f09da62021-10-19 00:48:12 +0000764sub get_function_line_ranges($$)
765{
766 my ($file_handle, $file_name) = @_;
767
768 # Try to determine the source language based on the file extension.
769
770 return get_function_line_ranges_for_cpp($file_handle, $file_name) if $file_name =~ /\.(c|cpp|m|mm|h)$/;
771 return get_function_line_ranges_for_java($file_handle, $file_name) if $file_name =~ /\.java$/;
772 return get_function_line_ranges_for_javascript($file_handle, $file_name) if $file_name =~ /\.js$/;
773 return get_selector_line_ranges_for_css($file_handle, $file_name) if $file_name =~ /\.css$/;
774 return get_function_line_ranges_for_perl($file_handle, $file_name) if $file_name =~ /\.p[lm]$/;
775 return get_function_line_ranges_for_python($file_handle, $file_name) if $file_name =~ /\.py$/ or $file_name =~ /master\.cfg$/;
776 return get_function_line_ranges_for_swift($file_handle, $file_name) if $file_name =~ /\.swift$/;
777
778 # Try to determine the source language based on the script interpreter.
779
780 my $first_line = <$file_handle>;
781 seek($file_handle, 0, 0);
782
783 return () unless $first_line =~ m|^#!(?:/usr/bin/env\s+)?(\S+)|;
784 my $interpreter = $1;
785
786 return get_function_line_ranges_for_perl($file_handle, $file_name) if $interpreter =~ /perl$/;
787 return get_function_line_ranges_for_python($file_handle, $file_name) if $interpreter =~ /python$/;
788
789 return ();
790}
791
792
ddkilzerb7b3b912006-06-25 06:01:41 +0000793sub method_decl_to_selector($)
aroben38e341c2007-04-28 06:57:49 +0000794{
mjsb85f64e2005-06-17 07:49:31 +0000795 (my $method_decl) = @_;
796
797 $_ = $method_decl;
798
aroben38e341c2007-04-28 06:57:49 +0000799 if ((my $comment_stripped) = m-([^/]*)(//|/*).*-) {
thatcherbefc69c2006-01-08 04:38:04 +0000800 $_ = $comment_stripped;
aroben38e341c2007-04-28 06:57:49 +0000801 }
mjsb85f64e2005-06-17 07:49:31 +0000802
803 s/,\s*...//;
804
mitz@apple.com4f638172014-06-27 05:35:59 +0000805 # Strip out the return type and parameter types. The extra )? takes care of most block parameter types.
806 s/\([^\)]*\)\)?//g;
807
aroben38e341c2007-04-28 06:57:49 +0000808 if (/:/) {
mjsb85f64e2005-06-17 07:49:31 +0000809 my @components = split /:/;
thatcherbefc69c2006-01-08 04:38:04 +0000810 pop @components if (scalar @components > 1);
811 $_ = (join ':', map {s/.*[^[:word:]]//; scalar $_;} @components) . ':';
aroben38e341c2007-04-28 06:57:49 +0000812 } else {
mjsb85f64e2005-06-17 07:49:31 +0000813 s/\s*$//;
thatcherbefc69c2006-01-08 04:38:04 +0000814 s/.*[^[:word:]]//;
aroben38e341c2007-04-28 06:57:49 +0000815 }
mjsb85f64e2005-06-17 07:49:31 +0000816
817 return $_;
aroben38e341c2007-04-28 06:57:49 +0000818}
mjsb85f64e2005-06-17 07:49:31 +0000819
jbedard@apple.com49f09da62021-10-19 00:48:12 +0000820
821
822# Read a file and get all the line ranges of the things that look like C functions.
823# A function name is the last word before an open parenthesis before the outer
824# level open brace. A function starts at the first character after the last close
825# brace or semicolon before the function name and ends at the close brace.
826# Comment handling is simple-minded but will work for all but pathological cases.
827#
828# Result is a list of triples: [ start_line, end_line, function_name ].
829
830sub get_function_line_ranges_for_cpp($$)
831{
832 my ($file_handle, $file_name) = @_;
833
834 my @ranges;
835
836 my $in_comment = 0;
837 my $in_macro = 0;
838 my $in_method_declaration = 0;
839 my $in_parentheses = 0;
840 my $quotation_mark;
841 my $in_braces = 0;
842 my $in_toplevel_array_brace = 0;
843 my $brace_start = 0;
844 my $brace_end = 0;
845 my $namespace_start = -1;
846 my $skip_til_brace_or_semicolon = 0;
847 my $equal_observed = 0;
848
849 my $word = "";
850 my $interface_name = "";
851
852 my $potential_method_char = "";
853 my $potential_method_spec = "";
854
855 my $potential_start = 0;
856 my $potential_name = "";
857
858 my $start = 0;
859 my $name = "";
860
861 my $next_word_could_be_namespace = 0;
862 my $potential_namespace = "";
863 my @namespaces;
864 my @all_namespaces;
865
866 while (<$file_handle>) {
867 # Handle continued quoted string.
868 if ($quotation_mark) {
869 if (!s-([^\\]|\\.)*$quotation_mark--) {
870 if (!m-\\$-) {
871 warn "mismatched quotes at line $. in $file_name\n";
872 undef $quotation_mark;
873 }
874 next;
875 }
876 undef $quotation_mark;
877 }
878
879 # Handle continued multi-line comment.
880 if ($in_comment) {
881 next unless s-.*\*/--;
882 $in_comment = 0;
883 }
884
885 # Handle continued macro.
886 if ($in_macro) {
887 $in_macro = 0 unless /\\$/;
888 next;
889 }
890
891 # Handle start of macro (or any preprocessor directive).
892 if (/^\s*\#/) {
893 $in_macro = 1 if /^([^\\]|\\.)*\\$/;
894 next;
895 }
896
897 # Handle comments and quoted text.
898 while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
899 my $match = $1;
900 if ($match eq "/*") {
901 if (!s-/\*.*?\*/--) {
902 s-/\*.*--;
903 $in_comment = 1;
904 }
905 } elsif ($match eq "//") {
906 s-//.*--;
907 } else { # ' or "
908 if (!s-$match([^\\]|\\.)*?$match--) {
909 if (!s-$match.*\\$--) {
910 warn "mismatched quotes at line $. in $file_name\n";
911 s-$match.*--;
912 } else {
913 $quotation_mark = $match;
914 }
915 }
916 }
917 }
918
919
920 # continued method declaration
921 if ($in_method_declaration) {
922 my $original = $_;
923 my $method_cont = $_;
924
925 chomp $method_cont;
926 $method_cont =~ s/[;\{].*//;
927 $potential_method_spec = "${potential_method_spec} ${method_cont}";
928
929 $_ = $original;
930 if (/;/) {
931 $potential_start = 0;
932 $potential_method_spec = "";
933 $potential_method_char = "";
934 $in_method_declaration = 0;
935 s/^[^;\{]*//;
936 } elsif (/{/) {
937 my $selector = method_decl_to_selector ($potential_method_spec);
938 $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
939
940 $potential_method_spec = "";
941 $potential_method_char = "";
942 $in_method_declaration = 0;
943
944 $_ = $original;
945 s/^[^;{]*//;
946 } elsif (/\@end/) {
947 $in_method_declaration = 0;
948 $interface_name = "";
949 $_ = $original;
950 } else {
951 next;
952 }
953 }
954
955
956 # start of method declaration
957 if ((my $method_char, my $method_spec) = m&^([-+])([^0-9;][^;]*);?$&) {
958 my $original = $_;
959
960 if ($interface_name) {
961 chomp $method_spec;
962 $method_spec =~ s/\{.*//;
963
964 $potential_method_char = $method_char;
965 $potential_method_spec = $method_spec;
966 $potential_start = $.;
967 $in_method_declaration = 1;
968 } else {
969 warn "declaring a method but don't have interface on line $. in $file_name\n";
970 }
971 $_ = $original;
972 if (/\{/) {
973 my $selector = method_decl_to_selector ($potential_method_spec);
974 $potential_name = "${potential_method_char}\[${interface_name} ${selector}\]";
975
976 $potential_method_spec = "";
977 $potential_method_char = "";
978 $in_method_declaration = 0;
979 $_ = $original;
980 s/^[^{]*//;
981 } elsif (/\@end/) {
982 $in_method_declaration = 0;
983 $interface_name = "";
984 $_ = $original;
985 } else {
986 next;
987 }
988 }
989
990
991 # Find function, interface and method names.
992 while (m&((?:[[:word:]]+::)*operator(?:[ \t]*\(\)|[^()]*)|[[:word:]<>:~]+|[(){}:;=])|\@(?:implementation|interface|protocol)\s+(\w+)[^{]*&g) {
993 # Skip an array definition at the top level.
994 # e.g. static int arr[] = { 1, 2, 3 };
995 if ($1) {
996 if ($1 eq "=" and !$in_parentheses and !$in_braces) {
997 $equal_observed = 1;
998 } elsif ($1 eq "{" and $equal_observed) {
999 # This '{' is the beginning of an array definition, not the beginning of a method.
1000 $in_toplevel_array_brace = 1;
1001 $in_braces++;
1002 $equal_observed = 0;
1003 next;
1004 } elsif ($1 !~ /[ \t]/) {
1005 $equal_observed = 0;
1006 }
1007 }
1008
1009 # interface name
1010 if ($2) {
1011 $interface_name = $2;
1012 next;
1013 }
1014
1015 # Open parenthesis.
1016 if ($1 eq "(") {
1017 $potential_name = $word unless $in_parentheses || $skip_til_brace_or_semicolon || grep { $word eq $_ } ("CF_ENUM", "CF_OPTIONS", "NS_ENUM", "NS_OPTIONS");
1018 $in_parentheses++;
1019 next;
1020 }
1021
1022 # Close parenthesis.
1023 if ($1 eq ")") {
1024 $in_parentheses--;
1025 next;
1026 }
1027
1028 if ($1 eq "const" and !$in_parentheses) {
1029 $potential_name .= " const";
1030 next;
1031 }
1032
1033 if ($1 eq "volatile" and !$in_parentheses) {
1034 $potential_name .= " volatile";
1035 next;
1036 }
1037
1038 # C++ auto function() -> type
1039 if ($1 eq ">") {
1040 $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
1041 next;
1042 }
1043
1044 # C++ constructor initializers
1045 if ($1 eq ":") {
1046 $skip_til_brace_or_semicolon = 1 unless ($in_parentheses || $in_braces);
1047 }
1048
1049 # Open brace.
1050 if ($1 eq "{") {
1051 $skip_til_brace_or_semicolon = 0;
1052
1053 if (!$in_braces) {
1054 if ($namespace_start >= 0 and $namespace_start < $potential_start) {
1055 push @ranges, [ $namespace_start . "", $potential_start - 1, $name ];
1056 }
1057
1058 if ($potential_namespace) {
1059 push @namespaces, $potential_namespace;
1060 push @all_namespaces, $potential_namespace;
1061 $potential_namespace = "";
1062 $name = $namespaces[-1];
1063 $namespace_start = $. + 1;
1064 next;
1065 }
1066
1067 # Promote potential name to real function name at the
1068 # start of the outer level set of braces (function body?).
1069 if ($potential_start) {
1070 $start = $potential_start;
1071 $name = $potential_name;
1072 if (@namespaces && $name && (length($name) < 2 || substr($name,1,1) ne "[")) {
1073 $name = join ('::', @namespaces, $name);
1074 }
1075 }
1076 }
1077
1078 $in_method_declaration = 0;
1079
1080 $brace_start = $. if (!$in_braces);
1081 $in_braces++;
1082 next;
1083 }
1084
1085 # Close brace.
1086 if ($1 eq "}") {
1087 if (!$in_braces && @namespaces) {
1088 if ($namespace_start >= 0 and $namespace_start < $.) {
1089 push @ranges, [ $namespace_start . "", $. - 1, $name ];
1090 }
1091
1092 pop @namespaces;
1093 if (@namespaces) {
1094 $name = $namespaces[-1];
1095 $namespace_start = $. + 1;
1096 } else {
1097 $name = "";
1098 $namespace_start = -1;
1099 }
1100 next;
1101 }
1102
1103 $in_braces--;
1104 $brace_end = $. if (!$in_braces);
1105
1106 # End of an outer level set of braces.
1107 # This could be a function body.
1108 if (!$in_braces and $name) {
1109 # This is the end of an array definition at the top level, not the end of a method.
1110 if ($in_toplevel_array_brace) {
1111 $in_toplevel_array_brace = 0;
1112 next;
1113 }
1114
1115 push @ranges, [ $start, $., $name ];
1116 if (@namespaces) {
1117 $name = $namespaces[-1];
1118 $namespace_start = $. + 1;
1119 } else {
1120 $name = "";
1121 $namespace_start = -1;
1122 }
1123 }
1124
1125 $potential_start = 0;
1126 $potential_name = "";
1127 next;
1128 }
1129
1130 # Semicolon.
1131 if ($1 eq ";") {
1132 $skip_til_brace_or_semicolon = 0;
1133 $potential_start = 0;
1134 $potential_name = "";
1135 $in_method_declaration = 0;
1136 next;
1137 }
1138
1139 # Ignore "const" method qualifier.
1140 if ($1 eq "const") {
1141 next;
1142 }
1143
1144 if ($1 eq "namespace" || $1 eq "class" || $1 eq "struct") {
1145 $next_word_could_be_namespace = 1;
1146 next;
1147 }
1148
1149 # Word.
1150 $word = $1;
1151 if (!$skip_til_brace_or_semicolon) {
1152 if ($next_word_could_be_namespace) {
1153 $potential_namespace = $word;
1154 $next_word_could_be_namespace = 0;
1155 } elsif ($potential_namespace) {
1156 $potential_namespace = "";
1157 }
1158
1159 if (!$in_parentheses) {
1160 $potential_start = 0;
1161 $potential_name = "";
1162 }
1163 if (!$potential_start) {
1164 $potential_start = $.;
1165 $potential_name = "";
1166 }
1167 }
1168 }
1169 }
1170
1171 warn "missing close braces in $file_name (probable start at $brace_start)\n" if ($in_braces > 0);
1172 warn "too many close braces in $file_name (probable start at $brace_end)\n" if ($in_braces < 0);
1173
1174 warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1175
1176 return delete_namespaces_from_ranges_for_cpp(@ranges, @all_namespaces);
1177}
1178
1179
1180# Take in references to an array of line ranges for C functions in a given file
1181# and an array of namespaces declared in that file and return an updated
1182# list of line ranges with the namespaces removed.
1183
1184sub delete_namespaces_from_ranges_for_cpp(\@\@)
1185{
1186 my ($ranges, $namespaces) = @_;
1187 return grep {!is_function_in_namespace($namespaces, $$_[2])} @$ranges;
1188}
1189
1190
1191sub is_function_in_namespace($$)
1192{
1193 my ($namespaces, $function_name) = @_;
1194 return grep {$_ eq $function_name} @$namespaces;
1195}
1196
1197
1198# Read a file and get all the line ranges of the things that look like Java
1199# classes, interfaces and methods.
1200#
1201# A class or interface name is the word that immediately follows
1202# `class' or `interface' when followed by an open curly brace and not
1203# a semicolon. It can appear at the top level, or inside another class
1204# or interface block, but not inside a function block
1205#
1206# A class or interface starts at the first character after the first close
1207# brace or after the function name and ends at the close brace.
1208#
1209# A function name is the last word before an open parenthesis before
1210# an open brace rather than a semicolon. It can appear at top level or
1211# inside a class or interface block, but not inside a function block.
1212#
1213# A function starts at the first character after the first close
1214# brace or after the function name and ends at the close brace.
1215#
1216# Comment handling is simple-minded but will work for all but pathological cases.
1217#
1218# Result is a list of triples: [ start_line, end_line, function_name ].
1219
1220sub get_function_line_ranges_for_java($$)
1221{
1222 my ($file_handle, $file_name) = @_;
1223
1224 my @current_scopes;
1225
1226 my @ranges;
1227
1228 my $in_comment = 0;
1229 my $in_macro = 0;
1230 my $in_parentheses = 0;
1231 my $in_braces = 0;
1232 my $in_non_block_braces = 0;
1233 my $class_or_interface_just_seen = 0;
1234 my $in_class_declaration = 0;
1235
1236 my $word = "";
1237
1238 my $potential_start = 0;
1239 my $potential_name = "";
1240 my $potential_name_is_class_or_interface = 0;
1241
1242 my $start = 0;
1243 my $name = "";
1244 my $current_name_is_class_or_interface = 0;
1245
1246 while (<$file_handle>) {
1247 # Handle continued multi-line comment.
1248 if ($in_comment) {
1249 next unless s-.*\*/--;
1250 $in_comment = 0;
1251 }
1252
1253 # Handle continued macro.
1254 if ($in_macro) {
1255 $in_macro = 0 unless /\\$/;
1256 next;
1257 }
1258
1259 # Handle start of macro (or any preprocessor directive).
1260 if (/^\s*\#/) {
1261 $in_macro = 1 if /^([^\\]|\\.)*\\$/;
1262 next;
1263 }
1264
1265 # Handle comments and quoted text.
1266 while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1267 my $match = $1;
1268 if ($match eq "/*") {
1269 if (!s-/\*.*?\*/--) {
1270 s-/\*.*--;
1271 $in_comment = 1;
1272 }
1273 } elsif ($match eq "//") {
1274 s-//.*--;
1275 } else { # ' or "
1276 if (!s-$match([^\\]|\\.)*?$match--) {
1277 warn "mismatched quotes at line $. in $file_name\n";
1278 s-$match.*--;
1279 }
1280 }
1281 }
1282
1283 # Find function names.
1284 while (m-(\w+|[(){};])-g) {
1285 # Open parenthesis.
1286 if ($1 eq "(") {
1287 if (!$in_parentheses) {
1288 $potential_name = $word;
1289 $potential_name_is_class_or_interface = 0;
1290 }
1291 $in_parentheses++;
1292 next;
1293 }
1294
1295 # Close parenthesis.
1296 if ($1 eq ")") {
1297 $in_parentheses--;
1298 next;
1299 }
1300
1301 # Open brace.
1302 if ($1 eq "{") {
1303 $in_class_declaration = 0;
1304
1305 # Promote potential name to real function name at the
1306 # start of the outer level set of braces (function/class/interface body?).
1307 if (!$in_non_block_braces
1308 and (!$in_braces or $current_name_is_class_or_interface)
1309 and $potential_start) {
1310 if ($name) {
1311 push @ranges, [ $start, ($. - 1),
1312 join ('.', @current_scopes) ];
1313 }
1314
1315
1316 $current_name_is_class_or_interface = $potential_name_is_class_or_interface;
1317
1318 $start = $potential_start;
1319 $name = $potential_name;
1320
1321 push (@current_scopes, $name);
1322 } else {
1323 $in_non_block_braces++;
1324 }
1325
1326 $potential_name = "";
1327 $potential_start = 0;
1328
1329 $in_braces++;
1330 next;
1331 }
1332
1333 # Close brace.
1334 if ($1 eq "}") {
1335 $in_braces--;
1336
1337 # End of an outer level set of braces.
1338 # This could be a function body.
1339 if (!$in_non_block_braces) {
1340 if ($name) {
1341 push @ranges, [ $start, $.,
1342 join ('.', @current_scopes) ];
1343
1344 pop (@current_scopes);
1345
1346 if (@current_scopes) {
1347 $current_name_is_class_or_interface = 1;
1348
1349 $start = $. + 1;
1350 $name = $current_scopes[$#current_scopes-1];
1351 } else {
1352 $current_name_is_class_or_interface = 0;
1353 $start = 0;
1354 $name = "";
1355 }
1356 }
1357 } else {
1358 $in_non_block_braces-- if $in_non_block_braces;
1359 }
1360
1361 $potential_start = 0;
1362 $potential_name = "";
1363 next;
1364 }
1365
1366 # Semicolon.
1367 if ($1 eq ";") {
1368 $potential_start = 0;
1369 $potential_name = "";
1370 next;
1371 }
1372
1373 if ($1 eq "class") {
1374 $in_class_declaration = 1;
1375 }
1376 if ($1 eq "class" or (!$in_class_declaration and $1 eq "interface")) {
1377 $class_or_interface_just_seen = 1;
1378 next;
1379 }
1380
1381 # Word.
1382 $word = $1;
1383 if (!$in_parentheses) {
1384 if ($class_or_interface_just_seen) {
1385 $potential_name = $word;
1386 $potential_start = $.;
1387 $class_or_interface_just_seen = 0;
1388 $potential_name_is_class_or_interface = 1;
1389 next;
1390 }
1391 }
1392 if (!$potential_start) {
1393 $potential_start = $.;
1394 $potential_name = "";
1395 }
1396 $class_or_interface_just_seen = 0;
1397 }
1398 }
1399
1400 warn "mismatched braces in $file_name\n" if $in_braces;
1401 warn "mismatched parentheses in $file_name\n" if $in_parentheses;
1402
1403 return @ranges;
1404}
1405
1406
1407
1408# Read a file and get all the line ranges of the things that look like
1409# JavaScript functions or methods.
1410#
1411# A function name is the word that immediately follows `function' when
1412# followed by an open curly brace. It can appear at the top level,
1413# or inside other functions. For example:
1414#
1415# function name() { // (name)
1416# function inner() { } // (name.inner)
1417# }
1418#
1419# An anonymous function name is the identifier on the left hand side of
1420# an assignment with the equals operator or object notation that has a
1421# value starting with `function' followed an open curly brace.
1422# For example:
1423#
1424# namespace = {
1425# name: function() {} // (namespace.name)
1426# }
1427# namespace.Foo = function() {} // (namespace.Foo)
1428#
1429# A getter or setter name is the word that immediately follows `get' or
1430# `set' when followed by params and an open curly brace. For example:
1431#
1432# namespace = {
1433# get foo() {} // (namespace.get foo)
1434# }
1435#
1436# A method name is the word immediately before parenthesis, with an open
1437# curly brace immediately following closing parenthesis. For a class expression
1438# we take the assignment identifier instead of the class name for namespacing.
1439#
1440# namespace.Foo = class DoesNotMatter extends Bar {
1441# constructor() {} // (namespace.Foo)
1442# static staticMethod() {} // (namespace.Foo.staticMethod)
1443# instanceMethod() {} // (namespace.Foo.prototype.instanceMethod)
1444# get getter() {} // (namespace.Foo.prototype.get getter)
1445# }
1446# class ClassName {
1447# constructor() {} // (ClassName)
1448# method() {} // (ClassName.prototype.method)
1449# }
1450#
1451# Methods may exist in object literals, outside of classes.
1452#
1453# Foo.prototype = {
1454# method() {}, // (Foo.prototype.method)
1455# otherMethod() {} // (Foo.prototype.otherMethod)
1456# }
1457#
1458# Comment handling is simple-minded but will work for all but pathological cases.
1459#
1460# Result is a list of triples: [ start_line, end_line, function_name ].
1461
1462sub get_function_line_ranges_for_javascript($$)
1463{
1464 my ($fileHandle, $fileName) = @_;
1465
1466 my @currentScopes;
1467 my @currentIdentifiers;
1468 my @currentParsingMode = ("global");
1469 my @currentFunctionNames;
1470 my @currentFunctionDepths;
1471 my @currentFunctionStartLines;
1472
1473 my @ranges;
1474
1475 my $inComment = 0;
1476 my $inQuotedText = "";
1477 my $inExtends = 0;
1478 my $inMethod = 0;
1479 my $inAnonymousFunctionParameters = 0;
1480 my $parenthesesDepth = 0;
1481 my $globalParenthesesDepth = 0;
1482 my $bracesDepth = 0;
1483
1484 my $classJustSeen = 0;
1485 my $parenthesisJustSeen = 0;
1486 my $functionJustSeen = 0;
1487 my $getterJustSeen = 0;
1488 my $setterJustSeen = 0;
1489 my $asyncJustSeen = 0;
1490 my $assignmentJustSeen = 0;
1491 my $staticOrContructorSeen = 0;
1492
1493 my $currentToken = "";
1494 my $lastToken = "";
1495 my $possibleMethodName = "";
1496 my $word = "";
1497
1498 while (<$fileHandle>) {
1499 # Handle continued multi-line comment.
1500 if ($inComment) {
1501 next unless s-.*\*/--;
1502 $inComment = 0;
1503 }
1504
1505 # Handle continued quoted text.
1506 if ($inQuotedText ne "") {
1507 next if /\\$/;
1508 s-([^\\]|\\.)*?$inQuotedText--;
1509 $inQuotedText = "";
1510 }
1511
1512 # Handle comments and quoted text.
1513 while (m-(/\*|//|\'|\")-) { # \' and \" keep emacs perl mode happy
1514 my $match = $1;
1515 if ($match eq '/*') {
1516 if (!s-/\*.*?\*/--) {
1517 s-/\*.*--;
1518 $inComment = 1;
1519 }
1520 } elsif ($match eq '//') {
1521 s-//.*--;
1522 } else { # ' or "
1523 if (!s-$match([^\\]|\\.)*?$match-string_appeared_here-) {
1524 $inQuotedText = $match if /\\$/;
1525 warn "mismatched quotes at line $. in $fileName\n" if $inQuotedText eq "";
1526 s-$match.*--;
1527 }
1528 }
1529 }
1530
1531 # Find function names.
1532 while (m-(\w+|[(){}=:;,.])-g) {
1533 # Skip everything until "{" after extends.
1534 if ($inExtends) {
1535 next if $1 ne '{';
1536 $inExtends = 0;
1537 }
1538
1539 $lastToken = $currentToken;
1540 $currentToken = $1;
1541
1542 # Open parenthesis.
1543 if ($1 eq '(') {
1544 $parenthesesDepth++;
1545 $globalParenthesesDepth++ if $currentParsingMode[$#currentParsingMode] eq "global";
1546 $possibleMethodName = join('.', @currentIdentifiers);
1547 $inAnonymousFunctionParameters = 1 if $functionJustSeen;
1548 $functionJustSeen = 0;
1549 next;
1550 }
1551
1552 # Close parenthesis.
1553 if ($1 eq ')') {
1554 $parenthesesDepth--;
1555 $globalParenthesesDepth-- if $currentParsingMode[$#currentParsingMode] eq "global";
1556 @currentIdentifiers = () if $inAnonymousFunctionParameters;
1557 $inAnonymousFunctionParameters = 0;
1558 $parenthesisJustSeen = 1;
1559 next;
1560 }
1561
1562 # Open brace.
1563 if ($1 eq '{') {
1564 my $methodName = "";
1565 my $mode = $currentParsingMode[$#currentParsingMode];
1566
1567 # Method.
1568 if (($mode eq 'class' or $mode eq 'global') and $parenthesisJustSeen and ($staticOrContructorSeen or $possibleMethodName)) {
1569 if ($mode eq 'class') {
1570 $methodName = join('.', $staticOrContructorSeen ? "" : "prototype", $possibleMethodName);
1571 } else {
1572 $methodName = $possibleMethodName;
1573 }
1574
1575 $methodName =~ s/\.{2,}/\./g; # Removes consecutive periods.
1576 $methodName =~ s/\.$//; # Remove trailing period.
1577
1578 my $currentMethod = join('.', @currentScopes, $methodName);
1579 $currentMethod =~ s/\.{2,}/\./g; # Removes consecutive periods.
1580 $currentMethod =~ s/\.$//; # Remove trailing period.
1581
1582 push(@currentParsingMode, "method");
1583 push(@currentFunctionNames, $currentMethod);
1584 push(@currentFunctionDepths, $bracesDepth);
1585 push(@currentFunctionStartLines, $.);
1586 }
1587
1588 $bracesDepth++;
1589 $functionJustSeen = 0;
1590
1591 push(@currentScopes, join('.', $methodName ? $methodName : @currentIdentifiers));
1592 @currentIdentifiers = ();
1593
1594 $staticOrContructorSeen = 0;
1595 next;
1596 }
1597
1598 # Close brace.
1599 if ($1 eq '}') {
1600 $bracesDepth--;
1601 $functionJustSeen = 0;
1602
1603 if (@currentFunctionDepths and $bracesDepth == $currentFunctionDepths[$#currentFunctionDepths]) {
1604 pop(@currentFunctionDepths);
1605 pop(@currentParsingMode);
1606
1607 my $currentName = pop(@currentFunctionNames);
1608 my $start = pop(@currentFunctionStartLines);
1609
1610 $currentName =~ s/^\.//g; # Removes leading periods.
1611
1612 push(@ranges, [$start, $., $currentName]);
1613 }
1614
1615 pop(@currentScopes);
1616 @currentIdentifiers = ();
1617
1618 next;
1619 }
1620
1621 # Dot.
1622 if ($1 eq '.') {
1623 next;
1624 }
1625
1626 # Semicolon or comma.
1627 if ($1 eq ';' or $1 eq ',') {
1628 @currentIdentifiers = ();
1629 next;
1630 }
1631
1632 # Class.
1633 if ($1 eq 'class') {
1634 $classJustSeen = 1;
1635 next;
1636 }
1637
1638 # Extends.
1639 if ($1 eq 'extends') {
1640 $inExtends = 1;
1641 next;
1642 }
1643
1644 # Function.
1645 if ($1 eq 'function') {
1646 $functionJustSeen = 1;
1647
1648 if ($assignmentJustSeen) {
1649 my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1650 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1651
1652 push(@currentParsingMode, "function");
1653 push(@currentFunctionNames, $currentFunction);
1654 push(@currentFunctionDepths, $bracesDepth);
1655 push(@currentFunctionStartLines, $.);
1656 }
1657
1658 next;
1659 }
1660
1661 # Getter prefix.
1662 if ($1 eq 'get') {
1663 next if $lastToken eq '.'; # Avoid map.get(...).
1664 $getterJustSeen = 1;
1665 next;
1666 }
1667
1668 # Setter prefix.
1669 if ($1 eq 'set') {
1670 next if $lastToken eq '.'; # Avoid map.set(...).
1671 $setterJustSeen = 1;
1672 next;
1673 }
1674
1675 # Async prefix.
1676 if ($1 eq 'async') {
1677 next if $lastToken eq '.';
1678 $asyncJustSeen = 1;
1679 next;
1680 }
1681
1682 # Static.
1683 if ($1 eq 'static' or $1 eq 'constructor') {
1684 $staticOrContructorSeen = 1;
1685 next;
1686 }
1687
1688 # Assignment operator.
1689 if ($1 eq '=' or $1 eq ':') {
1690 $assignmentJustSeen = 1;
1691 next;
1692 }
1693
1694 next if $parenthesesDepth > $globalParenthesesDepth;
1695
1696 # Word.
1697 $word = $1;
1698
1699 if ($classJustSeen) {
1700 push(@currentIdentifiers, $word) if !$assignmentJustSeen;
1701
1702 my $currentClass = join('.', (@currentScopes, @currentIdentifiers));
1703 $currentClass =~ s/\.{2,}/\./g; # Removes consecutive periods.
1704
1705 push(@currentParsingMode, "class");
1706 push(@currentFunctionNames, $currentClass);
1707 push(@currentFunctionDepths, $bracesDepth);
1708 push(@currentFunctionStartLines, $.);
1709 } elsif ($getterJustSeen or $setterJustSeen or $asyncJustSeen) {
1710 $word = "get $word" if $getterJustSeen;
1711 $word = "set $word" if $setterJustSeen;
1712 $word = "async $word" if $asyncJustSeen;
1713
1714 push(@currentIdentifiers, $word);
1715
1716 my $mode = $currentParsingMode[$#currentParsingMode];
1717 my $currentFunction = join('.', (@currentScopes, ($mode eq 'class' and !$staticOrContructorSeen) ? "prototype" : "", @currentIdentifiers));
1718 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1719
1720 push(@currentParsingMode, "function");
1721 push(@currentFunctionNames, $currentFunction);
1722 push(@currentFunctionDepths, $bracesDepth);
1723 push(@currentFunctionStartLines, $.);
1724 } elsif ($functionJustSeen and !$assignmentJustSeen) {
1725 push(@currentIdentifiers, $word);
1726
1727 my $currentFunction = join('.', (@currentScopes, @currentIdentifiers));
1728 $currentFunction =~ s/\.{2,}/\./g; # Removes consecutive periods.
1729
1730 push(@currentParsingMode, "function");
1731 push(@currentFunctionNames, $currentFunction);
1732 push(@currentFunctionDepths, $bracesDepth);
1733 push(@currentFunctionStartLines, $.);
1734 } elsif ($word ne 'if' and $word ne 'for' and $word ne 'do' and $word ne 'while' and $word ne 'which' and $word ne 'var') {
1735 push(@currentIdentifiers, $word);
1736 }
1737
1738 $classJustSeen = 0;
1739 $parenthesisJustSeen = 0;
1740 $functionJustSeen = 0;
1741 $getterJustSeen = 0;
1742 $setterJustSeen = 0;
1743 $asyncJustSeen = 0;
1744 $assignmentJustSeen = 0;
1745 }
1746 }
1747
1748 warn "mismatched braces in $fileName\n" if $bracesDepth;
1749 warn "mismatched parentheses in $fileName\n" if $parenthesesDepth;
1750
1751 return @ranges;
1752}
1753
1754# Read a file and get all the line ranges of the things that look like Perl functions. Functions
1755# start on a line that starts with "sub ", and end on the first line starting with "}" thereafter.
1756#
1757# Result is a list of triples: [ start_line, end_line, function ].
1758
1759sub get_function_line_ranges_for_perl($$)
1760{
1761 my ($fileHandle, $fileName) = @_;
1762
1763 my @ranges;
1764
1765 my $currentFunction = "";
1766 my $start = 0;
1767 my $hereDocumentIdentifier = "";
1768
1769 while (<$fileHandle>) {
1770 chomp;
1771 if (!$hereDocumentIdentifier) {
1772 if (/^sub\s+([\w_][\w\d_]*)/) {
1773 # Skip over forward declarations, which don't contain a brace and end with a semicolon.
1774 next if /;\s*$/;
1775
1776 if ($currentFunction) {
1777 warn "nested functions found at top-level at $fileName:$.\n";
1778 next;
1779 }
1780 $currentFunction = $1;
1781 $start = $.;
1782 }
1783 if (/<<\s*[\"\']?([\w_][\w_\d]*)/) {
1784 # Enter here-document.
1785 $hereDocumentIdentifier = $1;
1786 }
1787 if (index($_, "}") == 0) {
1788 next unless $start;
1789 push(@ranges, [$start, $., $currentFunction]);
1790 $currentFunction = "";
1791 $start = 0;
1792 }
1793 } elsif ($_ eq $hereDocumentIdentifier) {
1794 # Escape from here-document.
1795 $hereDocumentIdentifier = "";
1796 }
1797 }
1798
1799 return @ranges;
1800}
1801
1802# Read a file and get all the line ranges of the things that look like Python classes, methods, or functions.
1803#
1804# FIXME: Maybe we should use Python's ast module to do the parsing for us?
1805#
1806# Result is a list of triples: [ start_line, end_line, function ].
1807
1808sub get_function_line_ranges_for_python($$)
1809{
1810 my ($fileHandle, $fileName) = @_;
1811
1812 my @ranges;
1813
1814 my $multilineStringLiteralSentinelRegEx = qr#(?:"""|''')#;
1815 my $multilineStringLiteralStartRegEx = qr#\s*$multilineStringLiteralSentinelRegEx#;
1816 my $multilineStringLiteralEndRegEx = qr#$multilineStringLiteralSentinelRegEx\s*$#;
1817
1818 my @scopeStack = ({ line => 0, indent => -1, name => undef });
1819 my $lastLine = 0;
1820 my $inComment = 0;
1821 until ($lastLine) {
1822 $_ = <$fileHandle>;
1823 unless ($_) {
1824 # To pop out all popped scopes, run the loop once more after
1825 # we encountered the end of the file.
1826 $_ = "pass\n";
1827 $.++;
1828 $lastLine = 1;
1829 }
1830 chomp;
1831 next unless /^(\s*)([^#].*)$/; # Skip non-indented lines that begin with a comment.
1832
1833 my $indent = length $1;
1834 my $rest = $2;
1835 my $scope = $scopeStack[-1];
1836
1837 if ($indent <= $scope->{indent}) {
1838 # Find all the scopes that we have just exited.
1839 my $i = 0;
1840 for (; $i < @scopeStack; ++$i) {
1841 last if $indent <= $scopeStack[$i]->{indent};
1842 }
1843 my @poppedScopes = splice @scopeStack, $i;
1844
1845 # For each scope that was just exited, add a range that goes from the start of that
1846 # scope to the start of the next nested scope, or to the line just before this one for
1847 # the innermost scope.
1848 for ($i = 0; $i < @poppedScopes; ++$i) {
1849 my $lineAfterEnd = $i + 1 == @poppedScopes ? $. : $poppedScopes[$i + 1]->{line};
1850 push @ranges, [$poppedScopes[$i]->{line}, $lineAfterEnd - 1, $poppedScopes[$i]->{name}];
1851 }
1852 @scopeStack or warn "Popped off last scope at $fileName:$.\n";
1853
1854 # Set the now-current scope to start at the current line. Any lines within this scope
1855 # before this point should already have been added to @ranges.
1856 $scope = $scopeStack[-1];
1857 $scope->{line} = $.;
1858 }
1859
1860 # Skip multi-line string literals and docstrings.
1861 next if /$multilineStringLiteralStartRegEx.*$multilineStringLiteralEndRegEx/;
1862 if (!$inComment && /$multilineStringLiteralStartRegEx/) {
1863 $inComment = 1;
1864 } elsif ($inComment && /$multilineStringLiteralEndRegEx/) {
1865 $inComment = 0;
1866 }
1867 next if $inComment;
1868
1869 next if /^\s*[#'"]/; # Skip indented and non-indented lines that begin with a comment or string literal (includes docstrings).
1870
1871 next unless $rest =~ /(?:class|def)\s+(\w+)/;
1872 my $name = $1;
1873 my $fullName = $scope->{name} ? join('.', $scope->{name}, $name) : $name;
1874 push @scopeStack, { line => $., indent => $indent, name => $fullName };
1875
1876 if ($scope->{indent} >= 0) {
1877 push @ranges, [$scope->{line}, $. - 1, $scope->{name}];
1878 }
1879 }
1880
1881 return @ranges;
1882}
1883
1884# Read a file and get all the line ranges of the things that look like CSS selectors. A selector is
1885# anything before an opening brace on a line. A selector starts at the line containing the opening
1886# brace and ends at the closing brace.
1887#
1888# Result is a list of triples: [ start_line, end_line, selector ].
1889
1890sub get_selector_line_ranges_for_css($$)
1891{
1892 my ($fileHandle, $fileName) = @_;
1893
1894 my @ranges;
1895
1896 my $inComment = 0;
1897 my $inBrace = 0;
1898 my @stack;
1899 my $context;
1900 my @currentParseMode = ("global");
1901 my @groupingStack;
1902 my $selectorBraces = 0;
1903
1904 while (<$fileHandle>) {
1905 foreach my $token (split m-(\{|\}|/\*|\*/)-, $_) {
1906 if ($token eq "{") {
1907 if (!$inComment) {
1908 $inBrace += 1;
1909 $selectorBraces += 1 if $currentParseMode[$#currentParseMode] eq "selector";
1910 warn "mismatched opening brace found in $fileName:$.\n" if $selectorBraces > 1;
1911 }
1912 } elsif ($token eq "}") {
1913 if (!$inComment) {
1914 if (!$inBrace or $currentParseMode[$#currentParseMode] eq "global") {
1915 warn "mismatched closing brace found in $fileName:$.\n";
1916 next;
1917 }
1918
1919 $inBrace -= 1;
1920
1921 my $parseMode = pop(@currentParseMode);
1922 if ($parseMode eq "selector") {
1923 my $name = pop(@stack);
1924 my $startLine = pop(@stack);
1925 my $endLine = $.;
1926 my $groupingPrefix = join(" ", @groupingStack);
1927 if (length $groupingPrefix) {
1928 $groupingPrefix .= " "
1929 }
1930 push(@ranges, [$startLine, $endLine, $groupingPrefix . $name]);
1931 } elsif ($parseMode eq "media") {
1932 pop(@groupingStack);
1933 }
1934
1935 $selectorBraces = 0;
1936 }
1937 } elsif ($token eq "/*") {
1938 $inComment = 1;
1939 } elsif ($token eq "*/") {
1940 warn "mismatched comment found in $fileName:$.\n" if !$inComment;
1941 $inComment = 0;
1942 } else {
1943 if (!$inComment and $currentParseMode[$#currentParseMode] ne "selector" and $token !~ /^[\s\t]*$/) {
1944 $token =~ s/^[\s\t]*|[\s\t]*$//g;
1945 my $startLine = $.;
1946 if ($token =~ /^\@media/) {
1947 push(@currentParseMode, "media");
1948 push(@groupingStack, $token);
1949 } else {
1950 push(@currentParseMode, "selector");
1951 push(@stack, ($startLine, $token));
1952 }
1953 }
1954 }
1955 }
1956 }
1957
1958 # Sort by start line.
1959 return sort {$a->[0] <=> $b->[0]} @ranges;
1960}
1961
1962# Read a file and get all the line ranges of the things that look like Swift classes, methods,
1963# or functions.
1964#
1965# Result is a list of triples: [ start_line, end_line, function ].
1966
1967sub get_function_line_ranges_for_swift($$)
1968{
1969 my ($fileHandle, $fileName) = @_;
1970
1971 my @ranges;
1972
1973 my $currentFunction = "";
1974 my $currentType = "";
1975 my $functionStart = 0;
1976 my $typeStart = 0;
1977 my $functionScopeDepth = 0;
1978 my $typeScopeDepth = 0;
1979 my $scopeDepth = 0;
1980
1981 while (<$fileHandle>) {
1982 chomp;
1983 next if (/^\s*\/\/.*/);
1984 if (/func\s+([\w_][\w\d_]*)\((.*)\)/ || /var\s+([\w_][\w\d_]*):\s+/) {
1985 $functionScopeDepth = $scopeDepth;
1986 $currentFunction = $1;
1987 if ($2) {
1988 $currentFunction = "$currentFunction(". parseSwiftFunctionArgs($2) . ")";
1989 }
1990 if ($currentType) {
1991 $currentFunction = "$currentType.$currentFunction";
1992 }
1993 $functionStart = $.;
1994 } elsif (/(?:class|struct|enum|protocol|extension)\s+([\w_][\w\d_]*)/) {
1995 $typeScopeDepth = $scopeDepth;
1996 $currentType = $1;
1997 $typeStart = $.;
1998 }
1999 if (index($_, "{") > -1) {
2000 $scopeDepth++;
2001 }
2002 if (index($_, "}") > -1) {
2003 $scopeDepth--;
2004 }
2005 if ($scopeDepth == $functionScopeDepth) {
2006 next unless $functionStart;
2007 push(@ranges, [$functionStart, $., $currentFunction]);
2008 $currentFunction = "";
2009 $functionStart = 0;
2010 } elsif ($scopeDepth == $typeScopeDepth) {
2011 next unless $typeStart;
2012 $currentType = "";
2013 $typeStart = 0;
2014 }
2015 }
2016
2017 return @ranges;
2018}
2019
2020sub parseSwiftFunctionArgs($)
2021{
2022 my ($functionArgs) = @_;
2023 my @words = split /, /, $functionArgs;
2024 my $argCount = scalar(@words);
2025 if ($argCount == 0) {
2026 return "";
2027 } elsif ($argCount > 0) {
2028 # If the first argument is unnamed, give it the name "_"
2029 $words[0] =~ s/^(\w+: .*)/_ $1/;
2030 return join("", map { $_ =~ s/^(\w+).*/$1/; "$_:" } @words);
2031 } else {
2032 warn "Unknown argument count.\n";
2033 }
2034}
2035
ddkilzerb7b3b912006-06-25 06:01:41 +00002036sub processPaths(\@)
aroben38e341c2007-04-28 06:57:49 +00002037{
ddkilzerb7b3b912006-06-25 06:01:41 +00002038 my ($paths) = @_;
2039 return ("." => 1) if (!@{$paths});
2040
2041 my %result = ();
2042
aroben38e341c2007-04-28 06:57:49 +00002043 for my $file (@{$paths}) {
ddkilzerb7b3b912006-06-25 06:01:41 +00002044 die "can't handle absolute paths like \"$file\"\n" if File::Spec->file_name_is_absolute($file);
2045 die "can't handle empty string path\n" if $file eq "";
2046 die "can't handle path with single quote in the name like \"$file\"\n" if $file =~ /'/; # ' (keep Xcode syntax highlighting happy)
2047
2048 my $untouchedFile = $file;
2049
2050 $file = canonicalizePath($file);
2051
2052 die "can't handle paths with .. like \"$untouchedFile\"\n" if $file =~ m|/\.\./|;
2053
2054 $result{$file} = 1;
aroben38e341c2007-04-28 06:57:49 +00002055 }
ddkilzerb7b3b912006-06-25 06:01:41 +00002056
2057 return ("." => 1) if ($result{"."});
2058
2059 # Remove any paths that also have a parent listed.
aroben38e341c2007-04-28 06:57:49 +00002060 for my $path (keys %result) {
2061 for (my $parent = dirname($path); $parent ne '.'; $parent = dirname($parent)) {
2062 if ($result{$parent}) {
ddkilzerb7b3b912006-06-25 06:01:41 +00002063 delete $result{$path};
2064 last;
aroben38e341c2007-04-28 06:57:49 +00002065 }
2066 }
2067 }
ddkilzerb7b3b912006-06-25 06:01:41 +00002068
2069 return %result;
aroben38e341c2007-04-28 06:57:49 +00002070}
aroben363e7842007-05-15 23:35:59 +00002071
haraken@chromium.org5711f062011-12-16 09:21:02 +00002072sub diffFromToString($$$)
aroben84c7c6a2007-07-19 01:41:03 +00002073{
haraken@chromium.org5711f062011-12-16 09:21:02 +00002074 my ($gitCommit, $gitIndex, $mergeBase) = @_;
2075
haraken@chromium.org562e5122011-12-15 06:32:27 +00002076 return "" if isSVN();
ddkilzer3b890b42007-10-02 20:38:36 +00002077 return $gitCommit if $gitCommit =~ m/.+\.\..+/;
commit-queue@webkit.org29cb7772021-09-16 18:08:14 +00002078 return "--cached \"$gitCommit^\"" if $gitCommit && $gitIndex;
aroben84c7c6a2007-07-19 01:41:03 +00002079 return "\"$gitCommit^\" \"$gitCommit\"" if $gitCommit;
treat@webkit.orgcaf90532009-06-23 23:10:55 +00002080 return "--cached" if $gitIndex;
ojan@chromium.org03d32662010-04-05 17:55:14 +00002081 return $mergeBase if $mergeBase;
haraken@chromium.org562e5122011-12-15 06:32:27 +00002082 return "HEAD" if isGit();
aroben84c7c6a2007-07-19 01:41:03 +00002083}
2084
haraken@chromium.org5711f062011-12-16 09:21:02 +00002085sub diffCommand($$$$)
aroben363e7842007-05-15 23:35:59 +00002086{
haraken@chromium.org5711f062011-12-16 09:21:02 +00002087 my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
aroben363e7842007-05-15 23:35:59 +00002088
jbedard@apple.com49f09da62021-10-19 00:48:12 +00002089 # The function overlap detection logic in computeModifiedFunctions() assumes that its line
2090 # ranges were from a unified diff without any context lines.
aroben84c7c6a2007-07-19 01:41:03 +00002091 my $command;
haraken@chromium.org562e5122011-12-15 06:32:27 +00002092 if (isSVN()) {
haraken@chromium.org5711f062011-12-16 09:21:02 +00002093 my @escapedPaths = map(escapeSubversionPath($_), @$paths);
bfulgham@apple.com3d033d42015-09-29 21:16:41 +00002094 my $escapedPathsString = qq(") . join(qq(" "), @escapedPaths) . qq(");
dbates@webkit.orgc2b3d212016-08-21 18:08:28 +00002095 $command = SVN . " diff --diff-cmd diff -x -U0 $escapedPathsString";
haraken@chromium.org562e5122011-12-15 06:32:27 +00002096 } elsif (isGit()) {
haraken@chromium.org5711f062011-12-16 09:21:02 +00002097 my $pathsString = "'" . join("' '", @$paths) . "'";
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002098 $command = GIT . " diff --no-ext-diff -U0 " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
ojan@chromium.org03d32662010-04-05 17:55:14 +00002099 $command .= " -- $pathsString" unless $gitCommit or $mergeBase;
aroben363e7842007-05-15 23:35:59 +00002100 }
2101
aroben84c7c6a2007-07-19 01:41:03 +00002102 return $command;
aroben363e7842007-05-15 23:35:59 +00002103}
2104
haraken@chromium.org5711f062011-12-16 09:21:02 +00002105sub statusCommand($$$$)
aroben363e7842007-05-15 23:35:59 +00002106{
haraken@chromium.org5711f062011-12-16 09:21:02 +00002107 my ($paths, $gitCommit, $gitIndex, $mergeBase) = @_;
aroben363e7842007-05-15 23:35:59 +00002108
aroben363e7842007-05-15 23:35:59 +00002109 my $command;
haraken@chromium.org562e5122011-12-15 06:32:27 +00002110 if (isSVN()) {
haraken@chromium.org5711f062011-12-16 09:21:02 +00002111 my @escapedFiles = map(escapeSubversionPath($_), keys %$paths);
bfulgham@apple.com3d033d42015-09-29 21:16:41 +00002112 my $escapedFilesString = qq(") . join(qq(" "), @escapedFiles) . qq(");
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002113 $command = SVN . " stat $escapedFilesString";
haraken@chromium.org562e5122011-12-15 06:32:27 +00002114 } elsif (isGit()) {
haraken@chromium.org5711f062011-12-16 09:21:02 +00002115 my $filesString = '"' . join('" "', keys %$paths) . '"';
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002116 $command = GIT . " diff -r --name-status -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
aroben84c7c6a2007-07-19 01:41:03 +00002117 $command .= " -- $filesString" unless $gitCommit;
aroben363e7842007-05-15 23:35:59 +00002118 }
2119
kmccullob7f3e552007-10-20 00:38:37 +00002120 return "$command 2>&1";
aroben363e7842007-05-15 23:35:59 +00002121}
2122
jbedard@apple.com49f09da62021-10-19 00:48:12 +00002123sub attributeCommand($$$)
2124{
2125 my ($attributeCache, $file, $attr) = @_;
2126
2127 my $result;
2128 if (isSVN()) {
2129 my $devNull = File::Spec->devnull();
2130 my $foundAttribute = 0;
2131 my $subPath = ".";
2132 my (@directoryParts) = File::Spec->splitdir($file);
2133 foreach my $part (@directoryParts) {
2134 if ($part eq ".") {
2135 next;
2136 }
2137 $subPath = File::Spec->join($subPath, $part);
2138 $subPath =~ s/^\.\///;
2139 if ($foundAttribute || exists $attributeCache->{$attr}{$subPath} && $attributeCache->{$attr}{$subPath} eq "1") {
2140 $attributeCache->{$attr}{$subPath} = "1";
2141 $foundAttribute = 1;
2142 next;
2143 }
2144 my $command = SVN . " propget $attr '$subPath'";
2145 my $attrib = $attributeCache->{$attr}{$subPath} || `$command 2> $devNull`;
2146 chomp $attrib;
2147 if ($attrib eq "1") {
2148 $foundAttribute = 1;
2149 }
2150 $attributeCache->{$attr}{$subPath} = $attrib || "0";
2151 }
2152 $result = $attributeCache->{$attr}{$file};
2153 } elsif (isGit()) {
2154 my $command = GIT . " check-attr $attr -- '$file'";
2155 $result = `$command`;
2156 chomp $result;
2157 $result =~ s/.*\W(\w)/$1/;
2158 }
2159
2160 $result =~ s/\D//g;
2161 return int($result || 0);
2162}
2163
haraken@chromium.org5711f062011-12-16 09:21:02 +00002164sub createPatchCommand($$$$)
aroben363e7842007-05-15 23:35:59 +00002165{
haraken@chromium.org5711f062011-12-16 09:21:02 +00002166 my ($changedFilesString, $gitCommit, $gitIndex, $mergeBase) = @_;
aroben363e7842007-05-15 23:35:59 +00002167
aroben84c7c6a2007-07-19 01:41:03 +00002168 my $command;
haraken@chromium.org562e5122011-12-15 06:32:27 +00002169 if (isSVN()) {
jcraig@apple.com159d8fd2014-04-24 07:07:06 +00002170 $command = "'$FindBin::Bin/svn-create-patch --no-style' $changedFilesString";
haraken@chromium.org562e5122011-12-15 06:32:27 +00002171 } elsif (isGit()) {
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002172 $command = GIT . " diff -M -C " . diffFromToString($gitCommit, $gitIndex, $mergeBase);
aroben84c7c6a2007-07-19 01:41:03 +00002173 $command .= " -- $changedFilesString" unless $gitCommit;
2174 }
2175
2176 return $command;
aroben363e7842007-05-15 23:35:59 +00002177}
2178
ddkilzer33ab5032007-05-28 20:48:56 +00002179sub findOriginalFileFromSvn($)
2180{
2181 my ($file) = @_;
2182 my $baseUrl;
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002183 open INFO, SVN . " info . |" or die;
ddkilzer33ab5032007-05-28 20:48:56 +00002184 while (<INFO>) {
pkasting@chromium.orgf189b702009-08-12 22:15:51 +00002185 if (/^URL: (.+?)[\r\n]*$/) {
ddkilzer33ab5032007-05-28 20:48:56 +00002186 $baseUrl = $1;
ddkilzer33ab5032007-05-28 20:48:56 +00002187 }
2188 }
2189 close INFO;
2190 my $sourceFile;
slewis@apple.com4875b632011-12-16 01:08:25 +00002191 my $escapedFile = escapeSubversionPath($file);
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002192 open INFO, SVN . " info '$escapedFile' |" or die;
ddkilzer33ab5032007-05-28 20:48:56 +00002193 while (<INFO>) {
pkasting@chromium.orgf189b702009-08-12 22:15:51 +00002194 if (/^Copied From URL: (.+?)[\r\n]*$/) {
ddkilzer33ab5032007-05-28 20:48:56 +00002195 $sourceFile = File::Spec->abs2rel($1, $baseUrl);
ddkilzer33ab5032007-05-28 20:48:56 +00002196 }
2197 }
2198 close INFO;
2199 return $sourceFile;
2200}
2201
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002202sub determinePropertyChanges($$$)
2203{
2204 my ($file, $isAdd, $original) = @_;
2205
slewis@apple.com4875b632011-12-16 01:08:25 +00002206 my $escapedFile = escapeSubversionPath($file);
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002207 my %changes;
2208 if ($isAdd) {
2209 my %addedProperties;
2210 my %removedProperties;
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002211 open PROPLIST, SVN . " proplist '$escapedFile' |" or die;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002212 while (<PROPLIST>) {
2213 $addedProperties{$1} = 1 if /^ (.+?)[\r\n]*$/ && $1 ne 'svn:mergeinfo';
2214 }
2215 close PROPLIST;
2216 if ($original) {
slewis@apple.com4875b632011-12-16 01:08:25 +00002217 my $escapedOriginal = escapeSubversionPath($original);
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002218 open PROPLIST, SVN . " proplist '$escapedOriginal' |" or die;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002219 while (<PROPLIST>) {
2220 next unless /^ (.+?)[\r\n]*$/;
2221 my $property = $1;
2222 if (exists $addedProperties{$property}) {
2223 delete $addedProperties{$1};
2224 } else {
2225 $removedProperties{$1} = 1;
2226 }
2227 }
2228 }
2229 $changes{"A"} = [sort keys %addedProperties] if %addedProperties;
2230 $changes{"D"} = [sort keys %removedProperties] if %removedProperties;
2231 } else {
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002232 open DIFF, SVN . " diff '$escapedFile' |" or die;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002233 while (<DIFF>) {
2234 if (/^Property changes on:/) {
2235 while (<DIFF>) {
2236 my $operation;
2237 my $property;
2238 if (/^Added: (\S*)/) {
2239 $operation = "A";
2240 $property = $1;
2241 } elsif (/^Modified: (\S*)/) {
2242 $operation = "M";
2243 $property = $1;
2244 } elsif (/^Deleted: (\S*)/) {
2245 $operation = "D";
2246 $property = $1;
2247 } elsif (/^Name: (\S*)/) {
2248 # Older versions of svn just say "Name" instead of the type
2249 # of property change.
2250 $operation = "C";
2251 $property = $1;
2252 }
2253 if ($operation) {
2254 $changes{$operation} = [] unless exists $changes{$operation};
2255 push @{$changes{$operation}}, $property;
2256 }
2257 }
2258 }
2259 }
2260 close DIFF;
2261 }
2262 return \%changes;
2263}
2264
2265sub pluralizeAndList($$@)
2266{
2267 my ($singular, $plural, @items) = @_;
2268
2269 return if @items == 0;
2270 return "$singular $items[0]" if @items == 1;
2271 return "$plural " . join(", ", @items[0 .. $#items - 1]) . " and " . $items[-1];
2272}
2273
ddkilzer@apple.com62990322020-12-10 18:05:39 +00002274sub generateFileList(\%$$$\%)
ddkilzer33ab5032007-05-28 20:48:56 +00002275{
ddkilzer@apple.com62990322020-12-10 18:05:39 +00002276 my ($paths, $gitCommit, $gitIndex, $mergeBase, $attributeCache) = @_;
haraken@chromium.org407dc832011-12-11 18:04:50 +00002277
haraken@chromium.org71adf4d2011-12-14 05:13:07 +00002278 my @changedFiles;
2279 my @conflictFiles;
2280 my %functionLists;
haraken@chromium.org407dc832011-12-11 18:04:50 +00002281 my @addedRegressionTests;
commit-queue@webkit.orgdbd629b2015-08-31 23:44:41 +00002282 my @requiresTests;
ddkilzer33ab5032007-05-28 20:48:56 +00002283 print STDERR " Running status to find changed, added, or removed files.\n";
haraken@chromium.org5711f062011-12-16 09:21:02 +00002284 open STAT, "-|", statusCommand($paths, $gitCommit, $gitIndex, $mergeBase) or die "The status failed: $!.\n";
ddkilzer33ab5032007-05-28 20:48:56 +00002285 while (<STAT>) {
2286 my $status;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002287 my $propertyStatus;
2288 my $propertyChanges;
ddkilzer33ab5032007-05-28 20:48:56 +00002289 my $original;
2290 my $file;
2291
haraken@chromium.org562e5122011-12-15 06:32:27 +00002292 if (isSVN()) {
weinig@apple.com5e65e9f52009-03-27 23:15:01 +00002293 my $matches;
ddkilzer@apple.comf647b6d22009-09-02 15:24:23 +00002294 if (isSVNVersion16OrNewer()) {
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002295 $matches = /^([ ACDMR])([ CM]).{5} (.+?)[\r\n]*$/;
ddkilzer33ab5032007-05-28 20:48:56 +00002296 $status = $1;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002297 $propertyStatus = $2;
2298 $file = $3;
weinig@apple.com5e65e9f52009-03-27 23:15:01 +00002299 } else {
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002300 $matches = /^([ ACDMR])([ CM]).{4} (.+?)[\r\n]*$/;
weinig@apple.com5e65e9f52009-03-27 23:15:01 +00002301 $status = $1;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002302 $propertyStatus = $2;
2303 $file = $3;
weinig@apple.com5e65e9f52009-03-27 23:15:01 +00002304 }
2305 if ($matches) {
pkasting@chromium.org7f4f6982009-07-16 22:55:51 +00002306 $file = normalizePath($file);
ddkilzer33ab5032007-05-28 20:48:56 +00002307 $original = findOriginalFileFromSvn($file) if substr($_, 3, 1) eq "+";
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002308 my $isAdd = isAddedStatus($status);
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002309 $propertyChanges = determinePropertyChanges($file, $isAdd, $original) if isModifiedStatus($propertyStatus) || $isAdd;
ddkilzer33ab5032007-05-28 20:48:56 +00002310 } else {
2311 print; # error output from svn stat
2312 }
haraken@chromium.org562e5122011-12-15 06:32:27 +00002313 } elsif (isGit()) {
aroben84c7c6a2007-07-19 01:41:03 +00002314 if (/^([ADM])\t(.+)$/) {
2315 $status = $1;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002316 $propertyStatus = " "; # git doesn't have properties
pkasting@chromium.org7f4f6982009-07-16 22:55:51 +00002317 $file = normalizePath($2);
aroben84c7c6a2007-07-19 01:41:03 +00002318 } elsif (/^([CR])[0-9]{1,3}\t([^\t]+)\t([^\t\n]+)$/) { # for example: R90% newfile oldfile
2319 $status = $1;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002320 $propertyStatus = " ";
pkasting@chromium.org7f4f6982009-07-16 22:55:51 +00002321 $original = normalizePath($2);
2322 $file = normalizePath($3);
hausmann46f26492007-06-23 08:49:16 +00002323 } else {
aroben84c7c6a2007-07-19 01:41:03 +00002324 print; # error output from git diff
ddkilzer33ab5032007-05-28 20:48:56 +00002325 }
2326 }
2327
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002328 next if !$status || isUnmodifiedStatus($status) && isUnmodifiedStatus($propertyStatus);
ddkilzer33ab5032007-05-28 20:48:56 +00002329
2330 $file = makeFilePathRelative($file);
2331
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002332 if (isModifiedStatus($status) || isAddedStatus($status) || isModifiedStatus($propertyStatus)) {
ddkilzer33ab5032007-05-28 20:48:56 +00002333 my @components = File::Spec->splitdir($file);
aroben762ddbf2007-08-20 21:41:29 +00002334 if ($components[0] eq "LayoutTests") {
ddkilzerfc34c912007-08-29 02:49:48 +00002335 push @addedRegressionTests, $file
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002336 if isAddedStatus($status)
ddkilzerfc34c912007-08-29 02:49:48 +00002337 && $file =~ /\.([a-zA-Z]+)$/
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002338 && SupportedTestExtensions->{lc($1)}
hayato@chromium.org958a9fb2012-05-07 06:52:46 +00002339 && $file !~ /-expected(-mismatch)?\.html$/
darin@apple.com418bfed2009-09-25 21:07:59 +00002340 && !scalar(grep(/^resources$/i, @components))
2341 && !scalar(grep(/^script-tests$/i, @components));
ddkilzer@apple.com62990322020-12-10 18:05:39 +00002342 } elsif (attributeCommand($attributeCache, $file, "test")) {
commit-queue@webkit.orgdbd629b2015-08-31 23:44:41 +00002343 push @addedRegressionTests, $file;
ddkilzer@apple.com62990322020-12-10 18:05:39 +00002344 } elsif (attributeCommand($attributeCache, $file, "requiresTests")) {
commit-queue@webkit.orgdbd629b2015-08-31 23:44:41 +00002345 push @requiresTests, $file
aroben762ddbf2007-08-20 21:41:29 +00002346 }
benjamin@webkit.org04476b22015-01-20 02:06:03 +00002347 push @changedFiles, $file if $components[$#components] ne "ChangeLog";
haraken@chromium.org5711f062011-12-16 09:21:02 +00002348 } elsif (isConflictStatus($status, $gitCommit, $gitIndex) || isConflictStatus($propertyStatus, $gitCommit, $gitIndex)) {
haraken@chromium.org71adf4d2011-12-14 05:13:07 +00002349 push @conflictFiles, $file;
ddkilzer33ab5032007-05-28 20:48:56 +00002350 }
benjamin@webkit.org04476b22015-01-20 02:06:03 +00002351 if (basename($file) ne "ChangeLog") {
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002352 my $description = statusDescription($status, $propertyStatus, $original, $propertyChanges);
haraken@chromium.org71adf4d2011-12-14 05:13:07 +00002353 $functionLists{$file} = $description if defined $description;
ddkilzer@apple.combc07ef42008-10-31 18:08:02 +00002354 }
ddkilzer33ab5032007-05-28 20:48:56 +00002355 }
2356 close STAT;
commit-queue@webkit.orgdbd629b2015-08-31 23:44:41 +00002357 return (\@changedFiles, \@conflictFiles, \%functionLists, \@addedRegressionTests, \@requiresTests);
ddkilzer33ab5032007-05-28 20:48:56 +00002358}
2359
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002360sub isUnmodifiedStatus($)
2361{
2362 my ($status) = @_;
2363
2364 my %statusCodes = (
2365 " " => 1,
2366 );
2367
2368 return $statusCodes{$status};
2369}
2370
aroben762ddbf2007-08-20 21:41:29 +00002371sub isModifiedStatus($)
2372{
2373 my ($status) = @_;
2374
2375 my %statusCodes = (
2376 "M" => 1,
2377 );
2378
2379 return $statusCodes{$status};
2380}
2381
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002382sub isAddedStatus($)
aroben363e7842007-05-15 23:35:59 +00002383{
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002384 my ($status) = @_;
aroben363e7842007-05-15 23:35:59 +00002385
aroben84c7c6a2007-07-19 01:41:03 +00002386 my %statusCodes = (
aroben363e7842007-05-15 23:35:59 +00002387 "A" => 1,
haraken@chromium.org562e5122011-12-15 06:32:27 +00002388 "C" => isGit(),
aroben363e7842007-05-15 23:35:59 +00002389 "R" => 1,
2390 );
2391
aroben84c7c6a2007-07-19 01:41:03 +00002392 return $statusCodes{$status};
aroben363e7842007-05-15 23:35:59 +00002393}
2394
haraken@chromium.org5711f062011-12-16 09:21:02 +00002395sub isConflictStatus($$$)
aroben363e7842007-05-15 23:35:59 +00002396{
haraken@chromium.org5711f062011-12-16 09:21:02 +00002397 my ($status, $gitCommit, $gitIndex) = @_;
aroben363e7842007-05-15 23:35:59 +00002398
2399 my %svn = (
2400 "C" => 1,
2401 );
2402
2403 my %git = (
aroben84c7c6a2007-07-19 01:41:03 +00002404 "U" => 1,
aroben363e7842007-05-15 23:35:59 +00002405 );
2406
treat@webkit.orgcaf90532009-06-23 23:10:55 +00002407 return 0 if ($gitCommit || $gitIndex); # an existing commit or staged change cannot have conflicts
haraken@chromium.org562e5122011-12-15 06:32:27 +00002408 return $svn{$status} if isSVN();
2409 return $git{$status} if isGit();
aroben363e7842007-05-15 23:35:59 +00002410}
2411
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002412sub statusDescription($$$$)
aroben363e7842007-05-15 23:35:59 +00002413{
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002414 my ($status, $propertyStatus, $original, $propertyChanges) = @_;
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002415
2416 my $propertyDescription = defined $propertyChanges ? propertyChangeDescription($propertyChanges) : "";
aroben363e7842007-05-15 23:35:59 +00002417
2418 my %svn = (
mcatanzaro@igalia.com2c40caf2016-09-06 16:24:02 +00002419 "A" => defined $original ? sprintf(" Copied from \%s.", $original) : " Added.",
aroben363e7842007-05-15 23:35:59 +00002420 "D" => " Removed.",
2421 "M" => "",
mcatanzaro@igalia.com2c40caf2016-09-06 16:24:02 +00002422 "R" => defined $original ? sprintf(" Replaced with \%s.", $original) : " Replaced.",
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002423 " " => "",
aroben363e7842007-05-15 23:35:59 +00002424 );
aroben363e7842007-05-15 23:35:59 +00002425
aroben84c7c6a2007-07-19 01:41:03 +00002426 my %git = %svn;
2427 $git{"A"} = " Added.";
mcatanzaro@igalia.com2c40caf2016-09-06 16:24:02 +00002428 if (defined $original) {
2429 $git{"C"} = sprintf(" Copied from \%s.", $original);
2430 $git{"R"} = sprintf(" Renamed from \%s.", $original);
2431 }
aroben84c7c6a2007-07-19 01:41:03 +00002432
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002433 my $description;
mcatanzaro@igalia.com2c40caf2016-09-06 16:24:02 +00002434 $description = $svn{$status} if isSVN() && exists $svn{$status};
2435 $description = $git{$status} if isGit() && exists $git{$status};
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002436 return unless defined $description;
2437
haraken@chromium.orge83c72c2011-12-15 00:12:23 +00002438 $description .= $propertyDescription unless isAddedStatus($status);
eric@webkit.orgec02a6f2009-08-27 06:38:22 +00002439 return $description;
2440}
2441
2442sub propertyChangeDescription($)
2443{
2444 my ($propertyChanges) = @_;
2445
2446 my %operations = (
2447 "A" => "Added",
2448 "M" => "Modified",
2449 "D" => "Removed",
2450 "C" => "Changed",
2451 );
2452
2453 my $description = "";
2454 while (my ($operation, $properties) = each %$propertyChanges) {
2455 my $word = $operations{$operation};
2456 my $list = pluralizeAndList("property", "properties", @$properties);
2457 $description .= " $word $list.";
2458 }
2459 return $description;
aroben363e7842007-05-15 23:35:59 +00002460}
2461
jbedard@apple.com49f09da62021-10-19 00:48:12 +00002462sub extractLineRangeAfterChange($)
2463{
2464 my ($string) = @_;
2465 my $chunkRange = parseChunkRange($string);
2466 if (!$chunkRange) {
2467 return (-1, -1); # Malformed
2468 }
2469 if (!$chunkRange->{newStartingLine} || !$chunkRange->{newLineCount}) {
2470 # Deletion; no lines exist after change.
2471 return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine});
2472 }
2473 return ($chunkRange->{newStartingLine}, $chunkRange->{newStartingLine} + $chunkRange->{newLineCount} - 1);
2474}
2475
2476sub extractLineRangeBeforeChange($)
2477{
2478 my ($string) = @_;
2479 my $chunkRange = parseChunkRange($string);
2480 if (!$chunkRange) {
2481 return (-1, -1); # Malformed
2482 }
2483 if (!$chunkRange->{startingLine} || !$chunkRange->{lineCount}) {
2484 # Addition; no lines existed before change.
2485 return ($chunkRange->{startingLine}, $chunkRange->{startingLine});
2486 }
2487 return ($chunkRange->{startingLine}, $chunkRange->{startingLine} + $chunkRange->{lineCount} - 1);
2488}
2489
aroben762ddbf2007-08-20 21:41:29 +00002490sub testListForChangeLog(@)
2491{
2492 my (@tests) = @_;
2493
2494 return "" unless @tests;
2495
2496 my $leadString = " Test" . (@tests == 1 ? "" : "s") . ": ";
2497 my $list = $leadString;
2498 foreach my $i (0..$#tests) {
2499 $list .= " " x length($leadString) if $i;
2500 my $test = $tests[$i];
2501 $test =~ s/^LayoutTests\///;
2502 $list .= "$test\n";
2503 }
2504 $list .= "\n";
2505
2506 return $list;
2507}
ddkilzer3b890b42007-10-02 20:38:36 +00002508
haraken@chromium.orge25b42a2011-12-15 23:13:40 +00002509sub reviewerAndDescriptionForGitCommit($$)
ddkilzer3b890b42007-10-02 20:38:36 +00002510{
haraken@chromium.orge25b42a2011-12-15 23:13:40 +00002511 my ($commit, $gitReviewer) = @_;
ddkilzer3b890b42007-10-02 20:38:36 +00002512
2513 my $description = '';
2514 my $reviewer;
2515
2516 my @args = qw(rev-list --pretty);
2517 push @args, '-1' if $commit !~ m/.+\.\..+/;
2518 my $gitLog;
2519 {
2520 local $/ = undef;
haraken@chromium.org577bbab2011-12-21 08:54:22 +00002521 open(GITLOG, "-|", GIT, @args, $commit) || die;
2522 $gitLog = <GITLOG>;
2523 close(GITLOG);
ddkilzer3b890b42007-10-02 20:38:36 +00002524 }
2525
2526 my @commitLogs = split(/^[Cc]ommit [a-f0-9]{40}/m, $gitLog);
2527 shift @commitLogs; # Remove initial blank commit log
2528 my $commitLogCount = 0;
2529 foreach my $commitLog (@commitLogs) {
2530 $description .= "\n" if $commitLogCount;
2531 $commitLogCount++;
2532 my $inHeader = 1;
eric@webkit.org40a261c2010-01-25 11:18:01 +00002533 my $commitLogIndent;
ddkilzer3b890b42007-10-02 20:38:36 +00002534 my @lines = split(/\n/, $commitLog);
2535 shift @lines; # Remove initial blank line
2536 foreach my $line (@lines) {
2537 if ($inHeader) {
2538 if (!$line) {
2539 $inHeader = 0;
ddkilzer3b890b42007-10-02 20:38:36 +00002540 }
2541 next;
hausmannff4297f2007-10-22 08:42:40 +00002542 } elsif ($line =~ /[Ss]igned-[Oo]ff-[Bb]y: (.+)/) {
2543 if (!$reviewer) {
2544 $reviewer = $1;
2545 } else {
2546 $reviewer .= ", " . $1;
2547 }
eric@webkit.org40a261c2010-01-25 11:18:01 +00002548 } elsif ($line =~ /^\s*$/) {
ddkilzer3b890b42007-10-02 20:38:36 +00002549 $description = $description . "\n";
2550 } else {
eric@webkit.org40a261c2010-01-25 11:18:01 +00002551 if (!defined($commitLogIndent)) {
2552 # Let the first line with non-white space determine
2553 # the global indent.
2554 $line =~ /^(\s*)\S/;
2555 $commitLogIndent = length($1);
2556 }
2557 # Strip at most the indent to preserve relative indents.
2558 $line =~ s/^\s{0,$commitLogIndent}//;
2559 $description = $description . (" " x 8) . $line . "\n";
ddkilzer3b890b42007-10-02 20:38:36 +00002560 }
2561 }
2562 }
lars077c3612007-10-09 13:52:51 +00002563 if (!$reviewer) {
2564 $reviewer = $gitReviewer;
2565 }
ddkilzer3b890b42007-10-02 20:38:36 +00002566
2567 return ($reviewer, $description);
2568}
abarth@webkit.orgc93f2152009-06-16 08:06:58 +00002569
2570sub normalizeLineEndings($$)
2571{
2572 my ($string, $endl) = @_;
2573 $string =~ s/\r?\n/$endl/g;
2574 return $string;
2575}
jbedard@apple.com49f09da62021-10-19 00:48:12 +00002576
2577sub decodeEntities($)
2578{
2579 my ($text) = @_;
2580 $text =~ s/\&lt;/</g;
2581 $text =~ s/\&gt;/>/g;
2582 $text =~ s/\&quot;/\"/g;
2583 $text =~ s/\&apos;/\'/g;
2584 $text =~ s/\&amp;/\&/g;
2585 return $text;
2586}