@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 HeraldTestConsoleController extends HeraldController {
4
5 private $testObject;
6 private $testAdapter;
7
8 public function setTestObject($test_object) {
9 $this->testObject = $test_object;
10 return $this;
11 }
12
13 public function getTestObject() {
14 return $this->testObject;
15 }
16
17 public function setTestAdapter(HeraldAdapter $test_adapter) {
18 $this->testAdapter = $test_adapter;
19 return $this;
20 }
21
22 public function getTestAdapter() {
23 return $this->testAdapter;
24 }
25
26 public function handleRequest(AphrontRequest $request) {
27 $viewer = $request->getViewer();
28
29 $response = $this->loadTestObject($request);
30 if ($response) {
31 return $response;
32 }
33
34 $response = $this->loadAdapter($request);
35 if ($response) {
36 return $response;
37 }
38
39 $object = $this->getTestObject();
40 $adapter = $this->getTestAdapter();
41 $source = $this->newContentSource($object);
42
43 $adapter
44 ->setContentSource($source)
45 ->setIsNewObject(false)
46 ->setActingAsPHID($viewer->getPHID())
47 ->setViewer($viewer);
48
49 $applied_xactions = $this->loadAppliedTransactions($object);
50 if ($applied_xactions !== null) {
51 $adapter->setAppliedTransactions($applied_xactions);
52 }
53
54 $rules = id(new HeraldRuleQuery())
55 ->setViewer($viewer)
56 ->withContentTypes(array($adapter->getAdapterContentType()))
57 ->withDisabled(false)
58 ->needConditionsAndActions(true)
59 ->needAppliedToPHIDs(array($object->getPHID()))
60 ->needValidateAuthors(true)
61 ->execute();
62
63 $engine = id(new HeraldEngine())
64 ->setDryRun(true);
65
66 $effects = $engine->applyRules($rules, $adapter);
67 $engine->applyEffects($effects, $adapter, $rules);
68
69 $xscript = $engine->getTranscript();
70
71 return id(new AphrontRedirectResponse())
72 ->setURI('/herald/transcript/'.$xscript->getID().'/');
73 }
74
75 private function loadTestObject(AphrontRequest $request) {
76 $viewer = $this->getViewer();
77
78 $e_name = true;
79 $v_name = null;
80 $errors = array();
81
82 if ($request->isFormPost()) {
83 $v_name = trim($request->getStr('object_name'));
84 if (!$v_name) {
85 $e_name = pht('Required');
86 $errors[] = pht('An object name is required.');
87 }
88
89 if (!$errors) {
90 $object = id(new PhabricatorObjectQuery())
91 ->setViewer($viewer)
92 ->withNames(array($v_name))
93 ->executeOne();
94
95 if (!$object) {
96 $e_name = pht('Invalid');
97 $errors[] = pht('No object exists with that name.');
98 }
99 }
100
101 if (!$errors) {
102 $this->setTestObject($object);
103 return null;
104 }
105 }
106
107 $form = id(new AphrontFormView())
108 ->setUser($viewer)
109 ->appendRemarkupInstructions(
110 pht(
111 'Enter an object to test rules for, like a Diffusion commit (e.g., '.
112 '`rX123`) or a Differential revision (e.g., `D123`). You will be '.
113 'shown the results of a dry run on the object.'))
114 ->appendChild(
115 id(new AphrontFormTextControl())
116 ->setLabel(pht('Object Name'))
117 ->setName('object_name')
118 ->setError($e_name)
119 ->setValue($v_name))
120 ->appendChild(
121 id(new AphrontFormSubmitControl())
122 ->setValue(pht('Continue')));
123
124 return $this->buildTestConsoleResponse($form, $errors);
125 }
126
127 private function loadAdapter(AphrontRequest $request) {
128 $viewer = $this->getViewer();
129 $object = $this->getTestObject();
130
131 $adapter_key = $request->getStr('adapter');
132
133 $adapters = HeraldAdapter::getAllAdapters();
134
135 $can_select = array();
136 $display_adapters = array();
137 foreach ($adapters as $key => $adapter) {
138 if (!$adapter->isTestAdapterForObject($object)) {
139 continue;
140 }
141
142 if (!$adapter->isAvailableToUser($viewer)) {
143 continue;
144 }
145
146 $display_adapters[$key] = $adapter;
147
148 if ($adapter->canCreateTestAdapterForObject($object)) {
149 $can_select[$key] = $adapter;
150 }
151 }
152
153 if ($request->isFormPost() && $adapter_key) {
154 if (isset($can_select[$adapter_key])) {
155 $adapter = $can_select[$adapter_key]->newTestAdapter(
156 $viewer,
157 $object);
158 $this->setTestAdapter($adapter);
159 return null;
160 }
161 }
162
163 $form = id(new AphrontFormView())
164 ->addHiddenInput('object_name', $request->getStr('object_name'))
165 ->setViewer($viewer);
166
167 $cancel_uri = $this->getApplicationURI();
168
169 if (!$display_adapters) {
170 $form
171 ->appendRemarkupInstructions(
172 pht('//There are no available Herald events for this object.//'))
173 ->appendControl(
174 id(new AphrontFormSubmitControl())
175 ->addCancelButton($cancel_uri));
176 } else {
177 $adapter_control = id(new AphrontFormRadioButtonControl())
178 ->setLabel(pht('Event'))
179 ->setName('adapter')
180 ->setValue(head_key($can_select));
181
182 foreach ($display_adapters as $adapter_key => $adapter) {
183 $is_disabled = empty($can_select[$adapter_key]);
184
185 $adapter_control->addButton(
186 $adapter_key,
187 $adapter->getAdapterContentName(),
188 $adapter->getAdapterTestDescription(),
189 null,
190 $is_disabled);
191 }
192
193 $form
194 ->appendControl($adapter_control)
195 ->appendControl(
196 id(new AphrontFormSubmitControl())
197 ->setValue(pht('Run Test')));
198 }
199
200 return $this->buildTestConsoleResponse($form, array());
201 }
202
203 private function buildTestConsoleResponse($form, array $errors) {
204 $box = id(new PHUIObjectBoxView())
205 ->setFormErrors($errors)
206 ->setForm($form);
207
208 $crumbs = id($this->buildApplicationCrumbs())
209 ->addTextCrumb(pht('Test Console'))
210 ->setBorder(true);
211
212 $title = pht('Test Console');
213
214 $header = id(new PHUIHeaderView())
215 ->setHeader($title)
216 ->setHeaderIcon('fa-desktop');
217
218 $view = id(new PHUITwoColumnView())
219 ->setHeader($header)
220 ->setFooter($box);
221
222 return $this->newPage()
223 ->setTitle($title)
224 ->setCrumbs($crumbs)
225 ->appendChild($view);
226 }
227
228 private function newContentSource($object) {
229 $viewer = $this->getViewer();
230
231 // Try using the content source associated with the most recent transaction
232 // on the object.
233
234 $query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
235
236 $xaction = $query
237 ->setViewer($viewer)
238 ->withObjectPHIDs(array($object->getPHID()))
239 ->setLimit(1)
240 ->setOrder('newest')
241 ->executeOne();
242 if ($xaction) {
243 return $xaction->getContentSource();
244 }
245
246 // If we couldn't find a transaction (which should be rare), fall back to
247 // building a new content source from the test console request itself.
248
249 $request = $this->getRequest();
250 return PhabricatorContentSource::newFromRequest($request);
251 }
252
253 private function loadAppliedTransactions($object) {
254 $viewer = $this->getViewer();
255
256 if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
257 return null;
258 }
259
260 $query = PhabricatorApplicationTransactionQuery::newQueryForObject(
261 $object);
262
263 $query
264 ->withObjectPHIDs(array($object->getPHID()))
265 ->setViewer($viewer);
266
267 $xactions = new PhabricatorQueryIterator($query);
268
269 $applied = array();
270
271 $recent_id = null;
272 $hard_limit = 1000;
273 foreach ($xactions as $xaction) {
274
275 // If this transaction has Herald transcript metadata, it was applied by
276 // Herald. Exclude it from the list because the Herald rule engine always
277 // runs before Herald transactions apply, so there's no way that real
278 // rules would have seen this transaction.
279 $transcript_id = $xaction->getMetadataValue('herald:transcriptID');
280 if ($transcript_id !== null) {
281 continue;
282 }
283
284 $group_id = $xaction->getTransactionGroupID();
285
286 // If this is the first transaction, save the group ID: we want to
287 // select all transactions in the same group.
288 if (!$applied) {
289 $recent_id = $group_id;
290 if ($recent_id === null) {
291 // If the first transaction has no group ID, it is likely an older
292 // transaction from before the introduction of group IDs. In this
293 // case, select only the most recent transaction and bail out.
294 $applied[] = $xaction;
295 break;
296 }
297 }
298
299 // If this transaction is from a different transaction group, we've
300 // found all the transactions applied in the most recent group.
301 if ($group_id !== $recent_id) {
302 break;
303 }
304
305 $applied[] = $xaction;
306
307 if (count($applied) > $hard_limit) {
308 throw new Exception(
309 pht(
310 'This object ("%s") has more than %s transactions in its most '.
311 'recent transaction group; this is too many.',
312 $object->getPHID(),
313 new PhutilNumber($hard_limit)));
314 }
315 }
316
317 return $applied;
318
319 }
320
321}