diff --git a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php index 8cefeceb4..3f4bd54e4 100644 --- a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php @@ -1,112 +1,128 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class DifferentialRevisionIDFieldSpecification extends DifferentialFieldSpecification { private $id; protected function didSetRevision() { $this->id = $this->getRevision()->getID(); } public function shouldAppearOnCommitMessage() { return true; } public function shouldAppearOnCommitMessageTemplate() { return false; } public function getCommitMessageKey() { return 'revisionID'; } public function setValueFromParsedCommitMessage($value) { $this->id = $value; return $this; } public function renderLabelForCommitMessage() { return 'Differential Revision'; } public function renderValueForCommitMessage($is_edit) { if (!$this->id) { return null; } return PhabricatorEnv::getProductionURI('/D'.$this->id); } public function parseValueFromCommitMessage($value) { $rev = trim($value); if (!strlen($rev)) { return null; } if (is_numeric($rev)) { // TODO: Eventually, remove support for bare revision numbers. return (int)$rev; } $rev = self::parseRevisionIDFromURI($rev); if ($rev !== null) { return $rev; } $example_uri = PhabricatorEnv::getProductionURI('/D123'); throw new DifferentialFieldParseException( "Commit references invalid 'Differential Revision'. Expected a ". "Phabricator URI like '{$example_uri}', got '{$value}'."); } public static function parseRevisionIDFromURI($uri) { $path = id(new PhutilURI($uri))->getPath(); $matches = null; if (preg_match('#^/D(\d+)$#', $path, $matches)) { $id = (int)$matches[1]; // Make sure the URI is the same as our URI. Basically, we want to ignore // commits from other Phabricator installs. if ($uri == PhabricatorEnv::getProductionURI('/D'.$id)) { return $id; } } return null; } public function shouldAppearOnRevisionList() { return true; } public function renderHeaderForRevisionList() { return 'ID'; } public function renderValueForRevisionList(DifferentialRevision $revision) { return 'D'.$revision->getID(); } public function renderValueForMail($phase) { - $uri = PhabricatorEnv::getProductionURI('/D'.$this->id); - return "REVISION DETAIL\n {$uri}"; + $body = array(); + $body[] = 'REVISION DETAIL'; + $body[] = ' '.PhabricatorEnv::getProductionURI('/D'.$this->id); + + if ($phase == DifferentialMailPhase::UPDATE) { + $diffs = id(new DifferentialDiff())->loadAllWhere( + 'revisionID = %d ORDER BY id DESC LIMIT 2', + $this->id); + if (count($diffs) == 2) { + list($new, $old) = array_values(mpull($diffs, 'getID')); + $body[] = null; + $body[] = 'CHANGE SINCE LAST DIFF'; + $body[] = ' '.PhabricatorEnv::getProductionURI( + "/D{$this->id}?vs={$old}&id={$new}#differential-review-toc"); + } + } + + return implode("\n", $body); } } diff --git a/src/applications/differential/mail/DifferentialMail.php b/src/applications/differential/mail/DifferentialMail.php index 4b607ae22..7014e2743 100644 --- a/src/applications/differential/mail/DifferentialMail.php +++ b/src/applications/differential/mail/DifferentialMail.php @@ -1,426 +1,426 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ abstract class DifferentialMail { protected $to = array(); protected $cc = array(); protected $actorHandle; protected $revision; protected $comment; protected $changesets; protected $inlineComments; protected $isFirstMailAboutRevision; protected $isFirstMailToRecipients; protected $heraldTranscriptURI; protected $heraldRulesHeader; protected $replyHandler; protected $parentMessageID; protected function renderSubject() { $revision = $this->getRevision(); $title = $revision->getTitle(); $id = $revision->getID(); return "D{$id}: {$title}"; } abstract protected function renderVaryPrefix(); abstract protected function renderBody(); public function setActorHandle($actor_handle) { $this->actorHandle = $actor_handle; return $this; } public function getActorHandle() { return $this->actorHandle; } protected function getActorName() { $handle = $this->getActorHandle(); if ($handle) { return $handle->getName(); } return '???'; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function setXHeraldRulesHeader($header) { $this->heraldRulesHeader = $header; return $this; } public function send() { $to_phids = $this->getToPHIDs(); if (!$to_phids) { throw new Exception('No "To:" users provided!'); } $cc_phids = $this->getCCPHIDs(); $body = $this->buildBody(); $attachments = $this->buildAttachments(); $template = new PhabricatorMetaMTAMail(); $actor_handle = $this->getActorHandle(); $reply_handler = $this->getReplyHandler(); if ($actor_handle) { $template->setFrom($actor_handle->getPHID()); } $template ->setSubject($this->renderSubject()) ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix($this->renderVaryPrefix()) ->setBody($body) ->setIsHTML($this->shouldMarkMailAsHTML()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', $this->getThreadTopic()); $template->setAttachments($attachments); $template->setThreadID( $this->getThreadID(), $this->isFirstMailAboutRevision()); if ($this->heraldRulesHeader) { $template->addHeader('X-Herald-Rules', $this->heraldRulesHeader); } $revision = $this->revision; if ($revision) { if ($revision->getAuthorPHID()) { $template->addHeader( 'X-Differential-Author', '<'.$revision->getAuthorPHID().'>'); } if ($revision->getReviewers()) { $template->addHeader( 'X-Differential-Reviewers', '<'.implode('>, <', $revision->getReviewers()).'>'); } if ($revision->getCCPHIDs()) { $template->addHeader( 'X-Differential-CCs', '<'.implode('>, <', $revision->getCCPHIDs()).'>'); // Determine explicit CCs (those added by humans) and put them in a // header so users can differentiate between Herald CCs and human CCs. $relation_subscribed = DifferentialRevision::RELATION_SUBSCRIBED; $raw = $revision->getRawRelations($relation_subscribed); $reason_phids = ipull($raw, 'reasonPHID'); $reason_handles = id(new PhabricatorObjectHandleData($reason_phids)) ->loadHandles(); $explicit_cc = array(); foreach ($raw as $relation) { if (!$relation['reasonPHID']) { continue; } $type = $reason_handles[$relation['reasonPHID']]->getType(); if ($type == PhabricatorPHIDConstants::PHID_TYPE_USER) { $explicit_cc[] = $relation['objectPHID']; } } if ($explicit_cc) { $template->addHeader( 'X-Differential-Explicit-CCs', '<'.implode('>, <', $explicit_cc).'>'); } } } $template->setIsBulk(true); $template->setRelatedPHID($this->getRevision()->getPHID()); $mailtags = $this->getMailTags(); if ($mailtags) { $template->setMailTags($mailtags); } $phids = array(); foreach ($to_phids as $phid) { $phids[$phid] = true; } foreach ($cc_phids as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLSENDMAIL, array( 'mail' => $template, ) ); PhutilEventEngine::dispatchEvent($event); $template = $event->getValue('mail'); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $to_phids), array_select_keys($handles, $cc_phids)); foreach ($mails as $mail) { $mail->saveAndSend(); } } protected function getMailTags() { return array(); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function shouldMarkMailAsHTML() { return false; } protected function buildBody() { $body = $this->renderBody(); $reply_handler = $this->getReplyHandler(); $reply_instructions = $reply_handler->getReplyHandlerInstructions(); if ($reply_instructions) { $body .= "\nREPLY HANDLER ACTIONS\n". " {$reply_instructions}\n"; } if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) { $manage_uri = PhabricatorEnv::getProductionURI( '/herald/view/differential/'); $xscript_uri = $this->getHeraldTranscriptURI(); $body .= <<<EOTEXT MANAGE HERALD DIFFERENTIAL RULES {$manage_uri} WHY DID I GET THIS EMAIL? {$xscript_uri} EOTEXT; } return $body; } /** * You can override this method in a subclass and return array of attachments * to be sent with the email. Each attachment is an instance of * PhabricatorMetaMTAAttachment. */ protected function buildAttachments() { return array(); } public function getReplyHandler() { if (!$this->replyHandler) { $this->replyHandler = self::newReplyHandlerForRevision($this->getRevision()); } return $this->replyHandler; } public static function newReplyHandlerForRevision( DifferentialRevision $revision) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.differential.reply-handler'); $reply_handler->setMailReceiver($revision); return $reply_handler; } protected function formatText($text) { - $text = explode("\n", $text); + $text = explode("\n", rtrim($text)); foreach ($text as &$line) { $line = rtrim(' '.$line); } unset($line); return implode("\n", $text); } public function setToPHIDs(array $to) { $this->to = $this->filterContactPHIDs($to); return $this; } public function setCCPHIDs(array $cc) { $this->cc = $this->filterContactPHIDs($cc); return $this; } protected function filterContactPHIDs(array $phids) { return $phids; // TODO: actually do this? // Differential revisions use Subscriptions for CCs, so any arbitrary // PHID can end up CC'd to them. Only try to actually send email PHIDs // which have ToolsHandle types that are marked emailable. If we don't // filter here, sending the email will fail. /* $handles = array(); prep(new ToolsHandleData($phids, $handles)); foreach ($handles as $phid => $handle) { if (!$handle->isEmailable()) { unset($handles[$phid]); } } return array_keys($handles); */ } protected function getToPHIDs() { return $this->to; } protected function getCCPHIDs() { return $this->cc; } public function setRevision($revision) { $this->revision = $revision; return $this; } public function getRevision() { return $this->revision; } protected function getThreadID() { $phid = $this->getRevision()->getPHID(); return "differential-rev-{$phid}-req"; } protected function getThreadTopic() { $phid = $this->getRevision()->getPHID(); return "Differential Revision {$phid}"; } public function setComment($comment) { $this->comment = $comment; return $this; } public function getComment() { return $this->comment; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->changesets; } public function setInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { return $this->inlineComments; } protected function renderAuxFields($phase) { $selector = DifferentialFieldSelector::newSelector(); $aux_fields = $selector->sortFieldsForMail( $selector->getFieldSpecifications()); $body = array(); foreach ($aux_fields as $field) { $field->setRevision($this->getRevision()); $text = $field->renderValueForMail($phase); if ($text !== null) { $body[] = $text; $body[] = null; } } return implode("\n", $body); } public function renderRevisionDetailLink() { $uri = $this->getRevisionURI(); return "REVISION DETAIL\n {$uri}"; } public function getRevisionURI() { return PhabricatorEnv::getProductionURI('/D'.$this->getRevision()->getID()); } public function setIsFirstMailToRecipients($first) { $this->isFirstMailToRecipients = $first; return $this; } public function isFirstMailToRecipients() { return $this->isFirstMailToRecipients; } public function setIsFirstMailAboutRevision($first) { $this->isFirstMailAboutRevision = $first; return $this; } public function isFirstMailAboutRevision() { return $this->isFirstMailAboutRevision; } public function setHeraldTranscriptURI($herald_transcript_uri) { $this->heraldTranscriptURI = $herald_transcript_uri; return $this; } public function getHeraldTranscriptURI() { return $this->heraldTranscriptURI; } protected function renderHandleList(array $handles, array $phids) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $names = array(); foreach ($phids as $phid) { $names[] = $handles[$phid]->getName(); } return implode(', ', $names); } }