mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-15 19:32:40 +01:00
377 lines
8.9 KiB
PHP
377 lines
8.9 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* Datastructure which follows lines of code across source changes.
|
||
|
*
|
||
|
* This map is used to update the positions of inline comments after diff
|
||
|
* updates. For example, if a inline comment appeared on line 30 of a diff
|
||
|
* but the next update adds 15 more lines above it, the comment should move
|
||
|
* down to line 45.
|
||
|
*
|
||
|
*/
|
||
|
final class DifferentialLineAdjustmentMap extends Phobject {
|
||
|
|
||
|
private $map;
|
||
|
private $nearestMap;
|
||
|
private $isInverse;
|
||
|
private $finalOffset;
|
||
|
private $nextMapInChain;
|
||
|
|
||
|
/**
|
||
|
* Get the raw adjustment map.
|
||
|
*/
|
||
|
public function getMap() {
|
||
|
return $this->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.')';
|
||
|
}
|
||
|
|
||
|
}
|