@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
at recaptime-dev/main 405 lines 11 kB view raw
1<?php 2 3final class HeraldCommitAdapter 4 extends HeraldAdapter 5 implements HarbormasterBuildableAdapterInterface { 6 7 protected $diff; 8 protected $revision; 9 10 protected $commit; 11 private $commitDiff; 12 13 protected $affectedPaths; 14 protected $affectedRevision; 15 protected $affectedPackages; 16 protected $auditNeededPackages; 17 18 private $buildRequests = array(); 19 20 public function getAdapterApplicationClass() { 21 return PhabricatorDiffusionApplication::class; 22 } 23 24 protected function newObject() { 25 return new PhabricatorRepositoryCommit(); 26 } 27 28 public function isTestAdapterForObject($object) { 29 return ($object instanceof PhabricatorRepositoryCommit); 30 } 31 32 public function getAdapterTestDescription() { 33 return pht( 34 'Test rules which run after a commit is discovered and imported.'); 35 } 36 37 public function newTestAdapter(PhabricatorUser $viewer, $object) { 38 return id(clone $this) 39 ->setObject($object); 40 } 41 42 protected function initializeNewAdapter() { 43 $this->commit = $this->newObject(); 44 } 45 46 public function setObject($object) { 47 $viewer = $this->getViewer(); 48 $commit_phid = $object->getPHID(); 49 50 $commit = id(new DiffusionCommitQuery()) 51 ->setViewer($viewer) 52 ->withPHIDs(array($commit_phid)) 53 ->needCommitData(true) 54 ->needIdentities(true) 55 ->needAuditRequests(true) 56 ->executeOne(); 57 if (!$commit) { 58 throw new Exception( 59 pht( 60 'Failed to reload commit ("%s") to fetch commit data.', 61 $commit_phid)); 62 } 63 64 $this->commit = $commit; 65 66 return $this; 67 } 68 69 public function getObject() { 70 return $this->commit; 71 } 72 73 public function getAdapterContentType() { 74 return 'commit'; 75 } 76 77 public function getAdapterContentName() { 78 return pht('Commits'); 79 } 80 81 public function getAdapterContentDescription() { 82 return pht( 83 "React to new commits appearing in tracked repositories.\n". 84 "Commit rules can send email, flag commits, trigger audits, ". 85 "and run build plans."); 86 } 87 88 public function supportsRuleType($rule_type) { 89 switch ($rule_type) { 90 case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: 91 case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: 92 case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 93 return true; 94 default: 95 return false; 96 } 97 } 98 99 public function canTriggerOnObject($object) { 100 if ($object instanceof PhabricatorRepository) { 101 return true; 102 } 103 if ($object instanceof PhabricatorProject) { 104 return true; 105 } 106 return false; 107 } 108 109 public function getTriggerObjectPHIDs() { 110 $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 111 112 $repository_phid = $this->getRepository()->getPHID(); 113 $commit_phid = $this->getObject()->getPHID(); 114 115 $phids = array(); 116 $phids[] = $commit_phid; 117 $phids[] = $repository_phid; 118 119 // NOTE: This is projects for the repository, not for the commit. When 120 // Herald evaluates, commits normally can not have any project tags yet. 121 $repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 122 $repository_phid, 123 $project_type); 124 foreach ($repository_project_phids as $phid) { 125 $phids[] = $phid; 126 } 127 128 $phids = array_unique($phids); 129 $phids = array_values($phids); 130 131 return $phids; 132 } 133 134 public function explainValidTriggerObjects() { 135 return pht('This rule can trigger for **repositories** and **projects**.'); 136 } 137 138 public function getHeraldName() { 139 return $this->commit->getMonogram(); 140 } 141 142 public function loadAffectedPaths() { 143 $viewer = $this->getViewer(); 144 145 if ($this->affectedPaths === null) { 146 $result = PhabricatorOwnerPathQuery::loadAffectedPaths( 147 $this->getRepository(), 148 $this->commit, 149 $viewer); 150 $this->affectedPaths = $result; 151 } 152 153 return $this->affectedPaths; 154 } 155 156 public function loadAffectedPackages() { 157 if ($this->affectedPackages === null) { 158 $packages = PhabricatorOwnersPackage::loadAffectedPackages( 159 $this->getRepository(), 160 $this->loadAffectedPaths()); 161 $this->affectedPackages = $packages; 162 } 163 return $this->affectedPackages; 164 } 165 166 public function loadAuditNeededPackages() { 167 if ($this->auditNeededPackages === null) { 168 $status_arr = array( 169 PhabricatorAuditRequestStatus::AUDIT_REQUIRED, 170 PhabricatorAuditRequestStatus::CONCERNED, 171 ); 172 $requests = id(new PhabricatorRepositoryAuditRequest()) 173 ->loadAllWhere( 174 'commitPHID = %s AND auditStatus IN (%Ls)', 175 $this->commit->getPHID(), 176 $status_arr); 177 $this->auditNeededPackages = $requests; 178 } 179 return $this->auditNeededPackages; 180 } 181 182 public function loadDifferentialRevision() { 183 if ($this->affectedRevision === null) { 184 $viewer = $this->getViewer(); 185 186 // NOTE: The viewer here is omnipotent, which means that Herald discloses 187 // some information users do not normally have access to when rules load 188 // the revision related to a commit. See D20468. 189 190 // A user who wants to learn about "Dxyz" can write a Herald rule which 191 // uses all the "Related revision..." fields, then push a commit which 192 // contains "Differential Revision: Dxyz" in the message to make Herald 193 // evaluate the commit with "Dxyz" as the related revision. 194 195 // At time of writing, this commit will link to the revision and the 196 // transcript for the commit will disclose some information about the 197 // revision (like reviewers, subscribers, and build status) which the 198 // commit author could not otherwise see. 199 200 // For now, we just accept this. The disclosures are relatively 201 // uninteresting and you have to jump through a lot of hoops (and leave 202 // a lot of evidence) to get this information. 203 204 $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( 205 $viewer, 206 $this->getObject()); 207 if ($revision) { 208 $this->affectedRevision = $revision; 209 } else { 210 $this->affectedRevision = false; 211 } 212 } 213 214 return $this->affectedRevision; 215 } 216 217 public static function getEnormousByteLimit() { 218 return 256 * 1024 * 1024; // 256MB. See T13142 and T13143. 219 } 220 221 public static function getEnormousTimeLimit() { 222 return 60 * 15; // 15 Minutes 223 } 224 225 private function loadCommitDiff() { 226 $viewer = $this->getViewer(); 227 228 $byte_limit = self::getEnormousByteLimit(); 229 $time_limit = self::getEnormousTimeLimit(); 230 231 $diff_info = $this->callConduit( 232 'diffusion.rawdiffquery', 233 array( 234 'commit' => $this->commit->getCommitIdentifier(), 235 'timeout' => $time_limit, 236 'byteLimit' => $byte_limit, 237 'linesOfContext' => 0, 238 )); 239 240 if ($diff_info['tooHuge']) { 241 throw new Exception( 242 pht( 243 'The raw text of this change is enormous (larger than %s bytes). '. 244 'Herald can not process it.', 245 new PhutilNumber($byte_limit))); 246 } 247 248 if ($diff_info['tooSlow']) { 249 throw new Exception( 250 pht( 251 'The raw text of this change took too long to process (longer '. 252 'than %s seconds). Herald can not process it.', 253 new PhutilNumber($time_limit))); 254 } 255 256 $file_phid = $diff_info['filePHID']; 257 $diff_file = id(new PhabricatorFileQuery()) 258 ->setViewer($viewer) 259 ->withPHIDs(array($file_phid)) 260 ->executeOne(); 261 if (!$diff_file) { 262 throw new Exception( 263 pht( 264 'Failed to load diff ("%s") for this change.', 265 $file_phid)); 266 } 267 268 $raw = $diff_file->loadFileData(); 269 270 // See T13667. This happens when a commit is empty and affects no files. 271 if (!strlen($raw)) { 272 return false; 273 } 274 275 $parser = new ArcanistDiffParser(); 276 $changes = $parser->parseDiff($raw); 277 278 $diff = DifferentialDiff::newEphemeralFromRawChanges( 279 $changes); 280 return $diff; 281 } 282 283 public function isDiffEnormous() { 284 $this->loadDiffContent('*'); 285 return ($this->commitDiff instanceof Exception); 286 } 287 288 public function loadDiffContent($type) { 289 if ($this->commitDiff === null) { 290 try { 291 $this->commitDiff = $this->loadCommitDiff(); 292 } catch (Exception $ex) { 293 $this->commitDiff = $ex; 294 phlog($ex); 295 } 296 } 297 298 if ($this->commitDiff === false) { 299 return array(); 300 } 301 302 if ($this->commitDiff instanceof Exception) { 303 $ex = $this->commitDiff; 304 $ex_class = get_class($ex); 305 $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); 306 307 return array( 308 '<'.$ex_class.'>' => $ex_message, 309 ); 310 } 311 312 $changes = $this->commitDiff->getChangesets(); 313 314 $result = array(); 315 foreach ($changes as $change) { 316 $lines = array(); 317 foreach ($change->getHunks() as $hunk) { 318 switch ($type) { 319 case '-': 320 $lines[] = $hunk->makeOldFile(); 321 break; 322 case '+': 323 $lines[] = $hunk->makeNewFile(); 324 break; 325 case '*': 326 $lines[] = $hunk->makeChanges(); 327 break; 328 default: 329 throw new Exception(pht("Unknown content selection '%s'!", $type)); 330 } 331 } 332 $result[$change->getFilename()] = implode("\n", $lines); 333 } 334 335 return $result; 336 } 337 338 public function loadIsMergeCommit() { 339 $parents = $this->callConduit( 340 'diffusion.commitparentsquery', 341 array( 342 'commit' => $this->getObject()->getCommitIdentifier(), 343 )); 344 345 return (count($parents) > 1); 346 } 347 348 private function callConduit($method, array $params) { 349 $viewer = $this->getViewer(); 350 351 $drequest = DiffusionRequest::newFromDictionary( 352 array( 353 'user' => $viewer, 354 'repository' => $this->getRepository(), 355 'commit' => $this->commit->getCommitIdentifier(), 356 )); 357 358 return DiffusionQuery::callConduitWithDiffusionRequest( 359 $viewer, 360 $drequest, 361 $method, 362 $params); 363 } 364 365 private function getRepository() { 366 return $this->getObject()->getRepository(); 367 } 368 369 public function getAuthorPHID() { 370 return $this->getObject()->getEffectiveAuthorPHID(); 371 } 372 373 public function getCommitterPHID() { 374 $commit = $this->getObject(); 375 376 if ($commit->hasCommitterIdentity()) { 377 $identity = $commit->getCommitterIdentity(); 378 return $identity->getCurrentEffectiveUserPHID(); 379 } 380 381 return null; 382 } 383 384 385/* -( HarbormasterBuildableAdapterInterface )------------------------------ */ 386 387 388 public function getHarbormasterBuildablePHID() { 389 return $this->getObject()->getPHID(); 390 } 391 392 public function getHarbormasterContainerPHID() { 393 return $this->getObject()->getRepository()->getPHID(); 394 } 395 396 public function getQueuedHarbormasterBuildRequests() { 397 return $this->buildRequests; 398 } 399 400 public function queueHarbormasterBuildRequest( 401 HarbormasterBuildRequest $request) { 402 $this->buildRequests[] = $request; 403 } 404 405}