Path: blob/master/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
12256 views
<?php12/**3* Datastructure which follows lines of code across source changes.4*5* This map is used to update the positions of inline comments after diff6* updates. For example, if a inline comment appeared on line 30 of a diff7* but the next update adds 15 more lines above it, the comment should move8* down to line 45.9*10*/11final class DifferentialLineAdjustmentMap extends Phobject {1213private $map;14private $nearestMap;15private $isInverse;16private $finalOffset;17private $nextMapInChain;1819/**20* Get the raw adjustment map.21*/22public function getMap() {23return $this->map;24}2526public function getNearestMap() {27if ($this->nearestMap === null) {28$this->buildNearestMap();29}3031return $this->nearestMap;32}3334public function getFinalOffset() {35// Make sure we've built this map already.36$this->getNearestMap();37return $this->finalOffset;38}394041/**42* Add a map to the end of the chain.43*44* When a line is mapped with @{method:mapLine}, it is mapped through all45* maps in the chain.46*/47public function addMapToChain(DifferentialLineAdjustmentMap $map) {48if ($this->nextMapInChain) {49$this->nextMapInChain->addMapToChain($map);50} else {51$this->nextMapInChain = $map;52}53return $this;54}555657/**58* Map a line across a change, or a series of changes.59*60* @param int Line to map61* @param bool True to map it as the end of a range.62* @return wild Spooky magic.63*/64public function mapLine($line, $is_end) {65$nmap = $this->getNearestMap();6667$deleted = false;68$offset = false;69if (isset($nmap[$line])) {70$line_range = $nmap[$line];71if ($is_end) {72$to_line = end($line_range);73} else {74$to_line = reset($line_range);75}76if ($to_line <= 0) {77// If we're tracing the first line and this block is collapsing,78// compute the offset from the top of the block.79if (!$is_end && $this->isInverse) {80$offset = 1;81$cursor = $line - 1;82while (isset($nmap[$cursor])) {83$prev = $nmap[$cursor];84$prev = reset($prev);85if ($prev == $to_line) {86$offset++;87} else {88break;89}90$cursor--;91}92}9394$to_line = -$to_line;95if (!$this->isInverse) {96$deleted = true;97}98}99$line = $to_line;100} else {101$line = $line + $this->finalOffset;102}103104if ($this->nextMapInChain) {105$chain = $this->nextMapInChain->mapLine($line, $is_end);106list($chain_deleted, $chain_offset, $line) = $chain;107$deleted = ($deleted || $chain_deleted);108if ($chain_offset !== false) {109if ($offset === false) {110$offset = 0;111}112$offset += $chain_offset;113}114}115116return array($deleted, $offset, $line);117}118119120/**121* Build a derived map which maps deleted lines to the nearest valid line.122*123* This computes a "nearest line" map and a final-line offset. These124* derived maps allow us to map deleted code to the previous (or next) line125* which actually exists.126*/127private function buildNearestMap() {128$map = $this->map;129$nmap = array();130131$nearest = 0;132foreach ($map as $key => $value) {133if ($value) {134$nmap[$key] = $value;135$nearest = end($value);136} else {137$nmap[$key][0] = -$nearest;138}139}140141if (isset($key)) {142$this->finalOffset = ($nearest - $key);143} else {144$this->finalOffset = 0;145}146147foreach (array_reverse($map, true) as $key => $value) {148if ($value) {149$nearest = reset($value);150} else {151$nmap[$key][1] = -$nearest;152}153}154155$this->nearestMap = $nmap;156157return $this;158}159160public static function newFromHunks(array $hunks) {161assert_instances_of($hunks, 'DifferentialHunk');162163$map = array();164$o = 0;165$n = 0;166167$hunks = msort($hunks, 'getOldOffset');168foreach ($hunks as $hunk) {169170// If the hunks are disjoint, add the implied missing lines where171// nothing changed.172$min = ($hunk->getOldOffset() - 1);173while ($o < $min) {174$o++;175$n++;176$map[$o][] = $n;177}178179$lines = $hunk->getStructuredLines();180foreach ($lines as $line) {181switch ($line['type']) {182case '-':183$o++;184$map[$o] = array();185break;186case '+':187$n++;188$map[$o][] = $n;189break;190case ' ':191$o++;192$n++;193$map[$o][] = $n;194break;195default:196break;197}198}199}200201$map = self::reduceMapRanges($map);202203return self::newFromMap($map);204}205206public static function newFromMap(array $map) {207$obj = new DifferentialLineAdjustmentMap();208$obj->map = $map;209return $obj;210}211212public static function newInverseMap(DifferentialLineAdjustmentMap $map) {213$old = $map->getMap();214$inv = array();215$last = 0;216foreach ($old as $k => $v) {217if (count($v) > 1) {218$v = range(reset($v), end($v));219}220if ($k == 0) {221foreach ($v as $line) {222$inv[$line] = array();223$last = $line;224}225} else if ($v) {226$first = true;227foreach ($v as $line) {228if ($first) {229$first = false;230$inv[$line][] = $k;231$last = $line;232} else {233$inv[$line] = array();234}235}236} else {237$inv[$last][] = $k;238}239}240241$inv = self::reduceMapRanges($inv);242243$obj = new DifferentialLineAdjustmentMap();244$obj->map = $inv;245$obj->isInverse = !$map->isInverse;246return $obj;247}248249private static function reduceMapRanges(array $map) {250foreach ($map as $key => $values) {251if (count($values) > 2) {252$map[$key] = array(reset($values), end($values));253}254}255return $map;256}257258259public static function loadMaps(array $maps) {260$keys = array();261foreach ($maps as $map) {262list($u, $v) = $map;263$keys[self::getCacheKey($u, $v)] = $map;264}265266$cache = new PhabricatorKeyValueDatabaseCache();267$cache = new PhutilKeyValueCacheProfiler($cache);268$cache->setProfiler(PhutilServiceProfiler::getInstance());269270$results = array();271272if ($keys) {273$caches = $cache->getKeys(array_keys($keys));274foreach ($caches as $key => $value) {275list($u, $v) = $keys[$key];276try {277$results[$u][$v] = self::newFromMap(278phutil_json_decode($value));279} catch (Exception $ex) {280// Ignore, rebuild below.281}282unset($keys[$key]);283}284}285286if ($keys) {287$built = self::buildMaps($maps);288289$write = array();290foreach ($built as $u => $list) {291foreach ($list as $v => $map) {292$write[self::getCacheKey($u, $v)] = json_encode($map->getMap());293$results[$u][$v] = $map;294}295}296297$cache->setKeys($write);298}299300return $results;301}302303private static function buildMaps(array $maps) {304$need = array();305foreach ($maps as $map) {306list($u, $v) = $map;307$need[$u] = $u;308$need[$v] = $v;309}310311if ($need) {312$changesets = id(new DifferentialChangesetQuery())313->setViewer(PhabricatorUser::getOmnipotentUser())314->withIDs($need)315->needHunks(true)316->execute();317$changesets = mpull($changesets, null, 'getID');318}319320$results = array();321foreach ($maps as $map) {322list($u, $v) = $map;323$u_set = idx($changesets, $u);324$v_set = idx($changesets, $v);325326if (!$u_set || !$v_set) {327continue;328}329330// This is the simple case.331if ($u == $v) {332$results[$u][$v] = self::newFromHunks(333$u_set->getHunks());334continue;335}336337$u_old = $u_set->makeOldFile();338$v_old = $v_set->makeOldFile();339340// No difference between the two left sides.341if ($u_old == $v_old) {342$results[$u][$v] = self::newFromMap(343array());344continue;345}346347// If we're missing context, this won't currently work. We can348// make this case work, but it's fairly rare.349$u_hunks = $u_set->getHunks();350$v_hunks = $v_set->getHunks();351if (count($u_hunks) != 1 ||352count($v_hunks) != 1 ||353head($u_hunks)->getOldOffset() != 1 ||354head($u_hunks)->getNewOffset() != 1 ||355head($v_hunks)->getOldOffset() != 1 ||356head($v_hunks)->getNewOffset() != 1) {357continue;358}359360$changeset = id(new PhabricatorDifferenceEngine())361->generateChangesetFromFileContent($u_old, $v_old);362363$results[$u][$v] = self::newFromHunks(364$changeset->getHunks());365}366367return $results;368}369370private static function getCacheKey($u, $v) {371return 'diffadjust.v1('.$u.','.$v.')';372}373374}375376377