4 * Datastructure which follows lines of code across source changes.
6 * This map is used to update the positions of inline comments after diff
7 * updates. For example, if a inline comment appeared on line 30 of a diff
8 * but the next update adds 15 more lines above it, the comment should move
12 final class DifferentialLineAdjustmentMap
extends Phobject
{
18 private $nextMapInChain;
21 * Get the raw adjustment map.
23 public function getMap() {
27 public function getNearestMap() {
28 if ($this->nearestMap
=== null) {
29 $this->buildNearestMap();
32 return $this->nearestMap
;
35 public function getFinalOffset() {
36 // Make sure we've built this map already.
37 $this->getNearestMap();
38 return $this->finalOffset
;
43 * Add a map to the end of the chain.
45 * When a line is mapped with @{method:mapLine}, it is mapped through all
48 public function addMapToChain(DifferentialLineAdjustmentMap
$map) {
49 if ($this->nextMapInChain
) {
50 $this->nextMapInChain
->addMapToChain($map);
52 $this->nextMapInChain
= $map;
59 * Map a line across a change, or a series of changes.
61 * @param int Line to map
62 * @param bool True to map it as the end of a range.
63 * @return wild Spooky magic.
65 public function mapLine($line, $is_end) {
66 $nmap = $this->getNearestMap();
70 if (isset($nmap[$line])) {
71 $line_range = $nmap[$line];
73 $to_line = end($line_range);
75 $to_line = reset($line_range);
78 // If we're tracing the first line and this block is collapsing,
79 // compute the offset from the top of the block.
80 if (!$is_end && $this->isInverse
) {
83 while (isset($nmap[$cursor])) {
84 $prev = $nmap[$cursor];
86 if ($prev == $to_line) {
96 if (!$this->isInverse
) {
102 $line = $line +
$this->finalOffset
;
105 if ($this->nextMapInChain
) {
106 $chain = $this->nextMapInChain
->mapLine($line, $is_end);
107 list($chain_deleted, $chain_offset, $line) = $chain;
108 $deleted = ($deleted ||
$chain_deleted);
109 if ($chain_offset !== false) {
110 if ($offset === false) {
113 $offset +
= $chain_offset;
117 return array($deleted, $offset, $line);
122 * Build a derived map which maps deleted lines to the nearest valid line.
124 * This computes a "nearest line" map and a final-line offset. These
125 * derived maps allow us to map deleted code to the previous (or next) line
126 * which actually exists.
128 private function buildNearestMap() {
133 foreach ($map as $key => $value) {
135 $nmap[$key] = $value;
136 $nearest = end($value);
138 $nmap[$key][0] = -$nearest;
143 $this->finalOffset
= ($nearest - $key);
145 $this->finalOffset
= 0;
148 foreach (array_reverse($map, true) as $key => $value) {
150 $nearest = reset($value);
152 $nmap[$key][1] = -$nearest;
156 $this->nearestMap
= $nmap;
161 public static function newFromHunks(array $hunks) {
162 assert_instances_of($hunks, 'DifferentialHunk');
168 $hunks = msort($hunks, 'getOldOffset');
169 foreach ($hunks as $hunk) {
171 // If the hunks are disjoint, add the implied missing lines where
173 $min = ($hunk->getOldOffset() - 1);
180 $lines = $hunk->getStructuredLines();
181 foreach ($lines as $line) {
182 switch ($line['type']) {
202 $map = self
::reduceMapRanges($map);
204 return self
::newFromMap($map);
207 public static function newFromMap(array $map) {
208 $obj = new DifferentialLineAdjustmentMap();
213 public static function newInverseMap(DifferentialLineAdjustmentMap
$map) {
214 $old = $map->getMap();
217 foreach ($old as $k => $v) {
219 $v = range(reset($v), end($v));
222 foreach ($v as $line) {
223 $inv[$line] = array();
228 foreach ($v as $line) {
234 $inv[$line] = array();
242 $inv = self
::reduceMapRanges($inv);
244 $obj = new DifferentialLineAdjustmentMap();
246 $obj->isInverse
= !$map->isInverse
;
250 private static function reduceMapRanges(array $map) {
251 foreach ($map as $key => $values) {
252 if (count($values) > 2) {
253 $map[$key] = array(reset($values), end($values));
260 public static function loadMaps(array $maps) {
262 foreach ($maps as $map) {
264 $keys[self
::getCacheKey($u, $v)] = $map;
267 $cache = new PhabricatorKeyValueDatabaseCache();
268 $cache = new PhutilKeyValueCacheProfiler($cache);
269 $cache->setProfiler(PhutilServiceProfiler
::getInstance());
274 $caches = $cache->getKeys(array_keys($keys));
275 foreach ($caches as $key => $value) {
276 list($u, $v) = $keys[$key];
278 $results[$u][$v] = self
::newFromMap(
279 phutil_json_decode($value));
280 } catch (Exception
$ex) {
281 // Ignore, rebuild below.
288 $built = self
::buildMaps($maps);
291 foreach ($built as $u => $list) {
292 foreach ($list as $v => $map) {
293 $write[self
::getCacheKey($u, $v)] = json_encode($map->getMap());
294 $results[$u][$v] = $map;
298 $cache->setKeys($write);
304 private static function buildMaps(array $maps) {
306 foreach ($maps as $map) {
313 $changesets = id(new DifferentialChangesetQuery())
314 ->setViewer(PhabricatorUser
::getOmnipotentUser())
318 $changesets = mpull($changesets, null, 'getID');
322 foreach ($maps as $map) {
324 $u_set = idx($changesets, $u);
325 $v_set = idx($changesets, $v);
327 if (!$u_set ||
!$v_set) {
331 // This is the simple case.
333 $results[$u][$v] = self
::newFromHunks(
338 $u_old = $u_set->makeOldFile();
339 $v_old = $v_set->makeOldFile();
341 // No difference between the two left sides.
342 if ($u_old == $v_old) {
343 $results[$u][$v] = self
::newFromMap(
348 // If we're missing context, this won't currently work. We can
349 // make this case work, but it's fairly rare.
350 $u_hunks = $u_set->getHunks();
351 $v_hunks = $v_set->getHunks();
352 if (count($u_hunks) != 1 ||
353 count($v_hunks) != 1 ||
354 head($u_hunks)->getOldOffset() != 1 ||
355 head($u_hunks)->getNewOffset() != 1 ||
356 head($v_hunks)->getOldOffset() != 1 ||
357 head($v_hunks)->getNewOffset() != 1) {
361 $changeset = id(new PhabricatorDifferenceEngine())
362 ->generateChangesetFromFileContent($u_old, $v_old);
364 $results[$u][$v] = self
::newFromHunks(
365 $changeset->getHunks());
371 private static function getCacheKey($u, $v) {
372 return 'diffadjust.v1('.$u.','.$v.')';