@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow users to receive email about pushes via Herald

Summary:
Fixes T4677. Implements a "send an email" pre-receive action, which sends push summaries.

For use cases where features are often pushed as a large number of commits (e.g., checkpoint commits are retained), using commit emails means users get a ton of email. Instead, this allows you to get an email about a push, which summarizes what changed.

Overall, this is basically the same as commit email, but more suitable for some workflows.

Test Plan:
Wrote some rules, then made a bunch of pushes. Got email like this:

{F134929}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T4677

Differential Revision: https://secure.phabricator.com/D8618

+364 -10
+4
src/__phutil_library_map__.php
··· 1971 1971 'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php', 1972 1972 'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php', 1973 1973 'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php', 1974 + 'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php', 1975 + 'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php', 1974 1976 'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php', 1975 1977 'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php', 1976 1978 'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php', ··· 4818 4820 ), 4819 4821 'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4820 4822 'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 4823 + 'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker', 4824 + 'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler', 4821 4825 'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4822 4826 'PhabricatorRepositoryRefCursor' => 4823 4827 array(
+5
src/applications/diffusion/data/DiffusionCommitRef.php
··· 74 74 return $this->formatUser($this->committerName, $this->committerEmail); 75 75 } 76 76 77 + public function getSummary() { 78 + return PhabricatorRepositoryCommitData::summarizeCommitMessage( 79 + $this->getMessage()); 80 + } 81 + 77 82 private function formatUser($name, $email) { 78 83 if (strlen($name) && strlen($email)) { 79 84 return "{$name} <{$email}>";
+45
src/applications/diffusion/engine/DiffusionCommitHookEngine.php
··· 32 32 private $heraldViewerProjects; 33 33 private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN; 34 34 private $rejectDetails; 35 + private $emailPHIDs = array(); 35 36 36 37 37 38 /* -( Config )------------------------------------------------------------- */ ··· 172 173 throw $caught; 173 174 } 174 175 176 + if ($this->emailPHIDs) { 177 + // If Herald rules triggered email to users, queue a worker to send the 178 + // mail. We do this out-of-process so that we block pushes as briefly 179 + // as possible. 180 + 181 + // (We do need to pull some commit info here because the commit objects 182 + // may not exist yet when this worker runs, which could be immediately.) 183 + 184 + PhabricatorWorker::scheduleTask( 185 + 'PhabricatorRepositoryPushMailWorker', 186 + array( 187 + 'eventPHID' => $event->getPHID(), 188 + 'emailPHIDs' => array_values($this->emailPHIDs), 189 + 'info' => $this->loadCommitInfoForWorker($all_updates), 190 + )); 191 + } 192 + 175 193 return 0; 176 194 } 177 195 ··· 281 299 $effects = $engine->applyRules($rules, $adapter); 282 300 $engine->applyEffects($effects, $adapter, $rules); 283 301 $xscript = $engine->getTranscript(); 302 + 303 + // Store any PHIDs we want to send email to for later. 304 + foreach ($adapter->getEmailPHIDs() as $email_phid) { 305 + $this->emailPHIDs[$email_phid] = $email_phid; 306 + } 284 307 285 308 if ($blocking_effect === null) { 286 309 foreach ($effects as $effect) { ··· 982 1005 return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) 983 1006 ->setPHID($phid) 984 1007 ->setRepositoryPHID($this->getRepository()->getPHID()) 1008 + ->attachRepository($this->getRepository()) 985 1009 ->setEpoch(time()); 986 1010 } 987 1011 ··· 1091 1115 } 1092 1116 } 1093 1117 1118 + private function loadCommitInfoForWorker(array $all_updates) { 1119 + $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT; 1120 + 1121 + $map = array(); 1122 + foreach ($all_updates as $update) { 1123 + if ($update->getRefType() != $type_commit) { 1124 + continue; 1125 + } 1126 + $map[$update->getRefNew()] = array(); 1127 + } 1128 + 1129 + foreach ($map as $identifier => $info) { 1130 + $ref = $this->loadCommitRefForCommit($identifier); 1131 + $map[$identifier] += array( 1132 + 'summary' => $ref->getSummary(), 1133 + 'branches' => $this->loadBranches($identifier), 1134 + ); 1135 + } 1136 + 1137 + return $map; 1138 + } 1094 1139 1095 1140 }
+17 -1
src/applications/diffusion/herald/HeraldPreCommitAdapter.php
··· 4 4 5 5 private $log; 6 6 private $hookEngine; 7 + private $emailPHIDs = array(); 7 8 8 9 public function setPushLog(PhabricatorRepositoryPushLog $log) { 9 10 $this->log = $log; ··· 25 26 26 27 public function getObject() { 27 28 return $this->log; 29 + } 30 + 31 + public function getEmailPHIDs() { 32 + return array_values($this->emailPHIDs); 28 33 } 29 34 30 35 public function supportsRuleType($rule_type) { 31 36 switch ($rule_type) { 32 37 case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: 33 38 case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 34 - return true; 35 39 case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: 40 + return true; 36 41 default: 37 42 return false; 38 43 } ··· 70 75 case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 71 76 return array( 72 77 self::ACTION_BLOCK, 78 + self::ACTION_EMAIL, 73 79 self::ACTION_NOTHING 74 80 ); 75 81 case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: 76 82 return array( 83 + self::ACTION_EMAIL, 77 84 self::ACTION_NOTHING, 78 85 ); 79 86 } ··· 95 102 $effect, 96 103 true, 97 104 pht('Did nothing.')); 105 + break; 106 + case self::ACTION_EMAIL: 107 + foreach ($effect->getTarget() as $phid) { 108 + $this->emailPHIDs[$phid] = $phid; 109 + } 110 + $result[] = new HeraldApplyTranscript( 111 + $effect, 112 + true, 113 + pht('Added mailable to mail targets.')); 98 114 break; 99 115 case self::ACTION_BLOCK: 100 116 $result[] = new HeraldApplyTranscript(
+1 -1
src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
··· 18 18 public function getAdapterContentDescription() { 19 19 return pht( 20 20 "React to commits being pushed to hosted repositories.\n". 21 - "Hook rules can block changes."); 21 + "Hook rules can block changes and send push summary mail."); 22 22 } 23 23 24 24 public function getFields() {
+1 -1
src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php
··· 20 20 public function getAdapterContentDescription() { 21 21 return pht( 22 22 "React to branches and tags being pushed to hosted repositories.\n". 23 - "Hook rules can block changes."); 23 + "Hook rules can block changes and send push summary mail."); 24 24 } 25 25 26 26 public function getFieldNameMap() {
+1 -1
src/applications/herald/storage/HeraldRule.php
··· 17 17 protected $isDisabled = 0; 18 18 protected $triggerObjectPHID; 19 19 20 - protected $configVersion = 34; 20 + protected $configVersion = 35; 21 21 22 22 // phids for which this rule has been applied 23 23 private $ruleApplied = self::ATTACHABLE;
+27
src/applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php
··· 1 + <?php 2 + 3 + final class PhabricatorRepositoryPushReplyHandler 4 + extends PhabricatorMailReplyHandler { 5 + 6 + public function validateMailReceiver($mail_receiver) { 7 + return; 8 + } 9 + 10 + public function getPrivateReplyHandlerEmailAddress( 11 + PhabricatorObjectHandle $handle) { 12 + return null; 13 + } 14 + 15 + public function getReplyHandlerDomain() { 16 + return null; 17 + } 18 + 19 + public function getReplyHandlerInstructions() { 20 + return null; 21 + } 22 + 23 + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { 24 + return; 25 + } 26 + 27 + }
+3
src/applications/repository/storage/PhabricatorRepositoryCommitData.php
··· 24 24 25 25 public function getSummary() { 26 26 $message = $this->getCommitMessage(); 27 + return self::summarizeCommitMessage($message); 28 + } 27 29 30 + public static function summarizeCommitMessage($message) { 28 31 $summary = phutil_split_lines($message, $retain_endings = false); 29 32 $summary = head($summary); 30 33 $summary = phutil_utf8_shorten($summary, self::SUMMARY_MAX_LENGTH);
+21 -6
src/applications/repository/storage/PhabricatorRepositoryPushLog.php
··· 45 45 46 46 private $dangerousChangeDescription = self::ATTACHABLE; 47 47 private $pushEvent = self::ATTACHABLE; 48 + private $repository = self::ATTACHABLE; 48 49 49 50 public static function initializeNewLog(PhabricatorUser $viewer) { 50 51 return id(new PhabricatorRepositoryPushLog()) ··· 64 65 public function generatePHID() { 65 66 return PhabricatorPHID::generateNewPHID( 66 67 PhabricatorRepositoryPHIDTypePushLog::TYPECONST); 67 - } 68 - 69 - public function getRepository() { 70 - return $this->getPushEvent()->getRepository(); 71 68 } 72 69 73 70 public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) { ··· 120 117 return $this->assertAttached($this->dangerousChangeDescription); 121 118 } 122 119 120 + public function attachRepository(PhabricatorRepository $repository) { 121 + // NOTE: Some gymnastics around this because of object construction order 122 + // in the hook engine. Particularly, web build the logs before we build 123 + // their push event. 124 + $this->repository = $repository; 125 + return $this; 126 + } 127 + 128 + public function getRepository() { 129 + if ($this->repository == self::ATTACHABLE) { 130 + return $this->getPushEvent()->getRepository(); 131 + } 132 + return $this->assertAttached($this->repository); 133 + } 134 + 123 135 124 136 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 125 137 ··· 131 143 } 132 144 133 145 public function getPolicy($capability) { 134 - return $this->getPushEvent()->getPolicy($capability); 146 + // NOTE: We're passing through the repository rather than the push event 147 + // mostly because we need to do policy checks in Herald before we create 148 + // the event. The two approaches are equivalent in practice. 149 + return $this->getRepository()->getPolicy($capability); 135 150 } 136 151 137 152 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 138 - return $this->getPushEvent()->hasAutomaticCapability($capability, $viewer); 153 + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); 139 154 } 140 155 141 156 public function describeAutomaticCapability($capability) {
+225
src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
··· 1 + <?php 2 + 3 + final class PhabricatorRepositoryPushMailWorker 4 + extends PhabricatorWorker { 5 + 6 + public function doWork() { 7 + $viewer = PhabricatorUser::getOmnipotentUser(); 8 + 9 + $task_data = $this->getTaskData(); 10 + 11 + $email_phids = idx($task_data, 'emailPHIDs'); 12 + if (!$email_phids) { 13 + // If we don't have any email targets, don't send any email. 14 + return; 15 + } 16 + 17 + $event_phid = idx($task_data, 'eventPHID'); 18 + $event = id(new PhabricatorRepositoryPushEventQuery()) 19 + ->setViewer($viewer) 20 + ->withPHIDs(array($event_phid)) 21 + ->needLogs(true) 22 + ->executeOne(); 23 + 24 + $repository = $event->getRepository(); 25 + if ($repository->isImporting()) { 26 + // If the repository is still importing, don't send email. 27 + return; 28 + } 29 + 30 + if ($repository->getDetail('herald-disabled')) { 31 + // If publishing is disabled, don't send email. 32 + return; 33 + } 34 + 35 + $logs = $event->getLogs(); 36 + 37 + list($ref_lines, $ref_list) = $this->renderRefs($logs); 38 + list($commit_lines, $subject_line) = $this->renderCommits( 39 + $repository, 40 + $logs, 41 + idx($task_data, 'info', array())); 42 + 43 + $ref_count = count($ref_lines); 44 + $commit_count = count($commit_lines); 45 + 46 + $handles = id(new PhabricatorHandleQuery()) 47 + ->setViewer($viewer) 48 + ->withPHIDs(array($event->getPusherPHID())) 49 + ->execute(); 50 + 51 + $pusher_name = $handles[$event->getPusherPHID()]->getName(); 52 + $repo_name = $repository->getMonogram(); 53 + 54 + if ($commit_count) { 55 + $overview = pht( 56 + '%s pushed %d commit(s) to %s.', 57 + $pusher_name, 58 + $commit_count, 59 + $repo_name); 60 + } else { 61 + $overview = pht( 62 + '%s pushed to %s.', 63 + $pusher_name, 64 + $repo_name); 65 + } 66 + 67 + $details_uri = PhabricatorEnv::getProductionURI( 68 + '/diffusion/pushlog/view/'.$event->getID().'/'); 69 + 70 + $body = new PhabricatorMetaMTAMailBody(); 71 + $body->addRawSection($overview); 72 + 73 + $body->addTextSection(pht('DETAILS'), $details_uri); 74 + 75 + if ($commit_lines) { 76 + $body->addTextSection(pht('COMMITS'), implode("\n", $commit_lines)); 77 + } 78 + 79 + if ($ref_lines) { 80 + $body->addTextSection(pht('REFERENCES'), implode("\n", $ref_lines)); 81 + } 82 + 83 + $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); 84 + 85 + $parts = array(); 86 + if ($commit_count) { 87 + $parts[] = pht('%s commit(s)', $commit_count); 88 + } 89 + if ($ref_count) { 90 + $parts[] = implode(', ', $ref_list); 91 + } 92 + $parts = implode(', ', $parts); 93 + 94 + if ($subject_line) { 95 + $subject = pht('(%s) %s', $parts, $subject_line); 96 + } else { 97 + $subject = pht('(%s)', $parts); 98 + } 99 + 100 + $mail = id(new PhabricatorMetaMTAMail()) 101 + ->setRelatedPHID($event->getPHID()) 102 + ->setSubjectPrefix($prefix) 103 + ->setVarySubjectPrefix(pht('[Push]')) 104 + ->setSubject($subject) 105 + ->setFrom($event->getPusherPHID()) 106 + ->setBody($body->render()) 107 + ->setThreadID($event->getPHID(), $is_new = true) 108 + ->addHeader('Thread-Topic', $subject) 109 + ->setIsBulk(true); 110 + 111 + $to_handles = id(new PhabricatorHandleQuery()) 112 + ->setViewer($viewer) 113 + ->withPHIDs($email_phids) 114 + ->execute(); 115 + 116 + $reply_handler = new PhabricatorRepositoryPushReplyHandler(); 117 + $mails = $reply_handler->multiplexMail( 118 + $mail, 119 + $to_handles, 120 + array()); 121 + 122 + foreach ($mails as $mail) { 123 + $mail->saveAndSend(); 124 + } 125 + } 126 + 127 + public function renderForDisplay(PhabricatorUser $viewer) { 128 + // This data has some sensitive stuff, so don't show it. 129 + return null; 130 + } 131 + 132 + private function renderRefs(array $logs) { 133 + $ref_lines = array(); 134 + $ref_list = array(); 135 + 136 + foreach ($logs as $log) { 137 + $type_name = null; 138 + $type_prefix = null; 139 + switch ($log->getRefType()) { 140 + case PhabricatorRepositoryPushLog::REFTYPE_BRANCH: 141 + $type_name = pht('branch'); 142 + break; 143 + case PhabricatorRepositoryPushLog::REFTYPE_TAG: 144 + $type_name = pht('tag'); 145 + $type_prefix = pht('tag:'); 146 + break; 147 + case PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK: 148 + $type_name = pht('bookmark'); 149 + $type_prefix = pht('bookmark:'); 150 + break; 151 + case PhabricatorRepositoryPushLog::REFTYPE_COMMIT: 152 + default: 153 + break; 154 + } 155 + 156 + if ($type_name === null) { 157 + continue; 158 + } 159 + 160 + $flags = $log->getChangeFlags(); 161 + if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS) { 162 + $action = '!'; 163 + } else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE) { 164 + $action = '-'; 165 + } else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE) { 166 + $action = '~'; 167 + } else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND) { 168 + $action = ' '; 169 + } else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_ADD) { 170 + $action = '+'; 171 + } else { 172 + $action = '?'; 173 + } 174 + 175 + $old = nonempty($log->getRefOldShort(), pht('<null>')); 176 + $new = nonempty($log->getRefNewShort(), pht('<null>')); 177 + 178 + $name = $log->getRefName(); 179 + 180 + $ref_lines[] = "{$action} {$type_name} {$name} {$old} > {$new}"; 181 + $ref_list[] = $type_prefix.$name; 182 + } 183 + 184 + return array( 185 + $ref_lines, 186 + array_unique($ref_list), 187 + ); 188 + } 189 + 190 + private function renderCommits( 191 + PhabricatorRepository $repository, 192 + array $logs, 193 + array $info) { 194 + 195 + $commit_lines = array(); 196 + $subject_line = null; 197 + foreach ($logs as $log) { 198 + if ($log->getRefType() != PhabricatorRepositoryPushLog::REFTYPE_COMMIT) { 199 + continue; 200 + } 201 + 202 + $commit_info = idx($info, $log->getRefNew(), array()); 203 + 204 + $name = $repository->formatCommitName($log->getRefNew()); 205 + 206 + $branches = null; 207 + if (idx($commit_info, 'branches')) { 208 + $branches = ' ('.implode(', ', $commit_info['branches']).')'; 209 + } 210 + 211 + $summary = null; 212 + if (strlen(idx($commit_info, 'summary'))) { 213 + $summary = ' '.$commit_info['summary']; 214 + } 215 + 216 + $commit_lines[] = "{$name}{$branches}{$summary}"; 217 + if ($subject_line === null) { 218 + $subject_line = "{$name}{$summary}"; 219 + } 220 + } 221 + 222 + return array($commit_lines, $subject_line); 223 + } 224 + 225 + }
+14
src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php
··· 861 861 '%s, %d lines', 862 862 ), 863 863 864 + '%s pushed %d commit(s) to %s.' => array( 865 + array( 866 + array( 867 + '%s pushed a commit to %3$s.', 868 + '%s pushed %d commits to %s.', 869 + ), 870 + ), 871 + ), 872 + 873 + '%s commit(s)' => array( 874 + '1 commit', 875 + '%s commits', 876 + ), 877 + 864 878 ); 865 879 } 866 880