Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / differential / storage / DifferentialRevision.php
blob92b303d0baf007ea473f33ac0a5b4295b4a35810
1 <?php
3 final class DifferentialRevision extends DifferentialDAO
4 implements
5 PhabricatorTokenReceiverInterface,
6 PhabricatorPolicyInterface,
7 PhabricatorExtendedPolicyInterface,
8 PhabricatorFlaggableInterface,
9 PhrequentTrackableInterface,
10 HarbormasterBuildableInterface,
11 PhabricatorSubscribableInterface,
12 PhabricatorCustomFieldInterface,
13 PhabricatorApplicationTransactionInterface,
14 PhabricatorTimelineInterface,
15 PhabricatorMentionableInterface,
16 PhabricatorDestructibleInterface,
17 PhabricatorProjectInterface,
18 PhabricatorFulltextInterface,
19 PhabricatorFerretInterface,
20 PhabricatorConduitResultInterface,
21 PhabricatorDraftInterface {
23 protected $title = '';
24 protected $status;
26 protected $summary = '';
27 protected $testPlan = '';
29 protected $authorPHID;
30 protected $lastReviewerPHID;
32 protected $lineCount = 0;
33 protected $attached = array();
35 protected $mailKey;
36 protected $branchName;
37 protected $repositoryPHID;
38 protected $activeDiffPHID;
40 protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
41 protected $editPolicy = PhabricatorPolicies::POLICY_USER;
42 protected $properties = array();
44 private $commitPHIDs = self::ATTACHABLE;
45 private $activeDiff = self::ATTACHABLE;
46 private $diffIDs = self::ATTACHABLE;
47 private $hashes = self::ATTACHABLE;
48 private $repository = self::ATTACHABLE;
50 private $reviewerStatus = self::ATTACHABLE;
51 private $customFields = self::ATTACHABLE;
52 private $drafts = array();
53 private $flags = array();
54 private $forceMap = array();
56 const RELATION_REVIEWER = 'revw';
57 const RELATION_SUBSCRIBED = 'subd';
59 const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
60 const PROPERTY_DRAFT_HOLD = 'draft.hold';
61 const PROPERTY_SHOULD_BROADCAST = 'draft.broadcast';
62 const PROPERTY_LINES_ADDED = 'lines.added';
63 const PROPERTY_LINES_REMOVED = 'lines.removed';
64 const PROPERTY_BUILDABLES = 'buildables';
65 const PROPERTY_WRONG_BUILDS = 'wrong.builds';
67 public static function initializeNewRevision(PhabricatorUser $actor) {
68 $app = id(new PhabricatorApplicationQuery())
69 ->setViewer($actor)
70 ->withClasses(array('PhabricatorDifferentialApplication'))
71 ->executeOne();
73 $view_policy = $app->getPolicy(
74 DifferentialDefaultViewCapability::CAPABILITY);
76 $initial_state = DifferentialRevisionStatus::DRAFT;
77 $should_broadcast = false;
79 return id(new DifferentialRevision())
80 ->setViewPolicy($view_policy)
81 ->setAuthorPHID($actor->getPHID())
82 ->attachRepository(null)
83 ->attachActiveDiff(null)
84 ->attachReviewers(array())
85 ->setModernRevisionStatus($initial_state)
86 ->setShouldBroadcast($should_broadcast);
89 protected function getConfiguration() {
90 return array(
91 self::CONFIG_AUX_PHID => true,
92 self::CONFIG_SERIALIZATION => array(
93 'attached' => self::SERIALIZATION_JSON,
94 'unsubscribed' => self::SERIALIZATION_JSON,
95 'properties' => self::SERIALIZATION_JSON,
97 self::CONFIG_COLUMN_SCHEMA => array(
98 'title' => 'text255',
99 'status' => 'text32',
100 'summary' => 'text',
101 'testPlan' => 'text',
102 'authorPHID' => 'phid?',
103 'lastReviewerPHID' => 'phid?',
104 'lineCount' => 'uint32?',
105 'mailKey' => 'bytes40',
106 'branchName' => 'text255?',
107 'repositoryPHID' => 'phid?',
109 self::CONFIG_KEY_SCHEMA => array(
110 'authorPHID' => array(
111 'columns' => array('authorPHID', 'status'),
113 'repositoryPHID' => array(
114 'columns' => array('repositoryPHID'),
116 // If you (or a project you are a member of) is reviewing a significant
117 // fraction of the revisions on an install, the result set of open
118 // revisions may be smaller than the result set of revisions where you
119 // are a reviewer. In these cases, this key is better than keys on the
120 // edge table.
121 'key_status' => array(
122 'columns' => array('status', 'phid'),
124 'key_modified' => array(
125 'columns' => array('dateModified'),
128 ) + parent::getConfiguration();
131 public function setProperty($key, $value) {
132 $this->properties[$key] = $value;
133 return $this;
136 public function getProperty($key, $default = null) {
137 return idx($this->properties, $key, $default);
140 public function hasRevisionProperty($key) {
141 return array_key_exists($key, $this->properties);
144 public function getMonogram() {
145 $id = $this->getID();
146 return "D{$id}";
149 public function getURI() {
150 return '/'.$this->getMonogram();
153 public function getCommitPHIDs() {
154 return $this->assertAttached($this->commitPHIDs);
157 public function getActiveDiff() {
158 // TODO: Because it's currently technically possible to create a revision
159 // without an associated diff, we allow an attached-but-null active diff.
160 // It would be good to get rid of this once we make diff-attaching
161 // transactional.
163 return $this->assertAttached($this->activeDiff);
166 public function attachActiveDiff($diff) {
167 $this->activeDiff = $diff;
168 return $this;
171 public function getDiffIDs() {
172 return $this->assertAttached($this->diffIDs);
175 public function attachDiffIDs(array $ids) {
176 rsort($ids);
177 $this->diffIDs = array_values($ids);
178 return $this;
181 public function attachCommitPHIDs(array $phids) {
182 $this->commitPHIDs = $phids;
183 return $this;
186 public function getAttachedPHIDs($type) {
187 return array_keys(idx($this->attached, $type, array()));
190 public function setAttachedPHIDs($type, array $phids) {
191 $this->attached[$type] = array_fill_keys($phids, array());
192 return $this;
195 public function generatePHID() {
196 return PhabricatorPHID::generateNewPHID(
197 DifferentialRevisionPHIDType::TYPECONST);
200 public function loadActiveDiff() {
201 return id(new DifferentialDiff())->loadOneWhere(
202 'revisionID = %d ORDER BY id DESC LIMIT 1',
203 $this->getID());
206 public function save() {
207 if (!$this->getMailKey()) {
208 $this->mailKey = Filesystem::readRandomCharacters(40);
210 return parent::save();
213 public function getHashes() {
214 return $this->assertAttached($this->hashes);
217 public function attachHashes(array $hashes) {
218 $this->hashes = $hashes;
219 return $this;
222 public function canReviewerForceAccept(
223 PhabricatorUser $viewer,
224 DifferentialReviewer $reviewer) {
226 if (!$reviewer->isPackage()) {
227 return false;
230 $map = $this->getReviewerForceAcceptMap($viewer);
231 if (!$map) {
232 return false;
235 if (isset($map[$reviewer->getReviewerPHID()])) {
236 return true;
239 return false;
242 private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
243 $fragment = $viewer->getCacheFragment();
245 if (!array_key_exists($fragment, $this->forceMap)) {
246 $map = $this->newReviewerForceAcceptMap($viewer);
247 $this->forceMap[$fragment] = $map;
250 return $this->forceMap[$fragment];
253 private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
254 $diff = $this->getActiveDiff();
255 if (!$diff) {
256 return null;
259 $repository_phid = $diff->getRepositoryPHID();
260 if (!$repository_phid) {
261 return null;
264 $paths = array();
266 try {
267 $changesets = $diff->getChangesets();
268 } catch (Exception $ex) {
269 $changesets = id(new DifferentialChangesetQuery())
270 ->setViewer($viewer)
271 ->withDiffs(array($diff))
272 ->execute();
275 foreach ($changesets as $changeset) {
276 $paths[] = $changeset->getOwnersFilename();
279 if (!$paths) {
280 return null;
283 $reviewer_phids = array();
284 foreach ($this->getReviewers() as $reviewer) {
285 if (!$reviewer->isPackage()) {
286 continue;
289 $reviewer_phids[] = $reviewer->getReviewerPHID();
292 if (!$reviewer_phids) {
293 return null;
296 // Load all the reviewing packages which have control over some of the
297 // paths in the change. These are packages which the actor may be able
298 // to force-accept on behalf of.
299 $control_query = id(new PhabricatorOwnersPackageQuery())
300 ->setViewer($viewer)
301 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
302 ->withPHIDs($reviewer_phids)
303 ->withControl($repository_phid, $paths);
304 $control_packages = $control_query->execute();
305 if (!$control_packages) {
306 return null;
309 // Load all the packages which have potential control over some of the
310 // paths in the change and are owned by the actor. These are packages
311 // which the actor may be able to use their authority over to gain the
312 // ability to force-accept for other packages. This query doesn't apply
313 // dominion rules yet, and we'll bypass those rules later on.
315 // See T13657. We ignore "watcher" packages which don't grant their owners
316 // permission to force accept anything.
318 $authority_query = id(new PhabricatorOwnersPackageQuery())
319 ->setViewer($viewer)
320 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
321 ->withAuthorityModes(
322 array(
323 PhabricatorOwnersPackage::AUTHORITY_STRONG,
325 ->withAuthorityPHIDs(array($viewer->getPHID()))
326 ->withControl($repository_phid, $paths);
327 $authority_packages = $authority_query->execute();
328 if (!$authority_packages) {
329 return null;
331 $authority_packages = mpull($authority_packages, null, 'getPHID');
333 // Build a map from each path in the revision to the reviewer packages
334 // which control it.
335 $control_map = array();
336 foreach ($paths as $path) {
337 $control_packages = $control_query->getControllingPackagesForPath(
338 $repository_phid,
339 $path);
341 // Remove packages which the viewer has authority over. We don't need
342 // to check these for force-accept because they can just accept them
343 // normally.
344 $control_packages = mpull($control_packages, null, 'getPHID');
345 foreach ($control_packages as $phid => $control_package) {
346 if (isset($authority_packages[$phid])) {
347 unset($control_packages[$phid]);
351 if (!$control_packages) {
352 continue;
355 $control_map[$path] = $control_packages;
358 if (!$control_map) {
359 return null;
362 // From here on out, we only care about paths which we have at least one
363 // controlling package for.
364 $paths = array_keys($control_map);
366 // Now, build a map from each path to the packages which would control it
367 // if there were no dominion rules.
368 $authority_map = array();
369 foreach ($paths as $path) {
370 $authority_packages = $authority_query->getControllingPackagesForPath(
371 $repository_phid,
372 $path,
373 $ignore_dominion = true);
375 $authority_map[$path] = mpull($authority_packages, null, 'getPHID');
378 // For each path, find the most general package that the viewer has
379 // authority over. For example, we'll prefer a package that owns "/" to a
380 // package that owns "/src/".
381 $force_map = array();
382 foreach ($authority_map as $path => $package_map) {
383 $path_fragments = PhabricatorOwnersPackage::splitPath($path);
384 $fragment_count = count($path_fragments);
386 // Find the package that we have authority over which has the most
387 // general match for this path.
388 $best_match = null;
389 $best_package = null;
390 foreach ($package_map as $package_phid => $package) {
391 $package_paths = $package->getPathsForRepository($repository_phid);
392 foreach ($package_paths as $package_path) {
394 // NOTE: A strength of 0 means "no match". A strength of 1 means
395 // that we matched "/", so we can not possibly find another stronger
396 // match.
398 $strength = $package_path->getPathMatchStrength(
399 $path_fragments,
400 $fragment_count);
401 if (!$strength) {
402 continue;
405 if ($strength < $best_match || !$best_package) {
406 $best_match = $strength;
407 $best_package = $package;
408 if ($strength == 1) {
409 break 2;
415 if ($best_package) {
416 $force_map[$path] = array(
417 'strength' => $best_match,
418 'package' => $best_package,
423 // For each path which the viewer owns a package for, find other packages
424 // which that authority can be used to force-accept. Once we find a way to
425 // force-accept a package, we don't need to keep looking.
426 $has_control = array();
427 foreach ($force_map as $path => $spec) {
428 $path_fragments = PhabricatorOwnersPackage::splitPath($path);
429 $fragment_count = count($path_fragments);
431 $authority_strength = $spec['strength'];
433 $control_packages = $control_map[$path];
434 foreach ($control_packages as $control_phid => $control_package) {
435 if (isset($has_control[$control_phid])) {
436 continue;
439 $control_paths = $control_package->getPathsForRepository(
440 $repository_phid);
441 foreach ($control_paths as $control_path) {
442 $strength = $control_path->getPathMatchStrength(
443 $path_fragments,
444 $fragment_count);
446 if (!$strength) {
447 continue;
450 if ($strength > $authority_strength) {
451 $authority = $spec['package'];
452 $has_control[$control_phid] = array(
453 'authority' => $authority,
454 'phid' => $authority->getPHID(),
456 break;
462 // Return a map from packages which may be force accepted to the packages
463 // which permit that forced acceptance.
464 return ipull($has_control, 'phid');
468 /* -( PhabricatorPolicyInterface )----------------------------------------- */
471 public function getCapabilities() {
472 return array(
473 PhabricatorPolicyCapability::CAN_VIEW,
474 PhabricatorPolicyCapability::CAN_EDIT,
478 public function getPolicy($capability) {
479 switch ($capability) {
480 case PhabricatorPolicyCapability::CAN_VIEW:
481 return $this->getViewPolicy();
482 case PhabricatorPolicyCapability::CAN_EDIT:
483 return $this->getEditPolicy();
487 public function hasAutomaticCapability($capability, PhabricatorUser $user) {
488 // A revision's author (which effectively means "owner" after we added
489 // commandeering) can always view and edit it.
490 $author_phid = $this->getAuthorPHID();
491 if ($author_phid) {
492 if ($user->getPHID() == $author_phid) {
493 return true;
497 return false;
500 public function describeAutomaticCapability($capability) {
501 $description = array(
502 pht('The owner of a revision can always view and edit it.'),
505 switch ($capability) {
506 case PhabricatorPolicyCapability::CAN_VIEW:
507 $description[] = pht(
508 'If a revision belongs to a repository, other users must be able '.
509 'to view the repository in order to view the revision.');
510 break;
513 return $description;
517 /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
520 public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
521 $extended = array();
523 switch ($capability) {
524 case PhabricatorPolicyCapability::CAN_VIEW:
525 $repository_phid = $this->getRepositoryPHID();
526 $repository = $this->getRepository();
528 // Try to use the object if we have it, since it will save us some
529 // data fetching later on. In some cases, we might not have it.
530 $repository_ref = nonempty($repository, $repository_phid);
531 if ($repository_ref) {
532 $extended[] = array(
533 $repository_ref,
534 PhabricatorPolicyCapability::CAN_VIEW,
537 break;
540 return $extended;
544 /* -( PhabricatorTokenReceiverInterface )---------------------------------- */
547 public function getUsersToNotifyOfTokenGiven() {
548 return array(
549 $this->getAuthorPHID(),
553 public function getReviewers() {
554 return $this->assertAttached($this->reviewerStatus);
557 public function attachReviewers(array $reviewers) {
558 assert_instances_of($reviewers, 'DifferentialReviewer');
559 $reviewers = mpull($reviewers, null, 'getReviewerPHID');
560 $this->reviewerStatus = $reviewers;
561 return $this;
564 public function hasAttachedReviewers() {
565 return ($this->reviewerStatus !== self::ATTACHABLE);
568 public function getReviewerPHIDs() {
569 $reviewers = $this->getReviewers();
570 return mpull($reviewers, 'getReviewerPHID');
573 public function getReviewerPHIDsForEdit() {
574 $reviewers = $this->getReviewers();
576 $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
578 $value = array();
579 foreach ($reviewers as $reviewer) {
580 $phid = $reviewer->getReviewerPHID();
581 if ($reviewer->getReviewerStatus() == $status_blocking) {
582 $value[] = 'blocking('.$phid.')';
583 } else {
584 $value[] = $phid;
588 return $value;
591 public function getRepository() {
592 return $this->assertAttached($this->repository);
595 public function attachRepository(PhabricatorRepository $repository = null) {
596 $this->repository = $repository;
597 return $this;
600 public function setModernRevisionStatus($status) {
601 return $this->setStatus($status);
604 public function getModernRevisionStatus() {
605 return $this->getStatus();
608 public function getLegacyRevisionStatus() {
609 return $this->getStatusObject()->getLegacyKey();
612 public function isClosed() {
613 return $this->getStatusObject()->isClosedStatus();
616 public function isAbandoned() {
617 return $this->getStatusObject()->isAbandoned();
620 public function isAccepted() {
621 return $this->getStatusObject()->isAccepted();
624 public function isNeedsReview() {
625 return $this->getStatusObject()->isNeedsReview();
628 public function isNeedsRevision() {
629 return $this->getStatusObject()->isNeedsRevision();
632 public function isChangePlanned() {
633 return $this->getStatusObject()->isChangePlanned();
636 public function isPublished() {
637 return $this->getStatusObject()->isPublished();
640 public function isDraft() {
641 return $this->getStatusObject()->isDraft();
644 public function getStatusIcon() {
645 return $this->getStatusObject()->getIcon();
648 public function getStatusDisplayName() {
649 return $this->getStatusObject()->getDisplayName();
652 public function getStatusIconColor() {
653 return $this->getStatusObject()->getIconColor();
656 public function getStatusTagColor() {
657 return $this->getStatusObject()->getTagColor();
660 public function getStatusObject() {
661 $status = $this->getStatus();
662 return DifferentialRevisionStatus::newForStatus($status);
665 public function getFlag(PhabricatorUser $viewer) {
666 return $this->assertAttachedKey($this->flags, $viewer->getPHID());
669 public function attachFlag(
670 PhabricatorUser $viewer,
671 PhabricatorFlag $flag = null) {
672 $this->flags[$viewer->getPHID()] = $flag;
673 return $this;
676 public function getHasDraft(PhabricatorUser $viewer) {
677 return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
680 public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
681 $this->drafts[$viewer->getCacheFragment()] = $has_draft;
682 return $this;
685 public function getHoldAsDraft() {
686 return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
689 public function setHoldAsDraft($hold) {
690 return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
693 public function getShouldBroadcast() {
694 return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
697 public function setShouldBroadcast($should_broadcast) {
698 return $this->setProperty(
699 self::PROPERTY_SHOULD_BROADCAST,
700 $should_broadcast);
703 public function setAddedLineCount($count) {
704 return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
707 public function getAddedLineCount() {
708 return $this->getProperty(self::PROPERTY_LINES_ADDED);
711 public function setRemovedLineCount($count) {
712 return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
715 public function getRemovedLineCount() {
716 return $this->getProperty(self::PROPERTY_LINES_REMOVED);
719 public function hasLineCounts() {
720 // This data was not populated on older revisions, so it may not be
721 // present on all revisions.
722 return isset($this->properties[self::PROPERTY_LINES_ADDED]);
725 public function getRevisionScaleGlyphs() {
726 $add = $this->getAddedLineCount();
727 $rem = $this->getRemovedLineCount();
728 $all = ($add + $rem);
730 if (!$all) {
731 return ' ';
734 $map = array(
735 20 => 2,
736 50 => 3,
737 150 => 4,
738 375 => 5,
739 1000 => 6,
740 2500 => 7,
743 $n = 1;
744 foreach ($map as $size => $count) {
745 if ($size <= $all) {
746 $n = $count;
747 } else {
748 break;
752 $add_n = (int)ceil(($add / $all) * $n);
753 $rem_n = (int)ceil(($rem / $all) * $n);
755 while ($add_n + $rem_n > $n) {
756 if ($add_n > 1) {
757 $add_n--;
758 } else {
759 $rem_n--;
763 return
764 str_repeat('+', $add_n).
765 str_repeat('-', $rem_n).
766 str_repeat(' ', (7 - $n));
769 public function getBuildableStatus($phid) {
770 $buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
771 if (!is_array($buildables)) {
772 $buildables = array();
775 $buildable = idx($buildables, $phid);
776 if (!is_array($buildable)) {
777 $buildable = array();
780 return idx($buildable, 'status');
783 public function setBuildableStatus($phid, $status) {
784 $buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
785 if (!is_array($buildables)) {
786 $buildables = array();
789 $buildable = idx($buildables, $phid);
790 if (!is_array($buildable)) {
791 $buildable = array();
794 $buildable['status'] = $status;
796 $buildables[$phid] = $buildable;
798 return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables);
801 public function newBuildableStatus(PhabricatorUser $viewer, $phid) {
802 // For Differential, we're ignoring autobuilds (local lint and unit)
803 // when computing build status. Differential only cares about remote
804 // builds when making publishing and undrafting decisions.
806 $builds = $this->loadImpactfulBuildsForBuildablePHIDs(
807 $viewer,
808 array($phid));
810 return $this->newBuildableStatusForBuilds($builds);
813 public function newBuildableStatusForBuilds(array $builds) {
814 // If we have nothing but passing builds, the buildable passes.
815 if (!$builds) {
816 return HarbormasterBuildableStatus::STATUS_PASSED;
819 // If we have any completed, non-passing builds, the buildable fails.
820 foreach ($builds as $build) {
821 if ($build->isComplete()) {
822 return HarbormasterBuildableStatus::STATUS_FAILED;
826 // Otherwise, we're still waiting for the build to pass or fail.
827 return null;
830 public function loadImpactfulBuilds(PhabricatorUser $viewer) {
831 $diff = $this->getActiveDiff();
833 // NOTE: We can't use `withContainerPHIDs()` here because the container
834 // update in Harbormaster is not synchronous.
835 $buildables = id(new HarbormasterBuildableQuery())
836 ->setViewer($viewer)
837 ->withBuildablePHIDs(array($diff->getPHID()))
838 ->withManualBuildables(false)
839 ->execute();
840 if (!$buildables) {
841 return array();
844 return $this->loadImpactfulBuildsForBuildablePHIDs(
845 $viewer,
846 mpull($buildables, 'getPHID'));
849 private function loadImpactfulBuildsForBuildablePHIDs(
850 PhabricatorUser $viewer,
851 array $phids) {
853 $builds = id(new HarbormasterBuildQuery())
854 ->setViewer($viewer)
855 ->withBuildablePHIDs($phids)
856 ->withAutobuilds(false)
857 ->withBuildStatuses(
858 array(
859 HarbormasterBuildStatus::STATUS_INACTIVE,
860 HarbormasterBuildStatus::STATUS_PENDING,
861 HarbormasterBuildStatus::STATUS_BUILDING,
862 HarbormasterBuildStatus::STATUS_FAILED,
863 HarbormasterBuildStatus::STATUS_ABORTED,
864 HarbormasterBuildStatus::STATUS_ERROR,
865 HarbormasterBuildStatus::STATUS_PAUSED,
866 HarbormasterBuildStatus::STATUS_DEADLOCKED,
868 ->execute();
870 // Filter builds based on the "Hold Drafts" behavior of their associated
871 // build plans.
873 $hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS;
874 $behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts);
876 $key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER;
877 $key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING;
879 foreach ($builds as $key => $build) {
880 $plan = $build->getBuildPlan();
882 // See T13526. If the viewer can't see the build plan, pretend it has
883 // generic options. This is often wrong, but "often wrong" is better than
884 // "fatal".
885 if ($plan) {
886 $hold_key = $behavior->getPlanOption($plan)->getKey();
888 $hold_never = ($hold_key === $key_never);
889 $hold_building = ($hold_key === $key_building);
890 } else {
891 $hold_never = false;
892 $hold_building = false;
895 // If the build "Never" holds drafts from promoting, we don't care what
896 // the status is.
897 if ($hold_never) {
898 unset($builds[$key]);
899 continue;
902 // If the build holds drafts from promoting "While Building", we only
903 // care about the status until it completes.
904 if ($hold_building) {
905 if ($build->isComplete()) {
906 unset($builds[$key]);
907 continue;
912 return $builds;
916 /* -( HarbormasterBuildableInterface )------------------------------------- */
919 public function getHarbormasterBuildableDisplayPHID() {
920 return $this->getHarbormasterContainerPHID();
923 public function getHarbormasterBuildablePHID() {
924 return $this->loadActiveDiff()->getPHID();
927 public function getHarbormasterContainerPHID() {
928 return $this->getPHID();
931 public function getBuildVariables() {
932 return array();
935 public function getAvailableBuildVariables() {
936 return array();
939 public function newBuildableEngine() {
940 return new DifferentialBuildableEngine();
944 /* -( PhabricatorSubscribableInterface )----------------------------------- */
947 public function isAutomaticallySubscribed($phid) {
948 if ($phid == $this->getAuthorPHID()) {
949 return true;
952 // TODO: This only happens when adding or removing CCs, and is safe from a
953 // policy perspective, but the subscription pathway should have some
954 // opportunity to load this data properly. For now, this is the only case
955 // where implicit subscription is not an intrinsic property of the object.
956 if ($this->reviewerStatus == self::ATTACHABLE) {
957 $reviewers = id(new DifferentialRevisionQuery())
958 ->setViewer(PhabricatorUser::getOmnipotentUser())
959 ->withPHIDs(array($this->getPHID()))
960 ->needReviewers(true)
961 ->executeOne()
962 ->getReviewers();
963 } else {
964 $reviewers = $this->getReviewers();
967 foreach ($reviewers as $reviewer) {
968 if ($reviewer->getReviewerPHID() !== $phid) {
969 continue;
972 if ($reviewer->isResigned()) {
973 continue;
976 return true;
979 return false;
983 /* -( PhabricatorCustomFieldInterface )------------------------------------ */
986 public function getCustomFieldSpecificationForRole($role) {
987 return PhabricatorEnv::getEnvConfig('differential.fields');
990 public function getCustomFieldBaseClass() {
991 return 'DifferentialCustomField';
994 public function getCustomFields() {
995 return $this->assertAttached($this->customFields);
998 public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
999 $this->customFields = $fields;
1000 return $this;
1004 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
1007 public function getApplicationTransactionEditor() {
1008 return new DifferentialTransactionEditor();
1011 public function getApplicationTransactionTemplate() {
1012 return new DifferentialTransaction();
1016 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1019 public function destroyObjectPermanently(
1020 PhabricatorDestructionEngine $engine) {
1022 $viewer = $engine->getViewer();
1024 $this->openTransaction();
1025 $diffs = id(new DifferentialDiffQuery())
1026 ->setViewer($viewer)
1027 ->withRevisionIDs(array($this->getID()))
1028 ->execute();
1029 foreach ($diffs as $diff) {
1030 $engine->destroyObject($diff);
1033 id(new DifferentialAffectedPathEngine())
1034 ->setRevision($this)
1035 ->destroyAffectedPaths();
1037 $viewstate_query = id(new DifferentialViewStateQuery())
1038 ->setViewer($viewer)
1039 ->withObjectPHIDs(array($this->getPHID()));
1040 $viewstates = new PhabricatorQueryIterator($viewstate_query);
1041 foreach ($viewstates as $viewstate) {
1042 $viewstate->delete();
1045 $this->delete();
1046 $this->saveTransaction();
1050 /* -( PhabricatorFulltextInterface )--------------------------------------- */
1053 public function newFulltextEngine() {
1054 return new DifferentialRevisionFulltextEngine();
1058 /* -( PhabricatorFerretInterface )----------------------------------------- */
1061 public function newFerretEngine() {
1062 return new DifferentialRevisionFerretEngine();
1066 /* -( PhabricatorConduitResultInterface )---------------------------------- */
1069 public function getFieldSpecificationsForConduit() {
1070 return array(
1071 id(new PhabricatorConduitSearchFieldSpecification())
1072 ->setKey('title')
1073 ->setType('string')
1074 ->setDescription(pht('The revision title.')),
1075 id(new PhabricatorConduitSearchFieldSpecification())
1076 ->setKey('uri')
1077 ->setType('uri')
1078 ->setDescription(pht('View URI for the revision.')),
1079 id(new PhabricatorConduitSearchFieldSpecification())
1080 ->setKey('authorPHID')
1081 ->setType('phid')
1082 ->setDescription(pht('Revision author PHID.')),
1083 id(new PhabricatorConduitSearchFieldSpecification())
1084 ->setKey('status')
1085 ->setType('map<string, wild>')
1086 ->setDescription(pht('Information about revision status.')),
1087 id(new PhabricatorConduitSearchFieldSpecification())
1088 ->setKey('repositoryPHID')
1089 ->setType('phid?')
1090 ->setDescription(pht('Revision repository PHID.')),
1091 id(new PhabricatorConduitSearchFieldSpecification())
1092 ->setKey('diffPHID')
1093 ->setType('phid')
1094 ->setDescription(pht('Active diff PHID.')),
1095 id(new PhabricatorConduitSearchFieldSpecification())
1096 ->setKey('summary')
1097 ->setType('string')
1098 ->setDescription(pht('Revision summary.')),
1099 id(new PhabricatorConduitSearchFieldSpecification())
1100 ->setKey('testPlan')
1101 ->setType('string')
1102 ->setDescription(pht('Revision test plan.')),
1103 id(new PhabricatorConduitSearchFieldSpecification())
1104 ->setKey('isDraft')
1105 ->setType('bool')
1106 ->setDescription(
1107 pht(
1108 'True if this revision is in any draft state, and thus not '.
1109 'notifying reviewers and subscribers about changes.')),
1110 id(new PhabricatorConduitSearchFieldSpecification())
1111 ->setKey('holdAsDraft')
1112 ->setType('bool')
1113 ->setDescription(
1114 pht(
1115 'True if this revision is being held as a draft. It will not be '.
1116 'automatically submitted for review even if tests pass.')),
1120 public function getFieldValuesForConduit() {
1121 $status = $this->getStatusObject();
1122 $status_info = array(
1123 'value' => $status->getKey(),
1124 'name' => $status->getDisplayName(),
1125 'closed' => $status->isClosedStatus(),
1126 'color.ansi' => $status->getANSIColor(),
1129 return array(
1130 'title' => $this->getTitle(),
1131 'uri' => PhabricatorEnv::getURI($this->getURI()),
1132 'authorPHID' => $this->getAuthorPHID(),
1133 'status' => $status_info,
1134 'repositoryPHID' => $this->getRepositoryPHID(),
1135 'diffPHID' => $this->getActiveDiffPHID(),
1136 'summary' => $this->getSummary(),
1137 'testPlan' => $this->getTestPlan(),
1138 'isDraft' => !$this->getShouldBroadcast(),
1139 'holdAsDraft' => (bool)$this->getHoldAsDraft(),
1143 public function getConduitSearchAttachments() {
1144 return array(
1145 id(new DifferentialReviewersSearchEngineAttachment())
1146 ->setAttachmentKey('reviewers'),
1151 /* -( PhabricatorDraftInterface )------------------------------------------ */
1154 public function newDraftEngine() {
1155 return new DifferentialRevisionDraftEngine();
1159 /* -( PhabricatorTimelineInterface )--------------------------------------- */
1162 public function newTimelineEngine() {
1163 return new DifferentialRevisionTimelineEngine();