prepare-ChangeLog 68 KB

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