3 final class PhabricatorAuditEditor
4 extends PhabricatorApplicationTransactionEditor
{
6 const MAX_FILES_SHOWN_IN_EMAIL
= 1000;
8 private $affectedFiles;
10 private $auditorPHIDs = array();
12 private $didExpandInlineState = false;
13 private $oldAuditStatus = null;
15 public function setRawPatch($patch) {
16 $this->rawPatch
= $patch;
20 public function getRawPatch() {
21 return $this->rawPatch
;
24 public function getEditorApplicationClass() {
25 return 'PhabricatorDiffusionApplication';
28 public function getEditorObjectsDescription() {
32 public function getTransactionTypes() {
33 $types = parent
::getTransactionTypes();
35 $types[] = PhabricatorTransactions
::TYPE_COMMENT
;
36 $types[] = PhabricatorTransactions
::TYPE_EDGE
;
37 $types[] = PhabricatorTransactions
::TYPE_INLINESTATE
;
39 $types[] = PhabricatorAuditTransaction
::TYPE_COMMIT
;
41 // TODO: These will get modernized eventually, but that can happen one
42 // at a time later on.
43 $types[] = PhabricatorAuditActionConstants
::INLINE
;
48 protected function expandTransactions(
49 PhabricatorLiskDAO
$object,
52 foreach ($xactions as $xaction) {
53 switch ($xaction->getTransactionType()) {
54 case PhabricatorTransactions
::TYPE_INLINESTATE
:
55 $this->didExpandInlineState
= true;
60 $this->oldAuditStatus
= $object->getAuditStatus();
62 return parent
::expandTransactions($object, $xactions);
65 protected function transactionHasEffect(
66 PhabricatorLiskDAO
$object,
67 PhabricatorApplicationTransaction
$xaction) {
69 switch ($xaction->getTransactionType()) {
70 case PhabricatorAuditActionConstants
::INLINE
:
71 return $xaction->hasComment();
74 return parent
::transactionHasEffect($object, $xaction);
77 protected function getCustomTransactionOldValue(
78 PhabricatorLiskDAO
$object,
79 PhabricatorApplicationTransaction
$xaction) {
80 switch ($xaction->getTransactionType()) {
81 case PhabricatorAuditActionConstants
::INLINE
:
82 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
86 return parent
::getCustomTransactionOldValue($object, $xaction);
89 protected function getCustomTransactionNewValue(
90 PhabricatorLiskDAO
$object,
91 PhabricatorApplicationTransaction
$xaction) {
93 switch ($xaction->getTransactionType()) {
94 case PhabricatorAuditActionConstants
::INLINE
:
95 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
96 return $xaction->getNewValue();
99 return parent
::getCustomTransactionNewValue($object, $xaction);
102 protected function applyCustomInternalTransaction(
103 PhabricatorLiskDAO
$object,
104 PhabricatorApplicationTransaction
$xaction) {
106 switch ($xaction->getTransactionType()) {
107 case PhabricatorAuditActionConstants
::INLINE
:
108 $comment = $xaction->getComment();
110 $comment->setAttribute('editing', false);
112 PhabricatorVersionedDraft
::purgeDrafts(
114 $this->getActingAsPHID());
116 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
120 return parent
::applyCustomInternalTransaction($object, $xaction);
123 protected function applyCustomExternalTransaction(
124 PhabricatorLiskDAO
$object,
125 PhabricatorApplicationTransaction
$xaction) {
127 switch ($xaction->getTransactionType()) {
128 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
130 case PhabricatorAuditActionConstants
::INLINE
:
131 $reply = $xaction->getComment()->getReplyToComment();
132 if ($reply && !$reply->getHasReplies()) {
133 $reply->setHasReplies(1)->save();
138 return parent
::applyCustomExternalTransaction($object, $xaction);
141 protected function applyBuiltinExternalTransaction(
142 PhabricatorLiskDAO
$object,
143 PhabricatorApplicationTransaction
$xaction) {
145 switch ($xaction->getTransactionType()) {
146 case PhabricatorTransactions
::TYPE_INLINESTATE
:
147 $table = new PhabricatorAuditTransactionComment();
148 $conn_w = $table->establishConnection('w');
149 foreach ($xaction->getNewValue() as $phid => $state) {
152 'UPDATE %T SET fixedState = %s WHERE phid = %s',
153 $table->getTableName(),
160 return parent
::applyBuiltinExternalTransaction($object, $xaction);
163 protected function applyFinalEffects(
164 PhabricatorLiskDAO
$object,
167 // Load auditors explicitly; we may not have them if the caller was a
168 // generic piece of infrastructure.
170 $commit = id(new DiffusionCommitQuery())
171 ->setViewer($this->requireActor())
172 ->withIDs(array($object->getID()))
173 ->needAuditRequests(true)
177 pht('Failed to load commit during transaction finalization!'));
179 $object->attachAudits($commit->getAudits());
181 $actor_phid = $this->getActingAsPHID();
182 $actor_is_author = ($object->getAuthorPHID()) &&
183 ($actor_phid == $object->getAuthorPHID());
185 $import_status_flag = null;
186 foreach ($xactions as $xaction) {
187 switch ($xaction->getTransactionType()) {
188 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
189 $import_status_flag = PhabricatorRepositoryCommit
::IMPORTED_PUBLISH
;
194 $old_status = $this->oldAuditStatus
;
196 $requests = $object->getAudits();
197 $object->updateAuditStatus($requests);
199 $new_status = $object->getAuditStatus();
203 if ($import_status_flag) {
204 $object->writeImportStatusFlag($import_status_flag);
207 // If the commit has changed state after this edit, add an informational
208 // transaction about the state change.
209 if ($old_status != $new_status) {
210 if ($object->isAuditStatusPartiallyAudited()) {
211 // This state isn't interesting enough to get a transaction. The
212 // best way we could lead the user forward is something like "This
213 // commit still requires additional audits." but that's redundant and
214 // probably not very useful.
216 $xaction = $object->getApplicationTransactionTemplate()
217 ->setTransactionType(DiffusionCommitStateTransaction
::TRANSACTIONTYPE
)
218 ->setOldValue($old_status)
219 ->setNewValue($new_status);
221 $xaction = $this->populateTransaction($object, $xaction);
227 // Collect auditor PHIDs for building mail.
228 $this->auditorPHIDs
= mpull($object->getAudits(), 'getAuditorPHID');
233 protected function expandTransaction(
234 PhabricatorLiskDAO
$object,
235 PhabricatorApplicationTransaction
$xaction) {
237 $auditors_type = DiffusionCommitAuditorsTransaction
::TRANSACTIONTYPE
;
239 $xactions = parent
::expandTransaction($object, $xaction);
241 switch ($xaction->getTransactionType()) {
242 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
243 $phids = $this->getAuditRequestTransactionPHIDsFromCommitMessage(
246 $xactions[] = $object->getApplicationTransactionTemplate()
247 ->setTransactionType($auditors_type)
250 '+' => array_fuse($phids),
252 $this->addUnmentionablePHIDs($phids);
259 if (!$this->didExpandInlineState
) {
260 switch ($xaction->getTransactionType()) {
261 case PhabricatorTransactions
::TYPE_COMMENT
:
262 $this->didExpandInlineState
= true;
264 $query_template = id(new DiffusionDiffInlineCommentQuery())
265 ->withCommitPHIDs(array($object->getPHID()));
267 $state_xaction = $this->newInlineStateTransaction(
271 if ($state_xaction) {
272 $xactions[] = $state_xaction;
281 private function getAuditRequestTransactionPHIDsFromCommitMessage(
282 PhabricatorRepositoryCommit
$commit) {
284 $actor = $this->getActor();
285 $data = $commit->getCommitData();
286 $message = $data->getCommitMessage();
288 $result = DifferentialCommitMessageParser
::newStandardParser($actor)
289 ->setRaiseMissingFieldErrors(false)
290 ->parseFields($message);
292 $field_key = DifferentialAuditorsCommitMessageField
::FIELDKEY
;
293 $phids = idx($result, $field_key, null);
299 // If a commit lists its author as an auditor, just pretend it does not.
300 foreach ($phids as $key => $phid) {
301 if ($phid == $commit->getAuthorPHID()) {
313 protected function sortTransactions(array $xactions) {
314 $xactions = parent
::sortTransactions($xactions);
319 foreach ($xactions as $xaction) {
320 $type = $xaction->getTransactionType();
321 if ($type == PhabricatorAuditActionConstants
::INLINE
) {
328 return array_values(array_merge($head, $tail));
331 protected function supportsSearch() {
335 protected function expandCustomRemarkupBlockTransactions(
336 PhabricatorLiskDAO
$object,
339 PhutilMarkupEngine
$engine) {
341 $actor = $this->getActor();
344 // Some interactions (like "Fixes Txxx" interacting with Maniphest) have
345 // already been processed, so we're only re-parsing them here to avoid
346 // generating an extra redundant mention. Other interactions are being
347 // processed for the first time.
349 // We're only recognizing magic in the commit message itself, not in
353 foreach ($xactions as $xaction) {
354 switch ($xaction->getTransactionType()) {
355 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
365 $flat_blocks = mpull($changes, 'getNewValue');
366 $huge_block = implode("\n\n", $flat_blocks);
368 $monograms = array();
370 $task_refs = id(new ManiphestCustomFieldStatusParser())
371 ->parseCorpus($huge_block);
372 foreach ($task_refs as $match) {
373 foreach ($match['monograms'] as $monogram) {
374 $monograms[] = $monogram;
378 $rev_refs = id(new DifferentialCustomFieldDependsOnParser())
379 ->parseCorpus($huge_block);
380 foreach ($rev_refs as $match) {
381 foreach ($match['monograms'] as $monogram) {
382 $monograms[] = $monogram;
386 $objects = id(new PhabricatorObjectQuery())
387 ->setViewer($this->getActor())
388 ->withNames($monograms)
390 $phid_map[] = mpull($objects, 'getPHID', 'getPHID');
392 $reverts_refs = id(new DifferentialCustomFieldRevertsParser())
393 ->parseCorpus($huge_block);
394 $reverts = array_mergev(ipull($reverts_refs, 'monograms'));
396 $reverted_objects = DiffusionCommitRevisionQuery
::loadRevertedObjects(
400 $object->getRepository());
402 $reverted_phids = mpull($reverted_objects, 'getPHID', 'getPHID');
404 $reverts_edge = DiffusionCommitRevertsCommitEdgeType
::EDGECONST
;
405 $result[] = id(new PhabricatorAuditTransaction())
406 ->setTransactionType(PhabricatorTransactions
::TYPE_EDGE
)
407 ->setMetadataValue('edge:type', $reverts_edge)
408 ->setNewValue(array('+' => $reverted_phids));
410 $phid_map[] = $reverted_phids;
413 // See T13463. Copy "related task" edges from the associated revision, if
416 $revision = DiffusionCommitRevisionQuery
::loadRevisionForCommit(
420 $task_phids = PhabricatorEdgeQuery
::loadDestinationPHIDs(
421 $revision->getPHID(),
422 DifferentialRevisionHasTaskEdgeType
::EDGECONST
);
423 $task_phids = array_fuse($task_phids);
426 $related_edge = DiffusionCommitHasTaskEdgeType
::EDGECONST
;
427 $result[] = id(new PhabricatorAuditTransaction())
428 ->setTransactionType(PhabricatorTransactions
::TYPE_EDGE
)
429 ->setMetadataValue('edge:type', $related_edge)
430 ->setNewValue(array('+' => $task_phids));
433 // Mark these objects as unmentionable, since the explicit relationship
434 // is stronger and any mentions are redundant.
435 $phid_map[] = $task_phids;
438 $phid_map = array_mergev($phid_map);
439 $this->addUnmentionablePHIDs($phid_map);
444 protected function buildReplyHandler(PhabricatorLiskDAO
$object) {
445 $reply_handler = new PhabricatorAuditReplyHandler();
446 $reply_handler->setMailReceiver($object);
447 return $reply_handler;
450 protected function getMailSubjectPrefix() {
451 return pht('[Diffusion]');
454 protected function getMailThreadID(PhabricatorLiskDAO
$object) {
455 // For backward compatibility, use this legacy thread ID.
456 return 'diffusion-audit-'.$object->getPHID();
459 protected function buildMailTemplate(PhabricatorLiskDAO
$object) {
460 $identifier = $object->getCommitIdentifier();
461 $repository = $object->getRepository();
463 $summary = $object->getSummary();
464 $name = $repository->formatCommitName($identifier);
466 $subject = "{$name}: {$summary}";
468 $template = id(new PhabricatorMetaMTAMail())
469 ->setSubject($subject);
478 protected function getMailTo(PhabricatorLiskDAO
$object) {
479 $this->requireAuditors($object);
483 if ($object->getAuthorPHID()) {
484 $phids[] = $object->getAuthorPHID();
487 foreach ($object->getAudits() as $audit) {
488 if (!$audit->isResigned()) {
489 $phids[] = $audit->getAuditorPHID();
493 $phids[] = $this->getActingAsPHID();
498 protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO
$object) {
499 $this->requireAuditors($object);
503 foreach ($object->getAudits() as $auditor) {
504 if ($auditor->isResigned()) {
505 $phids[] = $auditor->getAuditorPHID();
512 protected function getObjectLinkButtonLabelForMail(
513 PhabricatorLiskDAO
$object) {
514 return pht('View Commit');
517 protected function buildMailBody(
518 PhabricatorLiskDAO
$object,
521 $body = parent
::buildMailBody($object, $xactions);
523 $type_inline = PhabricatorAuditActionConstants
::INLINE
;
524 $type_push = PhabricatorAuditTransaction
::TYPE_COMMIT
;
528 foreach ($xactions as $xaction) {
529 if ($xaction->getTransactionType() == $type_inline) {
530 $inlines[] = $xaction;
532 if ($xaction->getTransactionType() == $type_push) {
538 $body->addTextSection(
539 pht('INLINE COMMENTS'),
540 $this->renderInlineCommentsForMail($object, $inlines));
544 $data = $object->getCommitData();
545 $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles
);
551 $data = $object->getCommitData();
553 $user_phids = array();
555 $author_phid = $object->getAuthorPHID();
557 $user_phids[$author_phid][] = pht('Author');
560 $committer_phid = $data->getCommitDetail('committerPHID');
561 if ($committer_phid && ($committer_phid != $author_phid)) {
562 $user_phids[$committer_phid][] = pht('Committer');
565 foreach ($this->auditorPHIDs
as $auditor_phid) {
566 $user_phids[$auditor_phid][] = pht('Auditor');
569 // TODO: It would be nice to show pusher here too, but that information
570 // is a little tricky to get at right now.
573 $handle_phids = array_keys($user_phids);
574 $handles = id(new PhabricatorHandleQuery())
575 ->setViewer($this->requireActor())
576 ->withPHIDs($handle_phids)
579 $user_info = array();
580 foreach ($user_phids as $phid => $roles) {
583 $handles[$phid]->getName(),
584 implode(', ', $roles));
587 $body->addTextSection(
589 implode("\n", $user_info));
592 $monogram = $object->getRepository()->formatCommitName(
593 $object->getCommitIdentifier());
595 $body->addLinkSection(
597 PhabricatorEnv
::getProductionURI('/'.$monogram));
602 private function attachPatch(
603 PhabricatorMetaMTAMail
$template,
604 PhabricatorRepositoryCommit
$commit) {
606 if (!$this->getRawPatch()) {
610 $attach_key = 'metamta.diffusion.attach-patches';
611 $attach_patches = PhabricatorEnv
::getEnvConfig($attach_key);
612 if (!$attach_patches) {
616 $repository = $commit->getRepository();
617 $encoding = $repository->getDetail('encoding', 'UTF-8');
619 $raw_patch = $this->getRawPatch();
620 $commit_name = $repository->formatCommitName(
621 $commit->getCommitIdentifier());
623 $template->addAttachment(
624 new PhabricatorMailAttachment(
626 $commit_name.'.patch',
627 'text/x-patch; charset='.$encoding));
630 private function inlinePatch(
631 PhabricatorMetaMTAMailBody
$body,
632 PhabricatorRepositoryCommit
$commit) {
634 if (!$this->getRawPatch()) {
638 $inline_key = 'metamta.diffusion.inline-patches';
639 $inline_patches = PhabricatorEnv
::getEnvConfig($inline_key);
640 if (!$inline_patches) {
644 $repository = $commit->getRepository();
645 $raw_patch = $this->getRawPatch();
647 $len = substr_count($raw_patch, "\n");
648 if ($len <= $inline_patches) {
649 // We send email as utf8, so we need to convert the text to utf8 if
651 $encoding = $repository->getDetail('encoding', 'UTF-8');
653 $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
655 $result = phutil_utf8ize($raw_patch);
659 $result = "PATCH\n\n{$result}\n";
661 $body->addRawSection($result);
664 private function renderInlineCommentsForMail(
665 PhabricatorLiskDAO
$object,
666 array $inline_xactions) {
668 $inlines = mpull($inline_xactions, 'getComment');
672 $path_map = id(new DiffusionPathQuery())
673 ->withPathIDs(mpull($inlines, 'getPathID'))
675 $path_map = ipull($path_map, 'path', 'id');
677 foreach ($inlines as $inline) {
678 $path = idx($path_map, $inline->getPathID());
679 if ($path === null) {
683 $start = $inline->getLineNumber();
684 $len = $inline->getLineLength();
686 $range = $start.'-'.($start +
$len);
691 $content = $inline->getContent();
692 $block[] = "{$path}:{$range} {$content}";
695 return implode("\n", $block);
698 public function getMailTagsMap() {
700 PhabricatorAuditTransaction
::MAILTAG_COMMIT
=>
701 pht('A commit is created.'),
702 PhabricatorAuditTransaction
::MAILTAG_ACTION_CONCERN
=>
703 pht('A commit has a concerned raised against it.'),
704 PhabricatorAuditTransaction
::MAILTAG_ACTION_ACCEPT
=>
705 pht('A commit is accepted.'),
706 PhabricatorAuditTransaction
::MAILTAG_ACTION_RESIGN
=>
707 pht('A commit has an auditor resign.'),
708 PhabricatorAuditTransaction
::MAILTAG_ACTION_CLOSE
=>
709 pht('A commit is closed.'),
710 PhabricatorAuditTransaction
::MAILTAG_ADD_AUDITORS
=>
711 pht('A commit has auditors added.'),
712 PhabricatorAuditTransaction
::MAILTAG_ADD_CCS
=>
713 pht("A commit's subscribers change."),
714 PhabricatorAuditTransaction
::MAILTAG_PROJECTS
=>
715 pht("A commit's projects change."),
716 PhabricatorAuditTransaction
::MAILTAG_COMMENT
=>
717 pht('Someone comments on a commit.'),
718 PhabricatorAuditTransaction
::MAILTAG_OTHER
=>
719 pht('Other commit activity not listed above occurs.'),
723 protected function shouldApplyHeraldRules(
724 PhabricatorLiskDAO
$object,
727 foreach ($xactions as $xaction) {
728 switch ($xaction->getTransactionType()) {
729 case PhabricatorAuditTransaction
::TYPE_COMMIT
:
730 $repository = $object->getRepository();
731 $publisher = $repository->newPublisher();
732 if (!$publisher->shouldPublishCommit($object)) {
740 return parent
::shouldApplyHeraldRules($object, $xactions);
743 protected function buildHeraldAdapter(
744 PhabricatorLiskDAO
$object,
746 return id(new HeraldCommitAdapter())
747 ->setObject($object);
750 protected function didApplyHeraldRules(
751 PhabricatorLiskDAO
$object,
752 HeraldAdapter
$adapter,
753 HeraldTranscript
$transcript) {
755 $limit = self
::MAX_FILES_SHOWN_IN_EMAIL
;
756 $files = $adapter->loadAffectedPaths();
758 if (count($files) > $limit) {
759 array_splice($files, $limit);
761 '(This commit affected more than %d files. Only %d are shown here '.
762 'and additional ones are truncated.)',
766 $this->affectedFiles
= implode("\n", $files);
771 private function isCommitMostlyImported(PhabricatorLiskDAO
$object) {
772 $has_message = PhabricatorRepositoryCommit
::IMPORTED_MESSAGE
;
773 $has_changes = PhabricatorRepositoryCommit
::IMPORTED_CHANGE
;
775 // Don't publish feed stories or email about events which occur during
776 // import. In particular, this affects tasks being attached when they are
777 // closed by "Fixes Txxxx" in a commit message. See T5851.
779 $mask = ($has_message |
$has_changes);
781 return $object->isPartiallyImported($mask);
785 private function shouldPublishRepositoryActivity(
786 PhabricatorLiskDAO
$object,
789 // not every code path loads the repository so tread carefully
790 // TODO: They should, and then we should simplify this.
791 $repository = $object->getRepository($assert_attached = false);
792 if ($repository != PhabricatorLiskDAO
::ATTACHABLE
) {
793 $publisher = $repository->newPublisher();
794 if (!$publisher->shouldPublishCommit($object)) {
799 return $this->isCommitMostlyImported($object);
802 protected function shouldSendMail(
803 PhabricatorLiskDAO
$object,
805 return $this->shouldPublishRepositoryActivity($object, $xactions);
808 protected function shouldEnableMentions(
809 PhabricatorLiskDAO
$object,
811 return $this->shouldPublishRepositoryActivity($object, $xactions);
814 protected function shouldPublishFeedStory(
815 PhabricatorLiskDAO
$object,
817 return $this->shouldPublishRepositoryActivity($object, $xactions);
820 protected function getCustomWorkerState() {
822 'rawPatch' => $this->rawPatch
,
823 'affectedFiles' => $this->affectedFiles
,
824 'auditorPHIDs' => $this->auditorPHIDs
,
828 protected function getCustomWorkerStateEncoding() {
830 'rawPatch' => self
::STORAGE_ENCODING_BINARY
,
834 protected function loadCustomWorkerState(array $state) {
835 $this->rawPatch
= idx($state, 'rawPatch');
836 $this->affectedFiles
= idx($state, 'affectedFiles');
837 $this->auditorPHIDs
= idx($state, 'auditorPHIDs');
841 protected function willPublish(PhabricatorLiskDAO
$object, array $xactions) {
842 return id(new DiffusionCommitQuery())
843 ->setViewer($this->requireActor())
844 ->withIDs(array($object->getID()))
845 ->needAuditRequests(true)
846 ->needCommitData(true)
850 private function requireAuditors(PhabricatorRepositoryCommit
$commit) {
851 if ($commit->hasAttachedAudits()) {
855 $with_auditors = id(new DiffusionCommitQuery())
856 ->setViewer($this->getActor())
857 ->needAuditRequests(true)
858 ->withPHIDs(array($commit->getPHID()))
860 if (!$with_auditors) {
863 'Failed to reload commit ("%s").',
864 $commit->getPHID()));
867 $commit->attachAudits($with_auditors->getAudits());