@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 PHUIDiffTableOfContentsListView extends AphrontView {
4
5 private $items = array();
6 private $authorityPackages;
7 private $header;
8 private $infoView;
9 private $background;
10 private $bare;
11
12 private $components = array();
13
14 public function addItem(PHUIDiffTableOfContentsItemView $item) {
15 $this->items[] = $item;
16 return $this;
17 }
18
19 /**
20 * @param array<PhabricatorOwnersPackage> $authority_packages
21 */
22 public function setAuthorityPackages(array $authority_packages) {
23 assert_instances_of($authority_packages, PhabricatorOwnersPackage::class);
24 $this->authorityPackages = $authority_packages;
25 return $this;
26 }
27
28 public function getAuthorityPackages() {
29 return $this->authorityPackages;
30 }
31
32 public function setBackground($background) {
33 $this->background = $background;
34 return $this;
35 }
36
37 public function setHeader(PHUIHeaderView $header) {
38 $this->header = $header;
39 return $this;
40 }
41
42 public function setInfoView(PHUIInfoView $infoview) {
43 $this->infoView = $infoview;
44 return $this;
45 }
46
47 public function setBare($bare) {
48 $this->bare = $bare;
49 return $this;
50 }
51
52 public function getBare() {
53 return $this->bare;
54 }
55
56 public function render() {
57 $this->requireResource('differential-core-view-css');
58 $this->requireResource('differential-table-of-contents-css');
59
60 Javelin::initBehavior('phabricator-tooltips');
61
62 if ($this->getAuthorityPackages()) {
63 $authority = mpull($this->getAuthorityPackages(), null, 'getPHID');
64 } else {
65 $authority = array();
66 }
67
68 $items = $this->items;
69 $viewer = $this->getViewer();
70
71 $item_map = array();
72
73 $vector_tree = new ArcanistDiffVectorTree();
74 foreach ($items as $item) {
75 $item->setViewer($viewer);
76
77 $changeset = $item->getChangeset();
78
79 $old_vector = $changeset->getOldStatePathVector();
80 $new_vector = $changeset->getNewStatePathVector();
81
82 $tree_vector = $this->newTreeVector($old_vector, $new_vector);
83
84 $item_map[implode("\n", $tree_vector)] = $item;
85
86 $vector_tree->addVector($tree_vector);
87 }
88 $node_list = $vector_tree->newDisplayList();
89
90 $node_map = array();
91 foreach ($node_list as $node) {
92 $path_vector = $node->getVector();
93 $path_vector = implode("\n", $path_vector);
94 $node_map[$path_vector] = $node;
95 }
96
97 // Mark all nodes which contain at least one path which exists in the new
98 // state. Nodes we don't mark contain only deleted or moved files, so they
99 // can be rendered with a less-prominent style.
100
101 foreach ($node_map as $node_key => $node) {
102 $item = idx($item_map, $node_key);
103
104 if (!$item) {
105 continue;
106 }
107
108 $changeset = $item->getChangeset();
109 if (!$changeset->getIsLowImportanceChangeset()) {
110 $node->setAncestralAttribute('important', true);
111 }
112 }
113
114 $any_packages = false;
115 $any_coverage = false;
116 $any_context = false;
117
118 $rows = array();
119 $rowc = array();
120 foreach ($node_map as $node_key => $node) {
121 $display_vector = $node->getDisplayVector();
122 $item = idx($item_map, $node_key);
123
124 if ($item) {
125 $changeset = $item->getChangeset();
126 $icon = $changeset->newFileTreeIcon();
127 } else {
128 $changeset = null;
129 $icon = id(new PHUIIconView())
130 ->setIcon('fa-folder-open-o grey');
131 }
132
133 if ($node->getChildren()) {
134 $old_dir = true;
135 $new_dir = true;
136 } else {
137 // TODO: When properties are set on a directory in SVN directly, this
138 // might be incorrect.
139 $old_dir = false;
140 $new_dir = false;
141 }
142
143 $display_view = $this->newComponentView(
144 $icon,
145 $display_vector,
146 $old_dir,
147 $new_dir,
148 $item);
149
150 $depth = $node->getDisplayDepth();
151
152 $style = sprintf('padding-left: %dpx;', $depth * 16);
153
154 if ($item) {
155 $packages = $item->renderPackages();
156 } else {
157 $packages = null;
158 }
159
160 if ($packages) {
161 $any_packages = true;
162 }
163
164 if ($item) {
165 if ($item->getCoverage()) {
166 $any_coverage = true;
167 }
168 $coverage = $item->renderCoverage();
169 $modified_coverage = $item->renderModifiedCoverage();
170 } else {
171 $coverage = null;
172 $modified_coverage = null;
173 }
174
175 if ($item) {
176 $context = $item->getContext();
177 if ($context) {
178 $any_context = true;
179 }
180 } else {
181 $context = null;
182 }
183
184 if ($item) {
185 $lines = $item->renderChangesetLines();
186 } else {
187 $lines = null;
188 }
189
190 $rows[] = array(
191 $context,
192 phutil_tag(
193 'div',
194 array(
195 'style' => $style,
196 ),
197 $display_view),
198 $lines,
199 $coverage,
200 $modified_coverage,
201 $packages,
202 );
203
204 $classes = array();
205
206 $have_authority = false;
207
208 if ($item) {
209 $packages = $item->getPackages();
210 if ($packages) {
211 if (array_intersect_key($packages, $authority)) {
212 $have_authority = true;
213 }
214 }
215 }
216
217 if ($have_authority) {
218 $classes[] = 'highlighted';
219 }
220
221 if (!$node->getAttribute('important')) {
222 $classes[] = 'diff-toc-low-importance-row';
223 }
224
225 if ($changeset) {
226 $classes[] = 'diff-toc-changeset-row';
227 } else {
228 $classes[] = 'diff-toc-no-changeset-row';
229 }
230
231 $rowc[] = implode(' ', $classes);
232 }
233
234 $table = id(new AphrontTableView($rows))
235 ->setRowClasses($rowc)
236 ->setClassName('aphront-table-view-compact')
237 ->setHeaders(
238 array(
239 null,
240 pht('Path'),
241 pht('Size'),
242 pht('Coverage (All)'),
243 pht('Coverage (Touched)'),
244 pht('Packages'),
245 ))
246 ->setColumnClasses(
247 array(
248 null,
249 'diff-toc-path wide',
250 'right',
251 'differential-toc-cov',
252 'differential-toc-cov',
253 null,
254 ))
255 ->setColumnVisibility(
256 array(
257 $any_context,
258 true,
259 true,
260 $any_coverage,
261 $any_coverage,
262 $any_packages,
263 ))
264 ->setDeviceVisibility(
265 array(
266 true,
267 true,
268 false,
269 false,
270 false,
271 true,
272 ));
273
274 $anchor = id(new PhabricatorAnchorView())
275 ->setAnchorName('toc')
276 ->setNavigationMarker(true);
277
278 if ($this->bare) {
279 return $table;
280 }
281
282 $header = id(new PHUIHeaderView())
283 ->setHeader(pht('Table of Contents'));
284
285 if ($this->header) {
286 $header = $this->header;
287 }
288
289 $box = id(new PHUIObjectBoxView())
290 ->setHeader($header)
291 ->setBackground($this->background)
292 ->setTable($table)
293 ->appendChild($anchor);
294
295 if ($this->infoView) {
296 $box->setInfoView($this->infoView);
297 }
298
299 return $box;
300 }
301
302 private function newTreeVector($old, $new) {
303 if ($old === null && $new === null) {
304 throw new Exception(pht('Changeset has no path vectors!'));
305 }
306
307 $vector = null;
308 if ($old === null) {
309 $vector = $new;
310 } else if ($new === null) {
311 $vector = $old;
312 } else if ($old === $new) {
313 $vector = $new;
314 }
315
316 if ($vector) {
317 foreach ($vector as $k => $v) {
318 $vector[$k] = $this->newScalarComponent($v);
319 }
320 return $vector;
321 }
322
323 $matrix = id(new PhutilEditDistanceMatrix())
324 ->setSequences($old, $new)
325 ->setComputeString(true);
326 $edits = $matrix->getEditString();
327
328 // If the edit sequence contains deletions followed by edits, move
329 // the deletions to the end to left-align the new path.
330 $edits = preg_replace('/(d+)(x+)/', '\2\1', $edits);
331
332 $vector = array();
333 $length = strlen($edits);
334
335 $old_cursor = 0;
336 $new_cursor = 0;
337
338 for ($ii = 0; $ii < strlen($edits); $ii++) {
339 $c = $edits[$ii];
340 switch ($c) {
341 case 'i':
342 $vector[] = $this->newPairComponent(null, $new[$new_cursor]);
343 $new_cursor++;
344 break;
345 case 'd':
346 $vector[] = $this->newPairComponent($old[$old_cursor], null);
347 $old_cursor++;
348 break;
349 case 's':
350 case 'x':
351 case 't':
352 $vector[] = $this->newPairComponent(
353 $old[$old_cursor],
354 $new[$new_cursor]);
355 $old_cursor++;
356 $new_cursor++;
357 break;
358 default:
359 throw new Exception(pht('Unknown edit string "%s"!', $c));
360 }
361 }
362
363 return $vector;
364 }
365
366 private function newScalarComponent($v) {
367 $key = sprintf('path(%s)', $v);
368
369 if (!isset($this->components[$key])) {
370 $this->components[$key] = $v;
371 }
372
373 return $key;
374 }
375
376 private function newPairComponent($u, $v) {
377 if ($u === $v) {
378 return $this->newScalarComponent($u);
379 }
380
381 $key = sprintf('pair(%s > %s)', $u, $v);
382
383 if (!isset($this->components[$key])) {
384 $this->components[$key] = array($u, $v);
385 }
386
387 return $key;
388 }
389
390 private function newComponentView(
391 $icon,
392 array $keys,
393 $old_dir,
394 $new_dir,
395 $item) {
396
397 $is_simple = true;
398
399 $items = array();
400 foreach ($keys as $key) {
401 $component = $this->components[$key];
402
403 if (is_array($component)) {
404 $is_simple = false;
405 } else {
406 $component = array(
407 $component,
408 $component,
409 );
410 }
411
412 $items[] = $component;
413 }
414
415 $move_icon = id(new PHUIIconView())
416 ->setIcon('fa-angle-double-right pink');
417
418 $old_row = array(
419 phutil_tag('td', array(), $move_icon),
420 );
421 $new_row = array(
422 phutil_tag('td', array(), $icon),
423 );
424
425 $last_old_key = null;
426 $last_new_key = null;
427
428 foreach ($items as $key => $component) {
429 if (!is_array($component)) {
430 $last_old_key = $key;
431 $last_new_key = $key;
432 } else {
433 if ($component[0] !== null) {
434 $last_old_key = $key;
435 }
436 if ($component[1] !== null) {
437 $last_new_key = $key;
438 }
439 }
440 }
441
442 foreach ($items as $key => $component) {
443 if (!is_array($component)) {
444 $old = $component;
445 $new = $component;
446 } else {
447 $old = $component[0];
448 $new = $component[1];
449 }
450
451 $old_classes = array();
452 $new_classes = array();
453
454 if ($old === $new) {
455 // Do nothing.
456 } else if ($old === null) {
457 $new_classes[] = 'diff-path-component-new';
458 } else if ($new === null) {
459 $old_classes[] = 'diff-path-component-old';
460 } else {
461 $old_classes[] = 'diff-path-component-old';
462 $new_classes[] = 'diff-path-component-new';
463 }
464
465 if ($old !== null) {
466 if (($key === $last_old_key) && !$old_dir) {
467 // Do nothing.
468 } else {
469 $old = $old.'/';
470 }
471 }
472
473 if ($new !== null) {
474 if (($key === $last_new_key) && $item) {
475 $new = $item->newLink();
476 } else if (($key === $last_new_key) && !$new_dir) {
477 // Do nothing.
478 } else {
479 $new = $new.'/';
480 }
481 }
482
483 $old_row[] = phutil_tag(
484 'td',
485 array(),
486 phutil_tag(
487 'div',
488 array(
489 'class' => implode(' ', $old_classes),
490 ),
491 $old));
492 $new_row[] = phutil_tag(
493 'td',
494 array(),
495 phutil_tag(
496 'div',
497 array(
498 'class' => implode(' ', $new_classes),
499 ),
500 $new));
501 }
502
503 $old_row = phutil_tag(
504 'tr',
505 array(
506 'class' => 'diff-path-old',
507 ),
508 $old_row);
509
510 $new_row = phutil_tag(
511 'tr',
512 array(
513 'class' => 'diff-path-new',
514 ),
515 $new_row);
516
517 $rows = array();
518 $rows[] = $new_row;
519 if (!$is_simple) {
520 $rows[] = $old_row;
521 }
522
523 $body = phutil_tag('tbody', array(), $rows);
524
525 $table = phutil_tag(
526 'table',
527 array(
528 ),
529 $body);
530
531 return $table;
532 }
533
534}