[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/herald/adapter/ -> HeraldCommitAdapter.php (source)

   1  <?php
   2  
   3  final class HeraldCommitAdapter extends HeraldAdapter {
   4  
   5    const FIELD_NEED_AUDIT_FOR_PACKAGE      = 'need-audit-for-package';
   6    const FIELD_REPOSITORY_AUTOCLOSE_BRANCH = 'repository-autoclose-branch';
   7  
   8    protected $diff;
   9    protected $revision;
  10  
  11    protected $repository;
  12    protected $commit;
  13    protected $commitData;
  14    private $commitDiff;
  15  
  16    protected $emailPHIDs = array();
  17    protected $addCCPHIDs = array();
  18    protected $auditMap = array();
  19    protected $buildPlans = array();
  20  
  21    protected $affectedPaths;
  22    protected $affectedRevision;
  23    protected $affectedPackages;
  24    protected $auditNeededPackages;
  25  
  26    public function getAdapterApplicationClass() {
  27      return 'PhabricatorDiffusionApplication';
  28    }
  29  
  30    public function getObject() {
  31      return $this->commit;
  32    }
  33  
  34    public function getAdapterContentType() {
  35      return 'commit';
  36    }
  37  
  38    public function getAdapterContentName() {
  39      return pht('Commits');
  40    }
  41  
  42    public function getAdapterContentDescription() {
  43      return pht(
  44        "React to new commits appearing in tracked repositories.\n".
  45        "Commit rules can send email, flag commits, trigger audits, ".
  46        "and run build plans.");
  47    }
  48  
  49    public function supportsRuleType($rule_type) {
  50      switch ($rule_type) {
  51        case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
  52        case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
  53        case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
  54          return true;
  55        default:
  56          return false;
  57      }
  58    }
  59  
  60    public function canTriggerOnObject($object) {
  61      if ($object instanceof PhabricatorRepository) {
  62        return true;
  63      }
  64      if ($object instanceof PhabricatorProject) {
  65        return true;
  66      }
  67      return false;
  68    }
  69  
  70    public function getTriggerObjectPHIDs() {
  71      return array_merge(
  72        array(
  73          $this->repository->getPHID(),
  74          $this->getPHID(),
  75        ),
  76        $this->repository->getProjectPHIDs());
  77    }
  78  
  79    public function explainValidTriggerObjects() {
  80      return pht('This rule can trigger for **repositories** and **projects**.');
  81    }
  82  
  83    public function getFieldNameMap() {
  84      return array(
  85        self::FIELD_NEED_AUDIT_FOR_PACKAGE =>
  86          pht('Affected packages that need audit'),
  87        self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH
  88          => pht('Commit is on closing branch'),
  89      ) + parent::getFieldNameMap();
  90    }
  91  
  92    public function getFields() {
  93      return array_merge(
  94        array(
  95          self::FIELD_BODY,
  96          self::FIELD_AUTHOR,
  97          self::FIELD_COMMITTER,
  98          self::FIELD_REVIEWER,
  99          self::FIELD_REPOSITORY,
 100          self::FIELD_REPOSITORY_PROJECTS,
 101          self::FIELD_DIFF_FILE,
 102          self::FIELD_DIFF_CONTENT,
 103          self::FIELD_DIFF_ADDED_CONTENT,
 104          self::FIELD_DIFF_REMOVED_CONTENT,
 105          self::FIELD_DIFF_ENORMOUS,
 106          self::FIELD_AFFECTED_PACKAGE,
 107          self::FIELD_AFFECTED_PACKAGE_OWNER,
 108          self::FIELD_NEED_AUDIT_FOR_PACKAGE,
 109          self::FIELD_DIFFERENTIAL_REVISION,
 110          self::FIELD_DIFFERENTIAL_ACCEPTED,
 111          self::FIELD_DIFFERENTIAL_REVIEWERS,
 112          self::FIELD_DIFFERENTIAL_CCS,
 113          self::FIELD_BRANCHES,
 114          self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH,
 115        ),
 116        parent::getFields());
 117    }
 118  
 119    public function getConditionsForField($field) {
 120      switch ($field) {
 121        case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
 122          return array(
 123            self::CONDITION_INCLUDE_ANY,
 124            self::CONDITION_INCLUDE_NONE,
 125          );
 126        case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
 127          return array(
 128            self::CONDITION_UNCONDITIONALLY,
 129          );
 130      }
 131      return parent::getConditionsForField($field);
 132    }
 133  
 134    public function getActions($rule_type) {
 135      switch ($rule_type) {
 136        case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
 137        case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
 138          return array_merge(
 139            array(
 140              self::ACTION_ADD_CC,
 141              self::ACTION_EMAIL,
 142              self::ACTION_AUDIT,
 143              self::ACTION_APPLY_BUILD_PLANS,
 144              self::ACTION_NOTHING,
 145            ),
 146            parent::getActions($rule_type));
 147        case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
 148          return array_merge(
 149            array(
 150              self::ACTION_ADD_CC,
 151              self::ACTION_EMAIL,
 152              self::ACTION_FLAG,
 153              self::ACTION_AUDIT,
 154              self::ACTION_NOTHING,
 155            ),
 156            parent::getActions($rule_type));
 157      }
 158    }
 159  
 160    public function getValueTypeForFieldAndCondition($field, $condition) {
 161      switch ($field) {
 162        case self::FIELD_DIFFERENTIAL_CCS:
 163          return self::VALUE_EMAIL;
 164        case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
 165          return self::VALUE_OWNERS_PACKAGE;
 166      }
 167  
 168      return parent::getValueTypeForFieldAndCondition($field, $condition);
 169    }
 170  
 171    public static function newLegacyAdapter(
 172      PhabricatorRepository $repository,
 173      PhabricatorRepositoryCommit $commit,
 174      PhabricatorRepositoryCommitData $commit_data) {
 175  
 176      $object = new HeraldCommitAdapter();
 177  
 178      $commit->attachRepository($repository);
 179  
 180      $object->repository = $repository;
 181      $object->commit = $commit;
 182      $object->commitData = $commit_data;
 183  
 184      return $object;
 185    }
 186  
 187    public function setCommit(PhabricatorRepositoryCommit $commit) {
 188      $viewer = PhabricatorUser::getOmnipotentUser();
 189  
 190      $repository = id(new PhabricatorRepositoryQuery())
 191        ->setViewer($viewer)
 192        ->withIDs(array($commit->getRepositoryID()))
 193        ->needProjectPHIDs(true)
 194        ->executeOne();
 195      if (!$repository) {
 196        throw new Exception(pht('Unable to load repository!'));
 197      }
 198  
 199      $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
 200        'commitID = %d',
 201        $commit->getID());
 202      if (!$data) {
 203        throw new Exception(pht('Unable to load commit data!'));
 204      }
 205  
 206      $this->commit = clone $commit;
 207      $this->commit->attachRepository($repository);
 208      $this->commit->attachCommitData($data);
 209  
 210      $this->repository = $repository;
 211      $this->commitData = $data;
 212  
 213      return $this;
 214    }
 215  
 216    public function getPHID() {
 217      return $this->commit->getPHID();
 218    }
 219  
 220    public function getEmailPHIDs() {
 221      return array_keys($this->emailPHIDs);
 222    }
 223  
 224    public function getAddCCMap() {
 225      return $this->addCCPHIDs;
 226    }
 227  
 228    public function getAuditMap() {
 229      return $this->auditMap;
 230    }
 231  
 232    public function getBuildPlans() {
 233      return $this->buildPlans;
 234    }
 235  
 236    public function getHeraldName() {
 237      return
 238        'r'.
 239        $this->repository->getCallsign().
 240        $this->commit->getCommitIdentifier();
 241    }
 242  
 243    public function loadAffectedPaths() {
 244      if ($this->affectedPaths === null) {
 245        $result = PhabricatorOwnerPathQuery::loadAffectedPaths(
 246          $this->repository,
 247          $this->commit,
 248          PhabricatorUser::getOmnipotentUser());
 249        $this->affectedPaths = $result;
 250      }
 251      return $this->affectedPaths;
 252    }
 253  
 254    public function loadAffectedPackages() {
 255      if ($this->affectedPackages === null) {
 256        $packages = PhabricatorOwnersPackage::loadAffectedPackages(
 257          $this->repository,
 258          $this->loadAffectedPaths());
 259        $this->affectedPackages = $packages;
 260      }
 261      return $this->affectedPackages;
 262    }
 263  
 264    public function loadAuditNeededPackage() {
 265      if ($this->auditNeededPackages === null) {
 266        $status_arr = array(
 267          PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
 268          PhabricatorAuditStatusConstants::CONCERNED,
 269        );
 270        $requests = id(new PhabricatorRepositoryAuditRequest())
 271            ->loadAllWhere(
 272          'commitPHID = %s AND auditStatus IN (%Ls)',
 273          $this->commit->getPHID(),
 274          $status_arr);
 275  
 276        $packages = mpull($requests, 'getAuditorPHID');
 277        $this->auditNeededPackages = $packages;
 278      }
 279      return $this->auditNeededPackages;
 280    }
 281  
 282    public function loadDifferentialRevision() {
 283      if ($this->affectedRevision === null) {
 284        $this->affectedRevision = false;
 285        $data = $this->commitData;
 286        $revision_id = $data->getCommitDetail('differential.revisionID');
 287        if ($revision_id) {
 288          // NOTE: The Herald rule owner might not actually have access to
 289          // the revision, and can control which revision a commit is
 290          // associated with by putting text in the commit message. However,
 291          // the rules they can write against revisions don't actually expose
 292          // anything interesting, so it seems reasonable to load unconditionally
 293          // here.
 294  
 295          $revision = id(new DifferentialRevisionQuery())
 296            ->withIDs(array($revision_id))
 297            ->setViewer(PhabricatorUser::getOmnipotentUser())
 298            ->needRelationships(true)
 299            ->needReviewerStatus(true)
 300            ->executeOne();
 301          if ($revision) {
 302            $this->affectedRevision = $revision;
 303          }
 304        }
 305      }
 306      return $this->affectedRevision;
 307    }
 308  
 309    public static function getEnormousByteLimit() {
 310      return 1024 * 1024 * 1024; // 1GB
 311    }
 312  
 313    public static function getEnormousTimeLimit() {
 314      return 60 * 15; // 15 Minutes
 315    }
 316  
 317    private function loadCommitDiff() {
 318      $drequest = DiffusionRequest::newFromDictionary(
 319        array(
 320          'user' => PhabricatorUser::getOmnipotentUser(),
 321          'repository' => $this->repository,
 322          'commit' => $this->commit->getCommitIdentifier(),
 323        ));
 324  
 325      $byte_limit = self::getEnormousByteLimit();
 326  
 327      $raw = DiffusionQuery::callConduitWithDiffusionRequest(
 328        PhabricatorUser::getOmnipotentUser(),
 329        $drequest,
 330        'diffusion.rawdiffquery',
 331        array(
 332          'commit' => $this->commit->getCommitIdentifier(),
 333          'timeout' => self::getEnormousTimeLimit(),
 334          'byteLimit' => $byte_limit,
 335          'linesOfContext' => 0,
 336        ));
 337  
 338      if (strlen($raw) >= $byte_limit) {
 339        throw new Exception(
 340          pht(
 341            'The raw text of this change is enormous (larger than %d bytes). '.
 342            'Herald can not process it.',
 343            $byte_limit));
 344      }
 345  
 346      $parser = new ArcanistDiffParser();
 347      $changes = $parser->parseDiff($raw);
 348  
 349      $diff = DifferentialDiff::newFromRawChanges(
 350        PhabricatorUser::getOmnipotentUser(),
 351        $changes);
 352      return $diff;
 353    }
 354  
 355    private function getDiffContent($type) {
 356      if ($this->commitDiff === null) {
 357        try {
 358          $this->commitDiff = $this->loadCommitDiff();
 359        } catch (Exception $ex) {
 360          $this->commitDiff = $ex;
 361          phlog($ex);
 362        }
 363      }
 364  
 365      if ($this->commitDiff instanceof Exception) {
 366        $ex = $this->commitDiff;
 367        $ex_class = get_class($ex);
 368        $ex_message = pht('Failed to load changes: %s', $ex->getMessage());
 369  
 370        return array(
 371          '<'.$ex_class.'>' => $ex_message,
 372        );
 373      }
 374  
 375      $changes = $this->commitDiff->getChangesets();
 376  
 377      $result = array();
 378      foreach ($changes as $change) {
 379        $lines = array();
 380        foreach ($change->getHunks() as $hunk) {
 381          switch ($type) {
 382            case '-':
 383              $lines[] = $hunk->makeOldFile();
 384              break;
 385            case '+':
 386              $lines[] = $hunk->makeNewFile();
 387              break;
 388            case '*':
 389              $lines[] = $hunk->makeChanges();
 390              break;
 391            default:
 392              throw new Exception("Unknown content selection '{$type}'!");
 393          }
 394        }
 395        $result[$change->getFilename()] = implode("\n", $lines);
 396      }
 397  
 398      return $result;
 399    }
 400  
 401    public function getHeraldField($field) {
 402      $data = $this->commitData;
 403      switch ($field) {
 404        case self::FIELD_BODY:
 405          return $data->getCommitMessage();
 406        case self::FIELD_AUTHOR:
 407          return $data->getCommitDetail('authorPHID');
 408        case self::FIELD_COMMITTER:
 409          return $data->getCommitDetail('committerPHID');
 410        case self::FIELD_REVIEWER:
 411          return $data->getCommitDetail('reviewerPHID');
 412        case self::FIELD_DIFF_FILE:
 413          return $this->loadAffectedPaths();
 414        case self::FIELD_REPOSITORY:
 415          return $this->repository->getPHID();
 416        case self::FIELD_REPOSITORY_PROJECTS:
 417          return $this->repository->getProjectPHIDs();
 418        case self::FIELD_DIFF_CONTENT:
 419          return $this->getDiffContent('*');
 420        case self::FIELD_DIFF_ADDED_CONTENT:
 421          return $this->getDiffContent('+');
 422        case self::FIELD_DIFF_REMOVED_CONTENT:
 423          return $this->getDiffContent('-');
 424        case self::FIELD_DIFF_ENORMOUS:
 425          $this->getDiffContent('*');
 426          return ($this->commitDiff instanceof Exception);
 427        case self::FIELD_AFFECTED_PACKAGE:
 428          $packages = $this->loadAffectedPackages();
 429          return mpull($packages, 'getPHID');
 430        case self::FIELD_AFFECTED_PACKAGE_OWNER:
 431          $packages = $this->loadAffectedPackages();
 432          $owners = PhabricatorOwnersOwner::loadAllForPackages($packages);
 433          return mpull($owners, 'getUserPHID');
 434        case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
 435          return $this->loadAuditNeededPackage();
 436        case self::FIELD_DIFFERENTIAL_REVISION:
 437          $revision = $this->loadDifferentialRevision();
 438          if (!$revision) {
 439            return null;
 440          }
 441          return $revision->getID();
 442        case self::FIELD_DIFFERENTIAL_ACCEPTED:
 443          $revision = $this->loadDifferentialRevision();
 444          if (!$revision) {
 445            return null;
 446          }
 447  
 448          $status = $data->getCommitDetail(
 449            'precommitRevisionStatus',
 450            $revision->getStatus());
 451          switch ($status) {
 452            case ArcanistDifferentialRevisionStatus::ACCEPTED:
 453            case ArcanistDifferentialRevisionStatus::CLOSED:
 454              return $revision->getPHID();
 455          }
 456  
 457          return null;
 458        case self::FIELD_DIFFERENTIAL_REVIEWERS:
 459          $revision = $this->loadDifferentialRevision();
 460          if (!$revision) {
 461            return array();
 462          }
 463          return $revision->getReviewers();
 464        case self::FIELD_DIFFERENTIAL_CCS:
 465          $revision = $this->loadDifferentialRevision();
 466          if (!$revision) {
 467            return array();
 468          }
 469          return $revision->getCCPHIDs();
 470        case self::FIELD_BRANCHES:
 471          $params = array(
 472            'callsign' => $this->repository->getCallsign(),
 473            'contains' => $this->commit->getCommitIdentifier(),
 474          );
 475  
 476          $result = id(new ConduitCall('diffusion.branchquery', $params))
 477            ->setUser(PhabricatorUser::getOmnipotentUser())
 478            ->execute();
 479  
 480          $refs = DiffusionRepositoryRef::loadAllFromDictionaries($result);
 481          return mpull($refs, 'getShortName');
 482        case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
 483          return $this->repository->shouldAutocloseCommit($this->commit);
 484      }
 485  
 486      return parent::getHeraldField($field);
 487    }
 488  
 489    public function applyHeraldEffects(array $effects) {
 490      assert_instances_of($effects, 'HeraldEffect');
 491  
 492      $result = array();
 493      foreach ($effects as $effect) {
 494        $action = $effect->getAction();
 495        switch ($action) {
 496          case self::ACTION_NOTHING:
 497            $result[] = new HeraldApplyTranscript(
 498              $effect,
 499              true,
 500              pht('Great success at doing nothing.'));
 501            break;
 502          case self::ACTION_EMAIL:
 503            foreach ($effect->getTarget() as $phid) {
 504              $this->emailPHIDs[$phid] = true;
 505            }
 506            $result[] = new HeraldApplyTranscript(
 507              $effect,
 508              true,
 509              pht('Added address to email targets.'));
 510            break;
 511          case self::ACTION_ADD_CC:
 512            foreach ($effect->getTarget() as $phid) {
 513              if (empty($this->addCCPHIDs[$phid])) {
 514                $this->addCCPHIDs[$phid] = array();
 515              }
 516              $this->addCCPHIDs[$phid][] = $effect->getRuleID();
 517            }
 518            $result[] = new HeraldApplyTranscript(
 519              $effect,
 520              true,
 521              pht('Added address to CC.'));
 522            break;
 523          case self::ACTION_AUDIT:
 524            foreach ($effect->getTarget() as $phid) {
 525              if (empty($this->auditMap[$phid])) {
 526                $this->auditMap[$phid] = array();
 527              }
 528              $this->auditMap[$phid][] = $effect->getRuleID();
 529            }
 530            $result[] = new HeraldApplyTranscript(
 531              $effect,
 532              true,
 533              pht('Triggered an audit.'));
 534            break;
 535          case self::ACTION_APPLY_BUILD_PLANS:
 536            foreach ($effect->getTarget() as $phid) {
 537              $this->buildPlans[] = $phid;
 538            }
 539            $result[] = new HeraldApplyTranscript(
 540              $effect,
 541              true,
 542              pht('Applied build plans.'));
 543            break;
 544          case self::ACTION_FLAG:
 545            $result[] = parent::applyFlagEffect(
 546              $effect,
 547              $this->commit->getPHID());
 548            break;
 549          default:
 550            $custom_result = parent::handleCustomHeraldEffect($effect);
 551            if ($custom_result === null) {
 552              throw new Exception(pht(
 553                "No rules to handle action '%s'.",
 554                $action));
 555            }
 556  
 557            $result[] = $custom_result;
 558            break;
 559        }
 560      }
 561      return $result;
 562    }
 563  
 564  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1