Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / repository / worker / PhabricatorRepositoryCommitPublishWorker.php
blob9d70e3db1c84e9147fb13c8848e0bd78a0174fef
1 <?php
3 final class PhabricatorRepositoryCommitPublishWorker
4 extends PhabricatorRepositoryCommitParserWorker {
6 protected function getImportStepFlag() {
7 return PhabricatorRepositoryCommit::IMPORTED_PUBLISH;
10 public function getRequiredLeaseTime() {
11 // Herald rules may take a long time to process.
12 return phutil_units('4 hours in seconds');
15 protected function parseCommit(
16 PhabricatorRepository $repository,
17 PhabricatorRepositoryCommit $commit) {
19 if (!$this->shouldSkipImportStep()) {
20 $this->publishCommit($repository, $commit);
21 $commit->writeImportStatusFlag($this->getImportStepFlag());
24 // This is the last task in the sequence, so we don't need to queue any
25 // followup workers.
28 private function publishCommit(
29 PhabricatorRepository $repository,
30 PhabricatorRepositoryCommit $commit) {
31 $viewer = PhabricatorUser::getOmnipotentUser();
33 $commit_phid = $commit->getPHID();
35 // Reload the commit to get the commit data, identities, and any
36 // outstanding audit requests.
37 $commit = id(new DiffusionCommitQuery())
38 ->setViewer($viewer)
39 ->withPHIDs(array($commit_phid))
40 ->needCommitData(true)
41 ->needIdentities(true)
42 ->needAuditRequests(true)
43 ->executeOne();
44 if (!$commit) {
45 throw new PhabricatorWorkerPermanentFailureException(
46 pht(
47 'Failed to reload commit "%s".',
48 $commit_phid));
51 $publisher = $repository->newPublisher();
52 $should_publish = $publisher->shouldPublishCommit($commit);
54 if (!$should_publish) {
55 $hold_reasons = $publisher->getCommitHoldReasons($commit);
56 } else {
57 $hold_reasons = array();
60 $data = $commit->getCommitData();
61 if ($data->getCommitDetail('holdReasons') !== $hold_reasons) {
62 $data->setCommitDetail('holdReasons', $hold_reasons);
63 $data->save();
66 if (!$should_publish) {
67 return;
70 // NOTE: Close revisions and tasks before applying transactions, because
71 // we want a side effect of closure (the commit being associated with
72 // a revision) to occur before a side effect of transactions (Herald
73 // executing). The close methods queue tasks for the actual updates to
74 // commits/revisions, so those won't occur until after the commit gets
75 // transactions.
77 $this->closeRevisions($viewer, $commit);
78 $this->closeTasks($viewer, $commit);
80 $this->applyTransactions($viewer, $repository, $commit);
83 private function applyTransactions(
84 PhabricatorUser $actor,
85 PhabricatorRepository $repository,
86 PhabricatorRepositoryCommit $commit) {
88 $xactions = array(
89 $this->newAuditTransactions($commit),
90 $this->newPublishTransactions($commit),
92 $xactions = array_mergev($xactions);
94 $acting_phid = $this->getPublishAsPHID($commit);
95 $content_source = $this->newContentSource();
97 $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit(
98 $actor,
99 $commit);
101 // Prevent the commit from generating a mention of the associated
102 // revision, if one exists, so we don't double up because of the URI
103 // in the commit message.
104 $unmentionable_phids = array();
105 if ($revision) {
106 $unmentionable_phids[] = $revision->getPHID();
109 $editor = $commit->getApplicationTransactionEditor()
110 ->setActor($actor)
111 ->setActingAsPHID($acting_phid)
112 ->setContinueOnNoEffect(true)
113 ->setContinueOnMissingFields(true)
114 ->setContentSource($content_source)
115 ->addUnmentionablePHIDs($unmentionable_phids);
117 try {
118 $raw_patch = $this->loadRawPatchText($repository, $commit);
119 } catch (Exception $ex) {
120 $raw_patch = pht('Unable to generate patch: %s', $ex->getMessage());
122 $editor->setRawPatch($raw_patch);
124 $editor->applyTransactions($commit, $xactions);
127 private function getPublishAsPHID(PhabricatorRepositoryCommit $commit) {
128 if ($commit->hasCommitterIdentity()) {
129 return $commit->getCommitterIdentity()->getIdentityDisplayPHID();
132 if ($commit->hasAuthorIdentity()) {
133 return $commit->getAuthorIdentity()->getIdentityDisplayPHID();
136 return id(new PhabricatorDiffusionApplication())->getPHID();
139 private function newPublishTransactions(PhabricatorRepositoryCommit $commit) {
140 $data = $commit->getCommitData();
142 $xactions = array();
144 $xactions[] = $commit->getApplicationTransactionTemplate()
145 ->setTransactionType(PhabricatorAuditTransaction::TYPE_COMMIT)
146 ->setDateCreated($commit->getEpoch())
147 ->setNewValue(
148 array(
149 'description' => $data->getCommitMessage(),
150 'summary' => $data->getSummary(),
151 'authorName' => $data->getAuthorString(),
152 'authorPHID' => $commit->getAuthorPHID(),
153 'committerName' => $data->getCommitterString(),
154 'committerPHID' => $data->getCommitDetail('committerPHID'),
157 return $xactions;
160 private function newAuditTransactions(PhabricatorRepositoryCommit $commit) {
161 $viewer = PhabricatorUser::getOmnipotentUser();
163 $repository = $commit->getRepository();
165 $affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths(
166 $repository,
167 $commit,
168 PhabricatorUser::getOmnipotentUser());
170 $affected_packages = PhabricatorOwnersPackage::loadAffectedPackages(
171 $repository,
172 $affected_paths);
174 $commit->writeOwnersEdges(mpull($affected_packages, 'getPHID'));
176 if (!$affected_packages) {
177 return array();
180 $data = $commit->getCommitData();
182 $author_phid = $commit->getEffectiveAuthorPHID();
184 $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit(
185 $viewer,
186 $commit);
188 $requests = $commit->getAudits();
189 $requests = mpull($requests, null, 'getAuditorPHID');
191 $auditor_phids = array();
192 foreach ($affected_packages as $package) {
193 $request = idx($requests, $package->getPHID());
194 if ($request) {
195 // Don't update request if it exists already.
196 continue;
199 $should_audit = $this->shouldTriggerAudit(
200 $commit,
201 $package,
202 $author_phid,
203 $revision);
204 if (!$should_audit) {
205 continue;
208 $auditor_phids[] = $package->getPHID();
211 // If none of the packages are triggering audits, we're all done.
212 if (!$auditor_phids) {
213 return array();
216 $audit_type = DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE;
218 $xactions = array();
219 $xactions[] = $commit->getApplicationTransactionTemplate()
220 ->setTransactionType($audit_type)
221 ->setNewValue(
222 array(
223 '+' => array_fuse($auditor_phids),
226 return $xactions;
229 private function shouldTriggerAudit(
230 PhabricatorRepositoryCommit $commit,
231 PhabricatorOwnersPackage $package,
232 $author_phid,
233 $revision) {
235 $audit_uninvolved = false;
236 $audit_unreviewed = false;
238 $rule = $package->newAuditingRule();
239 switch ($rule->getKey()) {
240 case PhabricatorOwnersAuditRule::AUDITING_NONE:
241 return false;
242 case PhabricatorOwnersAuditRule::AUDITING_ALL:
243 return true;
244 case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER:
245 $audit_uninvolved = true;
246 break;
247 case PhabricatorOwnersAuditRule::AUDITING_UNREVIEWED:
248 $audit_unreviewed = true;
249 break;
250 case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER_AND_UNREVIEWED:
251 $audit_uninvolved = true;
252 $audit_unreviewed = true;
253 break;
256 // If auditing is configured to trigger on unreviewed changes, check if
257 // the revision was "Accepted" when it landed. If not, trigger an audit.
259 // We may be running before the revision actually closes, so we'll count
260 // either an "Accepted" or a "Closed, Previously Accepted" revision as
261 // good enough.
263 if ($audit_unreviewed) {
264 $commit_unreviewed = true;
265 if ($revision) {
266 if ($revision->isAccepted()) {
267 $commit_unreviewed = false;
268 } else {
269 $was_accepted = DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED;
270 if ($revision->isPublished()) {
271 if ($revision->getProperty($was_accepted)) {
272 $commit_unreviewed = false;
278 if ($commit_unreviewed) {
279 return true;
283 // If auditing is configured to trigger on changes with no involved owner,
284 // check for an owner. If we don't find one, trigger an audit.
285 if ($audit_uninvolved) {
286 $owner_involved = $this->isOwnerInvolved(
287 $commit,
288 $package,
289 $author_phid,
290 $revision);
291 if (!$owner_involved) {
292 return true;
296 // We can't find any reason to trigger an audit for this commit.
297 return false;
300 private function isOwnerInvolved(
301 PhabricatorRepositoryCommit $commit,
302 PhabricatorOwnersPackage $package,
303 $author_phid,
304 $revision) {
306 $owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
307 array(
308 $package->getID(),
310 $owner_phids = array_fuse($owner_phids);
312 // For the purposes of deciding whether the owners were involved in the
313 // revision or not, consider a review by the package itself to count as
314 // involvement. This can happen when human reviewers force-accept on
315 // behalf of packages they don't own but have authority over.
316 $owner_phids[$package->getPHID()] = $package->getPHID();
318 // If the commit author is identifiable and a package owner, they're
319 // involved.
320 if ($author_phid) {
321 if (isset($owner_phids[$author_phid])) {
322 return true;
326 // Otherwise, we need to find an owner as a reviewer.
328 // If we don't have a revision, this is hopeless: no owners are involved.
329 if (!$revision) {
330 return true;
333 $accepted_statuses = array(
334 DifferentialReviewerStatus::STATUS_ACCEPTED,
335 DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER,
337 $accepted_statuses = array_fuse($accepted_statuses);
339 $found_accept = false;
340 foreach ($revision->getReviewers() as $reviewer) {
341 $reviewer_phid = $reviewer->getReviewerPHID();
343 // If this reviewer isn't a package owner or the package itself,
344 // just ignore them.
345 if (empty($owner_phids[$reviewer_phid])) {
346 continue;
349 // If this reviewer accepted the revision and owns the package (or is
350 // the package), we've found an involved owner.
351 if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) {
352 $found_accept = true;
353 break;
357 if ($found_accept) {
358 return true;
361 return false;
364 private function loadRawPatchText(
365 PhabricatorRepository $repository,
366 PhabricatorRepositoryCommit $commit) {
367 $viewer = PhabricatorUser::getOmnipotentUser();
369 $identifier = $commit->getCommitIdentifier();
371 $drequest = DiffusionRequest::newFromDictionary(
372 array(
373 'user' => $viewer,
374 'repository' => $repository,
377 $time_key = 'metamta.diffusion.time-limit';
378 $byte_key = 'metamta.diffusion.byte-limit';
379 $time_limit = PhabricatorEnv::getEnvConfig($time_key);
380 $byte_limit = PhabricatorEnv::getEnvConfig($byte_key);
382 $diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
383 $viewer,
384 $drequest,
385 'diffusion.rawdiffquery',
386 array(
387 'commit' => $identifier,
388 'linesOfContext' => 3,
389 'timeout' => $time_limit,
390 'byteLimit' => $byte_limit,
393 if ($diff_info['tooSlow']) {
394 throw new Exception(
395 pht(
396 'Patch generation took longer than configured limit ("%s") of '.
397 '%s second(s).',
398 $time_key,
399 new PhutilNumber($time_limit)));
402 if ($diff_info['tooHuge']) {
403 $pretty_limit = phutil_format_bytes($byte_limit);
404 throw new Exception(
405 pht(
406 'Patch size exceeds configured byte size limit ("%s") of %s.',
407 $byte_key,
408 $pretty_limit));
411 $file_phid = $diff_info['filePHID'];
412 $file = id(new PhabricatorFileQuery())
413 ->setViewer($viewer)
414 ->withPHIDs(array($file_phid))
415 ->executeOne();
416 if (!$file) {
417 throw new Exception(
418 pht(
419 'Failed to load file ("%s") returned by "%s".',
420 $file_phid,
421 'diffusion.rawdiffquery'));
424 return $file->loadFileData();
427 private function closeRevisions(
428 PhabricatorUser $actor,
429 PhabricatorRepositoryCommit $commit) {
431 $differential = 'PhabricatorDifferentialApplication';
432 if (!PhabricatorApplication::isClassInstalled($differential)) {
433 return;
436 $repository = $commit->getRepository();
437 $data = $commit->getCommitData();
438 $ref = $data->getCommitRef();
440 $field_query = id(new DiffusionLowLevelCommitFieldsQuery())
441 ->setRepository($repository)
442 ->withCommitRef($ref);
444 $field_values = $field_query->execute();
446 $revision_id = idx($field_values, 'revisionID');
447 if (!$revision_id) {
448 return;
451 $revision = id(new DifferentialRevisionQuery())
452 ->setViewer($actor)
453 ->withIDs(array($revision_id))
454 ->executeOne();
455 if (!$revision) {
456 return;
459 // NOTE: This is very old code from when revisions had a single reviewer.
460 // It still powers the "Reviewer (Deprecated)" field in Herald, but should
461 // be removed.
462 if (!empty($field_values['reviewedByPHIDs'])) {
463 $data->setCommitDetail(
464 'reviewerPHID',
465 head($field_values['reviewedByPHIDs']));
468 $match_data = $field_query->getRevisionMatchData();
470 $data->setCommitDetail('differential.revisionID', $revision_id);
471 $data->setCommitDetail('revisionMatchData', $match_data);
473 $data->save();
475 $properties = array(
476 'revisionMatchData' => $match_data,
478 $this->queueObjectUpdate($commit, $revision, $properties);
481 private function closeTasks(
482 PhabricatorUser $actor,
483 PhabricatorRepositoryCommit $commit) {
485 $maniphest = 'PhabricatorManiphestApplication';
486 if (!PhabricatorApplication::isClassInstalled($maniphest)) {
487 return;
490 $data = $commit->getCommitData();
492 $prefixes = ManiphestTaskStatus::getStatusPrefixMap();
493 $suffixes = ManiphestTaskStatus::getStatusSuffixMap();
494 $message = $data->getCommitMessage();
496 $matches = id(new ManiphestCustomFieldStatusParser())
497 ->parseCorpus($message);
499 $task_map = array();
500 foreach ($matches as $match) {
501 $prefix = phutil_utf8_strtolower($match['prefix']);
502 $suffix = phutil_utf8_strtolower($match['suffix']);
504 $status = idx($suffixes, $suffix);
505 if (!$status) {
506 $status = idx($prefixes, $prefix);
509 foreach ($match['monograms'] as $task_monogram) {
510 $task_id = (int)trim($task_monogram, 'tT');
511 $task_map[$task_id] = $status;
515 if (!$task_map) {
516 return;
519 $tasks = id(new ManiphestTaskQuery())
520 ->setViewer($actor)
521 ->withIDs(array_keys($task_map))
522 ->execute();
523 foreach ($tasks as $task_id => $task) {
524 $status = $task_map[$task_id];
526 $properties = array(
527 'status' => $status,
530 $this->queueObjectUpdate($commit, $task, $properties);
534 private function queueObjectUpdate(
535 PhabricatorRepositoryCommit $commit,
536 $object,
537 array $properties) {
539 $this->queueTask(
540 'DiffusionUpdateObjectAfterCommitWorker',
541 array(
542 'commitPHID' => $commit->getPHID(),
543 'objectPHID' => $object->getPHID(),
544 'properties' => $properties,
546 array(
547 'priority' => PhabricatorWorker::PRIORITY_DEFAULT,