1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-15 19:32:40 +01:00
phorge-phorge/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
epriestley a238f6a759 Implement rough content-aware inline adjustment rules for ghosts
Summary:
Ref T7447. Fixes T7600. This likely needs significant adjustment, but implements content-aware comment porting for line changes.

Specifically, this moves lines around to adjust their position considering added and removed lines between the diffs and across rebases.

It does not try to do any actual content (line against line) matching.

Test Plan:
  - Unit tests.
  - Poking around in the web UI seems to generate mostly reasonable-ish results?
  - This may be a huge step backward in some cases that I just haven't hit.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: yelirekim, epriestley

Maniphest Tasks: T7600, T7447

Differential Revision: https://secure.phabricator.com/D12741
2015-05-07 14:09:41 -07:00

376 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.')';
}
}