[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class DifferentialChangesetParser { 4 5 protected $visible = array(); 6 protected $new = array(); 7 protected $old = array(); 8 protected $intra = array(); 9 protected $newRender = null; 10 protected $oldRender = null; 11 12 protected $filename = null; 13 protected $hunkStartLines = array(); 14 15 protected $comments = array(); 16 protected $specialAttributes = array(); 17 18 protected $changeset; 19 protected $whitespaceMode = null; 20 21 protected $renderCacheKey = null; 22 23 private $handles = array(); 24 private $user; 25 26 private $leftSideChangesetID; 27 private $leftSideAttachesToNewFile; 28 29 private $rightSideChangesetID; 30 private $rightSideAttachesToNewFile; 31 32 private $originalLeft; 33 private $originalRight; 34 35 private $renderingReference; 36 private $isSubparser; 37 38 private $isTopLevel; 39 private $coverage; 40 private $markupEngine; 41 private $highlightErrors; 42 private $disableCache; 43 private $renderer; 44 private $characterEncoding; 45 private $highlightAs; 46 47 public function setHighlightAs($highlight_as) { 48 $this->highlightAs = $highlight_as; 49 return $this; 50 } 51 52 public function getHighlightAs() { 53 return $this->highlightAs; 54 } 55 56 public function setCharacterEncoding($character_encoding) { 57 $this->characterEncoding = $character_encoding; 58 return $this; 59 } 60 61 public function getCharacterEncoding() { 62 return $this->characterEncoding; 63 } 64 65 public function setRenderer($renderer) { 66 $this->renderer = $renderer; 67 return $this; 68 } 69 70 public function getRenderer() { 71 if (!$this->renderer) { 72 return new DifferentialChangesetTwoUpRenderer(); 73 } 74 return $this->renderer; 75 } 76 77 public function setDisableCache($disable_cache) { 78 $this->disableCache = $disable_cache; 79 return $this; 80 } 81 82 public function getDisableCache() { 83 return $this->disableCache; 84 } 85 86 const CACHE_VERSION = 11; 87 const CACHE_MAX_SIZE = 8e6; 88 89 const ATTR_GENERATED = 'attr:generated'; 90 const ATTR_DELETED = 'attr:deleted'; 91 const ATTR_UNCHANGED = 'attr:unchanged'; 92 const ATTR_WHITELINES = 'attr:white'; 93 const ATTR_MOVEAWAY = 'attr:moveaway'; 94 95 const LINES_CONTEXT = 8; 96 97 const WHITESPACE_SHOW_ALL = 'show-all'; 98 const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing'; 99 100 // TODO: This is now "Ignore Most" in the UI. 101 const WHITESPACE_IGNORE_ALL = 'ignore-all'; 102 103 const WHITESPACE_IGNORE_FORCE = 'ignore-force'; 104 105 public function setOldLines(array $lines) { 106 $this->old = $lines; 107 return $this; 108 } 109 110 public function setNewLines(array $lines) { 111 $this->new = $lines; 112 return $this; 113 } 114 115 public function setSpecialAttributes(array $attributes) { 116 $this->specialAttributes = $attributes; 117 return $this; 118 } 119 120 public function setIntraLineDiffs(array $diffs) { 121 $this->intra = $diffs; 122 return $this; 123 } 124 125 public function setVisibileLinesMask(array $mask) { 126 $this->visible = $mask; 127 return $this; 128 } 129 130 /** 131 * Configure which Changeset comments added to the right side of the visible 132 * diff will be attached to. The ID must be the ID of a real Differential 133 * Changeset. 134 * 135 * The complexity here is that we may show an arbitrary side of an arbitrary 136 * changeset as either the left or right part of a diff. This method allows 137 * the left and right halves of the displayed diff to be correctly mapped to 138 * storage changesets. 139 * 140 * @param id The Differential Changeset ID that comments added to the right 141 * side of the visible diff should be attached to. 142 * @param bool If true, attach new comments to the right side of the storage 143 * changeset. Note that this may be false, if the left side of 144 * some storage changeset is being shown as the right side of 145 * a display diff. 146 * @return this 147 */ 148 public function setRightSideCommentMapping($id, $is_new) { 149 $this->rightSideChangesetID = $id; 150 $this->rightSideAttachesToNewFile = $is_new; 151 return $this; 152 } 153 154 /** 155 * See setRightSideCommentMapping(), but this sets information for the left 156 * side of the display diff. 157 */ 158 public function setLeftSideCommentMapping($id, $is_new) { 159 $this->leftSideChangesetID = $id; 160 $this->leftSideAttachesToNewFile = $is_new; 161 return $this; 162 } 163 164 public function setOriginals( 165 DifferentialChangeset $left, 166 DifferentialChangeset $right) { 167 168 $this->originalLeft = $left; 169 $this->originalRight = $right; 170 } 171 172 public function diffOriginals() { 173 $engine = new PhabricatorDifferenceEngine(); 174 $changeset = $engine->generateChangesetFromFileContent( 175 implode('', mpull($this->originalLeft->getHunks(), 'getChanges')), 176 implode('', mpull($this->originalRight->getHunks(), 'getChanges'))); 177 178 $parser = new DifferentialHunkParser(); 179 180 return $parser->parseHunksForHighlightMasks( 181 $changeset->getHunks(), 182 $this->originalLeft->getHunks(), 183 $this->originalRight->getHunks()); 184 } 185 186 /** 187 * Set a key for identifying this changeset in the render cache. If set, the 188 * parser will attempt to use the changeset render cache, which can improve 189 * performance for frequently-viewed changesets. 190 * 191 * By default, there is no render cache key and parsers do not use the cache. 192 * This is appropriate for rarely-viewed changesets. 193 * 194 * NOTE: Currently, this key must be a valid Differential Changeset ID. 195 * 196 * @param string Key for identifying this changeset in the render cache. 197 * @return this 198 */ 199 public function setRenderCacheKey($key) { 200 $this->renderCacheKey = $key; 201 return $this; 202 } 203 204 private function getRenderCacheKey() { 205 return $this->renderCacheKey; 206 } 207 208 public function setChangeset(DifferentialChangeset $changeset) { 209 $this->changeset = $changeset; 210 211 $this->setFilename($changeset->getFilename()); 212 213 return $this; 214 } 215 216 public function setWhitespaceMode($whitespace_mode) { 217 $this->whitespaceMode = $whitespace_mode; 218 return $this; 219 } 220 221 public function setRenderingReference($ref) { 222 $this->renderingReference = $ref; 223 return $this; 224 } 225 226 private function getRenderingReference() { 227 return $this->renderingReference; 228 } 229 230 public function getChangeset() { 231 return $this->changeset; 232 } 233 234 public function setFilename($filename) { 235 $this->filename = $filename; 236 return $this; 237 } 238 239 public function setHandles(array $handles) { 240 assert_instances_of($handles, 'PhabricatorObjectHandle'); 241 $this->handles = $handles; 242 return $this; 243 } 244 245 public function setMarkupEngine(PhabricatorMarkupEngine $engine) { 246 $this->markupEngine = $engine; 247 return $this; 248 } 249 250 public function setUser(PhabricatorUser $user) { 251 $this->user = $user; 252 return $this; 253 } 254 255 public function setCoverage($coverage) { 256 $this->coverage = $coverage; 257 return $this; 258 } 259 private function getCoverage() { 260 return $this->coverage; 261 } 262 263 public function parseInlineComment( 264 PhabricatorInlineCommentInterface $comment) { 265 266 // Parse only comments which are actually visible. 267 if ($this->isCommentVisibleOnRenderedDiff($comment)) { 268 $this->comments[] = $comment; 269 } 270 return $this; 271 } 272 273 private function loadCache() { 274 $render_cache_key = $this->getRenderCacheKey(); 275 if (!$render_cache_key) { 276 return false; 277 } 278 279 $data = null; 280 281 $changeset = new DifferentialChangeset(); 282 $conn_r = $changeset->establishConnection('r'); 283 $data = queryfx_one( 284 $conn_r, 285 'SELECT * FROM %T WHERE id = %d', 286 $changeset->getTableName().'_parse_cache', 287 $render_cache_key); 288 289 if (!$data) { 290 return false; 291 } 292 293 if ($data['cache'][0] == '{') { 294 // This is likely an old-style JSON cache which we will not be able to 295 // deserialize. 296 return false; 297 } 298 299 $data = unserialize($data['cache']); 300 if (!is_array($data) || !$data) { 301 return false; 302 } 303 304 foreach (self::getCacheableProperties() as $cache_key) { 305 if (!array_key_exists($cache_key, $data)) { 306 // If we're missing a cache key, assume we're looking at an old cache 307 // and ignore it. 308 return false; 309 } 310 } 311 312 if ($data['cacheVersion'] !== self::CACHE_VERSION) { 313 return false; 314 } 315 316 // Someone displays contents of a partially cached shielded file. 317 if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) { 318 return false; 319 } 320 321 unset($data['cacheVersion'], $data['cacheHost']); 322 $cache_prop = array_select_keys($data, self::getCacheableProperties()); 323 foreach ($cache_prop as $cache_key => $v) { 324 $this->$cache_key = $v; 325 } 326 327 return true; 328 } 329 330 protected static function getCacheableProperties() { 331 return array( 332 'visible', 333 'new', 334 'old', 335 'intra', 336 'newRender', 337 'oldRender', 338 'specialAttributes', 339 'hunkStartLines', 340 'cacheVersion', 341 'cacheHost', 342 ); 343 } 344 345 public function saveCache() { 346 if ($this->highlightErrors) { 347 return false; 348 } 349 350 $render_cache_key = $this->getRenderCacheKey(); 351 if (!$render_cache_key) { 352 return false; 353 } 354 355 $cache = array(); 356 foreach (self::getCacheableProperties() as $cache_key) { 357 switch ($cache_key) { 358 case 'cacheVersion': 359 $cache[$cache_key] = self::CACHE_VERSION; 360 break; 361 case 'cacheHost': 362 $cache[$cache_key] = php_uname('n'); 363 break; 364 default: 365 $cache[$cache_key] = $this->$cache_key; 366 break; 367 } 368 } 369 $cache = serialize($cache); 370 371 // We don't want to waste too much space by a single changeset. 372 if (strlen($cache) > self::CACHE_MAX_SIZE) { 373 return; 374 } 375 376 $changeset = new DifferentialChangeset(); 377 $conn_w = $changeset->establishConnection('w'); 378 379 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 380 try { 381 queryfx( 382 $conn_w, 383 'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d) 384 ON DUPLICATE KEY UPDATE cache = VALUES(cache)', 385 DifferentialChangeset::TABLE_CACHE, 386 $render_cache_key, 387 $cache, 388 time()); 389 } catch (AphrontQueryException $ex) { 390 // Ignore these exceptions. A common cause is that the cache is 391 // larger than 'max_allowed_packet', in which case we're better off 392 // not writing it. 393 394 // TODO: It would be nice to tailor this more narrowly. 395 } 396 unset($unguarded); 397 } 398 399 private function markGenerated($new_corpus_block = '') { 400 $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false); 401 402 if (!$generated_guess) { 403 $generated_path_regexps = PhabricatorEnv::getEnvConfig( 404 'differential.generated-paths'); 405 foreach ($generated_path_regexps as $regexp) { 406 if (preg_match($regexp, $this->changeset->getFilename())) { 407 $generated_guess = true; 408 break; 409 } 410 } 411 } 412 413 $event = new PhabricatorEvent( 414 PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED, 415 array( 416 'corpus' => $new_corpus_block, 417 'is_generated' => $generated_guess, 418 ) 419 ); 420 PhutilEventEngine::dispatchEvent($event); 421 422 $generated = $event->getValue('is_generated'); 423 $this->specialAttributes[self::ATTR_GENERATED] = $generated; 424 } 425 426 public function isGenerated() { 427 return idx($this->specialAttributes, self::ATTR_GENERATED, false); 428 } 429 430 public function isDeleted() { 431 return idx($this->specialAttributes, self::ATTR_DELETED, false); 432 } 433 434 public function isUnchanged() { 435 return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); 436 } 437 438 public function isWhitespaceOnly() { 439 return idx($this->specialAttributes, self::ATTR_WHITELINES, false); 440 } 441 442 public function isMoveAway() { 443 return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); 444 } 445 446 private function applyIntraline(&$render, $intra, $corpus) { 447 448 foreach ($render as $key => $text) { 449 if (isset($intra[$key])) { 450 $render[$key] = ArcanistDiffUtils::applyIntralineDiff( 451 $text, 452 $intra[$key]); 453 } 454 } 455 } 456 457 private function getHighlightFuture($corpus) { 458 $language = $this->highlightAs; 459 460 if (!$language) { 461 $language = $this->highlightEngine->getLanguageFromFilename( 462 $this->filename); 463 } 464 465 return $this->highlightEngine->getHighlightFuture( 466 $language, 467 $corpus); 468 } 469 470 protected function processHighlightedSource($data, $result) { 471 472 $result_lines = phutil_split_lines($result); 473 foreach ($data as $key => $info) { 474 if (!$info) { 475 unset($result_lines[$key]); 476 } 477 } 478 return $result_lines; 479 } 480 481 private function tryCacheStuff() { 482 $whitespace_mode = $this->whitespaceMode; 483 switch ($whitespace_mode) { 484 case self::WHITESPACE_SHOW_ALL: 485 case self::WHITESPACE_IGNORE_TRAILING: 486 case self::WHITESPACE_IGNORE_FORCE: 487 break; 488 default: 489 $whitespace_mode = self::WHITESPACE_IGNORE_ALL; 490 break; 491 } 492 493 $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL); 494 if ($this->disableCache) { 495 $skip_cache = true; 496 } 497 498 if ($this->characterEncoding) { 499 $skip_cache = true; 500 } 501 502 if ($this->highlightAs) { 503 $skip_cache = true; 504 } 505 506 $this->whitespaceMode = $whitespace_mode; 507 508 $changeset = $this->changeset; 509 510 if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT && 511 $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) { 512 513 $this->markGenerated(); 514 515 } else { 516 if ($skip_cache || !$this->loadCache()) { 517 $this->process(); 518 if (!$skip_cache) { 519 $this->saveCache(); 520 } 521 } 522 } 523 } 524 525 private function process() { 526 $whitespace_mode = $this->whitespaceMode; 527 $changeset = $this->changeset; 528 529 $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_ALL) || 530 ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE)); 531 532 $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE); 533 534 if (!$force_ignore) { 535 if ($ignore_all && $changeset->getWhitespaceMatters()) { 536 $ignore_all = false; 537 } 538 } 539 540 // The "ignore all whitespace" algorithm depends on rediffing the 541 // files, and we currently need complete representations of both 542 // files to do anything reasonable. If we only have parts of the files, 543 // don't use the "ignore all" algorithm. 544 if ($ignore_all) { 545 $hunks = $changeset->getHunks(); 546 if (count($hunks) !== 1) { 547 $ignore_all = false; 548 } else { 549 $first_hunk = reset($hunks); 550 if ($first_hunk->getOldOffset() != 1 || 551 $first_hunk->getNewOffset() != 1) { 552 $ignore_all = false; 553 } 554 } 555 } 556 557 if ($ignore_all) { 558 $old_file = $changeset->makeOldFile(); 559 $new_file = $changeset->makeNewFile(); 560 if ($old_file == $new_file) { 561 // If the old and new files are exactly identical, the synthetic 562 // diff below will give us nonsense and whitespace modes are 563 // irrelevant anyway. This occurs when you, e.g., copy a file onto 564 // itself in Subversion (see T271). 565 $ignore_all = false; 566 } 567 } 568 569 $hunk_parser = new DifferentialHunkParser(); 570 $hunk_parser->setWhitespaceMode($whitespace_mode); 571 $hunk_parser->parseHunksForLineData($changeset->getHunks()); 572 573 // Depending on the whitespace mode, we may need to compute a different 574 // set of changes than the set of changes in the hunk data (specificaly, 575 // we might want to consider changed lines which have only whitespace 576 // changes as unchanged). 577 if ($ignore_all) { 578 $engine = new PhabricatorDifferenceEngine(); 579 $engine->setIgnoreWhitespace(true); 580 $no_whitespace_changeset = $engine->generateChangesetFromFileContent( 581 $old_file, 582 $new_file); 583 584 $type_parser = new DifferentialHunkParser(); 585 $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks()); 586 587 $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); 588 $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); 589 } 590 591 $hunk_parser->reparseHunksForSpecialAttributes(); 592 593 $unchanged = false; 594 if (!$hunk_parser->getHasAnyChanges()) { 595 $filetype = $this->changeset->getFileType(); 596 if ($filetype == DifferentialChangeType::FILE_TEXT || 597 $filetype == DifferentialChangeType::FILE_SYMLINK) { 598 $unchanged = true; 599 } 600 } 601 602 $moveaway = false; 603 $changetype = $this->changeset->getChangeType(); 604 if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) { 605 // sometimes we show moved files as unchanged, sometimes deleted, 606 // and sometimes inconsistent with what actually happened at the 607 // destination of the move. Rather than make a false claim, 608 // omit the 'not changed' notice if this is the source of a move 609 $unchanged = false; 610 $moveaway = true; 611 } 612 613 $this->setSpecialAttributes(array( 614 self::ATTR_UNCHANGED => $unchanged, 615 self::ATTR_DELETED => $hunk_parser->getIsDeleted(), 616 self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), 617 self::ATTR_MOVEAWAY => $moveaway, 618 )); 619 620 $hunk_parser->generateIntraLineDiffs(); 621 $hunk_parser->generateVisibileLinesMask(); 622 623 $this->setOldLines($hunk_parser->getOldLines()); 624 $this->setNewLines($hunk_parser->getNewLines()); 625 $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); 626 $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); 627 $this->hunkStartLines = $hunk_parser->getHunkStartLines( 628 $changeset->getHunks()); 629 630 $new_corpus = $hunk_parser->getNewCorpus(); 631 $new_corpus_block = implode('', $new_corpus); 632 $this->markGenerated($new_corpus_block); 633 634 if ($this->isTopLevel && 635 !$this->comments && 636 ($this->isGenerated() || 637 $this->isUnchanged() || 638 $this->isDeleted())) { 639 return; 640 } 641 642 $old_corpus = $hunk_parser->getOldCorpus(); 643 $old_corpus_block = implode('', $old_corpus); 644 $old_future = $this->getHighlightFuture($old_corpus_block); 645 $new_future = $this->getHighlightFuture($new_corpus_block); 646 $futures = array( 647 'old' => $old_future, 648 'new' => $new_future, 649 ); 650 $corpus_blocks = array( 651 'old' => $old_corpus_block, 652 'new' => $new_corpus_block, 653 ); 654 655 $this->highlightErrors = false; 656 foreach (Futures($futures) as $key => $future) { 657 try { 658 try { 659 $highlighted = $future->resolve(); 660 } catch (PhutilSyntaxHighlighterException $ex) { 661 $this->highlightErrors = true; 662 $highlighted = id(new PhutilDefaultSyntaxHighlighter()) 663 ->getHighlightFuture($corpus_blocks[$key]) 664 ->resolve(); 665 } 666 switch ($key) { 667 case 'old': 668 $this->oldRender = $this->processHighlightedSource( 669 $this->old, 670 $highlighted); 671 break; 672 case 'new': 673 $this->newRender = $this->processHighlightedSource( 674 $this->new, 675 $highlighted); 676 break; 677 } 678 } catch (Exception $ex) { 679 phlog($ex); 680 throw $ex; 681 } 682 } 683 684 $this->applyIntraline( 685 $this->oldRender, 686 ipull($this->intra, 0), 687 $old_corpus); 688 $this->applyIntraline( 689 $this->newRender, 690 ipull($this->intra, 1), 691 $new_corpus); 692 } 693 694 private function shouldRenderPropertyChangeHeader($changeset) { 695 if (!$this->isTopLevel) { 696 // We render properties only at top level; otherwise we get multiple 697 // copies of them when a user clicks "Show More". 698 return false; 699 } 700 701 return true; 702 } 703 704 public function render( 705 $range_start = null, 706 $range_len = null, 707 $mask_force = array()) { 708 709 // "Top level" renders are initial requests for the whole file, versus 710 // requests for a specific range generated by clicking "show more". We 711 // generate property changes and "shield" UI elements only for toplevel 712 // requests. 713 $this->isTopLevel = (($range_start === null) && ($range_len === null)); 714 $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine(); 715 716 $encoding = null; 717 if ($this->characterEncoding) { 718 // We are forcing this changeset to be interpreted with a specific 719 // character encoding, so force all the hunks into that encoding and 720 // propagate it to the renderer. 721 $encoding = $this->characterEncoding; 722 foreach ($this->changeset->getHunks() as $hunk) { 723 $hunk->forceEncoding($this->characterEncoding); 724 } 725 } else { 726 // We're just using the default, so tell the renderer what that is 727 // (by reading the encoding from the first hunk). 728 foreach ($this->changeset->getHunks() as $hunk) { 729 $encoding = $hunk->getDataEncoding(); 730 break; 731 } 732 } 733 734 $this->tryCacheStuff(); 735 $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset); 736 737 $rows = max( 738 count($this->old), 739 count($this->new)); 740 741 $renderer = $this->getRenderer() 742 ->setChangeset($this->changeset) 743 ->setRenderPropertyChangeHeader($render_pch) 744 ->setIsTopLevel($this->isTopLevel) 745 ->setOldRender($this->oldRender) 746 ->setNewRender($this->newRender) 747 ->setHunkStartLines($this->hunkStartLines) 748 ->setOldChangesetID($this->leftSideChangesetID) 749 ->setNewChangesetID($this->rightSideChangesetID) 750 ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile) 751 ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile) 752 ->setCodeCoverage($this->getCoverage()) 753 ->setRenderingReference($this->getRenderingReference()) 754 ->setMarkupEngine($this->markupEngine) 755 ->setHandles($this->handles) 756 ->setOldLines($this->old) 757 ->setNewLines($this->new) 758 ->setOriginalCharacterEncoding($encoding); 759 760 if ($this->user) { 761 $renderer->setUser($this->user); 762 } 763 764 $shield = null; 765 if ($this->isTopLevel && !$this->comments) { 766 if ($this->isGenerated()) { 767 $shield = $renderer->renderShield( 768 pht( 769 'This file contains generated code, which does not normally '. 770 'need to be reviewed.')); 771 } else if ($this->isUnchanged()) { 772 $type = 'text'; 773 if (!$rows) { 774 // NOTE: Normally, diffs which don't change files do not include 775 // file content (for example, if you "chmod +x" a file and then 776 // run "git show", the file content is not available). Similarly, 777 // if you move a file from A to B without changing it, diffs normally 778 // do not show the file content. In some cases `arc` is able to 779 // synthetically generate content for these diffs, but for raw diffs 780 // we'll never have it so we need to be prepared to not render a link. 781 $type = 'none'; 782 } 783 $shield = $renderer->renderShield( 784 pht('The contents of this file were not changed.'), 785 $type); 786 } else if ($this->isMoveAway()) { 787 $shield = null; 788 } else if ($this->isWhitespaceOnly()) { 789 $shield = $renderer->renderShield( 790 pht('This file was changed only by adding or removing whitespace.'), 791 'whitespace'); 792 } else if ($this->isDeleted()) { 793 $shield = $renderer->renderShield( 794 pht('This file was completely deleted.')); 795 } else if ($this->changeset->getAffectedLineCount() > 2500) { 796 $lines = number_format($this->changeset->getAffectedLineCount()); 797 $shield = $renderer->renderShield( 798 pht( 799 'This file has a very large number of changes (%s lines).', 800 $lines)); 801 } 802 } 803 804 if ($shield) { 805 return $renderer->renderChangesetTable($shield); 806 } 807 808 $old_comments = array(); 809 $new_comments = array(); 810 $old_mask = array(); 811 $new_mask = array(); 812 $feedback_mask = array(); 813 814 if ($this->comments) { 815 foreach ($this->comments as $comment) { 816 $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0); 817 $end = $comment->getLineNumber() + 818 $comment->getLineLength() + 819 self::LINES_CONTEXT; 820 $new_side = $this->isCommentOnRightSideWhenDisplayed($comment); 821 for ($ii = $start; $ii <= $end; $ii++) { 822 if ($new_side) { 823 $new_mask[$ii] = true; 824 } else { 825 $old_mask[$ii] = true; 826 } 827 } 828 } 829 830 foreach ($this->old as $ii => $old) { 831 if (isset($old['line']) && isset($old_mask[$old['line']])) { 832 $feedback_mask[$ii] = true; 833 } 834 } 835 836 foreach ($this->new as $ii => $new) { 837 if (isset($new['line']) && isset($new_mask[$new['line']])) { 838 $feedback_mask[$ii] = true; 839 } 840 } 841 $this->comments = msort($this->comments, 'getID'); 842 foreach ($this->comments as $comment) { 843 $final = $comment->getLineNumber() + 844 $comment->getLineLength(); 845 $final = max(1, $final); 846 if ($this->isCommentOnRightSideWhenDisplayed($comment)) { 847 $new_comments[$final][] = $comment; 848 } else { 849 $old_comments[$final][] = $comment; 850 } 851 } 852 } 853 $renderer 854 ->setOldComments($old_comments) 855 ->setNewComments($new_comments); 856 857 switch ($this->changeset->getFileType()) { 858 case DifferentialChangeType::FILE_IMAGE: 859 $old = null; 860 $new = null; 861 // TODO: Improve the architectural issue as discussed in D955 862 // https://secure.phabricator.com/D955 863 $reference = $this->getRenderingReference(); 864 $parts = explode('/', $reference); 865 if (count($parts) == 2) { 866 list($id, $vs) = $parts; 867 } else { 868 $id = $parts[0]; 869 $vs = 0; 870 } 871 $id = (int)$id; 872 $vs = (int)$vs; 873 874 if (!$vs) { 875 $metadata = $this->changeset->getMetadata(); 876 $data = idx($metadata, 'attachment-data'); 877 878 $old_phid = idx($metadata, 'old:binary-phid'); 879 $new_phid = idx($metadata, 'new:binary-phid'); 880 } else { 881 $vs_changeset = id(new DifferentialChangeset())->load($vs); 882 $old_phid = null; 883 $new_phid = null; 884 885 // TODO: This is spooky, see D6851 886 if ($vs_changeset) { 887 $vs_metadata = $vs_changeset->getMetadata(); 888 $old_phid = idx($vs_metadata, 'new:binary-phid'); 889 } 890 891 $changeset = id(new DifferentialChangeset())->load($id); 892 if ($changeset) { 893 $metadata = $changeset->getMetadata(); 894 $new_phid = idx($metadata, 'new:binary-phid'); 895 } 896 } 897 898 if ($old_phid || $new_phid) { 899 // grab the files, (micro) optimization for 1 query not 2 900 $file_phids = array(); 901 if ($old_phid) { 902 $file_phids[] = $old_phid; 903 } 904 if ($new_phid) { 905 $file_phids[] = $new_phid; 906 } 907 908 // TODO: (T603) Probably fine to use omnipotent viewer here? 909 $files = id(new PhabricatorFile())->loadAllWhere( 910 'phid IN (%Ls)', 911 $file_phids); 912 foreach ($files as $file) { 913 if (empty($file)) { 914 continue; 915 } 916 if ($file->getPHID() == $old_phid) { 917 $old = $file; 918 } else if ($file->getPHID() == $new_phid) { 919 $new = $file; 920 } 921 } 922 } 923 924 $renderer->attachOldFile($old); 925 $renderer->attachNewFile($new); 926 927 return $renderer->renderFileChange($old, $new, $id, $vs); 928 case DifferentialChangeType::FILE_DIRECTORY: 929 case DifferentialChangeType::FILE_BINARY: 930 $output = $renderer->renderChangesetTable(null); 931 return $output; 932 } 933 934 if ($this->originalLeft && $this->originalRight) { 935 list($highlight_old, $highlight_new) = $this->diffOriginals(); 936 $highlight_old = array_flip($highlight_old); 937 $highlight_new = array_flip($highlight_new); 938 $renderer 939 ->setHighlightOld($highlight_old) 940 ->setHighlightNew($highlight_new); 941 } 942 $renderer 943 ->setOriginalOld($this->originalLeft) 944 ->setOriginalNew($this->originalRight); 945 946 if ($range_start === null) { 947 $range_start = 0; 948 } 949 if ($range_len === null) { 950 $range_len = $rows; 951 } 952 $range_len = min($range_len, $rows - $range_start); 953 954 list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths( 955 $mask_force, 956 $feedback_mask, 957 $range_start, 958 $range_len); 959 960 $renderer 961 ->setGaps($gaps) 962 ->setMask($mask) 963 ->setDepths($depths); 964 965 $html = $renderer->renderTextChange( 966 $range_start, 967 $range_len, 968 $rows); 969 970 return $renderer->renderChangesetTable($html); 971 } 972 973 /** 974 * This function calculates a lot of stuff we need to know to display 975 * the diff: 976 * 977 * Gaps - compute gaps in the visible display diff, where we will render 978 * "Show more context" spacers. If a gap is smaller than the context size, 979 * we just display it. Otherwise, we record it into $gaps and will render a 980 * "show more context" element instead of diff text below. A given $gap 981 * is a tuple of $gap_line_number_start and $gap_length. 982 * 983 * Mask - compute the actual lines that need to be shown (because they 984 * are near changes lines, near inline comments, or the request has 985 * explicitly asked for them, i.e. resulting from the user clicking 986 * "show more"). The $mask returned is a sparesely populated dictionary 987 * of $visible_line_number => true. 988 * 989 * Depths - compute how indented any given line is. The $depths returned 990 * is a sparesely populated dictionary of $visible_line_number => $depth. 991 * 992 * This function also has the side effect of modifying member variable 993 * new such that tabs are normalized to spaces for each line of the diff. 994 * 995 * @return array($gaps, $mask, $depths) 996 */ 997 private function calculateGapsMaskAndDepths($mask_force, 998 $feedback_mask, 999 $range_start, 1000 $range_len) { 1001 1002 // Calculate gaps and mask first 1003 $gaps = array(); 1004 $gap_start = 0; 1005 $in_gap = false; 1006 $base_mask = $this->visible + $mask_force + $feedback_mask; 1007 $base_mask[$range_start + $range_len] = true; 1008 for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) { 1009 if (isset($base_mask[$ii])) { 1010 if ($in_gap) { 1011 $gap_length = $ii - $gap_start; 1012 if ($gap_length <= self::LINES_CONTEXT) { 1013 for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) { 1014 $base_mask[$jj] = true; 1015 } 1016 } else { 1017 $gaps[] = array($gap_start, $gap_length); 1018 } 1019 $in_gap = false; 1020 } 1021 } else { 1022 if (!$in_gap) { 1023 $gap_start = $ii; 1024 $in_gap = true; 1025 } 1026 } 1027 } 1028 $gaps = array_reverse($gaps); 1029 $mask = $base_mask; 1030 1031 // Time to calculate depth. 1032 // We need to go backwards to properly indent whitespace in this code: 1033 // 1034 // 0: class C { 1035 // 1: 1036 // 1: function f() { 1037 // 2: 1038 // 2: return; 1039 // 1: 1040 // 1: } 1041 // 0: 1042 // 0: } 1043 // 1044 $depths = array(); 1045 $last_depth = 0; 1046 $range_end = $range_start + $range_len; 1047 if (!isset($this->new[$range_end])) { 1048 $range_end--; 1049 } 1050 for ($ii = $range_end; $ii >= $range_start; $ii--) { 1051 // We need to expand tabs to process mixed indenting and to round 1052 // correctly later. 1053 $line = str_replace("\t", ' ', $this->new[$ii]['text']); 1054 $trimmed = ltrim($line); 1055 if ($trimmed != '') { 1056 // We round down to flatten "/**" and " *". 1057 $last_depth = floor((strlen($line) - strlen($trimmed)) / 2); 1058 } 1059 $depths[$ii] = $last_depth; 1060 } 1061 1062 return array($gaps, $mask, $depths); 1063 } 1064 1065 /** 1066 * Determine if an inline comment will appear on the rendered diff, 1067 * taking into consideration which halves of which changesets will actually 1068 * be shown. 1069 * 1070 * @param PhabricatorInlineCommentInterface Comment to test for visibility. 1071 * @return bool True if the comment is visible on the rendered diff. 1072 */ 1073 private function isCommentVisibleOnRenderedDiff( 1074 PhabricatorInlineCommentInterface $comment) { 1075 1076 $changeset_id = $comment->getChangesetID(); 1077 $is_new = $comment->getIsNewFile(); 1078 1079 if ($changeset_id == $this->rightSideChangesetID && 1080 $is_new == $this->rightSideAttachesToNewFile) { 1081 return true; 1082 } 1083 1084 if ($changeset_id == $this->leftSideChangesetID && 1085 $is_new == $this->leftSideAttachesToNewFile) { 1086 return true; 1087 } 1088 1089 return false; 1090 } 1091 1092 1093 /** 1094 * Determine if a comment will appear on the right side of the display diff. 1095 * Note that the comment must appear somewhere on the rendered changeset, as 1096 * per isCommentVisibleOnRenderedDiff(). 1097 * 1098 * @param PhabricatorInlineCommentInterface Comment to test for display 1099 * location. 1100 * @return bool True for right, false for left. 1101 */ 1102 private function isCommentOnRightSideWhenDisplayed( 1103 PhabricatorInlineCommentInterface $comment) { 1104 1105 if (!$this->isCommentVisibleOnRenderedDiff($comment)) { 1106 throw new Exception('Comment is not visible on changeset!'); 1107 } 1108 1109 $changeset_id = $comment->getChangesetID(); 1110 $is_new = $comment->getIsNewFile(); 1111 1112 if ($changeset_id == $this->rightSideChangesetID && 1113 $is_new == $this->rightSideAttachesToNewFile) { 1114 return true; 1115 } 1116 1117 return false; 1118 } 1119 1120 /** 1121 * Parse the 'range' specification that this class and the client-side JS 1122 * emit to indicate that a user clicked "Show more..." on a diff. Generally, 1123 * use is something like this: 1124 * 1125 * $spec = $request->getStr('range'); 1126 * $parsed = DifferentialChangesetParser::parseRangeSpecification($spec); 1127 * list($start, $end, $mask) = $parsed; 1128 * $parser->render($start, $end, $mask); 1129 * 1130 * @param string Range specification, indicating the range of the diff that 1131 * should be rendered. 1132 * @return tuple List of <start, end, mask> suitable for passing to 1133 * @{method:render}. 1134 */ 1135 public static function parseRangeSpecification($spec) { 1136 $range_s = null; 1137 $range_e = null; 1138 $mask = array(); 1139 1140 if ($spec) { 1141 $match = null; 1142 if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) { 1143 $range_s = (int)$match[1]; 1144 $range_e = (int)$match[2]; 1145 if (count($match) > 3) { 1146 $start = (int)$match[3]; 1147 $len = (int)$match[4]; 1148 for ($ii = $start; $ii < $start + $len; $ii++) { 1149 $mask[$ii] = true; 1150 } 1151 } 1152 } 1153 } 1154 1155 return array($range_s, $range_e, $mask); 1156 } 1157 1158 /** 1159 * Render "modified coverage" information; test coverage on modified lines. 1160 * This synthesizes diff information with unit test information into a useful 1161 * indicator of how well tested a change is. 1162 */ 1163 public function renderModifiedCoverage() { 1164 $na = phutil_tag('em', array(), '-'); 1165 1166 $coverage = $this->getCoverage(); 1167 if (!$coverage) { 1168 return $na; 1169 } 1170 1171 $covered = 0; 1172 $not_covered = 0; 1173 1174 foreach ($this->new as $k => $new) { 1175 if (!$new['line']) { 1176 continue; 1177 } 1178 1179 if (!$new['type']) { 1180 continue; 1181 } 1182 1183 if (empty($coverage[$new['line'] - 1])) { 1184 continue; 1185 } 1186 1187 switch ($coverage[$new['line'] - 1]) { 1188 case 'C': 1189 $covered++; 1190 break; 1191 case 'U': 1192 $not_covered++; 1193 break; 1194 } 1195 } 1196 1197 if (!$covered && !$not_covered) { 1198 return $na; 1199 } 1200 1201 return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); 1202 } 1203 1204 public function detectCopiedCode( 1205 array $changesets, 1206 $min_width = 30, 1207 $min_lines = 3) { 1208 1209 assert_instances_of($changesets, 'DifferentialChangeset'); 1210 1211 $map = array(); 1212 $files = array(); 1213 $types = array(); 1214 foreach ($changesets as $changeset) { 1215 $file = $changeset->getFilename(); 1216 foreach ($changeset->getHunks() as $hunk) { 1217 $line = $hunk->getOldOffset(); 1218 foreach (explode("\n", $hunk->getChanges()) as $code) { 1219 $type = (isset($code[0]) ? $code[0] : ''); 1220 if ($type == '-' || $type == ' ') { 1221 $code = trim(substr($code, 1)); 1222 $files[$file][$line] = $code; 1223 $types[$file][$line] = $type; 1224 if (strlen($code) >= $min_width) { 1225 $map[$code][] = array($file, $line); 1226 } 1227 $line++; 1228 } 1229 } 1230 } 1231 } 1232 1233 foreach ($changesets as $changeset) { 1234 $copies = array(); 1235 foreach ($changeset->getHunks() as $hunk) { 1236 $added = array_map('trim', $hunk->getAddedLines()); 1237 for (reset($added); list($line, $code) = each($added); ) { 1238 if (isset($map[$code])) { // We found a long matching line. 1239 1240 if (count($map[$code]) > 16) { 1241 // If there are a large number of identical lines in this diff, 1242 // don't try to figure out where this block came from: the 1243 // analysis is O(N^2), since we need to compare every line 1244 // against every other line. Even if we arrive at a result, it 1245 // is unlikely to be meaningful. See T5041. 1246 continue 2; 1247 } 1248 1249 $best_length = 0; 1250 foreach ($map[$code] as $val) { // Explore all candidates. 1251 list($file, $orig_line) = $val; 1252 $length = 1; 1253 // Search also backwards for short lines. 1254 foreach (array(-1, 1) as $direction) { 1255 $offset = $direction; 1256 while (!isset($copies[$line + $offset]) && 1257 isset($added[$line + $offset]) && 1258 idx($files[$file], $orig_line + $offset) === 1259 $added[$line + $offset]) { 1260 $length++; 1261 $offset += $direction; 1262 } 1263 } 1264 if ($length > $best_length || 1265 ($length == $best_length && // Prefer moves. 1266 idx($types[$file], $orig_line) == '-')) { 1267 $best_length = $length; 1268 // ($offset - 1) contains number of forward matching lines. 1269 $best_offset = $offset - 1; 1270 $best_file = $file; 1271 $best_line = $orig_line; 1272 } 1273 } 1274 $file = ($best_file == $changeset->getFilename() ? '' : $best_file); 1275 for ($i = $best_length; $i--; ) { 1276 $type = idx($types[$best_file], $best_line + $best_offset - $i); 1277 $copies[$line + $best_offset - $i] = ($best_length < $min_lines 1278 ? array() // Ignore short blocks. 1279 : array($file, $best_line + $best_offset - $i, $type)); 1280 } 1281 for ($i = 0; $i < $best_offset; $i++) { 1282 next($added); 1283 } 1284 } 1285 } 1286 } 1287 $copies = array_filter($copies); 1288 if ($copies) { 1289 $metadata = $changeset->getMetadata(); 1290 $metadata['copy:lines'] = $copies; 1291 $changeset->setMetadata($metadata); 1292 } 1293 } 1294 return $changesets; 1295 } 1296 1297 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |