@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 379 lines 11 kB view raw
1<?php 2 3final class PhabricatorInlineCommentAdjustmentEngine 4 extends Phobject { 5 6 private $viewer; 7 private $inlines; 8 private $revision; 9 private $oldChangesets; 10 private $newChangesets; 11 12 public function setViewer(PhabricatorUser $viewer) { 13 $this->viewer = $viewer; 14 return $this; 15 } 16 17 public function getViewer() { 18 return $this->viewer; 19 } 20 21 /** 22 * @param array<DifferentialInlineComment> $inlines 23 */ 24 public function setInlines(array $inlines) { 25 assert_instances_of($inlines, DifferentialInlineComment::class); 26 $this->inlines = $inlines; 27 return $this; 28 } 29 30 public function getInlines() { 31 return $this->inlines; 32 } 33 34 /** 35 * @param array<DifferentialChangeset> $old_changesets 36 */ 37 public function setOldChangesets(array $old_changesets) { 38 assert_instances_of($old_changesets, DifferentialChangeset::class); 39 $this->oldChangesets = $old_changesets; 40 return $this; 41 } 42 43 public function getOldChangesets() { 44 return $this->oldChangesets; 45 } 46 47 /** 48 * @param array<DifferentialChangeset> $new_changesets 49 */ 50 public function setNewChangesets(array $new_changesets) { 51 assert_instances_of($new_changesets, DifferentialChangeset::class); 52 $this->newChangesets = $new_changesets; 53 return $this; 54 } 55 56 public function getNewChangesets() { 57 return $this->newChangesets; 58 } 59 60 public function setRevision(DifferentialRevision $revision) { 61 $this->revision = $revision; 62 return $this; 63 } 64 65 public function getRevision() { 66 return $this->revision; 67 } 68 69 public function execute() { 70 $viewer = $this->getViewer(); 71 $inlines = $this->getInlines(); 72 $revision = $this->getRevision(); 73 $old = $this->getOldChangesets(); 74 $new = $this->getNewChangesets(); 75 76 $no_ghosts = $viewer->compareUserSetting( 77 PhabricatorOlderInlinesSetting::SETTINGKEY, 78 PhabricatorOlderInlinesSetting::VALUE_GHOST_INLINES_DISABLED); 79 if ($no_ghosts) { 80 return $inlines; 81 } 82 83 $all = array_merge($old, $new); 84 85 $changeset_ids = mpull($inlines, 'getChangesetID'); 86 $changeset_ids = array_unique($changeset_ids); 87 88 $all_map = mpull($all, null, 'getID'); 89 90 // We already have at least some changesets, and we might not need to do 91 // any more data fetching. Remove everything we already have so we can 92 // tell if we need new stuff. 93 foreach ($changeset_ids as $key => $id) { 94 if (isset($all_map[$id])) { 95 unset($changeset_ids[$key]); 96 } 97 } 98 99 if ($changeset_ids) { 100 $changesets = id(new DifferentialChangesetQuery()) 101 ->setViewer($viewer) 102 ->withIDs($changeset_ids) 103 ->execute(); 104 $changesets = mpull($changesets, null, 'getID'); 105 } else { 106 $changesets = array(); 107 } 108 $changesets += $all_map; 109 110 $id_map = array(); 111 foreach ($all as $changeset) { 112 $id_map[$changeset->getID()] = $changeset->getID(); 113 } 114 115 // Generate filename maps for older and newer comments. If we're bringing 116 // an older comment forward in a diff-of-diffs, we want to put it on the 117 // left side of the screen, not the right side. Both sides are "new" files 118 // with the same name, so they're both appropriate targets, but the left 119 // is a better target conceptually for users because it's more consistent 120 // with the rest of the UI, which shows old information on the left and 121 // new information on the right. 122 $move_here = DifferentialChangeType::TYPE_MOVE_HERE; 123 124 $name_map_old = array(); 125 $name_map_new = array(); 126 $move_map = array(); 127 foreach ($all as $changeset) { 128 $changeset_id = $changeset->getID(); 129 130 $filenames = array(); 131 $filenames[] = $changeset->getFilename(); 132 133 // If this is the target of a move, also map comments on the old filename 134 // to this changeset. 135 if ($changeset->getChangeType() == $move_here) { 136 $old_file = $changeset->getOldFile(); 137 $filenames[] = $old_file; 138 $move_map[$changeset_id][$old_file] = true; 139 } 140 141 foreach ($filenames as $filename) { 142 // We update the old map only if we don't already have an entry (oldest 143 // changeset persists). 144 if (empty($name_map_old[$filename])) { 145 $name_map_old[$filename] = $changeset_id; 146 } 147 148 // We always update the new map (newest changeset overwrites). 149 $name_map_new[$changeset->getFilename()] = $changeset_id; 150 } 151 } 152 153 $new_id_map = mpull($new, null, 'getID'); 154 155 $results = array(); 156 foreach ($inlines as $inline) { 157 $changeset_id = $inline->getChangesetID(); 158 if (isset($id_map[$changeset_id])) { 159 // This inline is legitimately on one of the current changesets, so 160 // we can include it in the result set unmodified. 161 $results[] = $inline; 162 continue; 163 } 164 165 $changeset = idx($changesets, $changeset_id); 166 if (!$changeset) { 167 // Just discard this inline, as it has bogus data. 168 continue; 169 } 170 171 $target_id = null; 172 173 if (isset($new_id_map[$changeset_id])) { 174 $name_map = $name_map_new; 175 $is_new = true; 176 } else { 177 $name_map = $name_map_old; 178 $is_new = false; 179 } 180 181 $filename = $changeset->getFilename(); 182 if (isset($name_map[$filename])) { 183 // This changeset is on a file with the same name as the current 184 // changeset, so we're going to port it forward or backward. 185 $target_id = $name_map[$filename]; 186 187 $is_move = isset($move_map[$target_id][$filename]); 188 if ($is_new) { 189 if ($is_move) { 190 $reason = pht( 191 'This comment was made on a file with the same name as the '. 192 'file this file was moved from, but in a newer diff.'); 193 } else { 194 $reason = pht( 195 'This comment was made on a file with the same name, but '. 196 'in a newer diff.'); 197 } 198 } else { 199 if ($is_move) { 200 $reason = pht( 201 'This comment was made on a file with the same name as the '. 202 'file this file was moved from, but in an older diff.'); 203 } else { 204 $reason = pht( 205 'This comment was made on a file with the same name, but '. 206 'in an older diff.'); 207 } 208 } 209 } 210 211 212 // If we didn't find a target and this change is the target of a move, 213 // look for a match against the old filename. 214 if (!$target_id) { 215 if ($changeset->getChangeType() == $move_here) { 216 $filename = $changeset->getOldFile(); 217 if (isset($name_map[$filename])) { 218 $target_id = $name_map[$filename]; 219 if ($is_new) { 220 $reason = pht( 221 'This comment was made on a file which this file was moved '. 222 'to, but in a newer diff.'); 223 } else { 224 $reason = pht( 225 'This comment was made on a file which this file was moved '. 226 'to, but in an older diff.'); 227 } 228 } 229 } 230 } 231 232 233 // If we found a changeset to port this comment to, bring it forward 234 // or backward and mark it. 235 if ($target_id) { 236 $diff_id = $changeset->getDiffID(); 237 $inline_id = $inline->getID(); 238 $revision_id = $revision->getID(); 239 $href = "/D{$revision_id}?id={$diff_id}#inline-{$inline_id}"; 240 241 $inline 242 ->makeEphemeral(true) 243 ->setChangesetID($target_id) 244 ->setIsGhost( 245 array( 246 'new' => $is_new, 247 'reason' => $reason, 248 'href' => $href, 249 'originalID' => $changeset->getID(), 250 )); 251 252 $results[] = $inline; 253 } 254 } 255 256 // Filter out the inlines we ported forward which won't be visible because 257 // they appear on the wrong side of a file. 258 $keep_map = array(); 259 foreach ($old as $changeset) { 260 $keep_map[$changeset->getID()][0] = true; 261 } 262 foreach ($new as $changeset) { 263 $keep_map[$changeset->getID()][1] = true; 264 } 265 266 foreach ($results as $key => $inline) { 267 $is_new = (int)$inline->getIsNewFile(); 268 $changeset_id = $inline->getChangesetID(); 269 if (!isset($keep_map[$changeset_id][$is_new])) { 270 unset($results[$key]); 271 continue; 272 } 273 } 274 275 // Adjust inline line numbers to account for content changes across 276 // updates and rebases. 277 $plan = array(); 278 $need = array(); 279 foreach ($results as $inline) { 280 $ghost = $inline->getIsGhost(); 281 if (!$ghost) { 282 // If this isn't a "ghost" inline, ignore it. 283 continue; 284 } 285 286 $src_id = $ghost['originalID']; 287 $dst_id = $inline->getChangesetID(); 288 289 $xforms = array(); 290 291 // If the comment is on the right, transform it through the inverse map 292 // back to the left. 293 if ($inline->getIsNewFile()) { 294 $xforms[] = array($src_id, $src_id, true); 295 } 296 297 // Transform it across rebases. 298 $xforms[] = array($src_id, $dst_id, false); 299 300 // If the comment is on the right, transform it back onto the right. 301 if ($inline->getIsNewFile()) { 302 $xforms[] = array($dst_id, $dst_id, false); 303 } 304 305 $key = array(); 306 foreach ($xforms as $xform) { 307 list($u, $v, $inverse) = $xform; 308 309 $short = $u.'/'.$v; 310 $need[$short] = array($u, $v); 311 312 $part = $u.($inverse ? '<' : '>').$v; 313 $key[] = $part; 314 } 315 $key = implode(',', $key); 316 317 if (empty($plan[$key])) { 318 $plan[$key] = array( 319 'xforms' => $xforms, 320 'inlines' => array(), 321 ); 322 } 323 324 $plan[$key]['inlines'][] = $inline; 325 } 326 327 if ($need) { 328 $maps = DifferentialLineAdjustmentMap::loadMaps($need); 329 } else { 330 $maps = array(); 331 } 332 333 foreach ($plan as $step) { 334 $xforms = $step['xforms']; 335 336 $chain = null; 337 foreach ($xforms as $xform) { 338 list($u, $v, $inverse) = $xform; 339 $map = idx(idx($maps, $u, array()), $v); 340 if (!$map) { 341 continue 2; 342 } 343 344 if ($inverse) { 345 $map = DifferentialLineAdjustmentMap::newInverseMap($map); 346 } else { 347 $map = clone $map; 348 } 349 350 if ($chain) { 351 $chain->addMapToChain($map); 352 } else { 353 $chain = $map; 354 } 355 } 356 357 foreach ($step['inlines'] as $inline) { 358 $head_line = $inline->getLineNumber(); 359 $tail_line = ($head_line + $inline->getLineLength()); 360 361 $head_info = $chain->mapLine($head_line, false); 362 $tail_info = $chain->mapLine($tail_line, true); 363 364 list($head_deleted, $head_offset, $head_line) = $head_info; 365 list($tail_deleted, $tail_offset, $tail_line) = $tail_info; 366 367 if ($head_offset !== false) { 368 $inline->setLineNumber($head_line + $head_offset); 369 } else { 370 $inline->setLineNumber($head_line); 371 $inline->setLineLength($tail_line - $head_line); 372 } 373 } 374 } 375 376 return $results; 377 } 378 379}