@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
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}