diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 5bfd5ee836..ae662ab245 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -1,106 +1,94 @@ getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CREATE); } public function isStoryAboutObjectClosure($object) { $story = $this->getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CLOSE) || ($action == DifferentialAction::ACTION_ABANDON); } public function willPublishStory($object) { return id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) ->needRelationships(true) ->executeOne(); } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return $object->getReviewers(); } else { return array(); } } public function getPassiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return array(); } else { return $object->getReviewers(); } } public function getCCUserPHIDs($object) { return $object->getCCPHIDs(); } public function getObjectTitle($object) { $prefix = $this->getTitlePrefix($object); $lines = new PhutilNumber($object->getLineCount()); $lines = pht('[Request, %d lines]', $lines); $id = $object->getID(); $title = $object->getTitle(); return ltrim("{$prefix} {$lines} D{$id}: {$title}"); } public function getObjectURI($object) { return PhabricatorEnv::getProductionURI('/D'.$object->getID()); } public function getObjectDescription($object) { return $object->getSummary(); } public function isObjectClosed($object) { return $object->isClosed(); } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Review Request', $prefix); } - public function getStoryText($object) { - $implied_context = $this->getRenderWithImpliedContext(); - - $story = $this->getFeedStory(); - if ($story instanceof PhabricatorFeedStoryDifferential) { - $text = $story->renderForAsanaBridge($implied_context); - } else { - $text = $story->renderText(); - } - return $text; - } - private function getTitlePrefix(DifferentialRevision $revision) { $prefix_key = 'metamta.differential.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 55c589b6e3..4cc8c4e803 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -1,600 +1,654 @@ isCommandeerSideEffect = $is_side_effect; return $this; } public function getIsCommandeerSideEffect() { return $this->isCommandeerSideEffect; } const TYPE_INLINE = 'differential:inline'; const TYPE_UPDATE = 'differential:update'; const TYPE_ACTION = 'differential:action'; const TYPE_STATUS = 'differential:status'; public function getApplicationName() { return 'differential'; } public function getApplicationTransactionType() { return DifferentialRevisionPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new DifferentialTransactionComment(); } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_UPDATE: // Older versions of this transaction have an ID for the new value, // and/or do not record the old value. Only hide the transaction if // the new value is a PHID, indicating that this is a newer style // transaction. if ($old === null) { if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return true; } } break; case PhabricatorTransactions::TYPE_EDGE: $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); // Hide metadata-only edge transactions. These correspond to users // accepting or rejecting revisions, but the change is always explicit // because of the TYPE_ACTION transaction. Rendering these transactions // just creates clutter. if (!$add && !$rem) { return true; } break; } return parent::shouldHide(); } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case self::TYPE_INLINE: // Hide inlines when rendering mail transactions if any other // transaction type exists. foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != self::TYPE_INLINE) { return true; } } // If only inline transactions exist, we just render the first one. return ($this !== head($xactions)); } return parent::shouldHideForMail($xactions); } public function getBodyForMail() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: // Don't render inlines into the mail body; they render into a special // section immediately after the body instead. return null; } return parent::getBodyForMail(); } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ACTION: if ($new == DifferentialAction::ACTION_CLOSE && $this->getMetadataValue('isCommitClose')) { $phids[] = $this->getMetadataValue('commitPHID'); if ($this->getMetadataValue('committerPHID')) { $phids[] = $this->getMetadataValue('committerPHID'); } if ($this->getMetadataValue('authorPHID')) { $phids[] = $this->getMetadataValue('authorPHID'); } } break; case self::TYPE_UPDATE: if ($new) { $phids[] = $new; } break; } return $phids; } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_ACTION: return 3; case self::TYPE_UPDATE: return 2; case self::TYPE_INLINE: return 0.25; } return parent::getActionStrength(); } public function getActionName() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht('Commented On'); case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { return pht('Request'); } else { return pht('Updated'); } case self::TYPE_ACTION: $map = array( DifferentialAction::ACTION_ACCEPT => pht('Accepted'), DifferentialAction::ACTION_REJECT => pht('Requested Changes To'), DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'), DifferentialAction::ACTION_ABANDON => pht('Abandoned'), DifferentialAction::ACTION_CLOSE => pht('Closed'), DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'), DifferentialAction::ACTION_RESIGN => pht('Resigned From'), DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'), DifferentialAction::ACTION_CLAIM => pht('Commandeered'), DifferentialAction::ACTION_REOPEN => pht('Reopened'), ); $name = idx($map, $this->getNewValue()); if ($name !== null) { return $name; } break; } return parent::getActionName(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS; $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC; break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED; break; } break; case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST; } else { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED; } break; case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS; break; } break; case PhabricatorTransactions::TYPE_COMMENT: case self::TYPE_INLINE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT; break; } if (!$tags) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER; } return $tags; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $author_handle = $this->renderHandleLink($author_phid); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments.', $author_handle); case self::TYPE_UPDATE: if ($this->getMetadataValue('isCommitUpdate')) { return pht( 'This revision was automatically updated to reflect the '. 'committed changes.'); } else if ($new) { // TODO: Migrate to PHIDs and use handles here? if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return pht( '%s updated this revision to %s.', $author_handle, $this->renderHandleLink($new)); } else { return pht( '%s updated this revision.', $author_handle); } } else { return pht( '%s updated this revision.', $author_handle); } case self::TYPE_ACTION: switch ($new) { case DifferentialAction::ACTION_CLOSE: if (!$this->getMetadataValue('isCommitClose')) { return DifferentialAction::getBasicStoryText( $new, $author_handle); } $commit_name = $this->renderHandleLink( $this->getMetadataValue('commitPHID')); $committer_phid = $this->getMetadataValue('committerPHID'); $author_phid = $this->getMetadataValue('authorPHID'); if ($this->getHandleIfExists($committer_phid)) { $committer_name = $this->renderHandleLink($committer_phid); } else { $committer_name = $this->getMetadataValue('committerName'); } if ($this->getHandleIfExists($author_phid)) { $author_name = $this->renderHandleLink($author_phid); } else { $author_name = $this->getMetadataValue('authorName'); } if ($committer_name && ($committer_name != $author_name)) { return pht( 'Closed by commit %s (authored by %s, committed by %s).', $commit_name, $author_name, $committer_name); } else { return pht( 'Closed by commit %s (authored by %s).', $commit_name, $author_name); } break; default: return DifferentialAction::getBasicStoryText($new, $author_handle); } break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( 'This revision is now accepted and ready to land.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( 'This revision now requires changes to proceed.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( 'This revision now requires review to proceed.'); } } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $author_link = $this->renderHandleLink($author_phid); $object_link = $this->renderHandleLink($object_phid); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments to %s.', $author_link, $object_link); case self::TYPE_UPDATE: return pht( '%s updated the diff for %s.', $author_link, $object_link); case self::TYPE_ACTION: switch ($new) { case DifferentialAction::ACTION_ACCEPT: return pht( '%s accepted %s.', $author_link, $object_link); case DifferentialAction::ACTION_REJECT: return pht( '%s requested changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_RETHINK: return pht( '%s planned changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_ABANDON: return pht( '%s abandoned %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLOSE: if (!$this->getMetadataValue('isCommitClose')) { return pht( '%s closed %s.', $author_link, $object_link); } else { $commit_name = $this->renderHandleLink( $this->getMetadataValue('commitPHID')); $committer_phid = $this->getMetadataValue('committerPHID'); $author_phid = $this->getMetadataValue('authorPHID'); if ($this->getHandleIfExists($committer_phid)) { $committer_name = $this->renderHandleLink($committer_phid); } else { $committer_name = $this->getMetadataValue('committerName'); } if ($this->getHandleIfExists($author_phid)) { $author_name = $this->renderHandleLink($author_phid); } else { $author_name = $this->getMetadataValue('authorName'); } if ($committer_name && ($committer_name != $author_name)) { return pht( '%s closed %s by commit %s (authored by %s).', $author_link, $object_link, $commit_name, $author_name); } else { return pht( '%s closed %s by commit %s.', $author_link, $object_link, $commit_name); } } break; case DifferentialAction::ACTION_REQUEST: return pht( '%s requested review of %s.', $author_link, $object_link); case DifferentialAction::ACTION_RECLAIM: return pht( '%s reclaimed %s.', $author_link, $object_link); case DifferentialAction::ACTION_RESIGN: return pht( '%s resigned from %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLAIM: return pht( '%s commandeered %s.', $author_link, $object_link); case DifferentialAction::ACTION_REOPEN: return pht( '%s reopened %s.', $author_link, $object_link); } break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( '%s is now accepted and ready to land.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( '%s now requires changes to proceed.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( '%s now requires review to proceed.', $object_link); } } return parent::getTitleForFeed($story); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return 'fa-comment'; case self::TYPE_UPDATE: return 'fa-refresh'; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return 'fa-check'; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return 'fa-times'; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return 'fa-undo'; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return 'fa-check'; case DifferentialAction::ACTION_ACCEPT: return 'fa-check-circle-o'; case DifferentialAction::ACTION_REJECT: return 'fa-times-circle-o'; case DifferentialAction::ACTION_ABANDON: return 'fa-plane'; case DifferentialAction::ACTION_RETHINK: return 'fa-headphones'; case DifferentialAction::ACTION_REQUEST: return 'fa-refresh'; case DifferentialAction::ACTION_RECLAIM: case DifferentialAction::ACTION_REOPEN: return 'fa-bullhorn'; case DifferentialAction::ACTION_RESIGN: return 'fa-flag'; case DifferentialAction::ACTION_CLAIM: return 'fa-flag'; } case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: return 'fa-user'; } } return parent::getIcon(); } public function shouldDisplayGroupWith(array $group) { // Never group status changes with other types of actions, they're indirect // and don't make sense when combined with direct actions. $type_status = self::TYPE_STATUS; if ($this->getTransactionType() == $type_status) { return false; } foreach ($group as $xaction) { if ($xaction->getTransactionType() == $type_status) { return false; } } return parent::shouldDisplayGroupWith($group); } public function getColor() { switch ($this->getTransactionType()) { case self::TYPE_UPDATE: return PhabricatorTransactions::COLOR_SKY; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return PhabricatorTransactions::COLOR_GREEN; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return PhabricatorTransactions::COLOR_RED; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return PhabricatorTransactions::COLOR_ORANGE; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return PhabricatorTransactions::COLOR_BLUE; case DifferentialAction::ACTION_ACCEPT: return PhabricatorTransactions::COLOR_GREEN; case DifferentialAction::ACTION_REJECT: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_ABANDON: return PhabricatorTransactions::COLOR_BLACK; case DifferentialAction::ACTION_RETHINK: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_REQUEST: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RECLAIM: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_REOPEN: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RESIGN: return PhabricatorTransactions::COLOR_ORANGE; case DifferentialAction::ACTION_CLAIM: return PhabricatorTransactions::COLOR_YELLOW; } } return parent::getColor(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: return pht( 'The reviewers you are trying to add are already reviewing '. 'this revision.'); } break; case DifferentialTransaction::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return pht('This revision is already closed.'); case DifferentialAction::ACTION_ABANDON: return pht('This revision has already been abandoned.'); case DifferentialAction::ACTION_RECLAIM: return pht( 'You can not reclaim this revision because his revision is '. 'not abandoned.'); case DifferentialAction::ACTION_REOPEN: return pht( 'You can not reopen this revision because this revision is '. 'not closed.'); case DifferentialAction::ACTION_RETHINK: return pht('This revision already requires changes.'); case DifferentialAction::ACTION_REQUEST: return pht('Review is already requested for this revision.'); case DifferentialAction::ACTION_RESIGN: return pht( 'You can not resign from this revision because you are not '. 'a reviewer.'); case DifferentialAction::ACTION_CLAIM: return pht( 'You can not commandeer this revision because you already own '. 'it.'); case DifferentialAction::ACTION_ACCEPT: return pht( 'You have already accepted this revision.'); case DifferentialAction::ACTION_REJECT: return pht( 'You have already requested changes to this revision.'); } break; } return parent::getNoEffectDescription(); } + public function renderAsTextForDoorkeeper( + DoorkeeperFeedStoryPublisher $publisher, + PhabricatorFeedStory $story, + array $xactions) { + + $body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions); + + $inlines = array(); + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == self::TYPE_INLINE) { + $inlines[] = $xaction; + } + } + + // TODO: This is a bit gross, but far less bad than it used to be. It + // could be further cleaned up at some point. + + if ($inlines) { + $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) + ->setConfig('viewer', new PhabricatorUser()) + ->setMode(PhutilRemarkupEngine::MODE_TEXT); + + $body .= "\n\n"; + $body .= pht('Inline Comments'); + $body .= "\n"; + + $changeset_ids = array(); + foreach ($inlines as $inline) { + $changeset_ids[] = $inline->getComment()->getChangesetID(); + } + + $changesets = id(new DifferentialChangeset())->loadAllWhere( + 'id IN (%Ld)', + $changeset_ids); + + foreach ($inlines as $inline) { + $comment = $inline->getComment(); + $changeset = idx($changesets, $comment->getChangesetID()); + if (!$changeset) { + continue; + } + + $filename = $changeset->getDisplayFilename(); + $linenumber = $comment->getLineNumber(); + $inline_text = $engine->markupText($comment->getContent()); + $inline_text = rtrim($inline_text); + + $body .= "{$filename}:{$linenumber} {$inline_text}\n"; + } + } + + return $body; + } + } diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php index a8bab3a3f1..088f4b752e 100644 --- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php +++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php @@ -1,200 +1,188 @@ auditRequests; } public function canPublishStory(PhabricatorFeedStory $story, $object) { return ($story instanceof PhabricatorApplicationTransactionFeedStory) && ($object instanceof PhabricatorRepositoryCommit); } public function isStoryAboutObjectCreation($object) { // TODO: Although creation stories exist, they currently don't have a // primary object PHID set, so they'll never make it here because they // won't pass `canPublishStory()`. return false; } public function isStoryAboutObjectClosure($object) { // TODO: This isn't quite accurate, but pretty close: check if this story // is a close (which clearly is about object closure) or is an "Accept" and // the commit is fully audited (which is almost certainly a closure). // After ApplicationTransactions, we could annotate feed stories more // explicitly. $fully_audited = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED; $story = $this->getFeedStory(); $xaction = $story->getPrimaryTransaction(); switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: switch ($xaction->getNewValue()) { case PhabricatorAuditActionConstants::CLOSE: return true; case PhabricatorAuditActionConstants::ACCEPT: if ($object->getAuditStatus() == $fully_audited) { return true; } break; } } return false; } public function willPublishStory($commit) { $requests = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($commit->getPHID())) ->needAuditRequests(true) ->executeOne() ->getAudits(); // TODO: This is messy and should be generalized, but we don't have a good // query for it yet. Since we run in the daemons, just do the easiest thing // we can for the moment. Figure out who all of the "active" (need to // audit) and "passive" (no action necessary) users are. $auditor_phids = mpull($requests, 'getAuditorPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($auditor_phids) ->execute(); $active = array(); $passive = array(); foreach ($requests as $request) { $status = $request->getAuditStatus(); $object = idx($objects, $request->getAuditorPHID()); if (!$object) { continue; } $request_phids = array(); if ($object instanceof PhabricatorUser) { $request_phids = array($object->getPHID()); } else if ($object instanceof PhabricatorOwnersPackage) { $request_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array($object->getID())); } else if ($object instanceof PhabricatorProject) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) ->needMembers(true) ->executeOne(); $request_phids = $project->getMemberPHIDs(); } else { // Dunno what this is. $request_phids = array(); } switch ($status) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: case PhabricatorAuditStatusConstants::CONCERNED: $active += array_fuse($request_phids); break; default: $passive += array_fuse($request_phids); break; } } // Remove "Active" users from the "Passive" list. $passive = array_diff_key($passive, $active); $this->activePHIDs = $active; $this->passivePHIDs = $passive; $this->auditRequests = $requests; return $commit; } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { return $this->activePHIDs; } public function getPassiveUserPHIDs($object) { return $this->passivePHIDs; } public function getCCUserPHIDs($object) { return PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); } public function getObjectTitle($object) { $prefix = $this->getTitlePrefix($object); $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); $title = $object->getSummary(); return ltrim("{$prefix} {$name}: {$title}"); } public function getObjectURI($object) { $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); return PhabricatorEnv::getProductionURI('/'.$name); } public function getObjectDescription($object) { $data = $object->loadCommitData(); if ($data) { return $data->getCommitMessage(); } return null; } public function isObjectClosed($object) { switch ($object->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: return false; default: return true; } } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Audit', $prefix); } - public function getStoryText($object) { - $implied_context = $this->getRenderWithImpliedContext(); - - $story = $this->getFeedStory(); - if ($story instanceof PhabricatorFeedStoryAudit) { - $text = $story->renderForAsanaBridge($implied_context); - } else { - $text = $story->renderText(); - } - return $text; - } - private function getTitlePrefix(PhabricatorRepositoryCommit $commit) { $prefix_key = 'metamta.diffusion.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php index be7aebd892..ef98fdab49 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php +++ b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php @@ -1,97 +1,101 @@ renderWithImpliedContext = $render_with_implied_context; return $this; } /** * Determine if rendering should assume object context. For discussion, see * @{method:setRenderWithImpliedContext}. * * @return bool True if rendering should assume object context is implied. * @task config */ public function getRenderWithImpliedContext() { return $this->renderWithImpliedContext; } public function setFeedStory(PhabricatorFeedStory $feed_story) { $this->feedStory = $feed_story; return $this; } public function getFeedStory() { return $this->feedStory; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } abstract public function canPublishStory( PhabricatorFeedStory $story, $object); /** * Hook for publishers to mutate the story object, particularly by loading * and attaching additional data. */ public function willPublishStory($object) { return $object; } + + public function getStoryText($object) { + return $this->getFeedStory()->renderAsTextForDoorkeeper($this); + } + abstract public function isStoryAboutObjectCreation($object); abstract public function isStoryAboutObjectClosure($object); abstract public function getOwnerPHID($object); abstract public function getActiveUserPHIDs($object); abstract public function getPassiveUserPHIDs($object); abstract public function getCCUserPHIDs($object); abstract public function getObjectTitle($object); abstract public function getObjectURI($object); abstract public function getObjectDescription($object); abstract public function isObjectClosed($object); abstract public function getResponsibilityTitle($object); - abstract public function getStoryText($object); } diff --git a/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php b/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php index ba1b24892e..ce67ad1c7b 100644 --- a/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php +++ b/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php @@ -1,10 +1,9 @@ List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); try { $ok = class_exists($class) && is_subclass_of($class, 'PhabricatorFeedStory'); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, decline to load it. if (!$ok) { continue; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } $object_phids = array(); $key_phids = array(); foreach ($stories as $key => $story) { $phids = array(); foreach ($story->getRequiredObjectPHIDs() as $phid) { $phids[$phid] = true; } if ($story->getPrimaryObjectPHID()) { $phids[$story->getPrimaryObjectPHID()] = true; } $key_phids[$key] = $phids; $object_phids += $phids; } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($object_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_objects = array_select_keys($objects, array_keys($phids)); if (count($story_objects) != count($phids)) { // An object this story requires either does not exist or is not visible // to the user. Decline to render the story. unset($stories[$key]); unset($key_phids[$key]); continue; } $stories[$key]->setObjects($story_objects); } // If stories are about PhabricatorProjectInterface objects, load the // projects the objects are a part of so we can render project tags // on the stories. $project_phids = array(); foreach ($objects as $object) { if ($object instanceof PhabricatorProjectInterface) { $project_phids[$object->getPHID()] = array(); } } if ($project_phids) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($project_phids)) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($project_phids as $phid => $ignored) { $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid)); } } $handle_phids = array(); foreach ($stories as $key => $story) { foreach ($story->getRequiredHandlePHIDs() as $phid) { $key_phids[$key][$phid] = true; } if ($story->getAuthorPHID()) { $key_phids[$key][$story->getAuthorPHID()] = true; } $object_phid = $story->getPrimaryObjectPHID(); $object_project_phids = idx($project_phids, $object_phid, array()); $story->setProjectPHIDs($object_project_phids); foreach ($object_project_phids as $dst) { $key_phids[$key][$dst] = true; } $handle_phids += $key_phids[$key]; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($handle_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_handles = array_select_keys($handles, array_keys($phids)); $stories[$key]->setHandles($story_handles); } // Load and process story markup blocks. $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($stories as $story) { foreach ($story->getFieldStoryMarkupFields() as $field) { $engine->addObject($story, $field); } } $engine->process(); foreach ($stories as $story) { foreach ($story->getFieldStoryMarkupFields() as $field) { $story->setMarkupFieldOutput( $field, $engine->getOutput($story, $field)); } } return $stories; } public function setMarkupFieldOutput($field, $output) { $this->markupFieldOutput[$field] = $output; return $this; } public function getMarkupFieldOutput($field) { if (!array_key_exists($field, $this->markupFieldOutput)) { throw new Exception( pht( 'Trying to retrieve markup field key "%s", but this feed story '. 'did not request it be rendered.', $field)); } return $this->markupFieldOutput[$field]; } public function setHovercard($hover) { $this->hovercard = $hover; return $this; } public function setRenderingTarget($target) { $this->validateRenderingTarget($target); $this->renderingTarget = $target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } private function validateRenderingTarget($target) { switch ($target) { case PhabricatorApplicationTransaction::TARGET_HTML: case PhabricatorApplicationTransaction::TARGET_TEXT: break; default: throw new Exception('Unknown rendering target: '.$target); break; } } public function setObjects(array $objects) { $this->objects = $objects; return $this; } public function getObject($phid) { $object = idx($this->objects, $phid); if (!$object) { throw new Exception( "Story is asking for an object it did not request ('{$phid}')!"); } return $object; } public function getPrimaryObject() { $phid = $this->getPrimaryObjectPHID(); if (!$phid) { throw new Exception('Story has no primary object!'); } return $this->getObject($phid); } public function getPrimaryObjectPHID() { return null; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); + public function renderAsTextForDoorkeeper( + DoorkeeperFeedStoryPublisher $publisher) { + + // TODO: This (and text rendering) should be properly abstract and + // universal. However, this is far less bad than it used to be, and we + // need to clean up more old feed code to really make this reasonable. + + return pht( + '(Unable to render story of class %s for Doorkeeper.)', + get_class($this)); + } public function getRequiredHandlePHIDs() { return array(); } public function getRequiredObjectPHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getObjects() { return $this->objects; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final public function getValue($key, $default = null) { return $this->getStoryData()->getValue($key, $default); } final public function getAuthorPHID() { return $this->getStoryData()->getAuthorPHID(); } final protected function renderHandleList(array $phids) { $items = array(); foreach ($phids as $phid) { $items[] = $this->linkTo($phid); } $list = null; switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: $list = implode(', ', $items); break; case PhabricatorApplicationTransaction::TARGET_HTML: $list = phutil_implode_html(', ', $items); break; } return $list; } final protected function linkTo($phid) { $handle = $this->getHandle($phid); switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $handle->getLinkName(); } // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. $class = null; if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) { $class = 'phui-link-person'; } return javelin_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, 'sigil' => $this->hovercard ? 'hovercard' : null, 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 'class' => $class, ), $handle->getLinkName()); } final protected function renderString($str) { switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $str; case PhabricatorApplicationTransaction::TARGET_HTML: return phutil_tag('strong', array(), $str); } } final protected function renderSummary($text, $len = 128) { if ($len) { $text = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs($len) ->truncateString($text); } switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_HTML: $text = phutil_escape_html_newlines($text); break; } return $text; } public function getNotificationAggregations() { return array(); } protected function newStoryView() { $view = id(new PHUIFeedStoryView()) ->setChronologicalKey($this->getChronologicalKey()) ->setEpoch($this->getEpoch()) ->setViewed($this->getHasViewed()); $project_phids = $this->getProjectPHIDs(); if ($project_phids) { $view->setTags($this->renderHandleList($project_phids)); } return $view; } public function setProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function getProjectPHIDs() { return $this->projectPHIDs; } public function getFieldStoryMarkupFields() { return array(); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getPHID() { return null; } /** * @task policy */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } /** * @task policy */ public function getPolicy($capability) { // If this story's primary object is a policy-aware object, use its policy // to control story visiblity. $primary_phid = $this->getPrimaryObjectPHID(); if (isset($this->objects[$primary_phid])) { $object = $this->objects[$primary_phid]; if ($object instanceof PhabricatorPolicyInterface) { return $object->getPolicy($capability); } } // TODO: Remove this once all objects are policy-aware. For now, keep // respecting the `feed.public` setting. return PhabricatorEnv::getEnvConfig('feed.public') ? PhabricatorPolicies::POLICY_PUBLIC : PhabricatorPolicies::POLICY_USER; } /** * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorMarkupInterface Implementation )--------------------------- */ public function getMarkupFieldKey($field) { return 'feed:'.$this->getChronologicalKey().':'.$field; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { throw new PhutilMethodNotImplementedException(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return true; } } diff --git a/src/applications/feed/story/PhabricatorFeedStoryAudit.php b/src/applications/feed/story/PhabricatorFeedStoryAudit.php index bade63e766..60ac7b3058 100644 --- a/src/applications/feed/story/PhabricatorFeedStoryAudit.php +++ b/src/applications/feed/story/PhabricatorFeedStoryAudit.php @@ -1,84 +1,50 @@ getStoryData()->getValue('commitPHID'); } public function renderView() { $author_phid = $this->getAuthorPHID(); $commit_phid = $this->getPrimaryObjectPHID(); $view = $this->newStoryView(); $view->setAppIcon('audit-dark'); $action = $this->getValue('action'); $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action); $view->setTitle(hsprintf( '%s %s commit %s.', $this->linkTo($author_phid), $verb, $this->linkTo($commit_phid))); $comments = $this->getValue('content'); $view->setImage($this->getHandle($author_phid)->getImageURI()); if ($comments) { $content = $this->renderSummary($this->getValue('content')); $view->appendChild($content); } return $view; } public function renderText() { $author_name = $this->getHandle($this->getAuthorPHID())->getLinkName(); $commit_path = $this->getHandle($this->getPrimaryObjectPHID())->getURI(); $commit_uri = PhabricatorEnv::getURI($commit_path); $action = $this->getValue('action'); $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action); $text = "{$author_name} {$verb} commit {$commit_uri}"; return $text; } - - // TODO: At some point, make feed rendering not terrible and remove this - // hacky mess. - public function renderForAsanaBridge($implied_context = false) { - $data = $this->getStoryData(); - $comment = $data->getValue('content'); - - $author_name = $this->getHandle($this->getAuthorPHID())->getName(); - $action = $this->getValue('action'); - $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action); - - $commit_phid = $this->getPrimaryObjectPHID(); - $commit_name = $this->getHandle($commit_phid)->getFullName(); - - if ($implied_context) { - $title = "{$author_name} {$verb} this commit."; - } else { - $title = "{$author_name} {$verb} commit {$commit_name}."; - } - - if (strlen($comment)) { - $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) - ->setConfig('viewer', new PhabricatorUser()) - ->setMode(PhutilRemarkupEngine::MODE_TEXT); - - $comment = $engine->markupText($comment); - - $title .= "\n\n"; - $title .= $comment; - } - - return $title; - } - } diff --git a/src/applications/feed/story/PhabricatorFeedStoryDifferential.php b/src/applications/feed/story/PhabricatorFeedStoryDifferential.php index 20b6013608..094e19796b 100644 --- a/src/applications/feed/story/PhabricatorFeedStoryDifferential.php +++ b/src/applications/feed/story/PhabricatorFeedStoryDifferential.php @@ -1,393 +1,238 @@ getValue('revision_phid'); } public function renderView() { $data = $this->getStoryData(); $view = $this->newStoryView(); $view->setAppIcon('differential-dark'); $line = $this->getLineForData($data); $view->setTitle($line); $href = $this->getHandle($data->getValue('revision_phid'))->getURI(); $view->setHref($href); $action = $data->getValue('action'); switch ($action) { case DifferentialAction::ACTION_CREATE: case DifferentialAction::ACTION_CLOSE: case DifferentialAction::ACTION_COMMENT: $full_size = true; break; default: $full_size = false; break; } $view->setImage($this->getHandle($data->getAuthorPHID())->getImageURI()); if ($full_size) { $content = $this->renderSummary($data->getValue('feedback_content')); $view->appendChild($content); } return $view; } private function getLineForData($data) { $actor_phid = $data->getAuthorPHID(); $revision_phid = $data->getValue('revision_phid'); $action = $data->getValue('action'); $actor_link = $this->linkTo($actor_phid); $revision_link = $this->linkTo($revision_phid); switch ($action) { case DifferentialAction::ACTION_COMMENT: $one_line = pht('%s commented on revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_ACCEPT: $one_line = pht('%s accepted revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_REJECT: $one_line = pht('%s requested changes to revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_RETHINK: $one_line = pht('%s planned changes to revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_ABANDON: $one_line = pht('%s abandoned revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_CLOSE: $one_line = pht('%s closed revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_REQUEST: $one_line = pht('%s requested a review of revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_RECLAIM: $one_line = pht('%s reclaimed revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_UPDATE: $one_line = pht('%s updated revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_RESIGN: $one_line = pht('%s resigned from revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_SUMMARIZE: $one_line = pht('%s summarized revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_TESTPLAN: $one_line = pht('%s explained the test plan for revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_CREATE: $one_line = pht('%s created revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_ADDREVIEWERS: $one_line = pht('%s added reviewers to revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_ADDCCS: $one_line = pht('%s added CCs to revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_CLAIM: $one_line = pht('%s commandeered revision %s', $actor_link, $revision_link); break; case DifferentialAction::ACTION_REOPEN: $one_line = pht('%s reopened revision %s', $actor_link, $revision_link); break; case DifferentialTransaction::TYPE_INLINE: $one_line = pht('%s added inline comments to %s', $actor_link, $revision_link); break; default: $one_line = pht('%s edited %s', $actor_link, $revision_link); break; } return $one_line; } public function renderText() { $author_name = $this->getHandle($this->getAuthorPHID())->getLinkName(); $revision_handle = $this->getHandle($this->getPrimaryObjectPHID()); $revision_title = $revision_handle->getLinkName(); $revision_uri = PhabricatorEnv::getURI($revision_handle->getURI()); $action = $this->getValue('action'); switch ($action) { case DifferentialAction::ACTION_COMMENT: $one_line = pht('%s commented on revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_ACCEPT: $one_line = pht('%s accepted revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_REJECT: $one_line = pht('%s requested changes to revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_RETHINK: $one_line = pht('%s planned changes to revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_ABANDON: $one_line = pht('%s abandoned revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_CLOSE: $one_line = pht('%s closed revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_REQUEST: $one_line = pht('%s requested a review of revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_RECLAIM: $one_line = pht('%s reclaimed revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_UPDATE: $one_line = pht('%s updated revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_RESIGN: $one_line = pht('%s resigned from revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_SUMMARIZE: $one_line = pht('%s summarized revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_TESTPLAN: $one_line = pht('%s explained the test plan for revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_CREATE: $one_line = pht('%s created revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_ADDREVIEWERS: $one_line = pht('%s added reviewers to revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_ADDCCS: $one_line = pht('%s added CCs to revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_CLAIM: $one_line = pht('%s commandeered revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialAction::ACTION_REOPEN: $one_line = pht('%s reopened revision %s %s', $author_name, $revision_title, $revision_uri); break; case DifferentialTransaction::TYPE_INLINE: $one_line = pht('%s added inline comments to %s %s', $author_name, $revision_title, $revision_uri); break; default: $one_line = pht('%s edited %s %s', $author_name, $revision_title, $revision_uri); break; } return $one_line; } public function getNotificationAggregations() { $class = get_class($this); $phid = $this->getStoryData()->getValue('revision_phid'); $read = (int)$this->getHasViewed(); // Don't aggregate updates separated by more than 2 hours. $block = (int)($this->getEpoch() / (60 * 60 * 2)); return array( "{$class}:{$phid}:{$read}:{$block}" => 'PhabricatorFeedStoryDifferentialAggregate', ); } - // TODO: At some point, make feed rendering not terrible and remove this - // hacky mess. - public function renderForAsanaBridge($implied_context = false) { - $data = $this->getStoryData(); - $comment = $data->getValue('feedback_content'); - - $author_name = $this->getHandle($this->getAuthorPHID())->getName(); - $action = $this->getValue('action'); - - $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) - ->setConfig('viewer', new PhabricatorUser()) - ->setMode(PhutilRemarkupEngine::MODE_TEXT); - - $revision_phid = $this->getPrimaryObjectPHID(); - $revision_name = $this->getHandle($revision_phid)->getFullName(); - - if ($implied_context) { - $title = DifferentialAction::getBasicStoryText( - $action, $author_name); - } else { - switch ($action) { - case DifferentialAction::ACTION_COMMENT: - $title = pht('%s commented on revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_ACCEPT: - $title = pht('%s accepted revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_REJECT: - $title = pht('%s requested changes to revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_RETHINK: - $title = pht('%s planned changes to revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_ABANDON: - $title = pht('%s abandoned revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_CLOSE: - $title = pht('%s closed revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_REQUEST: - $title = pht('%s requested a review of revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_RECLAIM: - $title = pht('%s reclaimed revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_UPDATE: - $title = pht('%s updated revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_RESIGN: - $title = pht('%s resigned from revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_SUMMARIZE: - $title = pht('%s summarized revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_TESTPLAN: - $title = pht('%s explained the test plan for revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_CREATE: - $title = pht('%s created revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_ADDREVIEWERS: - $title = pht('%s added reviewers to revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_ADDCCS: - $title = pht('%s added CCs to revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_CLAIM: - $title = pht('%s commandeered revision %s', - $author_name, $revision_name); - break; - case DifferentialAction::ACTION_REOPEN: - $title = pht('%s reopened revision %s', - $author_name, $revision_name); - break; - case DifferentialTransaction::TYPE_INLINE: - $title = pht('%s added inline comments to %s', - $author_name, $revision_name); - break; - default: - $title = pht('%s edited revision %s', - $author_name, $revision_name); - break; - } - } - - if (strlen($comment)) { - $comment = $engine->markupText($comment); - - $title .= "\n\n"; - $title .= $comment; - } - - // Roughly render inlines into the comment. - $xaction_phids = $data->getValue('temporaryTransactionPHIDs'); - if ($xaction_phids) { - $inlines = id(new DifferentialTransactionQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs($xaction_phids) - ->needComments(true) - ->withTransactionTypes( - array( - DifferentialTransaction::TYPE_INLINE, - )) - ->execute(); - if ($inlines) { - $title .= "\n\n"; - $title .= pht('Inline Comments'); - $title .= "\n"; - - $changeset_ids = array(); - foreach ($inlines as $inline) { - $changeset_ids[] = $inline->getComment()->getChangesetID(); - } - - $changesets = id(new DifferentialChangeset())->loadAllWhere( - 'id IN (%Ld)', - $changeset_ids); - - foreach ($inlines as $inline) { - $comment = $inline->getComment(); - $changeset = idx($changesets, $comment->getChangesetID()); - if (!$changeset) { - continue; - } - - $filename = $changeset->getDisplayFilename(); - $linenumber = $comment->getLineNumber(); - $inline_text = $engine->markupText($comment->getContent()); - $inline_text = rtrim($inline_text); - - $title .= "{$filename}:{$linenumber} {$inline_text}\n"; - } - } - } - - - return $title; - } - - } diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php index 0e488b359a..923ac77e24 100644 --- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php +++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php @@ -1,107 +1,120 @@ getValue('objectPHID'); } public function getRequiredObjectPHIDs() { return $this->getValue('transactionPHIDs'); } public function getRequiredHandlePHIDs() { $phids = array(); $phids[] = $this->getValue('objectPHID'); foreach ($this->getValue('transactionPHIDs') as $xaction_phid) { $xaction = $this->getObject($xaction_phid); foreach ($xaction->getRequiredHandlePHIDs() as $handle_phid) { $phids[] = $handle_phid; } } return $phids; } protected function getPrimaryTransactionPHID() { return head($this->getValue('transactionPHIDs')); } public function getPrimaryTransaction() { return $this->getObject($this->getPrimaryTransactionPHID()); } public function getFieldStoryMarkupFields() { $xaction_phids = $this->getValue('transactionPHIDs'); $fields = array(); foreach ($xaction_phids as $xaction_phid) { $xaction = $this->getObject($xaction_phid); foreach ($xaction->getMarkupFieldsForFeed($this) as $field) { $fields[] = $field; } } return $fields; } public function getMarkupText($field) { $xaction_phids = $this->getValue('transactionPHIDs'); foreach ($xaction_phids as $xaction_phid) { $xaction = $this->getObject($xaction_phid); foreach ($xaction->getMarkupFieldsForFeed($this) as $xaction_field) { if ($xaction_field == $field) { return $xaction->getMarkupTextForFeed($this, $field); } } } return null; } public function renderView() { $view = $this->newStoryView(); $handle = $this->getHandle($this->getPrimaryObjectPHID()); $view->setHref($handle->getURI()); $view->setAppIconFromPHID($handle->getPHID()); $xaction_phids = $this->getValue('transactionPHIDs'); $xaction = $this->getPrimaryTransaction(); $xaction->setHandles($this->getHandles()); $view->setTitle($xaction->getTitleForFeed($this)); foreach ($xaction_phids as $xaction_phid) { $secondary_xaction = $this->getObject($xaction_phid); $secondary_xaction->setHandles($this->getHandles()); $body = $secondary_xaction->getBodyForFeed($this); if (nonempty($body)) { $view->appendChild($body); } } $view->setImage( $this->getHandle($xaction->getAuthorPHID())->getImageURI()); return $view; } public function renderText() { $xaction = $this->getPrimaryTransaction(); $old_target = $xaction->getRenderingTarget(); $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; $xaction->setRenderingTarget($new_target); $xaction->setHandles($this->getHandles()); $text = $xaction->getTitleForFeed($this); $xaction->setRenderingTarget($old_target); return $text; } + public function renderAsTextForDoorkeeper( + DoorkeeperFeedStoryPublisher $publisher) { + + $xactions = array(); + $xaction_phids = $this->getValue('transactionPHIDs'); + foreach ($xaction_phids as $xaction_phid) { + $xactions[] = $this->getObject($xaction_phid); + } + + $primary = $this->getPrimaryTransaction(); + return $primary->renderAsTextForDoorkeeper($publisher, $this, $xactions); + } + } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index da9561ac71..570c67af7f 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,1139 +1,1177 @@ ignoreOnNoEffect = $ignore; return $this; } public function getIgnoreOnNoEffect() { return $this->ignoreOnNoEffect; } public function shouldGenerateOldValue() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_BUILDABLE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_CUSTOMFIELD: return false; } return true; } abstract public function getApplicationTransactionType(); private function getApplicationObjectTypeName() { $types = PhabricatorPHIDType::getAllTypes(); $type = idx($types, $this->getApplicationTransactionType()); if ($type) { return $type->getTypeName(); } return pht('Object'); } public function getApplicationTransactionCommentObject() { throw new PhutilMethodNotImplementedException(); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function generatePHID() { $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST; $subtype = $this->getApplicationTransactionType(); return PhabricatorPHID::generateNewPHID($type, $subtype); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'commentPHID' => 'phid?', 'commentVersion' => 'uint32', 'contentSource' => 'text', 'transactionType' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function hasComment() { return $this->getComment() && strlen($this->getComment()->getContent()); } public function getComment() { if ($this->commentNotLoaded) { throw new Exception('Comment for this transaction was not loaded.'); } return $this->comment; } public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; $this->commentNotLoaded = false; return $this; } public function setCommentNotLoaded($not_loaded) { $this->commentNotLoaded = $not_loaded; return $this; } public function attachObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->assertAttached($this->object); } public function getRemarkupBlocks() { $blocks = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $custom_blocks = $field->getApplicationTransactionRemarkupBlocks( $this); foreach ($custom_blocks as $custom_block) { $blocks[] = $custom_block; } } break; } if ($this->getComment()) { $blocks[] = $this->getComment()->getContent(); } return $blocks; } public function setOldValue($value) { $this->oldValueHasBeenSet = true; $this->writeField('oldValue', $value); return $this; } public function hasOldValue() { return $this->oldValueHasBeenSet; } /* -( Rendering )---------------------------------------------------------- */ public function setRenderingTarget($rendering_target) { $this->renderingTarget = $rendering_target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } public function attachViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->assertAttached($this->viewer); } public function getRequiredHandlePHIDs() { $phids = array(); $old = $this->getOldValue(); $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs( $this); } break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $phids[] = $old; $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { $phids[] = array($old); } if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { $phids[] = array($new); } break; case PhabricatorTransactions::TYPE_TOKEN: break; case PhabricatorTransactions::TYPE_BUILDABLE: $phid = $this->getMetadataValue('harbormaster:buildablePHID'); if ($phid) { $phids[] = array($phid); } break; } if ($this->getComment()) { $phids[] = array($this->getComment()->getAuthorPHID()); } return array_mergev($phids); } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( pht( 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '. 'did not load.', $this->getPHID(), $this->getTransactionType(), $phid)); } return $this->handles[$phid]; } public function getHandleIfExists($phid) { return idx($this->handles, $phid); } public function getHandles() { if ($this->handles === null) { throw new Exception( 'Transaction requires handles and it did not load them.' ); } return $this->handles; } public function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); } else { return $this->getHandle($phid)->getLinkName(); } } public function renderHandleList(array $phids) { $links = array(); foreach ($phids as $phid) { $links[] = $this->renderHandleLink($phid); } if ($this->renderingTarget == self::TARGET_HTML) { return phutil_implode_html(', ', $links); } else { return implode(', ', $links); } } private function renderSubscriberList(array $phids, $change_type) { if ($this->getRenderingTarget() == self::TARGET_TEXT) { return $this->renderHandleList($phids); } else { $handles = array_select_keys($this->getHandles(), $phids); return id(new SubscriptionListStringBuilder()) ->setHandles($handles) ->setObjectPHID($this->getPHID()) ->buildTransactionString($change_type); } } protected function renderPolicyName($phid, $state = 'old') { $policy = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $this->getHandleIfExists($phid)); if ($this->renderingTarget == self::TARGET_HTML) { switch ($policy->getType()) { case PhabricatorPolicyType::TYPE_CUSTOM: $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/'); $policy->setWorkflow(true); break; default: break; } $output = $policy->renderDescription(); } else { $output = hsprintf('%s', $policy->getFullName()); } return $output; } public function getIcon() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'fa-eraser'; } return 'fa-comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return 'fa-envelope'; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'fa-lock'; case PhabricatorTransactions::TYPE_EDGE: return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; case PhabricatorTransactions::TYPE_TOKEN: return 'fa-trophy'; } return 'fa-pencil'; } public function getToken() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: $old = $this->getOldValue(); $new = $this->getNewValue(); if ($new) { $icon = substr($new, 10); } else { $icon = substr($old, 10); } return array($icon, !$this->getNewValue()); } return array(null, null); } public function getColor() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT; $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'black'; } break; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return 'green'; case HarbormasterBuildable::STATUS_FAILED: return 'red'; } break; } return null; } protected function getTransactionCustomField() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); if (!$key) { return null; } $field = PhabricatorCustomField::getObjectField( $this->getObject(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { return null; } $field->setViewer($this->getViewer()); return $field; } return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getOldValue() === null) { return true; } else { return false; } break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->shouldHideInApplicationTransactions($this); } case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObject::EDGECONST: return true; break; case PhabricatorObjectMentionedByObject::EDGECONST: $new = ipull($this->getNewValue(), 'dst'); $old = ipull($this->getOldValue(), 'dst'); $add = array_diff($new, $old); $add_value = reset($add); $add_handle = $this->getHandle($add_value); if ($add_handle->getPolicyFiltered()) { return true; } return false; break; default: break; } break; } return false; } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, only ever send mail when builds fail. We might let // you customize this later, but in most cases this is probably // completely uninteresting. return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObject::EDGECONST: case PhabricatorObjectMentionedByObject::EDGECONST: return true; break; default: break; } break; } return $this->shouldHide(); } public function shouldHideForFeed() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, don't notify on build passes either. These are pretty // high volume and annoying, with very little present value. We // might want to turn them back on in the specific case of // build successes on the current document? return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObject::EDGECONST: case PhabricatorObjectMentionedByObject::EDGECONST: return true; break; default: break; } break; } return $this->shouldHide(); } public function getTitleForMail() { return id(clone $this)->setRenderingTarget('text')->getTitle(); } public function getBodyForMail() { $comment = $this->getComment(); if ($comment && strlen($comment->getContent())) { return $comment->getContent(); } return null; } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('You can not post an empty comment.'); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( 'This %s already has that view policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( 'This %s already has that edit policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( 'This %s already has that join policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited subscriber(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add'), count($rem), $this->renderSubscriberList($rem, 'rem')); } else if ($add) { return pht( '%s added %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add')); } else if ($rem) { return pht( '%s removed %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderSubscriberList($rem, 'rem')); } else { // This is used when rendering previews, before the user actually // selects any CCs. return pht( '%s updated subscribers...', $this->renderHandleLink($author_phid)); } break; case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getTransactionEditString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getTransactionAddString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getTransactionRemoveString( $this->renderHandleLink($author_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return pht( '%s edited edge metadata.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitle($this); } else { return pht( '%s edited a custom field.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_TOKEN: if ($old && $new) { return pht( '%s updated a token.', $this->renderHandleLink($author_phid)); } else if ($old) { return pht( '%s rescinded a token.', $this->renderHandleLink($author_phid)); } else { return pht( '%s awarded a token.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s!', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); default: return null; } default: return pht( '%s edited this %s.', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName()); } } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getFeedEditString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getFeedAddString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getFeedRemoveString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return pht( '%s edited edge metadata for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitleForFeed($this, $story); } else { return pht( '%s edited a custom field on %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); default: return null; } } return $this->getTitle(); } public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) { $fields = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $fields[] = 'comment/'.$this->getID(); } break; } return $fields; } public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); return PhabricatorMarkupEngine::summarize($text); } return null; } public function getBodyForFeed(PhabricatorFeedStory $story) { $old = $this->getOldValue(); $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $body = $story->getMarkupFieldOutput('comment/'.$this->getID()); } break; } return $body; } public function getActionStrength() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $old = $this->getOldValue(); $new = $this->getNewValue(); $add = array_diff($old, $new); $rem = array_diff($new, $old); // If this action is the actor subscribing or unsubscribing themselves, // it is less interesting. In particular, if someone makes a comment and // also implicitly subscribes themselves, we should treat the // transaction group as "comment", not "subscribe". In this specific // case (one affected user, and that affected user it the actor), // decrease the action strength. if ((count($add) + count($rem)) != 1) { // Not exactly one CC change. break; } $affected_phid = head(array_merge($add, $rem)); if ($affected_phid != $this->getAuthorPHID()) { // Affected user is someone else. break; } // Make this weaker than TYPE_COMMENT. return 0.25; } return 1.0; } public function isCommentTransaction() { if ($this->hasComment()) { return true; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return true; } return false; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('Commented On'); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht('Changed Policy'); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht('Changed Subscribers'); case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return pht('Build Passed'); case HarbormasterBuildable::STATUS_FAILED: return pht('Build Failed'); default: return pht('Build Status'); } default: return pht('Updated'); } } public function getMailTags() { return array(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionHasChangeDetails($this); } break; } return false; } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionChangeDetails($this, $viewer); } break; } return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function renderTextCorpusChangeDetails( PhabricatorUser $viewer, $old, $new) { require_celerity_resource('differential-changeset-view-css'); $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) ->setUser($viewer) ->setOldText($old) ->setNewText($new); return $view->render(); } public function attachTransactionGroup(array $group) { assert_instances_of($group, 'PhabricatorApplicationTransaction'); $this->transactionGroup = $group; return $this; } public function getTransactionGroup() { return $this->transactionGroup; } /** * Should this transaction be visually grouped with an existing transaction * group? * * @param list List of transactions. * @return bool True to display in a group with the other transactions. */ public function shouldDisplayGroupWith(array $group) { $this_source = null; if ($this->getContentSource()) { $this_source = $this->getContentSource()->getSource(); } foreach ($group as $xaction) { // Don't group transactions by different authors. if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) { return false; } // Don't group transactions for different objects. if ($xaction->getObjectPHID() != $this->getObjectPHID()) { return false; } // Don't group anything into a group which already has a comment. if ($xaction->isCommentTransaction()) { return false; } // Don't group transactions from different content sources. $other_source = null; if ($xaction->getContentSource()) { $other_source = $xaction->getContentSource()->getSource(); } if ($other_source != $this_source) { return false; } // Don't group transactions which happened more than 2 minutes apart. $apart = abs($xaction->getDateCreated() - $this->getDateCreated()); if ($apart > (60 * 2)) { return false; } } return true; } public function renderExtraInformationLink() { $herald_xscript_id = $this->getMetadataValue('herald:transcriptID'); if ($herald_xscript_id) { return phutil_tag( 'a', array( 'href' => '/herald/transcript/'.$herald_xscript_id.'/', ), pht('View Herald Transcript')); } return null; } + public function renderAsTextForDoorkeeper( + DoorkeeperFeedStoryPublisher $publisher, + PhabricatorFeedStory $story, + array $xactions) { + + $text = array(); + $body = array(); + + foreach ($xactions as $xaction) { + $xaction_body = $xaction->getBodyForMail(); + if ($xaction_body !== null) { + $body[] = $xaction_body; + } + + if ($xaction->shouldHideForMail($xactions)) { + continue; + } + + $old_target = $xaction->getRenderingTarget(); + $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; + $xaction->setRenderingTarget($new_target); + + if ($publisher->getRenderWithImpliedContext()) { + $text[] = $xaction->getTitle(); + } else { + $text[] = $xaction->getTitleForFeed($story); + } + + $xaction->setRenderingTarget($old_target); + } + + $text = implode("\n", $text); + $body = implode("\n\n", $body); + + return rtrim($text."\n\n".$body); + } + + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { // TODO: (T603) Exact policies are unclear here. return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $comment_template = null; try { $comment_template = $this->getApplicationTransactionCommentObject(); } catch (Exception $ex) { // Continue; no comments for these transactions. } if ($comment_template) { $comments = $comment_template->loadAllWhere( 'transactionPHID = %s', $this->getPHID()); foreach ($comments as $comment) { $engine->destroyObject($comment); } } $this->delete(); $this->saveTransaction(); } }