@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Datastructure which follows lines of code across source changes.
5 *
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
9 * down to line 45.
10 *
11 */
12final class DifferentialLineAdjustmentMap extends Phobject {
13
14 private $map;
15 private $nearestMap;
16 private $isInverse;
17 private $finalOffset;
18 private $nextMapInChain;
19
20 /**
21 * Get the raw adjustment map.
22 */
23 public function getMap() {
24 return $this->map;
25 }
26
27 public function getNearestMap() {
28 if ($this->nearestMap === null) {
29 $this->buildNearestMap();
30 }
31
32 return $this->nearestMap;
33 }
34
35 public function getFinalOffset() {
36 // Make sure we've built this map already.
37 $this->getNearestMap();
38 return $this->finalOffset;
39 }
40
41
42 /**
43 * Add a map to the end of the chain.
44 *
45 * When a line is mapped with @{method:mapLine}, it is mapped through all
46 * maps in the chain.
47 */
48 public function addMapToChain(DifferentialLineAdjustmentMap $map) {
49 if ($this->nextMapInChain) {
50 $this->nextMapInChain->addMapToChain($map);
51 } else {
52 $this->nextMapInChain = $map;
53 }
54 return $this;
55 }
56
57
58 /**
59 * Map a line across a change, or a series of changes.
60 *
61 * @param int $line Line to map
62 * @param bool $is_end True to map it as the end of a range.
63 * @return array{bool,int,int} Tuple of (deleted, offset, line).
64 */
65 public function mapLine($line, $is_end) {
66 $nmap = $this->getNearestMap();
67
68 $deleted = false;
69 $offset = false;
70 if (isset($nmap[$line])) {
71 $line_range = $nmap[$line];
72 if ($is_end) {
73 $to_line = end($line_range);
74 } else {
75 $to_line = reset($line_range);
76 }
77 if ($to_line <= 0) {
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) {
81 $offset = 1;
82 $cursor = $line - 1;
83 while (isset($nmap[$cursor])) {
84 $prev = $nmap[$cursor];
85 $prev = reset($prev);
86 if ($prev == $to_line) {
87 $offset++;
88 } else {
89 break;
90 }
91 $cursor--;
92 }
93 }
94
95 $to_line = -$to_line;
96 if (!$this->isInverse) {
97 $deleted = true;
98 }
99 }
100 $line = $to_line;
101 } else {
102 $line = $line + $this->finalOffset;
103 }
104
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) {
111 $offset = 0;
112 }
113 $offset += $chain_offset;
114 }
115 }
116
117 return array($deleted, $offset, $line);
118 }
119
120
121 /**
122 * Build a derived map which maps deleted lines to the nearest valid line.
123 *
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.
127 */
128 private function buildNearestMap() {
129 $map = $this->map;
130 $nmap = array();
131
132 $nearest = 0;
133 foreach ($map as $key => $value) {
134 if ($value) {
135 $nmap[$key] = $value;
136 $nearest = end($value);
137 } else {
138 $nmap[$key][0] = -$nearest;
139 }
140 }
141
142 if (isset($key)) {
143 $this->finalOffset = ($nearest - $key);
144 } else {
145 $this->finalOffset = 0;
146 }
147
148 foreach (array_reverse($map, true) as $key => $value) {
149 if ($value) {
150 $nearest = reset($value);
151 } else {
152 $nmap[$key][1] = -$nearest;
153 }
154 }
155
156 $this->nearestMap = $nmap;
157
158 return $this;
159 }
160
161 /**
162 * @param array<DifferentialHunk> $hunks
163 */
164 public static function newFromHunks(array $hunks) {
165 assert_instances_of($hunks, DifferentialHunk::class);
166
167 $map = array();
168 $o = 0;
169 $n = 0;
170
171 $hunks = msort($hunks, 'getOldOffset');
172 foreach ($hunks as $hunk) {
173
174 // If the hunks are disjoint, add the implied missing lines where
175 // nothing changed.
176 $min = ($hunk->getOldOffset() - 1);
177 while ($o < $min) {
178 $o++;
179 $n++;
180 $map[$o][] = $n;
181 }
182
183 $lines = $hunk->getStructuredLines();
184 foreach ($lines as $line) {
185 switch ($line['type']) {
186 case '-':
187 $o++;
188 $map[$o] = array();
189 break;
190 case '+':
191 $n++;
192 $map[$o][] = $n;
193 break;
194 case ' ':
195 $o++;
196 $n++;
197 $map[$o][] = $n;
198 break;
199 default:
200 break;
201 }
202 }
203 }
204
205 $map = self::reduceMapRanges($map);
206
207 return self::newFromMap($map);
208 }
209
210 public static function newFromMap(array $map) {
211 $obj = new DifferentialLineAdjustmentMap();
212 $obj->map = $map;
213 return $obj;
214 }
215
216 public static function newInverseMap(DifferentialLineAdjustmentMap $map) {
217 $old = $map->getMap();
218 $inv = array();
219 $last = 0;
220 foreach ($old as $k => $v) {
221 if (count($v) > 1) {
222 $v = range(reset($v), end($v));
223 }
224 if ($k == 0) {
225 foreach ($v as $line) {
226 $inv[$line] = array();
227 $last = $line;
228 }
229 } else if ($v) {
230 $first = true;
231 foreach ($v as $line) {
232 if ($first) {
233 $first = false;
234 $inv[$line][] = $k;
235 $last = $line;
236 } else {
237 $inv[$line] = array();
238 }
239 }
240 } else {
241 $inv[$last][] = $k;
242 }
243 }
244
245 $inv = self::reduceMapRanges($inv);
246
247 $obj = new DifferentialLineAdjustmentMap();
248 $obj->map = $inv;
249 $obj->isInverse = !$map->isInverse;
250 return $obj;
251 }
252
253 private static function reduceMapRanges(array $map) {
254 foreach ($map as $key => $values) {
255 if (count($values) > 2) {
256 $map[$key] = array(reset($values), end($values));
257 }
258 }
259 return $map;
260 }
261
262
263 public static function loadMaps(array $maps) {
264 $keys = array();
265 foreach ($maps as $map) {
266 list($u, $v) = $map;
267 $keys[self::getCacheKey($u, $v)] = $map;
268 }
269
270 $cache = new PhabricatorKeyValueDatabaseCache();
271 $cache = new PhutilKeyValueCacheProfiler($cache);
272 $cache->setProfiler(PhutilServiceProfiler::getInstance());
273
274 $results = array();
275
276 if ($keys) {
277 $caches = $cache->getKeys(array_keys($keys));
278 foreach ($caches as $key => $value) {
279 list($u, $v) = $keys[$key];
280 try {
281 $results[$u][$v] = self::newFromMap(
282 phutil_json_decode($value));
283 } catch (Exception $ex) {
284 // Ignore, rebuild below.
285 }
286 unset($keys[$key]);
287 }
288 }
289
290 if ($keys) {
291 $built = self::buildMaps($maps);
292
293 $write = array();
294 foreach ($built as $u => $list) {
295 foreach ($list as $v => $map) {
296 $write[self::getCacheKey($u, $v)] = json_encode($map->getMap());
297 $results[$u][$v] = $map;
298 }
299 }
300
301 $cache->setKeys($write);
302 }
303
304 return $results;
305 }
306
307 private static function buildMaps(array $maps) {
308 $need = array();
309 foreach ($maps as $map) {
310 list($u, $v) = $map;
311 $need[$u] = $u;
312 $need[$v] = $v;
313 }
314
315 if ($need) {
316 $changesets = id(new DifferentialChangesetQuery())
317 ->setViewer(PhabricatorUser::getOmnipotentUser())
318 ->withIDs($need)
319 ->needHunks(true)
320 ->execute();
321 $changesets = mpull($changesets, null, 'getID');
322 }
323
324 $results = array();
325 foreach ($maps as $map) {
326 list($u, $v) = $map;
327 $u_set = idx($changesets, $u);
328 $v_set = idx($changesets, $v);
329
330 if (!$u_set || !$v_set) {
331 continue;
332 }
333
334 // This is the simple case.
335 if ($u == $v) {
336 $results[$u][$v] = self::newFromHunks(
337 $u_set->getHunks());
338 continue;
339 }
340
341 $u_old = $u_set->makeOldFile();
342 $v_old = $v_set->makeOldFile();
343
344 // No difference between the two left sides.
345 if ($u_old == $v_old) {
346 $results[$u][$v] = self::newFromMap(
347 array());
348 continue;
349 }
350
351 // If we're missing context, this won't currently work. We can
352 // make this case work, but it's fairly rare.
353 $u_hunks = $u_set->getHunks();
354 $v_hunks = $v_set->getHunks();
355 if (count($u_hunks) != 1 ||
356 count($v_hunks) != 1 ||
357 head($u_hunks)->getOldOffset() != 1 ||
358 head($u_hunks)->getNewOffset() != 1 ||
359 head($v_hunks)->getOldOffset() != 1 ||
360 head($v_hunks)->getNewOffset() != 1) {
361 continue;
362 }
363
364 $changeset = id(new PhabricatorDifferenceEngine())
365 ->generateChangesetFromFileContent($u_old, $v_old);
366
367 $results[$u][$v] = self::newFromHunks(
368 $changeset->getHunks());
369 }
370
371 return $results;
372 }
373
374 private static function getCacheKey($u, $v) {
375 return 'diffadjust.v1('.$u.','.$v.')';
376 }
377
378}