@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
3abstract class PhabricatorObjectGraph
4 extends AbstractDirectedGraph {
5
6 private $viewer;
7 private $edges = array();
8 private $edgeReach = array();
9 private $seedPHID;
10 private $objects;
11 private $loadEntireGraph = false;
12 private $limit;
13 private $adjacent;
14 private $height;
15
16 public function setViewer(PhabricatorUser $viewer) {
17 $this->viewer = $viewer;
18 return $this;
19 }
20
21 public function getViewer() {
22 if (!$this->viewer) {
23 throw new PhutilInvalidStateException('setViewer');
24 }
25
26 return $this->viewer;
27 }
28
29 public function setLimit($limit) {
30 $this->limit = $limit;
31 return $this;
32 }
33
34 public function getLimit() {
35 return $this->limit;
36 }
37
38 public function setHeight($height) {
39 $this->height = $height;
40 return $this;
41 }
42
43 public function getHeight() {
44 return $this->height;
45 }
46
47 final public function setRenderOnlyAdjacentNodes($adjacent) {
48 $this->adjacent = $adjacent;
49 return $this;
50 }
51
52 final public function getRenderOnlyAdjacentNodes() {
53 return $this->adjacent;
54 }
55
56 abstract protected function getEdgeTypes();
57 abstract protected function getParentEdgeType();
58 abstract protected function newQuery();
59 abstract protected function newTableRow($phid, $object, $trace);
60 abstract protected function newTable(AphrontTableView $table);
61 abstract protected function isClosed($object);
62
63 protected function newEllipsisRow() {
64 return array(
65 '...',
66 );
67 }
68
69 final public function setSeedPHID($phid) {
70 $this->seedPHID = $phid;
71 $this->edgeReach[$phid] = array_fill_keys($this->getEdgeTypes(), true);
72
73 return $this->addNodes(
74 array(
75 '<seed>' => array($phid),
76 ));
77 }
78
79 final public function getSeedPHID() {
80 return $this->seedPHID;
81 }
82
83 final public function isEmpty() {
84 return (count($this->getNodes()) <= 2);
85 }
86
87 final public function isOverLimit() {
88 $limit = $this->getLimit();
89
90 if (!$limit) {
91 return false;
92 }
93
94 return (count($this->edgeReach) > $limit);
95 }
96
97 final public function getEdges($type) {
98 $edges = idx($this->edges, $type, array());
99
100 // Remove any nodes which we never reached. We can get these when loading
101 // only part of the graph: for example, they point at other subtasks of
102 // parents or other parents of subtasks.
103 $nodes = $this->getNodes();
104 foreach ($edges as $src => $dsts) {
105 foreach ($dsts as $key => $dst) {
106 if (!isset($nodes[$dst])) {
107 unset($edges[$src][$key]);
108 }
109 }
110 }
111
112 return $edges;
113 }
114
115 final public function setLoadEntireGraph($load_entire_graph) {
116 $this->loadEntireGraph = $load_entire_graph;
117 return $this;
118 }
119
120 final public function getLoadEntireGraph() {
121 return $this->loadEntireGraph;
122 }
123
124 final protected function loadEdges(array $nodes) {
125 if ($this->isOverLimit()) {
126 return array_fill_keys($nodes, array());
127 }
128
129 $edge_types = $this->getEdgeTypes();
130
131 $query = id(new PhabricatorEdgeQuery())
132 ->withSourcePHIDs($nodes)
133 ->withEdgeTypes($edge_types);
134
135 $query->execute();
136
137 $whole_graph = $this->getLoadEntireGraph();
138
139 $map = array();
140 foreach ($nodes as $node) {
141 $map[$node] = array();
142
143 foreach ($edge_types as $edge_type) {
144 $dst_phids = $query->getDestinationPHIDs(
145 array($node),
146 array($edge_type));
147
148 $this->edges[$edge_type][$node] = $dst_phids;
149 foreach ($dst_phids as $dst_phid) {
150 if ($whole_graph || isset($this->edgeReach[$node][$edge_type])) {
151 $map[$node][] = $dst_phid;
152 }
153 $this->edgeReach[$dst_phid][$edge_type] = true;
154 }
155 }
156
157 $map[$node] = array_values(array_fuse($map[$node]));
158 }
159
160 return $map;
161 }
162
163 final public function newGraphTable() {
164 $viewer = $this->getViewer();
165
166 $ancestry = $this->getEdges($this->getParentEdgeType());
167
168 $only_adjacent = $this->getRenderOnlyAdjacentNodes();
169 if ($only_adjacent) {
170 $adjacent = array(
171 $this->getSeedPHID() => $this->getSeedPHID(),
172 );
173
174 foreach ($this->getEdgeTypes() as $edge_type) {
175 $map = $this->getEdges($edge_type);
176 $direct = idx($map, $this->getSeedPHID(), array());
177 $adjacent += array_fuse($direct);
178 }
179
180 foreach ($ancestry as $key => $list) {
181 if (!isset($adjacent[$key])) {
182 unset($ancestry[$key]);
183 continue;
184 }
185
186 foreach ($list as $list_key => $item) {
187 if (!isset($adjacent[$item])) {
188 unset($ancestry[$key][$list_key]);
189 }
190 }
191 }
192 }
193
194 $objects = $this->newQuery()
195 ->setViewer($viewer)
196 ->withPHIDs(array_keys($ancestry))
197 ->execute();
198 $objects = mpull($objects, null, 'getPHID');
199
200 $order = id(new PhutilDirectedScalarGraph())
201 ->addNodes($ancestry)
202 ->getNodesInTopologicalOrder();
203
204 $ancestry = array_select_keys($ancestry, $order);
205
206 $graph_view = new PHUIDiffGraphView();
207
208 $height = $this->getHeight();
209 if ($height !== null) {
210 $graph_view->setHeight($height);
211 }
212
213 $traces = $graph_view->renderGraph($ancestry);
214
215 $ii = 0;
216 $rows = array();
217 $rowc = array();
218
219 if ($only_adjacent) {
220 $rows[] = $this->newEllipsisRow();
221 $rowc[] = 'more';
222 }
223
224 foreach ($ancestry as $phid => $ignored) {
225 $object = idx($objects, $phid);
226 $rows[] = $this->newTableRow($phid, $object, $traces[$ii++]);
227
228 $classes = array();
229 if ($phid == $this->seedPHID) {
230 $classes[] = 'highlighted';
231 }
232
233 if ($object) {
234 if ($this->isClosed($object)) {
235 $classes[] = 'closed';
236 }
237 }
238
239 if ($classes) {
240 $classes = implode(' ', $classes);
241 } else {
242 $classes = null;
243 }
244
245 $rowc[] = $classes;
246 }
247
248 if ($only_adjacent) {
249 $rows[] = $this->newEllipsisRow();
250 $rowc[] = 'more';
251 }
252
253 $table = id(new AphrontTableView($rows))
254 ->setClassName('object-graph-table')
255 ->setRowClasses($rowc);
256
257 $this->objects = $objects;
258
259 return $this->newTable($table);
260 }
261
262 final public function getReachableObjects($edge_type) {
263 if ($this->objects === null) {
264 throw new PhutilInvalidStateException('newGraphTable');
265 }
266
267 $graph = $this->getEdges($edge_type);
268
269 $seen = array();
270 $look = array($this->seedPHID);
271 while ($look) {
272 $phid = array_pop($look);
273
274 $parents = idx($graph, $phid, array());
275 foreach ($parents as $parent) {
276 if (isset($seen[$parent])) {
277 continue;
278 }
279
280 $seen[$parent] = $parent;
281 $look[] = $parent;
282 }
283 }
284
285 $reachable = array();
286 foreach ($seen as $phid) {
287 if ($phid == $this->seedPHID) {
288 continue;
289 }
290
291 $object = idx($this->objects, $phid);
292 if (!$object) {
293 continue;
294 }
295
296 $reachable[] = $object;
297 }
298
299 return $reachable;
300 }
301
302}