map; } public function getNearestMap() { if ($this->nearestMap === null) { $this->buildNearestMap(); } return $this->nearestMap; } public function getFinalOffset() { // Make sure we've built this map already. $this->getNearestMap(); return $this->finalOffset; } /** * Add a map to the end of the chain. * * When a line is mapped with @{method:mapLine}, it is mapped through all * maps in the chain. */ public function addMapToChain(DifferentialLineAdjustmentMap $map) { if ($this->nextMapInChain) { $this->nextMapInChain->addMapToChain($map); } else { $this->nextMapInChain = $map; } return $this; } /** * Map a line across a change, or a series of changes. * * @param int Line to map * @param bool True to map it as the end of a range. * @return wild Spooky magic. */ public function mapLine($line, $is_end) { $nmap = $this->getNearestMap(); $deleted = false; $offset = false; if (isset($nmap[$line])) { $line_range = $nmap[$line]; if ($is_end) { $to_line = end($line_range); } else { $to_line = reset($line_range); } if ($to_line <= 0) { // If we're tracing the first line and this block is collapsing, // compute the offset from the top of the block. if (!$is_end && $this->isInverse) { $offset = 0; $cursor = $line - 1; while (isset($nmap[$cursor])) { $prev = $nmap[$cursor]; $prev = reset($prev); if ($prev == $to_line) { $offset++; } else { break; } $cursor--; } } $to_line = -$to_line; if (!$this->isInverse) { $deleted = true; } } $line = $to_line; } else { $line = $line + $this->finalOffset; } if ($this->nextMapInChain) { $chain = $this->nextMapInChain->mapLine($line, $is_end); list($chain_deleted, $chain_offset, $line) = $chain; $deleted = ($deleted || $chain_deleted); if ($chain_offset !== false) { if ($offset === false) { $offset = 0; } $offset += $chain_offset; } } return array($deleted, $offset, $line); } /** * Build a derived map which maps deleted lines to the nearest valid line. * * This computes a "nearest line" map and a final-line offset. These * derived maps allow us to map deleted code to the previous (or next) line * which actually exists. */ private function buildNearestMap() { $map = $this->map; $nmap = array(); $nearest = 0; foreach ($map as $key => $value) { if ($value) { $nmap[$key] = $value; $nearest = end($value); } else { $nmap[$key][0] = -$nearest; } } if (isset($key)) { $this->finalOffset = ($nearest - $key); } else { $this->finalOffset = 0; } foreach (array_reverse($map, true) as $key => $value) { if ($value) { $nearest = reset($value); } else { $nmap[$key][1] = -$nearest; } } $this->nearestMap = $nmap; return $this; } public static function newFromHunks(array $hunks) { assert_instances_of($hunks, 'DifferentialHunk'); $map = array(); $o = 0; $n = 0; $hunks = msort($hunks, 'getOldOffset'); foreach ($hunks as $hunk) { // If the hunks are disjoint, add the implied missing lines where // nothing changed. $min = ($hunk->getOldOffset() - 1); while ($o < $min) { $o++; $n++; $map[$o][] = $n; } $lines = $hunk->getStructuredLines(); foreach ($lines as $line) { switch ($line['type']) { case '-': $o++; $map[$o] = array(); break; case '+': $n++; $map[$o][] = $n; break; case ' ': $o++; $n++; $map[$o][] = $n; break; default: break; } } } $map = self::reduceMapRanges($map); return self::newFromMap($map); } public static function newFromMap(array $map) { $obj = new DifferentialLineAdjustmentMap(); $obj->map = $map; return $obj; } public static function newInverseMap(DifferentialLineAdjustmentMap $map) { $old = $map->getMap(); $inv = array(); $last = 0; foreach ($old as $k => $v) { if (count($v) > 1) { $v = range(reset($v), end($v)); } if ($k == 0) { foreach ($v as $line) { $inv[$line] = array(); $last = $line; } } else if ($v) { $first = true; foreach ($v as $line) { if ($first) { $first = false; $inv[$line][] = $k; $last = $line; } else { $inv[$line] = array(); } } } else { $inv[$last][] = $k; } } $inv = self::reduceMapRanges($inv); $obj = new DifferentialLineAdjustmentMap(); $obj->map = $inv; $obj->isInverse = !$map->isInverse; return $obj; } private static function reduceMapRanges(array $map) { foreach ($map as $key => $values) { if (count($values) > 2) { $map[$key] = array(reset($values), end($values)); } } return $map; } public static function loadMaps(array $maps) { $keys = array(); foreach ($maps as $map) { list($u, $v) = $map; $keys[self::getCacheKey($u, $v)] = $map; } $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); $results = array(); if ($keys) { $caches = $cache->getKeys(array_keys($keys)); foreach ($caches as $key => $value) { list($u, $v) = $keys[$key]; try { $results[$u][$v] = self::newFromMap( phutil_json_decode($value)); } catch (Exception $ex) { // Ignore, rebuild below. } unset($keys[$key]); } } if ($keys) { $built = self::buildMaps($maps); $write = array(); foreach ($built as $u => $list) { foreach ($list as $v => $map) { $write[self::getCacheKey($u, $v)] = json_encode($map->getMap()); $results[$u][$v] = $map; } } $cache->setKeys($write); } return $results; } private static function buildMaps(array $maps) { $need = array(); foreach ($maps as $map) { list($u, $v) = $map; $need[$u] = $u; $need[$v] = $v; } if ($need) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs($need) ->needHunks(true) ->execute(); $changesets = mpull($changesets, null, 'getID'); } $results = array(); foreach ($maps as $map) { list($u, $v) = $map; $u_set = idx($changesets, $u); $v_set = idx($changesets, $v); if (!$u_set || !$v_set) { continue; } // This is the simple case. if ($u == $v) { $results[$u][$v] = self::newFromHunks( $u_set->getHunks()); continue; } $u_old = $u_set->makeOldFile(); $v_old = $v_set->makeOldFile(); // No difference between the two left sides. if ($u_old == $v_old) { $results[$u][$v] = self::newFromMap( array()); continue; } // If we're missing context, this won't currently work. We can // make this case work, but it's fairly rare. $u_hunks = $u_set->getHunks(); $v_hunks = $v_set->getHunks(); if (count($u_hunks) != 1 || count($v_hunks) != 1 || head($u_hunks)->getOldOffset() != 1 || head($u_hunks)->getNewOffset() != 1 || head($v_hunks)->getOldOffset() != 1 || head($v_hunks)->getNewOffset() != 1) { continue; } $changeset = id(new PhabricatorDifferenceEngine()) ->setIgnoreWhitespace(true) ->generateChangesetFromFileContent($u_old, $v_old); $results[$u][$v] = self::newFromHunks( $changeset->getHunks()); } return $results; } private static function getCacheKey($u, $v) { return 'diffadjust.v1('.$u.','.$v.')'; } }