@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
at recaptime-dev/main 378 lines 9.2 kB view raw
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}