[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/differential/parser/ -> DifferentialChangesetParser.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1