@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 HeraldCommitAdapter
4 extends HeraldAdapter
5 implements HarbormasterBuildableAdapterInterface {
6
7 protected $diff;
8 protected $revision;
9
10 protected $commit;
11 private $commitDiff;
12
13 protected $affectedPaths;
14 protected $affectedRevision;
15 protected $affectedPackages;
16 protected $auditNeededPackages;
17
18 private $buildRequests = array();
19
20 public function getAdapterApplicationClass() {
21 return PhabricatorDiffusionApplication::class;
22 }
23
24 protected function newObject() {
25 return new PhabricatorRepositoryCommit();
26 }
27
28 public function isTestAdapterForObject($object) {
29 return ($object instanceof PhabricatorRepositoryCommit);
30 }
31
32 public function getAdapterTestDescription() {
33 return pht(
34 'Test rules which run after a commit is discovered and imported.');
35 }
36
37 public function newTestAdapter(PhabricatorUser $viewer, $object) {
38 return id(clone $this)
39 ->setObject($object);
40 }
41
42 protected function initializeNewAdapter() {
43 $this->commit = $this->newObject();
44 }
45
46 public function setObject($object) {
47 $viewer = $this->getViewer();
48 $commit_phid = $object->getPHID();
49
50 $commit = id(new DiffusionCommitQuery())
51 ->setViewer($viewer)
52 ->withPHIDs(array($commit_phid))
53 ->needCommitData(true)
54 ->needIdentities(true)
55 ->needAuditRequests(true)
56 ->executeOne();
57 if (!$commit) {
58 throw new Exception(
59 pht(
60 'Failed to reload commit ("%s") to fetch commit data.',
61 $commit_phid));
62 }
63
64 $this->commit = $commit;
65
66 return $this;
67 }
68
69 public function getObject() {
70 return $this->commit;
71 }
72
73 public function getAdapterContentType() {
74 return 'commit';
75 }
76
77 public function getAdapterContentName() {
78 return pht('Commits');
79 }
80
81 public function getAdapterContentDescription() {
82 return pht(
83 "React to new commits appearing in tracked repositories.\n".
84 "Commit rules can send email, flag commits, trigger audits, ".
85 "and run build plans.");
86 }
87
88 public function supportsRuleType($rule_type) {
89 switch ($rule_type) {
90 case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
91 case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
92 case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
93 return true;
94 default:
95 return false;
96 }
97 }
98
99 public function canTriggerOnObject($object) {
100 if ($object instanceof PhabricatorRepository) {
101 return true;
102 }
103 if ($object instanceof PhabricatorProject) {
104 return true;
105 }
106 return false;
107 }
108
109 public function getTriggerObjectPHIDs() {
110 $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
111
112 $repository_phid = $this->getRepository()->getPHID();
113 $commit_phid = $this->getObject()->getPHID();
114
115 $phids = array();
116 $phids[] = $commit_phid;
117 $phids[] = $repository_phid;
118
119 // NOTE: This is projects for the repository, not for the commit. When
120 // Herald evaluates, commits normally can not have any project tags yet.
121 $repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
122 $repository_phid,
123 $project_type);
124 foreach ($repository_project_phids as $phid) {
125 $phids[] = $phid;
126 }
127
128 $phids = array_unique($phids);
129 $phids = array_values($phids);
130
131 return $phids;
132 }
133
134 public function explainValidTriggerObjects() {
135 return pht('This rule can trigger for **repositories** and **projects**.');
136 }
137
138 public function getHeraldName() {
139 return $this->commit->getMonogram();
140 }
141
142 public function loadAffectedPaths() {
143 $viewer = $this->getViewer();
144
145 if ($this->affectedPaths === null) {
146 $result = PhabricatorOwnerPathQuery::loadAffectedPaths(
147 $this->getRepository(),
148 $this->commit,
149 $viewer);
150 $this->affectedPaths = $result;
151 }
152
153 return $this->affectedPaths;
154 }
155
156 public function loadAffectedPackages() {
157 if ($this->affectedPackages === null) {
158 $packages = PhabricatorOwnersPackage::loadAffectedPackages(
159 $this->getRepository(),
160 $this->loadAffectedPaths());
161 $this->affectedPackages = $packages;
162 }
163 return $this->affectedPackages;
164 }
165
166 public function loadAuditNeededPackages() {
167 if ($this->auditNeededPackages === null) {
168 $status_arr = array(
169 PhabricatorAuditRequestStatus::AUDIT_REQUIRED,
170 PhabricatorAuditRequestStatus::CONCERNED,
171 );
172 $requests = id(new PhabricatorRepositoryAuditRequest())
173 ->loadAllWhere(
174 'commitPHID = %s AND auditStatus IN (%Ls)',
175 $this->commit->getPHID(),
176 $status_arr);
177 $this->auditNeededPackages = $requests;
178 }
179 return $this->auditNeededPackages;
180 }
181
182 public function loadDifferentialRevision() {
183 if ($this->affectedRevision === null) {
184 $viewer = $this->getViewer();
185
186 // NOTE: The viewer here is omnipotent, which means that Herald discloses
187 // some information users do not normally have access to when rules load
188 // the revision related to a commit. See D20468.
189
190 // A user who wants to learn about "Dxyz" can write a Herald rule which
191 // uses all the "Related revision..." fields, then push a commit which
192 // contains "Differential Revision: Dxyz" in the message to make Herald
193 // evaluate the commit with "Dxyz" as the related revision.
194
195 // At time of writing, this commit will link to the revision and the
196 // transcript for the commit will disclose some information about the
197 // revision (like reviewers, subscribers, and build status) which the
198 // commit author could not otherwise see.
199
200 // For now, we just accept this. The disclosures are relatively
201 // uninteresting and you have to jump through a lot of hoops (and leave
202 // a lot of evidence) to get this information.
203
204 $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit(
205 $viewer,
206 $this->getObject());
207 if ($revision) {
208 $this->affectedRevision = $revision;
209 } else {
210 $this->affectedRevision = false;
211 }
212 }
213
214 return $this->affectedRevision;
215 }
216
217 public static function getEnormousByteLimit() {
218 return 256 * 1024 * 1024; // 256MB. See T13142 and T13143.
219 }
220
221 public static function getEnormousTimeLimit() {
222 return 60 * 15; // 15 Minutes
223 }
224
225 private function loadCommitDiff() {
226 $viewer = $this->getViewer();
227
228 $byte_limit = self::getEnormousByteLimit();
229 $time_limit = self::getEnormousTimeLimit();
230
231 $diff_info = $this->callConduit(
232 'diffusion.rawdiffquery',
233 array(
234 'commit' => $this->commit->getCommitIdentifier(),
235 'timeout' => $time_limit,
236 'byteLimit' => $byte_limit,
237 'linesOfContext' => 0,
238 ));
239
240 if ($diff_info['tooHuge']) {
241 throw new Exception(
242 pht(
243 'The raw text of this change is enormous (larger than %s bytes). '.
244 'Herald can not process it.',
245 new PhutilNumber($byte_limit)));
246 }
247
248 if ($diff_info['tooSlow']) {
249 throw new Exception(
250 pht(
251 'The raw text of this change took too long to process (longer '.
252 'than %s seconds). Herald can not process it.',
253 new PhutilNumber($time_limit)));
254 }
255
256 $file_phid = $diff_info['filePHID'];
257 $diff_file = id(new PhabricatorFileQuery())
258 ->setViewer($viewer)
259 ->withPHIDs(array($file_phid))
260 ->executeOne();
261 if (!$diff_file) {
262 throw new Exception(
263 pht(
264 'Failed to load diff ("%s") for this change.',
265 $file_phid));
266 }
267
268 $raw = $diff_file->loadFileData();
269
270 // See T13667. This happens when a commit is empty and affects no files.
271 if (!strlen($raw)) {
272 return false;
273 }
274
275 $parser = new ArcanistDiffParser();
276 $changes = $parser->parseDiff($raw);
277
278 $diff = DifferentialDiff::newEphemeralFromRawChanges(
279 $changes);
280 return $diff;
281 }
282
283 public function isDiffEnormous() {
284 $this->loadDiffContent('*');
285 return ($this->commitDiff instanceof Exception);
286 }
287
288 public function loadDiffContent($type) {
289 if ($this->commitDiff === null) {
290 try {
291 $this->commitDiff = $this->loadCommitDiff();
292 } catch (Exception $ex) {
293 $this->commitDiff = $ex;
294 phlog($ex);
295 }
296 }
297
298 if ($this->commitDiff === false) {
299 return array();
300 }
301
302 if ($this->commitDiff instanceof Exception) {
303 $ex = $this->commitDiff;
304 $ex_class = get_class($ex);
305 $ex_message = pht('Failed to load changes: %s', $ex->getMessage());
306
307 return array(
308 '<'.$ex_class.'>' => $ex_message,
309 );
310 }
311
312 $changes = $this->commitDiff->getChangesets();
313
314 $result = array();
315 foreach ($changes as $change) {
316 $lines = array();
317 foreach ($change->getHunks() as $hunk) {
318 switch ($type) {
319 case '-':
320 $lines[] = $hunk->makeOldFile();
321 break;
322 case '+':
323 $lines[] = $hunk->makeNewFile();
324 break;
325 case '*':
326 $lines[] = $hunk->makeChanges();
327 break;
328 default:
329 throw new Exception(pht("Unknown content selection '%s'!", $type));
330 }
331 }
332 $result[$change->getFilename()] = implode("\n", $lines);
333 }
334
335 return $result;
336 }
337
338 public function loadIsMergeCommit() {
339 $parents = $this->callConduit(
340 'diffusion.commitparentsquery',
341 array(
342 'commit' => $this->getObject()->getCommitIdentifier(),
343 ));
344
345 return (count($parents) > 1);
346 }
347
348 private function callConduit($method, array $params) {
349 $viewer = $this->getViewer();
350
351 $drequest = DiffusionRequest::newFromDictionary(
352 array(
353 'user' => $viewer,
354 'repository' => $this->getRepository(),
355 'commit' => $this->commit->getCommitIdentifier(),
356 ));
357
358 return DiffusionQuery::callConduitWithDiffusionRequest(
359 $viewer,
360 $drequest,
361 $method,
362 $params);
363 }
364
365 private function getRepository() {
366 return $this->getObject()->getRepository();
367 }
368
369 public function getAuthorPHID() {
370 return $this->getObject()->getEffectiveAuthorPHID();
371 }
372
373 public function getCommitterPHID() {
374 $commit = $this->getObject();
375
376 if ($commit->hasCommitterIdentity()) {
377 $identity = $commit->getCommitterIdentity();
378 return $identity->getCurrentEffectiveUserPHID();
379 }
380
381 return null;
382 }
383
384
385/* -( HarbormasterBuildableAdapterInterface )------------------------------ */
386
387
388 public function getHarbormasterBuildablePHID() {
389 return $this->getObject()->getPHID();
390 }
391
392 public function getHarbormasterContainerPHID() {
393 return $this->getObject()->getRepository()->getPHID();
394 }
395
396 public function getQueuedHarbormasterBuildRequests() {
397 return $this->buildRequests;
398 }
399
400 public function queueHarbormasterBuildRequest(
401 HarbormasterBuildRequest $request) {
402 $this->buildRequests[] = $request;
403 }
404
405}