VCSUtils.pm 80 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196
  1. # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc. All rights reserved.
  2. # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
  3. # Copyright (C) 2010, 2011 Research In Motion Limited. All rights reserved.
  4. # Copyright (C) 2012 Daniel Bates (dbates@intudata.com)
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. #
  10. # 1. Redistributions of source code must retain the above copyright
  11. # notice, this list of conditions and the following disclaimer.
  12. # 2. Redistributions in binary form must reproduce the above copyright
  13. # notice, this list of conditions and the following disclaimer in the
  14. # documentation and/or other materials provided with the distribution.
  15. # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
  16. # its contributors may be used to endorse or promote products derived
  17. # from this software without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
  20. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  21. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  22. # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
  23. # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  24. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  25. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  26. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  28. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. # Module to share code to work with various version control systems.
  30. package VCSUtils;
  31. use strict;
  32. use warnings;
  33. use Cwd qw(); # "qw()" prevents warnings about redefining getcwd() with "use POSIX;"
  34. use English; # for $POSTMATCH, etc.
  35. use File::Basename;
  36. use File::Spec;
  37. use POSIX;
  38. use Term::ANSIColor qw(colored);
  39. BEGIN {
  40. use Exporter ();
  41. our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
  42. $VERSION = 1.00;
  43. @ISA = qw(Exporter);
  44. @EXPORT = qw(
  45. &applyGitBinaryPatchDelta
  46. &callSilently
  47. &canonicalizePath
  48. &changeLogEmailAddress
  49. &changeLogFileName
  50. &changeLogName
  51. &chdirReturningRelativePath
  52. &decodeGitBinaryChunk
  53. &decodeGitBinaryPatch
  54. &determineSVNRoot
  55. &determineVCSRoot
  56. &escapeSubversionPath
  57. &exitStatus
  58. &fixChangeLogPatch
  59. &gitBranch
  60. &gitdiff2svndiff
  61. &isGit
  62. &isGitSVN
  63. &isGitBranchBuild
  64. &isGitDirectory
  65. &isSVN
  66. &isSVNDirectory
  67. &isSVNVersion16OrNewer
  68. &makeFilePathRelative
  69. &mergeChangeLogs
  70. &normalizePath
  71. &parseChunkRange
  72. &parseFirstEOL
  73. &parsePatch
  74. &pathRelativeToSVNRepositoryRootForPath
  75. &possiblyColored
  76. &prepareParsedPatch
  77. &removeEOL
  78. &runCommand
  79. &runPatchCommand
  80. &scmMoveOrRenameFile
  81. &scmToggleExecutableBit
  82. &setChangeLogDateAndReviewer
  83. &svnRevisionForDirectory
  84. &svnStatus
  85. &toWindowsLineEndings
  86. &gitCommitForSVNRevision
  87. &listOfChangedFilesBetweenRevisions
  88. );
  89. %EXPORT_TAGS = ( );
  90. @EXPORT_OK = ();
  91. }
  92. our @EXPORT_OK;
  93. my $gitBranch;
  94. my $gitRoot;
  95. my $isGit;
  96. my $isGitSVN;
  97. my $isGitBranchBuild;
  98. my $isSVN;
  99. my $svnVersion;
  100. # Project time zone for Cupertino, CA, US
  101. my $changeLogTimeZone = "PST8PDT";
  102. my $gitDiffStartRegEx = qr#^diff --git [^\r\n]+#;
  103. my $gitDiffStartWithPrefixRegEx = qr#^diff --git \w/(.+) \w/([^\r\n]+)#; # We suppose that --src-prefix and --dst-prefix don't contain a non-word character (\W) and end with '/'.
  104. my $gitDiffStartWithoutPrefixNoSpaceRegEx = qr#^diff --git (\S+) (\S+)$#;
  105. my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#;
  106. my $gitDiffStartWithoutPrefixSourceDirectoryPrefixRegExp = qr#^diff --git ([^/]+/)#;
  107. my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path.
  108. my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property.
  109. my $svnPropertyValueStartRegEx = qr#^\s*(\+|-|Merged|Reverse-merged)\s*([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines).
  110. my $svnPropertyValueNoNewlineRegEx = qr#\ No newline at end of property#;
  111. # This method is for portability. Return the system-appropriate exit
  112. # status of a child process.
  113. #
  114. # Args: pass the child error status returned by the last pipe close,
  115. # for example "$?".
  116. sub exitStatus($)
  117. {
  118. my ($returnvalue) = @_;
  119. if ($^O eq "MSWin32") {
  120. return $returnvalue >> 8;
  121. }
  122. if (!WIFEXITED($returnvalue)) {
  123. return 254;
  124. }
  125. return WEXITSTATUS($returnvalue);
  126. }
  127. # Call a function while suppressing STDERR, and return the return values
  128. # as an array.
  129. sub callSilently($@) {
  130. my ($func, @args) = @_;
  131. # The following pattern was taken from here:
  132. # http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html
  133. #
  134. # Also see this Perl documentation (search for "open OLDERR"):
  135. # http://perldoc.perl.org/functions/open.html
  136. open(OLDERR, ">&STDERR");
  137. close(STDERR);
  138. my @returnValue = &$func(@args);
  139. open(STDERR, ">&OLDERR");
  140. close(OLDERR);
  141. return @returnValue;
  142. }
  143. sub toWindowsLineEndings
  144. {
  145. my ($text) = @_;
  146. $text =~ s/\n/\r\n/g;
  147. return $text;
  148. }
  149. # Note, this method will not error if the file corresponding to the $source path does not exist.
  150. sub scmMoveOrRenameFile
  151. {
  152. my ($source, $destination) = @_;
  153. return if ! -e $source;
  154. if (isSVN()) {
  155. my $escapedDestination = escapeSubversionPath($destination);
  156. my $escapedSource = escapeSubversionPath($source);
  157. system("svn", "move", $escapedSource, $escapedDestination);
  158. } elsif (isGit()) {
  159. system("git", "mv", $source, $destination);
  160. }
  161. }
  162. # Note, this method will not error if the file corresponding to the path does not exist.
  163. sub scmToggleExecutableBit
  164. {
  165. my ($path, $executableBitDelta) = @_;
  166. return if ! -e $path;
  167. if ($executableBitDelta == 1) {
  168. scmAddExecutableBit($path);
  169. } elsif ($executableBitDelta == -1) {
  170. scmRemoveExecutableBit($path);
  171. }
  172. }
  173. sub scmAddExecutableBit($)
  174. {
  175. my ($path) = @_;
  176. if (isSVN()) {
  177. my $escapedPath = escapeSubversionPath($path);
  178. system("svn", "propset", "svn:executable", "on", $escapedPath) == 0 or die "Failed to run 'svn propset svn:executable on $escapedPath'.";
  179. } elsif (isGit()) {
  180. chmod(0755, $path);
  181. }
  182. }
  183. sub scmRemoveExecutableBit($)
  184. {
  185. my ($path) = @_;
  186. if (isSVN()) {
  187. my $escapedPath = escapeSubversionPath($path);
  188. system("svn", "propdel", "svn:executable", $escapedPath) == 0 or die "Failed to run 'svn propdel svn:executable $escapedPath'.";
  189. } elsif (isGit()) {
  190. chmod(0664, $path);
  191. }
  192. }
  193. sub isGitDirectory($)
  194. {
  195. my ($dir) = @_;
  196. return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0;
  197. }
  198. sub isGit()
  199. {
  200. return $isGit if defined $isGit;
  201. $isGit = isGitDirectory(".");
  202. return $isGit;
  203. }
  204. sub isGitSVN()
  205. {
  206. return $isGitSVN if defined $isGitSVN;
  207. # There doesn't seem to be an officially documented way to determine
  208. # if you're in a git-svn checkout. The best suggestions seen so far
  209. # all use something like the following:
  210. my $output = `git config --get svn-remote.svn.fetch 2>& 1`;
  211. $isGitSVN = $output ne '';
  212. return $isGitSVN;
  213. }
  214. sub gitBranch()
  215. {
  216. unless (defined $gitBranch) {
  217. chomp($gitBranch = `git symbolic-ref -q HEAD`);
  218. $gitBranch = "" if exitStatus($?);
  219. $gitBranch =~ s#^refs/heads/##;
  220. $gitBranch = "" if $gitBranch eq "master";
  221. }
  222. return $gitBranch;
  223. }
  224. sub isGitBranchBuild()
  225. {
  226. my $branch = gitBranch();
  227. chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`);
  228. return 1 if $override eq "true";
  229. return 0 if $override eq "false";
  230. unless (defined $isGitBranchBuild) {
  231. chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`);
  232. $isGitBranchBuild = $gitBranchBuild eq "true";
  233. }
  234. return $isGitBranchBuild;
  235. }
  236. sub isSVNDirectory($)
  237. {
  238. my ($dir) = @_;
  239. return system("cd $dir && svn info > " . File::Spec->devnull() . " 2>&1") == 0;
  240. }
  241. sub isSVN()
  242. {
  243. return $isSVN if defined $isSVN;
  244. $isSVN = isSVNDirectory(".");
  245. return $isSVN;
  246. }
  247. sub svnVersion()
  248. {
  249. return $svnVersion if defined $svnVersion;
  250. if (!isSVN()) {
  251. $svnVersion = 0;
  252. } else {
  253. chomp($svnVersion = `svn --version --quiet`);
  254. }
  255. return $svnVersion;
  256. }
  257. sub isSVNVersion16OrNewer()
  258. {
  259. my $version = svnVersion();
  260. return eval "v$version" ge v1.6;
  261. }
  262. sub chdirReturningRelativePath($)
  263. {
  264. my ($directory) = @_;
  265. my $previousDirectory = Cwd::getcwd();
  266. chdir $directory;
  267. my $newDirectory = Cwd::getcwd();
  268. return "." if $newDirectory eq $previousDirectory;
  269. return File::Spec->abs2rel($previousDirectory, $newDirectory);
  270. }
  271. sub determineGitRoot()
  272. {
  273. chomp(my $gitDir = `git rev-parse --git-dir`);
  274. return dirname($gitDir);
  275. }
  276. sub determineSVNRoot()
  277. {
  278. my $last = '';
  279. my $path = '.';
  280. my $parent = '..';
  281. my $repositoryRoot;
  282. my $repositoryUUID;
  283. while (1) {
  284. my $thisRoot;
  285. my $thisUUID;
  286. my $escapedPath = escapeSubversionPath($path);
  287. # Ignore error messages in case we've run past the root of the checkout.
  288. open INFO, "svn info '$escapedPath' 2> " . File::Spec->devnull() . " |" or die;
  289. while (<INFO>) {
  290. if (/^Repository Root: (.+)/) {
  291. $thisRoot = $1;
  292. }
  293. if (/^Repository UUID: (.+)/) {
  294. $thisUUID = $1;
  295. }
  296. if ($thisRoot && $thisUUID) {
  297. local $/ = undef;
  298. <INFO>; # Consume the rest of the input.
  299. }
  300. }
  301. close INFO;
  302. # It's possible (e.g. for developers of some ports) to have a WebKit
  303. # checkout in a subdirectory of another checkout. So abort if the
  304. # repository root or the repository UUID suddenly changes.
  305. last if !$thisUUID;
  306. $repositoryUUID = $thisUUID if !$repositoryUUID;
  307. last if $thisUUID ne $repositoryUUID;
  308. last if !$thisRoot;
  309. $repositoryRoot = $thisRoot if !$repositoryRoot;
  310. last if $thisRoot ne $repositoryRoot;
  311. $last = $path;
  312. $path = File::Spec->catdir($parent, $path);
  313. }
  314. return File::Spec->rel2abs($last);
  315. }
  316. sub determineVCSRoot()
  317. {
  318. if (isGit()) {
  319. return determineGitRoot();
  320. }
  321. if (!isSVN()) {
  322. # Some users have a workflow where svn-create-patch, svn-apply and
  323. # svn-unapply are used outside of multiple svn working directores,
  324. # so warn the user and assume Subversion is being used in this case.
  325. warn "Unable to determine VCS root for '" . Cwd::getcwd() . "'; assuming Subversion";
  326. $isSVN = 1;
  327. }
  328. return determineSVNRoot();
  329. }
  330. sub isWindows()
  331. {
  332. return ($^O eq "MSWin32") || 0;
  333. }
  334. sub svnRevisionForDirectory($)
  335. {
  336. my ($dir) = @_;
  337. my $revision;
  338. if (isSVNDirectory($dir)) {
  339. my $escapedDir = escapeSubversionPath($dir);
  340. my $command = "svn info $escapedDir | grep Revision:";
  341. $command = "LC_ALL=C $command" if !isWindows();
  342. my $svnInfo = `$command`;
  343. ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g);
  344. } elsif (isGitDirectory($dir)) {
  345. my $command = "git log --grep=\"git-svn-id: \" -n 1 | grep git-svn-id:";
  346. $command = "LC_ALL=C $command" if !isWindows();
  347. $command = "cd $dir && $command";
  348. my $gitLog = `$command`;
  349. ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g);
  350. }
  351. if (!defined($revision)) {
  352. $revision = "unknown";
  353. warn "Unable to determine current SVN revision in $dir";
  354. }
  355. return $revision;
  356. }
  357. sub pathRelativeToSVNRepositoryRootForPath($)
  358. {
  359. my ($file) = @_;
  360. my $relativePath = File::Spec->abs2rel($file);
  361. my $svnInfo;
  362. if (isSVN()) {
  363. my $escapedRelativePath = escapeSubversionPath($relativePath);
  364. my $command = "svn info $escapedRelativePath";
  365. $command = "LC_ALL=C $command" if !isWindows();
  366. $svnInfo = `$command`;
  367. } elsif (isGit()) {
  368. my $command = "git svn info $relativePath";
  369. $command = "LC_ALL=C $command" if !isWindows();
  370. $svnInfo = `$command`;
  371. }
  372. $svnInfo =~ /.*^URL: (.*?)$/m;
  373. my $svnURL = $1;
  374. $svnInfo =~ /.*^Repository Root: (.*?)$/m;
  375. my $repositoryRoot = $1;
  376. $svnURL =~ s/$repositoryRoot\///;
  377. return $svnURL;
  378. }
  379. sub makeFilePathRelative($)
  380. {
  381. my ($path) = @_;
  382. return $path unless isGit();
  383. unless (defined $gitRoot) {
  384. chomp($gitRoot = `git rev-parse --show-cdup`);
  385. }
  386. return $gitRoot . $path;
  387. }
  388. sub normalizePath($)
  389. {
  390. my ($path) = @_;
  391. $path =~ s/\\/\//g;
  392. return $path;
  393. }
  394. sub possiblyColored($$)
  395. {
  396. my ($colors, $string) = @_;
  397. if (-t STDOUT) {
  398. return colored([$colors], $string);
  399. } else {
  400. return $string;
  401. }
  402. }
  403. sub adjustPathForRecentRenamings($)
  404. {
  405. my ($fullPath) = @_;
  406. $fullPath =~ s|WebCore/webaudio|WebCore/Modules/webaudio|g;
  407. $fullPath =~ s|JavaScriptCore/wtf|WTF/wtf|g;
  408. $fullPath =~ s|test_expectations.txt|TestExpectations|g;
  409. return $fullPath;
  410. }
  411. sub canonicalizePath($)
  412. {
  413. my ($file) = @_;
  414. # Remove extra slashes and '.' directories in path
  415. $file = File::Spec->canonpath($file);
  416. # Remove '..' directories in path
  417. my @dirs = ();
  418. foreach my $dir (File::Spec->splitdir($file)) {
  419. if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') {
  420. pop(@dirs);
  421. } else {
  422. push(@dirs, $dir);
  423. }
  424. }
  425. return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : ".";
  426. }
  427. sub removeEOL($)
  428. {
  429. my ($line) = @_;
  430. return "" unless $line;
  431. $line =~ s/[\r\n]+$//g;
  432. return $line;
  433. }
  434. sub parseFirstEOL($)
  435. {
  436. my ($fileHandle) = @_;
  437. # Make input record separator the new-line character to simplify regex matching below.
  438. my $savedInputRecordSeparator = $INPUT_RECORD_SEPARATOR;
  439. $INPUT_RECORD_SEPARATOR = "\n";
  440. my $firstLine = <$fileHandle>;
  441. $INPUT_RECORD_SEPARATOR = $savedInputRecordSeparator;
  442. return unless defined($firstLine);
  443. my $eol;
  444. if ($firstLine =~ /\r\n/) {
  445. $eol = "\r\n";
  446. } elsif ($firstLine =~ /\r/) {
  447. $eol = "\r";
  448. } elsif ($firstLine =~ /\n/) {
  449. $eol = "\n";
  450. }
  451. return $eol;
  452. }
  453. sub firstEOLInFile($)
  454. {
  455. my ($file) = @_;
  456. my $eol;
  457. if (open(FILE, $file)) {
  458. $eol = parseFirstEOL(*FILE);
  459. close(FILE);
  460. }
  461. return $eol;
  462. }
  463. # Parses a chunk range line into its components.
  464. #
  465. # A chunk range line has the form: @@ -L_1,N_1 +L_2,N_2 @@, where the pairs (L_1, N_1),
  466. # (L_2, N_2) are ranges that represent the starting line number and line count in the
  467. # original file and new file, respectively.
  468. #
  469. # Note, some versions of GNU diff may omit the comma and trailing line count (e.g. N_1),
  470. # in which case the omitted line count defaults to 1. For example, GNU diff may output
  471. # @@ -1 +1 @@, which is equivalent to @@ -1,1 +1,1 @@.
  472. #
  473. # This subroutine returns undef if given an invalid or malformed chunk range.
  474. #
  475. # Args:
  476. # $line: the line to parse.
  477. # $chunkSentinel: the sentinel that surrounds the chunk range information (defaults to "@@").
  478. #
  479. # Returns $chunkRangeHashRef
  480. # $chunkRangeHashRef: a hash reference representing the parts of a chunk range, as follows--
  481. # startingLine: the starting line in the original file.
  482. # lineCount: the line count in the original file.
  483. # newStartingLine: the new starting line in the new file.
  484. # newLineCount: the new line count in the new file.
  485. sub parseChunkRange($;$)
  486. {
  487. my ($line, $chunkSentinel) = @_;
  488. $chunkSentinel = "@@" if !$chunkSentinel;
  489. my $chunkRangeRegEx = qr#^\Q$chunkSentinel\E -(\d+)(,(\d+))? \+(\d+)(,(\d+))? \Q$chunkSentinel\E#;
  490. if ($line !~ /$chunkRangeRegEx/) {
  491. return;
  492. }
  493. my %chunkRange;
  494. $chunkRange{startingLine} = $1;
  495. $chunkRange{lineCount} = defined($2) ? $3 : 1;
  496. $chunkRange{newStartingLine} = $4;
  497. $chunkRange{newLineCount} = defined($5) ? $6 : 1;
  498. return \%chunkRange;
  499. }
  500. sub svnStatus($)
  501. {
  502. my ($fullPath) = @_;
  503. my $escapedFullPath = escapeSubversionPath($fullPath);
  504. my $svnStatus;
  505. open SVN, "svn status --non-interactive --non-recursive '$escapedFullPath' |" or die;
  506. if (-d $fullPath) {
  507. # When running "svn stat" on a directory, we can't assume that only one
  508. # status will be returned (since any files with a status below the
  509. # directory will be returned), and we can't assume that the directory will
  510. # be first (since any files with unknown status will be listed first).
  511. my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath));
  512. while (<SVN>) {
  513. # Input may use a different EOL sequence than $/, so avoid chomp.
  514. $_ = removeEOL($_);
  515. my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7)));
  516. if ($normalizedFullPath eq $normalizedStatPath) {
  517. $svnStatus = "$_\n";
  518. last;
  519. }
  520. }
  521. # Read the rest of the svn command output to avoid a broken pipe warning.
  522. local $/ = undef;
  523. <SVN>;
  524. }
  525. else {
  526. # Files will have only one status returned.
  527. $svnStatus = removeEOL(<SVN>) . "\n";
  528. }
  529. close SVN;
  530. return $svnStatus;
  531. }
  532. # Return whether the given file mode is executable in the source control
  533. # sense. We make this determination based on whether the executable bit
  534. # is set for "others" rather than the stronger condition that it be set
  535. # for the user, group, and others. This is sufficient for distinguishing
  536. # the default behavior in Git and SVN.
  537. #
  538. # Args:
  539. # $fileMode: A number or string representing a file mode in octal notation.
  540. sub isExecutable($)
  541. {
  542. my $fileMode = shift;
  543. return $fileMode % 2;
  544. }
  545. # Parse the Git diff header start line.
  546. #
  547. # Args:
  548. # $line: "diff --git" line.
  549. #
  550. # Returns the path of the target file.
  551. sub parseGitDiffStartLine($)
  552. {
  553. my $line = shift;
  554. $_ = $line;
  555. if (/$gitDiffStartWithPrefixRegEx/ || /$gitDiffStartWithoutPrefixNoSpaceRegEx/) {
  556. return $2;
  557. }
  558. # Assume the diff was generated with --no-prefix (e.g. git diff --no-prefix).
  559. if (!/$gitDiffStartWithoutPrefixSourceDirectoryPrefixRegExp/) {
  560. # FIXME: Moving top directory file is not supported (e.g diff --git A.txt B.txt).
  561. die("Could not find '/' in \"diff --git\" line: \"$line\"; only non-prefixed git diffs (i.e. not generated with --no-prefix) that move a top-level directory file are supported.");
  562. }
  563. my $pathPrefix = $1;
  564. if (!/^diff --git \Q$pathPrefix\E.+ (\Q$pathPrefix\E.+)$/) {
  565. # FIXME: Moving a file through sub directories of top directory is not supported (e.g diff --git A/B.txt C/B.txt).
  566. die("Could not find '/' in \"diff --git\" line: \"$line\"; only non-prefixed git diffs (i.e. not generated with --no-prefix) that move a file between top-level directories are supported.");
  567. }
  568. return $1;
  569. }
  570. # Parse the next Git diff header from the given file handle, and advance
  571. # the handle so the last line read is the first line after the header.
  572. #
  573. # This subroutine dies if given leading junk.
  574. #
  575. # Args:
  576. # $fileHandle: advanced so the last line read from the handle is the first
  577. # line of the header to parse. This should be a line
  578. # beginning with "diff --git".
  579. # $line: the line last read from $fileHandle
  580. #
  581. # Returns ($headerHashRef, $lastReadLine):
  582. # $headerHashRef: a hash reference representing a diff header, as follows--
  583. # copiedFromPath: the path from which the file was copied or moved if
  584. # the diff is a copy or move.
  585. # executableBitDelta: the value 1 or -1 if the executable bit was added or
  586. # removed, respectively. New and deleted files have
  587. # this value only if the file is executable, in which
  588. # case the value is 1 and -1, respectively.
  589. # indexPath: the path of the target file.
  590. # isBinary: the value 1 if the diff is for a binary file.
  591. # isDeletion: the value 1 if the diff is a file deletion.
  592. # isCopyWithChanges: the value 1 if the file was copied or moved and
  593. # the target file was changed in some way after being
  594. # copied or moved (e.g. if its contents or executable
  595. # bit were changed).
  596. # isNew: the value 1 if the diff is for a new file.
  597. # shouldDeleteSource: the value 1 if the file was copied or moved and
  598. # the source file was deleted -- i.e. if the copy
  599. # was actually a move.
  600. # svnConvertedText: the header text with some lines converted to SVN
  601. # format. Git-specific lines are preserved.
  602. # $lastReadLine: the line last read from $fileHandle.
  603. sub parseGitDiffHeader($$)
  604. {
  605. my ($fileHandle, $line) = @_;
  606. $_ = $line;
  607. my $indexPath;
  608. if (/$gitDiffStartRegEx/) {
  609. # Use $POSTMATCH to preserve the end-of-line character.
  610. my $eol = $POSTMATCH;
  611. # The first and second paths can differ in the case of copies
  612. # and renames. We use the second file path because it is the
  613. # destination path.
  614. $indexPath = adjustPathForRecentRenamings(parseGitDiffStartLine($_));
  615. $_ = "Index: $indexPath$eol"; # Convert to SVN format.
  616. } else {
  617. die("Could not parse leading \"diff --git\" line: \"$line\".");
  618. }
  619. my $copiedFromPath;
  620. my $foundHeaderEnding;
  621. my $isBinary;
  622. my $isDeletion;
  623. my $isNew;
  624. my $newExecutableBit = 0;
  625. my $oldExecutableBit = 0;
  626. my $shouldDeleteSource = 0;
  627. my $similarityIndex = 0;
  628. my $svnConvertedText;
  629. while (1) {
  630. # Temporarily strip off any end-of-line characters to simplify
  631. # regex matching below.
  632. s/([\n\r]+)$//;
  633. my $eol = $1;
  634. if (/^(deleted file|old) mode (\d+)/) {
  635. $oldExecutableBit = (isExecutable($2) ? 1 : 0);
  636. $isDeletion = 1 if $1 eq "deleted file";
  637. } elsif (/^new( file)? mode (\d+)/) {
  638. $newExecutableBit = (isExecutable($2) ? 1 : 0);
  639. $isNew = 1 if $1;
  640. } elsif (/^similarity index (\d+)%/) {
  641. $similarityIndex = $1;
  642. } elsif (/^copy from ([^\t\r\n]+)/) {
  643. $copiedFromPath = $1;
  644. } elsif (/^rename from ([^\t\r\n]+)/) {
  645. # FIXME: Record this as a move rather than as a copy-and-delete.
  646. # This will simplify adding rename support to svn-unapply.
  647. # Otherwise, the hash for a deletion would have to know
  648. # everything about the file being deleted in order to
  649. # support undoing itself. Recording as a move will also
  650. # permit us to use "svn move" and "git move".
  651. $copiedFromPath = $1;
  652. $shouldDeleteSource = 1;
  653. } elsif (/^--- \S+/) {
  654. # Convert to SVN format.
  655. # We emit the suffix "\t(revision 0)" to handle $indexPath which contains a space character.
  656. # The patch(1) command thinks a file path is characters before a tab.
  657. # This suffix make our diff more closely match the SVN diff format.
  658. $_ = "--- $indexPath\t(revision 0)";
  659. } elsif (/^\+\+\+ \S+/) {
  660. # Convert to SVN format.
  661. # We emit the suffix "\t(working copy)" to handle $indexPath which contains a space character.
  662. # The patch(1) command thinks a file path is characters before a tab.
  663. # This suffix make our diff more closely match the SVN diff format.
  664. $_ = "+++ $indexPath\t(working copy)";
  665. $foundHeaderEnding = 1;
  666. } elsif (/^GIT binary patch$/ ) {
  667. $isBinary = 1;
  668. $foundHeaderEnding = 1;
  669. # The "git diff" command includes a line of the form "Binary files
  670. # <path1> and <path2> differ" if the --binary flag is not used.
  671. } elsif (/^Binary files / ) {
  672. die("Error: the Git diff contains a binary file without the binary data in ".
  673. "line: \"$_\". Be sure to use the --binary flag when invoking \"git diff\" ".
  674. "with diffs containing binary files.");
  675. }
  676. $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
  677. $_ = <$fileHandle>; # Not defined if end-of-file reached.
  678. last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding);
  679. }
  680. my $executableBitDelta = $newExecutableBit - $oldExecutableBit;
  681. my %header;
  682. $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
  683. $header{executableBitDelta} = $executableBitDelta if $executableBitDelta;
  684. $header{indexPath} = $indexPath;
  685. $header{isBinary} = $isBinary if $isBinary;
  686. $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta));
  687. $header{isDeletion} = $isDeletion if $isDeletion;
  688. $header{isNew} = $isNew if $isNew;
  689. $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource;
  690. $header{svnConvertedText} = $svnConvertedText;
  691. return (\%header, $_);
  692. }
  693. # Parse the next SVN diff header from the given file handle, and advance
  694. # the handle so the last line read is the first line after the header.
  695. #
  696. # This subroutine dies if given leading junk or if it could not detect
  697. # the end of the header block.
  698. #
  699. # Args:
  700. # $fileHandle: advanced so the last line read from the handle is the first
  701. # line of the header to parse. This should be a line
  702. # beginning with "Index:".
  703. # $line: the line last read from $fileHandle
  704. #
  705. # Returns ($headerHashRef, $lastReadLine):
  706. # $headerHashRef: a hash reference representing a diff header, as follows--
  707. # copiedFromPath: the path from which the file was copied if the diff
  708. # is a copy.
  709. # indexPath: the path of the target file, which is the path found in
  710. # the "Index:" line.
  711. # isBinary: the value 1 if the diff is for a binary file.
  712. # isNew: the value 1 if the diff is for a new file.
  713. # sourceRevision: the revision number of the source, if it exists. This
  714. # is the same as the revision number the file was copied
  715. # from, in the case of a file copy.
  716. # svnConvertedText: the header text converted to a header with the paths
  717. # in some lines corrected.
  718. # $lastReadLine: the line last read from $fileHandle.
  719. sub parseSvnDiffHeader($$)
  720. {
  721. my ($fileHandle, $line) = @_;
  722. $_ = $line;
  723. my $indexPath;
  724. if (/$svnDiffStartRegEx/) {
  725. $indexPath = adjustPathForRecentRenamings($1);
  726. } else {
  727. die("First line of SVN diff does not begin with \"Index \": \"$_\"");
  728. }
  729. my $copiedFromPath;
  730. my $foundHeaderEnding;
  731. my $isBinary;
  732. my $isNew;
  733. my $sourceRevision;
  734. my $svnConvertedText;
  735. while (1) {
  736. # Temporarily strip off any end-of-line characters to simplify
  737. # regex matching below.
  738. s/([\n\r]+)$//;
  739. my $eol = $1;
  740. # Fix paths on "---" and "+++" lines to match the leading
  741. # index line.
  742. if (s/^--- [^\t\n\r]+/--- $indexPath/) {
  743. # ---
  744. if (/^--- .+\(revision (\d+)\)/) {
  745. $sourceRevision = $1;
  746. $isNew = 1 if !$sourceRevision; # if revision 0.
  747. if (/\(from (\S+):(\d+)\)$/) {
  748. # The "from" clause is created by svn-create-patch, in
  749. # which case there is always also a "revision" clause.
  750. $copiedFromPath = $1;
  751. die("Revision number \"$2\" in \"from\" clause does not match " .
  752. "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision);
  753. }
  754. }
  755. } elsif (s/^\+\+\+ [^\t\n\r]+/+++ $indexPath/ || $isBinary && /^$/) {
  756. $foundHeaderEnding = 1;
  757. } elsif (/^Cannot display: file marked as a binary type.$/) {
  758. $isBinary = 1;
  759. # SVN 1.7 has an unusual display format for a binary diff. It repeats the first
  760. # two lines of the diff header. For example:
  761. # Index: test_file.swf
  762. # ===================================================================
  763. # Cannot display: file marked as a binary type.
  764. # svn:mime-type = application/octet-stream
  765. # Index: test_file.swf
  766. # ===================================================================
  767. # --- test_file.swf
  768. # +++ test_file.swf
  769. #
  770. # ...
  771. # Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
  772. # Therefore, we continue reading the diff header until we either encounter a line
  773. # that begins with "+++" (SVN 1.7 or greater) or an empty line (SVN version less
  774. # than 1.7).
  775. }
  776. $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters.
  777. $_ = <$fileHandle>; # Not defined if end-of-file reached.
  778. last if (!defined($_) || !$isBinary && /$svnDiffStartRegEx/ || $foundHeaderEnding);
  779. }
  780. if (!$foundHeaderEnding) {
  781. die("Did not find end of header block corresponding to index path \"$indexPath\".");
  782. }
  783. my %header;
  784. $header{copiedFromPath} = $copiedFromPath if $copiedFromPath;
  785. $header{indexPath} = $indexPath;
  786. $header{isBinary} = $isBinary if $isBinary;
  787. $header{isNew} = $isNew if $isNew;
  788. $header{sourceRevision} = $sourceRevision if $sourceRevision;
  789. $header{svnConvertedText} = $svnConvertedText;
  790. return (\%header, $_);
  791. }
  792. # Parse the next diff header from the given file handle, and advance
  793. # the handle so the last line read is the first line after the header.
  794. #
  795. # This subroutine dies if given leading junk or if it could not detect
  796. # the end of the header block.
  797. #
  798. # Args:
  799. # $fileHandle: advanced so the last line read from the handle is the first
  800. # line of the header to parse. For SVN-formatted diffs, this
  801. # is a line beginning with "Index:". For Git, this is a line
  802. # beginning with "diff --git".
  803. # $line: the line last read from $fileHandle
  804. #
  805. # Returns ($headerHashRef, $lastReadLine):
  806. # $headerHashRef: a hash reference representing a diff header
  807. # copiedFromPath: the path from which the file was copied if the diff
  808. # is a copy.
  809. # executableBitDelta: the value 1 or -1 if the executable bit was added or
  810. # removed, respectively. New and deleted files have
  811. # this value only if the file is executable, in which
  812. # case the value is 1 and -1, respectively.
  813. # indexPath: the path of the target file.
  814. # isBinary: the value 1 if the diff is for a binary file.
  815. # isGit: the value 1 if the diff is Git-formatted.
  816. # isSvn: the value 1 if the diff is SVN-formatted.
  817. # sourceRevision: the revision number of the source, if it exists. This
  818. # is the same as the revision number the file was copied
  819. # from, in the case of a file copy.
  820. # svnConvertedText: the header text with some lines converted to SVN
  821. # format. Git-specific lines are preserved.
  822. # $lastReadLine: the line last read from $fileHandle.
  823. sub parseDiffHeader($$)
  824. {
  825. my ($fileHandle, $line) = @_;
  826. my $header; # This is a hash ref.
  827. my $isGit;
  828. my $isSvn;
  829. my $lastReadLine;
  830. if ($line =~ $svnDiffStartRegEx) {
  831. $isSvn = 1;
  832. ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line);
  833. } elsif ($line =~ $gitDiffStartRegEx) {
  834. $isGit = 1;
  835. ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line);
  836. } else {
  837. die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\"");
  838. }
  839. $header->{isGit} = $isGit if $isGit;
  840. $header->{isSvn} = $isSvn if $isSvn;
  841. return ($header, $lastReadLine);
  842. }
  843. # FIXME: The %diffHash "object" should not have an svnConvertedText property.
  844. # Instead, the hash object should store its information in a
  845. # structured way as properties. This should be done in a way so
  846. # that, if necessary, the text of an SVN or Git patch can be
  847. # reconstructed from the information in those hash properties.
  848. #
  849. # A %diffHash is a hash representing a source control diff of a single
  850. # file operation (e.g. a file modification, copy, or delete).
  851. #
  852. # These hashes appear, for example, in the parseDiff(), parsePatch(),
  853. # and prepareParsedPatch() subroutines of this package.
  854. #
  855. # The corresponding values are--
  856. #
  857. # copiedFromPath: the path from which the file was copied if the diff
  858. # is a copy.
  859. # executableBitDelta: the value 1 or -1 if the executable bit was added or
  860. # removed from the target file, respectively.
  861. # indexPath: the path of the target file. For SVN-formatted diffs,
  862. # this is the same as the path in the "Index:" line.
  863. # isBinary: the value 1 if the diff is for a binary file.
  864. # isDeletion: the value 1 if the diff is known from the header to be a deletion.
  865. # isGit: the value 1 if the diff is Git-formatted.
  866. # isNew: the value 1 if the dif is known from the header to be a new file.
  867. # isSvn: the value 1 if the diff is SVN-formatted.
  868. # sourceRevision: the revision number of the source, if it exists. This
  869. # is the same as the revision number the file was copied
  870. # from, in the case of a file copy.
  871. # svnConvertedText: the diff with some lines converted to SVN format.
  872. # Git-specific lines are preserved.
  873. # Parse one diff from a patch file created by svn-create-patch, and
  874. # advance the file handle so the last line read is the first line
  875. # of the next header block.
  876. #
  877. # This subroutine preserves any leading junk encountered before the header.
  878. #
  879. # Composition of an SVN diff
  880. #
  881. # There are three parts to an SVN diff: the header, the property change, and
  882. # the binary contents, in that order. Either the header or the property change
  883. # may be ommitted, but not both. If there are binary changes, then you always
  884. # have all three.
  885. #
  886. # Args:
  887. # $fileHandle: a file handle advanced to the first line of the next
  888. # header block. Leading junk is okay.
  889. # $line: the line last read from $fileHandle.
  890. # $optionsHashRef: a hash reference representing optional options to use
  891. # when processing a diff.
  892. # shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
  893. # instead of the line endings in the target file; the
  894. # value of 1 if svnConvertedText should use the line
  895. # endings in the diff.
  896. #
  897. # Returns ($diffHashRefs, $lastReadLine):
  898. # $diffHashRefs: A reference to an array of references to %diffHash hashes.
  899. # See the %diffHash documentation above.
  900. # $lastReadLine: the line last read from $fileHandle
  901. sub parseDiff($$;$)
  902. {
  903. # FIXME: Adjust this method so that it dies if the first line does not
  904. # match the start of a diff. This will require a change to
  905. # parsePatch() so that parsePatch() skips over leading junk.
  906. my ($fileHandle, $line, $optionsHashRef) = @_;
  907. my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default
  908. my $headerHashRef; # Last header found, as returned by parseDiffHeader().
  909. my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties().
  910. my $svnText;
  911. my $indexPathEOL;
  912. my $numTextChunks = 0;
  913. while (defined($line)) {
  914. if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) {
  915. # Then assume all diffs in the patch are Git-formatted. This
  916. # block was made to be enterable at most once since we assume
  917. # all diffs in the patch are formatted the same (SVN or Git).
  918. $headerStartRegEx = $gitDiffStartRegEx;
  919. }
  920. if ($line =~ $svnPropertiesStartRegEx) {
  921. my $propertyPath = $1;
  922. if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) {
  923. # This is the start of the second diff in the while loop, which happens to
  924. # be a property diff. If $svnPropertiesHasRef is defined, then this is the
  925. # second consecutive property diff, otherwise it's the start of a property
  926. # diff for a file that only has property changes.
  927. last;
  928. }
  929. ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line);
  930. next;
  931. }
  932. if ($line !~ $headerStartRegEx) {
  933. # Then we are in the body of the diff.
  934. my $isChunkRange = defined(parseChunkRange($line));
  935. $numTextChunks += 1 if $isChunkRange;
  936. my $nextLine = <$fileHandle>;
  937. my $willAddNewLineAtEndOfFile = defined($nextLine) && $nextLine =~ /^\\ No newline at end of file$/;
  938. if ($willAddNewLineAtEndOfFile) {
  939. # Diff(1) always emits a LF character preceeding the line "\ No newline at end of file".
  940. # We must preserve both the added LF character and the line ending of this sentinel line
  941. # or patch(1) will complain.
  942. $svnText .= $line . $nextLine;
  943. $line = <$fileHandle>;
  944. next;
  945. }
  946. if ($indexPathEOL && !$isChunkRange) {
  947. # The chunk range is part of the body of the diff, but its line endings should't be
  948. # modified or patch(1) will complain. So, we only modify non-chunk range lines.
  949. $line =~ s/\r\n|\r|\n/$indexPathEOL/g;
  950. }
  951. $svnText .= $line;
  952. $line = $nextLine;
  953. next;
  954. } # Otherwise, we found a diff header.
  955. if ($svnPropertiesHashRef || $headerHashRef) {
  956. # Then either we just processed an SVN property change or this
  957. # is the start of the second diff header of this while loop.
  958. last;
  959. }
  960. ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line);
  961. if (!$optionsHashRef || !$optionsHashRef->{shouldNotUseIndexPathEOL}) {
  962. # FIXME: We shouldn't query the file system (via firstEOLInFile()) to determine the
  963. # line endings of the file indexPath. Instead, either the caller to parseDiff()
  964. # should provide this information or parseDiff() should take a delegate that it
  965. # can use to query for this information.
  966. $indexPathEOL = firstEOLInFile($headerHashRef->{indexPath}) if !$headerHashRef->{isNew} && !$headerHashRef->{isBinary};
  967. }
  968. $svnText .= $headerHashRef->{svnConvertedText};
  969. }
  970. my @diffHashRefs;
  971. if ($headerHashRef->{shouldDeleteSource}) {
  972. my %deletionHash;
  973. $deletionHash{indexPath} = $headerHashRef->{copiedFromPath};
  974. $deletionHash{isDeletion} = 1;
  975. push @diffHashRefs, \%deletionHash;
  976. }
  977. if ($headerHashRef->{copiedFromPath}) {
  978. my %copyHash;
  979. $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath};
  980. $copyHash{indexPath} = $headerHashRef->{indexPath};
  981. $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
  982. if ($headerHashRef->{isSvn}) {
  983. $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
  984. }
  985. push @diffHashRefs, \%copyHash;
  986. }
  987. # Note, the order of evaluation for the following if conditional has been explicitly chosen so that
  988. # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that
  989. # only has property changes).
  990. if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) {
  991. # Then add the usual file modification.
  992. my %diffHash;
  993. # FIXME: We should expand this code to support other properties. In the future,
  994. # parseSvnDiffProperties may return a hash whose keys are the properties.
  995. if ($headerHashRef->{isSvn}) {
  996. # SVN records the change to the executable bit in a separate property change diff
  997. # that follows the contents of the diff, except for binary diffs. For binary
  998. # diffs, the property change diff follows the diff header.
  999. $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
  1000. } elsif ($headerHashRef->{isGit}) {
  1001. # Git records the change to the executable bit in the header of a diff.
  1002. $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta};
  1003. }
  1004. $diffHash{indexPath} = $headerHashRef->{indexPath};
  1005. $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary};
  1006. $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion};
  1007. $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit};
  1008. $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew};
  1009. $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn};
  1010. if (!$headerHashRef->{copiedFromPath}) {
  1011. # If the file was copied, then we have already incorporated the
  1012. # sourceRevision information into the change.
  1013. $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision};
  1014. }
  1015. # FIXME: Remove the need for svnConvertedText. See the %diffHash
  1016. # code comments above for more information.
  1017. #
  1018. # Note, we may not always have SVN converted text since we intend
  1019. # to deprecate it in the future. For example, a property change
  1020. # diff for a file that only has property changes will not return
  1021. # any SVN converted text.
  1022. $diffHash{svnConvertedText} = $svnText if $svnText;
  1023. $diffHash{numTextChunks} = $numTextChunks if $svnText && !$headerHashRef->{isBinary};
  1024. push @diffHashRefs, \%diffHash;
  1025. }
  1026. if (!%$headerHashRef && $svnPropertiesHashRef) {
  1027. # A property change diff for a file that only has property changes.
  1028. my %propertyChangeHash;
  1029. $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta};
  1030. $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath};
  1031. $propertyChangeHash{isSvn} = 1;
  1032. push @diffHashRefs, \%propertyChangeHash;
  1033. }
  1034. return (\@diffHashRefs, $line);
  1035. }
  1036. # Parse an SVN property change diff from the given file handle, and advance
  1037. # the handle so the last line read is the first line after this diff.
  1038. #
  1039. # For the case of an SVN binary diff, the binary contents will follow the
  1040. # the property changes.
  1041. #
  1042. # This subroutine dies if the first line does not begin with "Property changes on"
  1043. # or if the separator line that follows this line is missing.
  1044. #
  1045. # Args:
  1046. # $fileHandle: advanced so the last line read from the handle is the first
  1047. # line of the footer to parse. This line begins with
  1048. # "Property changes on".
  1049. # $line: the line last read from $fileHandle.
  1050. #
  1051. # Returns ($propertyHashRef, $lastReadLine):
  1052. # $propertyHashRef: a hash reference representing an SVN diff footer.
  1053. # propertyPath: the path of the target file.
  1054. # executableBitDelta: the value 1 or -1 if the executable bit was added or
  1055. # removed from the target file, respectively.
  1056. # $lastReadLine: the line last read from $fileHandle.
  1057. sub parseSvnDiffProperties($$)
  1058. {
  1059. my ($fileHandle, $line) = @_;
  1060. $_ = $line;
  1061. my %footer;
  1062. if (/$svnPropertiesStartRegEx/) {
  1063. $footer{propertyPath} = $1;
  1064. } else {
  1065. die("Failed to find start of SVN property change, \"Property changes on \": \"$_\"");
  1066. }
  1067. # We advance $fileHandle two lines so that the next line that
  1068. # we process is $svnPropertyStartRegEx in a well-formed footer.
  1069. # A well-formed footer has the form:
  1070. # Property changes on: FileA
  1071. # ___________________________________________________________________
  1072. # Added: svn:executable
  1073. # + *
  1074. $_ = <$fileHandle>; # Not defined if end-of-file reached.
  1075. my $separator = "_" x 67;
  1076. if (defined($_) && /^$separator[\r\n]+$/) {
  1077. $_ = <$fileHandle>;
  1078. } else {
  1079. die("Failed to find separator line: \"$_\".");
  1080. }
  1081. # FIXME: We should expand this to support other SVN properties
  1082. # (e.g. return a hash of property key-values that represents
  1083. # all properties).
  1084. #
  1085. # Notice, we keep processing until we hit end-of-file or some
  1086. # line that does not resemble $svnPropertyStartRegEx, such as
  1087. # the empty line that precedes the start of the binary contents
  1088. # of a patch, or the start of the next diff (e.g. "Index:").
  1089. my $propertyHashRef;
  1090. while (defined($_) && /$svnPropertyStartRegEx/) {
  1091. ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_);
  1092. if ($propertyHashRef->{name} eq "svn:executable") {
  1093. # Notice, for SVN properties, propertyChangeDelta is always non-zero
  1094. # because a property can only be added or removed.
  1095. $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta};
  1096. }
  1097. }
  1098. return(\%footer, $_);
  1099. }
  1100. # Parse the next SVN property from the given file handle, and advance the handle so the last
  1101. # line read is the first line after the property.
  1102. #
  1103. # This subroutine dies if the first line is not a valid start of an SVN property,
  1104. # or the property is missing a value, or the property change type (e.g. "Added")
  1105. # does not correspond to the property value type (e.g. "+").
  1106. #
  1107. # Args:
  1108. # $fileHandle: advanced so the last line read from the handle is the first
  1109. # line of the property to parse. This should be a line
  1110. # that matches $svnPropertyStartRegEx.
  1111. # $line: the line last read from $fileHandle.
  1112. #
  1113. # Returns ($propertyHashRef, $lastReadLine):
  1114. # $propertyHashRef: a hash reference representing a SVN property.
  1115. # name: the name of the property.
  1116. # value: the last property value. For instance, suppose the property is "Modified".
  1117. # Then it has both a '-' and '+' property value in that order. Therefore,
  1118. # the value of this key is the value of the '+' property by ordering (since
  1119. # it is the last value).
  1120. # propertyChangeDelta: the value 1 or -1 if the property was added or
  1121. # removed, respectively.
  1122. # $lastReadLine: the line last read from $fileHandle.
  1123. sub parseSvnProperty($$)
  1124. {
  1125. my ($fileHandle, $line) = @_;
  1126. $_ = $line;
  1127. my $propertyName;
  1128. my $propertyChangeType;
  1129. if (/$svnPropertyStartRegEx/) {
  1130. $propertyChangeType = $1;
  1131. $propertyName = $2;
  1132. } else {
  1133. die("Failed to find SVN property: \"$_\".");
  1134. }
  1135. $_ = <$fileHandle>; # Not defined if end-of-file reached.
  1136. if (defined($_) && defined(parseChunkRange($_, "##"))) {
  1137. # FIXME: We should validate the chunk range line that is part of an SVN 1.7
  1138. # property diff. For now, we ignore this line.
  1139. $_ = <$fileHandle>;
  1140. }
  1141. # The "svn diff" command neither inserts newline characters between property values
  1142. # nor between successive properties.
  1143. #
  1144. # As of SVN 1.7, "svn diff" may insert "\ No newline at end of property" after a
  1145. # property value that doesn't end in a newline.
  1146. #
  1147. # FIXME: We do not support property values that contain tailing newline characters
  1148. # as it is difficult to disambiguate these trailing newlines from the empty
  1149. # line that precedes the contents of a binary patch.
  1150. my $propertyValue;
  1151. my $propertyValueType;
  1152. while (defined($_) && /$svnPropertyValueStartRegEx/) {
  1153. # Note, a '-' property may be followed by a '+' property in the case of a "Modified"
  1154. # or "Name" property. We only care about the ending value (i.e. the '+' property)
  1155. # in such circumstances. So, we take the property value for the property to be its
  1156. # last parsed property value.
  1157. #
  1158. # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or
  1159. # add error checking to prevent '+', '+', ..., '+' and other invalid combinations.
  1160. $propertyValueType = $1;
  1161. ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_);
  1162. $_ = <$fileHandle> if defined($_) && /$svnPropertyValueNoNewlineRegEx/;
  1163. }
  1164. if (!$propertyValue) {
  1165. die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\".");
  1166. }
  1167. my $propertyChangeDelta;
  1168. if ($propertyValueType eq "+" || $propertyValueType eq "Merged") {
  1169. $propertyChangeDelta = 1;
  1170. } elsif ($propertyValueType eq "-" || $propertyValueType eq "Reverse-merged") {
  1171. $propertyChangeDelta = -1;
  1172. } else {
  1173. die("Not reached.");
  1174. }
  1175. # We perform a simple validation that an "Added" or "Deleted" property
  1176. # change type corresponds with a "+" and "-" value type, respectively.
  1177. my $expectedChangeDelta;
  1178. if ($propertyChangeType eq "Added") {
  1179. $expectedChangeDelta = 1;
  1180. } elsif ($propertyChangeType eq "Deleted") {
  1181. $expectedChangeDelta = -1;
  1182. }
  1183. if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) {
  1184. die("The final property value type found \"$propertyValueType\" does not " .
  1185. "correspond to the property change type found \"$propertyChangeType\".");
  1186. }
  1187. my %propertyHash;
  1188. $propertyHash{name} = $propertyName;
  1189. $propertyHash{propertyChangeDelta} = $propertyChangeDelta;
  1190. $propertyHash{value} = $propertyValue;
  1191. return (\%propertyHash, $_);
  1192. }
  1193. # Parse the value of an SVN property from the given file handle, and advance
  1194. # the handle so the last line read is the first line after the property value.
  1195. #
  1196. # This subroutine dies if the first line is an invalid SVN property value line
  1197. # (i.e. a line that does not begin with " +" or " -").
  1198. #
  1199. # Args:
  1200. # $fileHandle: advanced so the last line read from the handle is the first
  1201. # line of the property value to parse. This should be a line
  1202. # beginning with " +" or " -".
  1203. # $line: the line last read from $fileHandle.
  1204. #
  1205. # Returns ($propertyValue, $lastReadLine):
  1206. # $propertyValue: the value of the property.
  1207. # $lastReadLine: the line last read from $fileHandle.
  1208. sub parseSvnPropertyValue($$)
  1209. {
  1210. my ($fileHandle, $line) = @_;
  1211. $_ = $line;
  1212. my $propertyValue;
  1213. my $eol;
  1214. if (/$svnPropertyValueStartRegEx/) {
  1215. $propertyValue = $2; # Does not include the end-of-line character(s).
  1216. $eol = $POSTMATCH;
  1217. } else {
  1218. die("Failed to find property value beginning with '+', '-', 'Merged', or 'Reverse-merged': \"$_\".");
  1219. }
  1220. while (<$fileHandle>) {
  1221. if (/^[\r\n]+$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/ || /$svnPropertyValueNoNewlineRegEx/) {
  1222. # Note, we may encounter an empty line before the contents of a binary patch.
  1223. # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be
  1224. # followed by a '+' property in the case of a "Modified" or "Name" property.
  1225. # We check for $svnPropertyStartRegEx because it indicates the start of the
  1226. # next property to parse.
  1227. last;
  1228. }
  1229. # Temporarily strip off any end-of-line characters. We add the end-of-line characters
  1230. # from the previously processed line to the start of this line so that the last line
  1231. # of the property value does not end in end-of-line characters.
  1232. s/([\n\r]+)$//;
  1233. $propertyValue .= "$eol$_";
  1234. $eol = $1;
  1235. }
  1236. return ($propertyValue, $_);
  1237. }
  1238. # Parse a patch file created by svn-create-patch.
  1239. #
  1240. # Args:
  1241. # $fileHandle: A file handle to the patch file that has not yet been
  1242. # read from.
  1243. # $optionsHashRef: a hash reference representing optional options to use
  1244. # when processing a diff.
  1245. # shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead
  1246. # instead of the line endings in the target file; the
  1247. # value of 1 if svnConvertedText should use the line
  1248. # endings in the diff.
  1249. #
  1250. # Returns:
  1251. # @diffHashRefs: an array of diff hash references.
  1252. # See the %diffHash documentation above.
  1253. sub parsePatch($;$)
  1254. {
  1255. my ($fileHandle, $optionsHashRef) = @_;
  1256. my $newDiffHashRefs;
  1257. my @diffHashRefs; # return value
  1258. my $line = <$fileHandle>;
  1259. while (defined($line)) { # Otherwise, at EOF.
  1260. ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line, $optionsHashRef);
  1261. push @diffHashRefs, @$newDiffHashRefs;
  1262. }
  1263. return @diffHashRefs;
  1264. }
  1265. # Prepare the results of parsePatch() for use in svn-apply and svn-unapply.
  1266. #
  1267. # Args:
  1268. # $shouldForce: Whether to continue processing if an unexpected
  1269. # state occurs.
  1270. # @diffHashRefs: An array of references to %diffHashes.
  1271. # See the %diffHash documentation above.
  1272. #
  1273. # Returns $preparedPatchHashRef:
  1274. # copyDiffHashRefs: A reference to an array of the $diffHashRefs in
  1275. # @diffHashRefs that represent file copies. The original
  1276. # ordering is preserved.
  1277. # nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in
  1278. # @diffHashRefs that do not represent file copies.
  1279. # The original ordering is preserved.
  1280. # sourceRevisionHash: A reference to a hash of source path to source
  1281. # revision number.
  1282. sub prepareParsedPatch($@)
  1283. {
  1284. my ($shouldForce, @diffHashRefs) = @_;
  1285. my %copiedFiles;
  1286. # Return values
  1287. my @copyDiffHashRefs = ();
  1288. my @nonCopyDiffHashRefs = ();
  1289. my %sourceRevisionHash = ();
  1290. for my $diffHashRef (@diffHashRefs) {
  1291. my $copiedFromPath = $diffHashRef->{copiedFromPath};
  1292. my $indexPath = $diffHashRef->{indexPath};
  1293. my $sourceRevision = $diffHashRef->{sourceRevision};
  1294. my $sourcePath;
  1295. if (defined($copiedFromPath)) {
  1296. # Then the diff is a copy operation.
  1297. $sourcePath = $copiedFromPath;
  1298. # FIXME: Consider printing a warning or exiting if
  1299. # exists($copiedFiles{$indexPath}) is true -- i.e. if
  1300. # $indexPath appears twice as a copy target.
  1301. $copiedFiles{$indexPath} = $sourcePath;
  1302. push @copyDiffHashRefs, $diffHashRef;
  1303. } else {
  1304. # Then the diff is not a copy operation.
  1305. $sourcePath = $indexPath;
  1306. push @nonCopyDiffHashRefs, $diffHashRef;
  1307. }
  1308. if (defined($sourceRevision)) {
  1309. if (exists($sourceRevisionHash{$sourcePath}) &&
  1310. ($sourceRevisionHash{$sourcePath} != $sourceRevision)) {
  1311. if (!$shouldForce) {
  1312. die "Two revisions of the same file required as a source:\n".
  1313. " $sourcePath:$sourceRevisionHash{$sourcePath}\n".
  1314. " $sourcePath:$sourceRevision";
  1315. }
  1316. }
  1317. $sourceRevisionHash{$sourcePath} = $sourceRevision;
  1318. }
  1319. }
  1320. my %preparedPatchHash;
  1321. $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs;
  1322. $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs;
  1323. $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash;
  1324. return \%preparedPatchHash;
  1325. }
  1326. # Return localtime() for the project's time zone, given an integer time as
  1327. # returned by Perl's time() function.
  1328. sub localTimeInProjectTimeZone($)
  1329. {
  1330. my $epochTime = shift;
  1331. # Change the time zone temporarily for the localtime() call.
  1332. my $savedTimeZone = $ENV{'TZ'};
  1333. $ENV{'TZ'} = $changeLogTimeZone;
  1334. my @localTime = localtime($epochTime);
  1335. if (defined $savedTimeZone) {
  1336. $ENV{'TZ'} = $savedTimeZone;
  1337. } else {
  1338. delete $ENV{'TZ'};
  1339. }
  1340. return @localTime;
  1341. }
  1342. # Set the reviewer and date in a ChangeLog patch, and return the new patch.
  1343. #
  1344. # Args:
  1345. # $patch: a ChangeLog patch as a string.
  1346. # $reviewer: the name of the reviewer, or undef if the reviewer should not be set.
  1347. # $epochTime: an integer time as returned by Perl's time() function.
  1348. sub setChangeLogDateAndReviewer($$$)
  1349. {
  1350. my ($patch, $reviewer, $epochTime) = @_;
  1351. my @localTime = localTimeInProjectTimeZone($epochTime);
  1352. my $newDate = strftime("%Y-%m-%d", @localTime);
  1353. my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}( )#;
  1354. $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/;
  1355. if (defined($reviewer)) {
  1356. # We include a leading plus ("+") in the regular expression to make
  1357. # the regular expression less likely to match text in the leading junk
  1358. # for the patch, if the patch has leading junk.
  1359. $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/;
  1360. }
  1361. return $patch;
  1362. }
  1363. # If possible, returns a ChangeLog patch equivalent to the given one,
  1364. # but with the newest ChangeLog entry inserted at the top of the
  1365. # file -- i.e. no leading context and all lines starting with "+".
  1366. #
  1367. # If given a patch string not representable as a patch with the above
  1368. # properties, it returns the input back unchanged.
  1369. #
  1370. # WARNING: This subroutine can return an inequivalent patch string if
  1371. # both the beginning of the new ChangeLog file matches the beginning
  1372. # of the source ChangeLog, and the source beginning was modified.
  1373. # Otherwise, it is guaranteed to return an equivalent patch string,
  1374. # if it returns.
  1375. #
  1376. # Applying this subroutine to ChangeLog patches allows svn-apply to
  1377. # insert new ChangeLog entries at the top of the ChangeLog file.
  1378. # svn-apply uses patch with --fuzz=3 to do this. We need to apply
  1379. # this subroutine because the diff(1) command is greedy when matching
  1380. # lines. A new ChangeLog entry with the same date and author as the
  1381. # previous will match and cause the diff to have lines of starting
  1382. # context.
  1383. #
  1384. # This subroutine has unit tests in VCSUtils_unittest.pl.
  1385. #
  1386. # Returns $changeLogHashRef:
  1387. # $changeLogHashRef: a hash reference representing a change log patch.
  1388. # patch: a ChangeLog patch equivalent to the given one, but with the
  1389. # newest ChangeLog entry inserted at the top of the file, if possible.
  1390. sub fixChangeLogPatch($)
  1391. {
  1392. my $patch = shift; # $patch will only contain patch fragments for ChangeLog.
  1393. $patch =~ s|test_expectations.txt:|TestExpectations:|g;
  1394. $patch =~ /(\r?\n)/;
  1395. my $lineEnding = $1;
  1396. my @lines = split(/$lineEnding/, $patch);
  1397. my $i = 0; # We reuse the same index throughout.
  1398. # Skip to beginning of first chunk.
  1399. for (; $i < @lines; ++$i) {
  1400. if (substr($lines[$i], 0, 1) eq "@") {
  1401. last;
  1402. }
  1403. }
  1404. my $chunkStartIndex = ++$i;
  1405. my %changeLogHashRef;
  1406. # Optimization: do not process if new lines already begin the chunk.
  1407. if (substr($lines[$i], 0, 1) eq "+") {
  1408. $changeLogHashRef{patch} = $patch;
  1409. return \%changeLogHashRef;
  1410. }
  1411. # Skip to first line of newly added ChangeLog entry.
  1412. # For example, +2009-06-03 Eric Seidel <eric@webkit.org>
  1413. my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date
  1414. . '\s+(.+)\s+' # name
  1415. . '<([^<>]+)>$'; # e-mail address
  1416. for (; $i < @lines; ++$i) {
  1417. my $line = $lines[$i];
  1418. my $firstChar = substr($line, 0, 1);
  1419. if ($line =~ /$dateStartRegEx/) {
  1420. last;
  1421. } elsif ($firstChar eq " " or $firstChar eq "+") {
  1422. next;
  1423. }
  1424. $changeLogHashRef{patch} = $patch; # Do not change if, for example, "-" or "@" found.
  1425. return \%changeLogHashRef;
  1426. }
  1427. if ($i >= @lines) {
  1428. $changeLogHashRef{patch} = $patch; # Do not change if date not found.
  1429. return \%changeLogHashRef;
  1430. }
  1431. my $dateStartIndex = $i;
  1432. # Rewrite overlapping lines to lead with " ".
  1433. my @overlappingLines = (); # These will include a leading "+".
  1434. for (; $i < @lines; ++$i) {
  1435. my $line = $lines[$i];
  1436. if (substr($line, 0, 1) ne "+") {
  1437. last;
  1438. }
  1439. push(@overlappingLines, $line);
  1440. $lines[$i] = " " . substr($line, 1);
  1441. }
  1442. # Remove excess ending context, if necessary.
  1443. my $shouldTrimContext = 1;
  1444. for (; $i < @lines; ++$i) {
  1445. my $firstChar = substr($lines[$i], 0, 1);
  1446. if ($firstChar eq " ") {
  1447. next;
  1448. } elsif ($firstChar eq "@") {
  1449. last;
  1450. }
  1451. $shouldTrimContext = 0; # For example, if "+" or "-" encountered.
  1452. last;
  1453. }
  1454. my $deletedLineCount = 0;
  1455. if ($shouldTrimContext) { # Also occurs if end of file reached.
  1456. splice(@lines, $i - @overlappingLines, @overlappingLines);
  1457. $deletedLineCount = @overlappingLines;
  1458. }
  1459. # Work backwards, shifting overlapping lines towards front
  1460. # while checking that patch stays equivalent.
  1461. for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) {
  1462. my $line = $lines[$i];
  1463. if (substr($line, 0, 1) ne " ") {
  1464. next;
  1465. }
  1466. my $text = substr($line, 1);
  1467. my $newLine = pop(@overlappingLines);
  1468. if ($text ne substr($newLine, 1)) {
  1469. $changeLogHashRef{patch} = $patch; # Unexpected difference.
  1470. return \%changeLogHashRef;
  1471. }
  1472. $lines[$i] = "+$text";
  1473. }
  1474. # If @overlappingLines > 0, this is where we make use of the
  1475. # assumption that the beginning of the source file was not modified.
  1476. splice(@lines, $chunkStartIndex, 0, @overlappingLines);
  1477. # Update the date start index as it may have changed after shifting
  1478. # the overlapping lines towards the front.
  1479. for ($i = $chunkStartIndex; $i < $dateStartIndex; ++$i) {
  1480. $dateStartIndex = $i if $lines[$i] =~ /$dateStartRegEx/;
  1481. }
  1482. splice(@lines, $chunkStartIndex, $dateStartIndex - $chunkStartIndex); # Remove context of later entry.
  1483. $deletedLineCount += $dateStartIndex - $chunkStartIndex;
  1484. # Update the initial chunk range.
  1485. my $chunkRangeHashRef = parseChunkRange($lines[$chunkStartIndex - 1]);
  1486. if (!$chunkRangeHashRef) {
  1487. # FIXME: Handle errors differently from ChangeLog files that
  1488. # are okay but should not be altered. That way we can find out
  1489. # if improvements to the script ever become necessary.
  1490. $changeLogHashRef{patch} = $patch; # Error: unexpected patch string format.
  1491. return \%changeLogHashRef;
  1492. }
  1493. my $oldSourceLineCount = $chunkRangeHashRef->{lineCount};
  1494. my $oldTargetLineCount = $chunkRangeHashRef->{newLineCount};
  1495. my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount;
  1496. my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount;
  1497. $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@";
  1498. $changeLogHashRef{patch} = join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline.
  1499. return \%changeLogHashRef;
  1500. }
  1501. # This is a supporting method for runPatchCommand.
  1502. #
  1503. # Arg: the optional $args parameter passed to runPatchCommand (can be undefined).
  1504. #
  1505. # Returns ($patchCommand, $isForcing).
  1506. #
  1507. # This subroutine has unit tests in VCSUtils_unittest.pl.
  1508. sub generatePatchCommand($)
  1509. {
  1510. my ($passedArgsHashRef) = @_;
  1511. my $argsHashRef = { # Defaults
  1512. ensureForce => 0,
  1513. shouldReverse => 0,
  1514. options => []
  1515. };
  1516. # Merges hash references. It's okay here if passed hash reference is undefined.
  1517. @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef};
  1518. my $ensureForce = $argsHashRef->{ensureForce};
  1519. my $shouldReverse = $argsHashRef->{shouldReverse};
  1520. my $options = $argsHashRef->{options};
  1521. if (! $options) {
  1522. $options = [];
  1523. } else {
  1524. $options = [@{$options}]; # Copy to avoid side effects.
  1525. }
  1526. my $isForcing = 0;
  1527. if (grep /^--force$/, @{$options}) {
  1528. $isForcing = 1;
  1529. } elsif ($ensureForce) {
  1530. push @{$options}, "--force";
  1531. $isForcing = 1;
  1532. }
  1533. if ($shouldReverse) { # No check: --reverse should never be passed explicitly.
  1534. push @{$options}, "--reverse";
  1535. }
  1536. @{$options} = sort(@{$options}); # For easier testing.
  1537. my $patchCommand = join(" ", "patch -p0", @{$options});
  1538. return ($patchCommand, $isForcing);
  1539. }
  1540. # Apply the given patch using the patch(1) command.
  1541. #
  1542. # On success, return the resulting exit status. Otherwise, exit with the
  1543. # exit status. If "--force" is passed as an option, however, then never
  1544. # exit and always return the exit status.
  1545. #
  1546. # Args:
  1547. # $patch: a patch string.
  1548. # $repositoryRootPath: an absolute path to the repository root.
  1549. # $pathRelativeToRoot: the path of the file to be patched, relative to the
  1550. # repository root. This should normally be the path
  1551. # found in the patch's "Index:" line. It is passed
  1552. # explicitly rather than reparsed from the patch
  1553. # string for optimization purposes.
  1554. # This is used only for error reporting. The
  1555. # patch command gleans the actual file to patch
  1556. # from the patch string.
  1557. # $args: a reference to a hash of optional arguments. The possible
  1558. # keys are --
  1559. # ensureForce: whether to ensure --force is passed (defaults to 0).
  1560. # shouldReverse: whether to pass --reverse (defaults to 0).
  1561. # options: a reference to an array of options to pass to the
  1562. # patch command. The subroutine passes the -p0 option
  1563. # no matter what. This should not include --reverse.
  1564. #
  1565. # This subroutine has unit tests in VCSUtils_unittest.pl.
  1566. sub runPatchCommand($$$;$)
  1567. {
  1568. my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_;
  1569. my ($patchCommand, $isForcing) = generatePatchCommand($args);
  1570. # Temporarily change the working directory since the path found
  1571. # in the patch's "Index:" line is relative to the repository root
  1572. # (i.e. the same as $pathRelativeToRoot).
  1573. my $cwd = Cwd::getcwd();
  1574. chdir $repositoryRootPath;
  1575. open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!";
  1576. print PATCH $patch;
  1577. close PATCH;
  1578. my $exitStatus = exitStatus($?);
  1579. chdir $cwd;
  1580. if ($exitStatus && !$isForcing) {
  1581. print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " .
  1582. "status $exitStatus. Pass --force to ignore patch failures.\n";
  1583. exit $exitStatus;
  1584. }
  1585. return $exitStatus;
  1586. }
  1587. # Merge ChangeLog patches using a three-file approach.
  1588. #
  1589. # This is used by resolve-ChangeLogs when it's operated as a merge driver
  1590. # and when it's used to merge conflicts after a patch is applied or after
  1591. # an svn update.
  1592. #
  1593. # It's also used for traditional rejected patches.
  1594. #
  1595. # Args:
  1596. # $fileMine: The merged version of the file. Also known in git as the
  1597. # other branch's version (%B) or "ours".
  1598. # For traditional patch rejects, this is the *.rej file.
  1599. # $fileOlder: The base version of the file. Also known in git as the
  1600. # ancestor version (%O) or "base".
  1601. # For traditional patch rejects, this is the *.orig file.
  1602. # $fileNewer: The current version of the file. Also known in git as the
  1603. # current version (%A) or "theirs".
  1604. # For traditional patch rejects, this is the original-named
  1605. # file.
  1606. #
  1607. # Returns 1 if merge was successful, else 0.
  1608. sub mergeChangeLogs($$$)
  1609. {
  1610. my ($fileMine, $fileOlder, $fileNewer) = @_;
  1611. my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0;
  1612. local $/ = undef;
  1613. my $patch;
  1614. if ($traditionalReject) {
  1615. open(DIFF, "<", $fileMine) or die $!;
  1616. $patch = <DIFF>;
  1617. close(DIFF);
  1618. rename($fileMine, "$fileMine.save");
  1619. rename($fileOlder, "$fileOlder.save");
  1620. } else {
  1621. open(DIFF, "diff -u -a --binary \"$fileOlder\" \"$fileMine\" |") or die $!;
  1622. $patch = <DIFF>;
  1623. close(DIFF);
  1624. }
  1625. unlink("${fileNewer}.orig");
  1626. unlink("${fileNewer}.rej");
  1627. open(PATCH, "| patch --force --fuzz=3 --binary \"$fileNewer\" > " . File::Spec->devnull()) or die $!;
  1628. if ($traditionalReject) {
  1629. print PATCH $patch;
  1630. } else {
  1631. my $changeLogHash = fixChangeLogPatch($patch);
  1632. print PATCH $changeLogHash->{patch};
  1633. }
  1634. close(PATCH);
  1635. my $result = !exitStatus($?);
  1636. # Refuse to merge the patch if it did not apply cleanly
  1637. if (-e "${fileNewer}.rej") {
  1638. unlink("${fileNewer}.rej");
  1639. if (-f "${fileNewer}.orig") {
  1640. unlink($fileNewer);
  1641. rename("${fileNewer}.orig", $fileNewer);
  1642. }
  1643. } else {
  1644. unlink("${fileNewer}.orig");
  1645. }
  1646. if ($traditionalReject) {
  1647. rename("$fileMine.save", $fileMine);
  1648. rename("$fileOlder.save", $fileOlder);
  1649. }
  1650. return $result;
  1651. }
  1652. sub gitConfig($)
  1653. {
  1654. return unless $isGit;
  1655. my ($config) = @_;
  1656. my $result = `git config $config`;
  1657. chomp $result;
  1658. return $result;
  1659. }
  1660. sub changeLogSuffix()
  1661. {
  1662. my $rootPath = determineVCSRoot();
  1663. my $changeLogSuffixFile = File::Spec->catfile($rootPath, ".changeLogSuffix");
  1664. return "" if ! -e $changeLogSuffixFile;
  1665. open FILE, $changeLogSuffixFile or die "Could not open $changeLogSuffixFile: $!";
  1666. my $changeLogSuffix = <FILE>;
  1667. chomp $changeLogSuffix;
  1668. close FILE;
  1669. return $changeLogSuffix;
  1670. }
  1671. sub changeLogFileName()
  1672. {
  1673. return "ChangeLog" . changeLogSuffix()
  1674. }
  1675. sub changeLogNameError($)
  1676. {
  1677. my ($message) = @_;
  1678. print STDERR "$message\nEither:\n";
  1679. print STDERR " set CHANGE_LOG_NAME in your environment\n";
  1680. print STDERR " OR pass --name= on the command line\n";
  1681. print STDERR " OR set REAL_NAME in your environment";
  1682. print STDERR " OR git users can set 'git config user.name'\n";
  1683. exit(1);
  1684. }
  1685. sub changeLogName()
  1686. {
  1687. my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name") || (split /\s*,\s*/, (getpwuid $<)[6])[0];
  1688. changeLogNameError("Failed to determine ChangeLog name.") unless $name;
  1689. # getpwuid seems to always succeed on windows, returning the username instead of the full name. This check will catch that case.
  1690. changeLogNameError("'$name' does not contain a space! ChangeLogs should contain your full name.") unless ($name =~ /\S\s\S/);
  1691. return $name;
  1692. }
  1693. sub changeLogEmailAddressError($)
  1694. {
  1695. my ($message) = @_;
  1696. print STDERR "$message\nEither:\n";
  1697. print STDERR " set CHANGE_LOG_EMAIL_ADDRESS in your environment\n";
  1698. print STDERR " OR pass --email= on the command line\n";
  1699. print STDERR " OR set EMAIL_ADDRESS in your environment\n";
  1700. print STDERR " OR git users can set 'git config user.email'\n";
  1701. exit(1);
  1702. }
  1703. sub changeLogEmailAddress()
  1704. {
  1705. my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email");
  1706. changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress;
  1707. changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/);
  1708. return $emailAddress;
  1709. }
  1710. # http://tools.ietf.org/html/rfc1924
  1711. sub decodeBase85($)
  1712. {
  1713. my ($encoded) = @_;
  1714. my %table;
  1715. my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~');
  1716. for (my $i = 0; $i < 85; $i++) {
  1717. $table{$characters[$i]} = $i;
  1718. }
  1719. my $decoded = '';
  1720. my @encodedChars = $encoded =~ /./g;
  1721. for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) {
  1722. my $digit = 0;
  1723. for (my $i = 0; $i < 5; $i++) {
  1724. $digit *= 85;
  1725. my $char = $encodedChars[$encodedIter];
  1726. $digit += $table{$char};
  1727. $encodedIter++;
  1728. }
  1729. for (my $i = 0; $i < 4; $i++) {
  1730. $decoded .= chr(($digit >> (3 - $i) * 8) & 255);
  1731. }
  1732. }
  1733. return $decoded;
  1734. }
  1735. sub decodeGitBinaryChunk($$)
  1736. {
  1737. my ($contents, $fullPath) = @_;
  1738. # Load this module lazily in case the user don't have this module
  1739. # and won't handle git binary patches.
  1740. require Compress::Zlib;
  1741. my $encoded = "";
  1742. my $compressedSize = 0;
  1743. while ($contents =~ /^([A-Za-z])(.*)$/gm) {
  1744. my $line = $2;
  1745. next if $line eq "";
  1746. die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0;
  1747. my $actualSize = length($2) / 5 * 4;
  1748. my $encodedExpectedSize = ord($1);
  1749. my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27;
  1750. die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize;
  1751. $compressedSize += $expectedSize;
  1752. $encoded .= $line;
  1753. }
  1754. my $compressed = decodeBase85($encoded);
  1755. $compressed = substr($compressed, 0, $compressedSize);
  1756. return Compress::Zlib::uncompress($compressed);
  1757. }
  1758. sub decodeGitBinaryPatch($$)
  1759. {
  1760. my ($contents, $fullPath) = @_;
  1761. # Git binary patch has two chunks. One is for the normal patching
  1762. # and another is for the reverse patching.
  1763. #
  1764. # Each chunk a line which starts from either "literal" or "delta",
  1765. # followed by a number which specifies decoded size of the chunk.
  1766. #
  1767. # Then, content of the chunk comes. To decode the content, we
  1768. # need decode it with base85 first, and then zlib.
  1769. my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n';
  1770. if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp\Z") {
  1771. die "$fullPath: unknown git binary patch format"
  1772. }
  1773. my $binaryChunkType = $1;
  1774. my $binaryChunkExpectedSize = $2;
  1775. my $encodedChunk = $3;
  1776. my $reverseBinaryChunkType = $4;
  1777. my $reverseBinaryChunkExpectedSize = $5;
  1778. my $encodedReverseChunk = $6;
  1779. my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath);
  1780. my $binaryChunkActualSize = length($binaryChunk);
  1781. my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath);
  1782. my $reverseBinaryChunkActualSize = length($reverseBinaryChunk);
  1783. die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkType eq "literal" and $binaryChunkExpectedSize != $binaryChunkActualSize);
  1784. die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkType eq "literal" and $reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize);
  1785. return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk);
  1786. }
  1787. sub readByte($$)
  1788. {
  1789. my ($data, $location) = @_;
  1790. # Return the byte at $location in $data as a numeric value.
  1791. return ord(substr($data, $location, 1));
  1792. }
  1793. # The git binary delta format is undocumented, except in code:
  1794. # - https://github.com/git/git/blob/master/delta.h:get_delta_hdr_size is the source
  1795. # of the algorithm in decodeGitBinaryPatchDeltaSize.
  1796. # - https://github.com/git/git/blob/master/patch-delta.c:patch_delta is the source
  1797. # of the algorithm in applyGitBinaryPatchDelta.
  1798. sub decodeGitBinaryPatchDeltaSize($)
  1799. {
  1800. my ($binaryChunk) = @_;
  1801. # Source and destination buffer sizes are stored in 7-bit chunks at the
  1802. # start of the binary delta patch data. The highest bit in each byte
  1803. # except the last is set; the remaining 7 bits provide the next
  1804. # chunk of the size. The chunks are stored in ascending significance
  1805. # order.
  1806. my $cmd;
  1807. my $size = 0;
  1808. my $shift = 0;
  1809. for (my $i = 0; $i < length($binaryChunk);) {
  1810. $cmd = readByte($binaryChunk, $i++);
  1811. $size |= ($cmd & 0x7f) << $shift;
  1812. $shift += 7;
  1813. if (!($cmd & 0x80)) {
  1814. return ($size, $i);
  1815. }
  1816. }
  1817. }
  1818. sub applyGitBinaryPatchDelta($$)
  1819. {
  1820. my ($binaryChunk, $originalContents) = @_;
  1821. # Git delta format consists of two headers indicating source buffer size
  1822. # and result size, then a series of commands. Each command is either
  1823. # a copy-from-old-version (the 0x80 bit is set) or a copy-from-delta
  1824. # command. Commands are applied sequentially to generate the result.
  1825. #
  1826. # A copy-from-old-version command encodes an offset and size to copy
  1827. # from in subsequent bits, while a copy-from-delta command consists only
  1828. # of the number of bytes to copy from the delta.
  1829. # We don't use these values, but we need to know how big they are so that
  1830. # we can skip to the diff data.
  1831. my ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
  1832. $binaryChunk = substr($binaryChunk, $bytesUsed);
  1833. ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk);
  1834. $binaryChunk = substr($binaryChunk, $bytesUsed);
  1835. my $out = "";
  1836. for (my $i = 0; $i < length($binaryChunk); ) {
  1837. my $cmd = ord(substr($binaryChunk, $i++, 1));
  1838. if ($cmd & 0x80) {
  1839. # Extract an offset and size from the delta data, then copy
  1840. # $size bytes from $offset in the original data into the output.
  1841. my $offset = 0;
  1842. my $size = 0;
  1843. if ($cmd & 0x01) { $offset = readByte($binaryChunk, $i++); }
  1844. if ($cmd & 0x02) { $offset |= readByte($binaryChunk, $i++) << 8; }
  1845. if ($cmd & 0x04) { $offset |= readByte($binaryChunk, $i++) << 16; }
  1846. if ($cmd & 0x08) { $offset |= readByte($binaryChunk, $i++) << 24; }
  1847. if ($cmd & 0x10) { $size = readByte($binaryChunk, $i++); }
  1848. if ($cmd & 0x20) { $size |= readByte($binaryChunk, $i++) << 8; }
  1849. if ($cmd & 0x40) { $size |= readByte($binaryChunk, $i++) << 16; }
  1850. if ($size == 0) { $size = 0x10000; }
  1851. $out .= substr($originalContents, $offset, $size);
  1852. } elsif ($cmd) {
  1853. # Copy $cmd bytes from the delta data into the output.
  1854. $out .= substr($binaryChunk, $i, $cmd);
  1855. $i += $cmd;
  1856. } else {
  1857. die "unexpected delta opcode 0";
  1858. }
  1859. }
  1860. return $out;
  1861. }
  1862. sub escapeSubversionPath($)
  1863. {
  1864. my ($path) = @_;
  1865. $path .= "@" if $path =~ /@/;
  1866. return $path;
  1867. }
  1868. sub runCommand(@)
  1869. {
  1870. my @args = @_;
  1871. my $pid = open(CHILD, "-|");
  1872. if (!defined($pid)) {
  1873. die "Failed to fork(): $!";
  1874. }
  1875. if ($pid) {
  1876. # Parent process
  1877. my $childStdout;
  1878. while (<CHILD>) {
  1879. $childStdout .= $_;
  1880. }
  1881. close(CHILD);
  1882. my %childOutput;
  1883. $childOutput{exitStatus} = exitStatus($?);
  1884. $childOutput{stdout} = $childStdout if $childStdout;
  1885. return \%childOutput;
  1886. }
  1887. # Child process
  1888. # FIXME: Consider further hardening of this function, including sanitizing the environment.
  1889. exec { $args[0] } @args or die "Failed to exec(): $!";
  1890. }
  1891. sub gitCommitForSVNRevision
  1892. {
  1893. my ($svnRevision) = @_;
  1894. my $command = "git svn find-rev r" . $svnRevision;
  1895. $command = "LC_ALL=C $command" if !isWindows();
  1896. my $gitHash = `$command`;
  1897. if (!defined($gitHash)) {
  1898. $gitHash = "unknown";
  1899. warn "Unable to determine GIT commit from SVN revision";
  1900. } else {
  1901. chop($gitHash);
  1902. }
  1903. return $gitHash;
  1904. }
  1905. sub listOfChangedFilesBetweenRevisions
  1906. {
  1907. my ($sourceDir, $firstRevision, $lastRevision) = @_;
  1908. my $command;
  1909. if ($firstRevision eq "unknown" or $lastRevision eq "unknown") {
  1910. return ();
  1911. }
  1912. # Some VCS functions don't work from within the build dir, so always
  1913. # go to the source dir first.
  1914. my $cwd = Cwd::getcwd();
  1915. chdir $sourceDir;
  1916. if (isGit()) {
  1917. my $firstCommit = gitCommitForSVNRevision($firstRevision);
  1918. my $lastCommit = gitCommitForSVNRevision($lastRevision);
  1919. $command = "git diff --name-status $firstCommit..$lastCommit";
  1920. } elsif (isSVN()) {
  1921. $command = "svn diff --summarize -r $firstRevision:$lastRevision";
  1922. }
  1923. my @result = ();
  1924. if ($command) {
  1925. my $diffOutput = `$command`;
  1926. $diffOutput =~ s/^[A-Z]\s+//gm;
  1927. @result = split(/[\r\n]+/, $diffOutput);
  1928. }
  1929. chdir $cwd;
  1930. return @result;
  1931. }
  1932. 1;