diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 3397f9cb03..a2a058568b 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,1148 +1,1183 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { $initial_state = DifferentialRevisionStatus::DRAFT; $should_broadcast = false; } else { $initial_state = DifferentialRevisionStatus::NEEDS_REVIEW; $should_broadcast = true; } return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewers(array()) ->setModernRevisionStatus($initial_state) ->setShouldBroadcast($should_broadcast); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), // If you (or a project you are a member of) is reviewing a significant // fraction of the revisions on an install, the result set of open // revisions may be smaller than the result set of revisions where you // are a reviewer. In these cases, this key is better than keys on the // edge table. 'key_status' => array( 'columns' => array('status', 'phid'), ), ), ) + parent::getConfiguration(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function hasRevisionProperty($key) { return array_key_exists($key, $this->properties); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function getURI() { return '/'.$this->getMonogram(); } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function canReviewerForceAccept( PhabricatorUser $viewer, DifferentialReviewer $reviewer) { if (!$reviewer->isPackage()) { return false; } $map = $this->getReviewerForceAcceptMap($viewer); if (!$map) { return false; } if (isset($map[$reviewer->getReviewerPHID()])) { return true; } return false; } private function getReviewerForceAcceptMap(PhabricatorUser $viewer) { $fragment = $viewer->getCacheFragment(); if (!array_key_exists($fragment, $this->forceMap)) { $map = $this->newReviewerForceAcceptMap($viewer); $this->forceMap[$fragment] = $map; } return $this->forceMap[$fragment]; } private function newReviewerForceAcceptMap(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); if (!$diff) { return null; } $repository_phid = $diff->getRepositoryPHID(); if (!$repository_phid) { return null; } $paths = array(); try { $changesets = $diff->getChangesets(); } catch (Exception $ex) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($viewer) ->withDiffs(array($diff)) ->execute(); } foreach ($changesets as $changeset) { $paths[] = $changeset->getOwnersFilename(); } if (!$paths) { return null; } $reviewer_phids = array(); foreach ($this->getReviewers() as $reviewer) { if (!$reviewer->isPackage()) { continue; } $reviewer_phids[] = $reviewer->getReviewerPHID(); } if (!$reviewer_phids) { return null; } // Load all the reviewing packages which have control over some of the // paths in the change. These are packages which the actor may be able // to force-accept on behalf of. $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withPHIDs($reviewer_phids) ->withControl($repository_phid, $paths); $control_packages = $control_query->execute(); if (!$control_packages) { return null; } // Load all the packages which have potential control over some of the // paths in the change and are owned by the actor. These are packages // which the actor may be able to use their authority over to gain the // ability to force-accept for other packages. This query doesn't apply // dominion rules yet, and we'll bypass those rules later on. $authority_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->withControl($repository_phid, $paths); $authority_packages = $authority_query->execute(); if (!$authority_packages) { return null; } $authority_packages = mpull($authority_packages, null, 'getPHID'); // Build a map from each path in the revision to the reviewer packages // which control it. $control_map = array(); foreach ($paths as $path) { $control_packages = $control_query->getControllingPackagesForPath( $repository_phid, $path); // Remove packages which the viewer has authority over. We don't need // to check these for force-accept because they can just accept them // normally. $control_packages = mpull($control_packages, null, 'getPHID'); foreach ($control_packages as $phid => $control_package) { if (isset($authority_packages[$phid])) { unset($control_packages[$phid]); } } if (!$control_packages) { continue; } $control_map[$path] = $control_packages; } if (!$control_map) { return null; } // From here on out, we only care about paths which we have at least one // controlling package for. $paths = array_keys($control_map); // Now, build a map from each path to the packages which would control it // if there were no dominion rules. $authority_map = array(); foreach ($paths as $path) { $authority_packages = $authority_query->getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = true); $authority_map[$path] = mpull($authority_packages, null, 'getPHID'); } // For each path, find the most general package that the viewer has // authority over. For example, we'll prefer a package that owns "/" to a // package that owns "/src/". $force_map = array(); foreach ($authority_map as $path => $package_map) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); // Find the package that we have authority over which has the most // general match for this path. $best_match = null; $best_package = null; foreach ($package_map as $package_phid => $package) { $package_paths = $package->getPathsForRepository($repository_phid); foreach ($package_paths as $package_path) { // NOTE: A strength of 0 means "no match". A strength of 1 means // that we matched "/", so we can not possibly find another stronger // match. $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength < $best_match || !$best_package) { $best_match = $strength; $best_package = $package; if ($strength == 1) { break 2; } } } } if ($best_package) { $force_map[$path] = array( 'strength' => $best_match, 'package' => $best_package, ); } } // For each path which the viewer owns a package for, find other packages // which that authority can be used to force-accept. Once we find a way to // force-accept a package, we don't need to keep looking. $has_control = array(); foreach ($force_map as $path => $spec) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $authority_strength = $spec['strength']; $control_packages = $control_map[$path]; foreach ($control_packages as $control_phid => $control_package) { if (isset($has_control[$control_phid])) { continue; } $control_paths = $control_package->getPathsForRepository( $repository_phid); foreach ($control_paths as $control_path) { $strength = $control_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength > $authority_strength) { $authority = $spec['package']; $has_control[$control_phid] = array( 'authority' => $authority, 'phid' => $authority->getPHID(), ); break; } } } } // Return a map from packages which may be force accepted to the packages // which permit that forced acceptance. return ipull($has_control, 'phid'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $repository_phid = $this->getRepositoryPHID(); $repository = $this->getRepository(); // Try to use the object if we have it, since it will save us some // data fetching later on. In some cases, we might not have it. $repository_ref = nonempty($repository, $repository_phid); if ($repository_ref) { $extended[] = array( $repository_ref, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewers() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewers(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); $this->reviewerStatus = $reviewers; return $this; } public function hasAttachedReviewers() { return ($this->reviewerStatus !== self::ATTACHABLE); } public function getReviewerPHIDs() { $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); } public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewers(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getReviewerStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } return $value; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function setModernRevisionStatus($status) { return $this->setStatus($status); } public function getModernRevisionStatus() { return $this->getStatus(); } public function getLegacyRevisionStatus() { return $this->getStatusObject()->getLegacyKey(); } public function isClosed() { return $this->getStatusObject()->isClosedStatus(); } public function isAbandoned() { return $this->getStatusObject()->isAbandoned(); } public function isAccepted() { return $this->getStatusObject()->isAccepted(); } public function isNeedsReview() { return $this->getStatusObject()->isNeedsReview(); } public function isNeedsRevision() { return $this->getStatusObject()->isNeedsRevision(); } public function isChangePlanned() { return $this->getStatusObject()->isChangePlanned(); } public function isPublished() { return $this->getStatusObject()->isPublished(); } public function isDraft() { return $this->getStatusObject()->isDraft(); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function getStatusIconColor() { return $this->getStatusObject()->getIconColor(); } public function getStatusTagColor() { return $this->getStatusObject()->getTagColor(); } public function getStatusObject() { $status = $this->getStatus(); return DifferentialRevisionStatus::newForStatus($status); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } public function getHoldAsDraft() { return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false); } public function setHoldAsDraft($hold) { return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold); } public function getShouldBroadcast() { return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true); } public function setShouldBroadcast($should_broadcast) { return $this->setProperty( self::PROPERTY_SHOULD_BROADCAST, $should_broadcast); } public function setAddedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_ADDED, $count); } public function getAddedLineCount() { return $this->getProperty(self::PROPERTY_LINES_ADDED); } public function setRemovedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count); } public function getRemovedLineCount() { return $this->getProperty(self::PROPERTY_LINES_REMOVED); } public function hasLineCounts() { // This data was not populated on older revisions, so it may not be // present on all revisions. return isset($this->properties[self::PROPERTY_LINES_ADDED]); } public function getRevisionScaleGlyphs() { $add = $this->getAddedLineCount(); $rem = $this->getRemovedLineCount(); $all = ($add + $rem); if (!$all) { return ' '; } $map = array( 20 => 2, 50 => 3, 150 => 4, 375 => 5, 1000 => 6, 2500 => 7, ); $n = 1; foreach ($map as $size => $count) { if ($size <= $all) { $n = $count; } else { break; } } $add_n = (int)ceil(($add / $all) * $n); $rem_n = (int)ceil(($rem / $all) * $n); while ($add_n + $rem_n > $n) { if ($add_n > 1) { $add_n--; } else { $rem_n--; } } return str_repeat('+', $add_n). str_repeat('-', $rem_n). str_repeat(' ', (7 - $n)); } public function getBuildableStatus($phid) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } return idx($buildable, 'status'); } public function setBuildableStatus($phid, $status) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } $buildable['status'] = $status; $buildables[$phid] = $buildable; return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables); } public function newBuildableStatus(PhabricatorUser $viewer, $phid) { // For Differential, we're ignoring autobuilds (local lint and unit) // when computing build status. Differential only cares about remote // builds when making publishing and undrafting decisions. $builds = $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, array($phid)); return $this->newBuildableStatusForBuilds($builds); } public function newBuildableStatusForBuilds(array $builds) { // If we have nothing but passing builds, the buildable passes. if (!$builds) { return HarbormasterBuildableStatus::STATUS_PASSED; } // If we have any completed, non-passing builds, the buildable fails. foreach ($builds as $build) { if ($build->isComplete()) { return HarbormasterBuildableStatus::STATUS_FAILED; } } // Otherwise, we're still waiting for the build to pass or fail. return null; } public function loadImpactfulBuilds(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); // NOTE: We can't use `withContainerPHIDs()` here because the container // update in Harbormaster is not synchronous. $buildables = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($diff->getPHID())) ->withManualBuildables(false) ->execute(); if (!$buildables) { return array(); } return $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, mpull($buildables, 'getPHID')); } private function loadImpactfulBuildsForBuildablePHIDs( PhabricatorUser $viewer, array $phids) { - return id(new HarbormasterBuildQuery()) + $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs($phids) ->withAutobuilds(false) ->withBuildStatuses( array( HarbormasterBuildStatus::STATUS_INACTIVE, HarbormasterBuildStatus::STATUS_PENDING, HarbormasterBuildStatus::STATUS_BUILDING, HarbormasterBuildStatus::STATUS_FAILED, HarbormasterBuildStatus::STATUS_ABORTED, HarbormasterBuildStatus::STATUS_ERROR, HarbormasterBuildStatus::STATUS_PAUSED, HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); + + // Filter builds based on the "Hold Drafts" behavior of their associated + // build plans. + + $hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts); + + $key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER; + $key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING; + + foreach ($builds as $key => $build) { + $plan = $build->getBuildPlan(); + $hold_key = $behavior->getPlanOption($plan)->getKey(); + + $hold_never = ($hold_key === $key_never); + $hold_building = ($hold_key === $key_building); + + // If the build "Never" holds drafts from promoting, we don't care what + // the status is. + if ($hold_never) { + unset($builds[$key]); + continue; + } + + // If the build holds drafts from promoting "While Building", we only + // care about the status until it completes. + if ($hold_building) { + if ($build->isComplete()) { + unset($builds[$key]); + continue; + } + } + } + + return $builds; } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterContainerPHID(); } public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } public function newBuildableEngine() { return new DifferentialBuildableEngine(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewers(true) ->executeOne() ->getReviewers(); } else { $reviewers = $this->getReviewers(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() !== $phid) { continue; } if ($reviewer->isResigned()) { continue; } return true; } return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($engine->getViewer()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); // we have to do paths a little differently as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DifferentialRevisionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DifferentialRevisionFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The revision title.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about revision status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid?') ->setDescription(pht('Revision repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('diffPHID') ->setType('phid') ->setDescription(pht('Active diff PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('summary') ->setType('string') ->setDescription(pht('Revision summary.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('testPlan') ->setType('string') ->setDescription(pht('Revision test plan.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isDraft') ->setType('bool') ->setDescription( pht( 'True if this revision is in any draft state, and thus not '. 'notifying reviewers and subscribers about changes.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('holdAsDraft') ->setType('bool') ->setDescription( pht( 'True if this revision is being held as a draft. It will not be '. 'automatically submitted for review even if tests pass.')), ); } public function getFieldValuesForConduit() { $status = $this->getStatusObject(); $status_info = array( 'value' => $status->getKey(), 'name' => $status->getDisplayName(), 'closed' => $status->isClosedStatus(), 'color.ansi' => $status->getANSIColor(), ); return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'status' => $status_info, 'repositoryPHID' => $this->getRepositoryPHID(), 'diffPHID' => $this->getActiveDiffPHID(), 'summary' => $this->getSummary(), 'testPlan' => $this->getTestPlan(), 'isDraft' => !$this->getShouldBroadcast(), 'holdAsDraft' => (bool)$this->getHoldAsDraft(), ); } public function getConduitSearchAttachments() { return array( id(new DifferentialReviewersSearchEngineAttachment()) ->setAttachmentKey('reviewers'), ); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DifferentialRevisionDraftEngine(); } /* -( PhabricatorTimelineInterface )--------------------------------------- */ public function newTimelineEngine() { return new DifferentialRevisionTimelineEngine(); } } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 66c186a0d1..63fc263fcb 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -1,368 +1,373 @@ key = $key; return $this; } public function getKey() { return $this->key; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setEditInstructions($edit_instructions) { $this->editInstructions = $edit_instructions; return $this; } public function getEditInstructions() { return $this->editInstructions; } public function getOptionMap() { return mpull($this->options, 'getName', 'getKey'); } public function setOptions(array $options) { assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption'); $key_map = array(); $default = null; foreach ($options as $option) { $key = $option->getKey(); if (isset($key_map[$key])) { throw new Exception( pht( 'Multiple behavior options (for behavior "%s") have the same '. 'key ("%s"). Each option must have a unique key.', $this->getKey(), $key)); } $key_map[$key] = true; if ($option->getIsDefault()) { if ($default === null) { $default = $key; } else { throw new Exception( pht( 'Multiple behavior options (for behavior "%s") are marked as '. 'default options ("%s" and "%s"). Exactly one option must be '. 'marked as the default option.', $this->getKey(), $default, $key)); } } } if ($default === null) { throw new Exception( pht( 'No behavior option is marked as the default option (for '. 'behavior "%s"). Exactly one option must be marked as the '. 'default option.', $this->getKey())); } $this->options = mpull($options, null, 'getKey'); $this->defaultKey = $default; return $this; } public function getOptions() { return $this->options; } public function getPlanOption(HarbormasterBuildPlan $plan) { $behavior_key = $this->getKey(); $storage_key = self::getStorageKeyForBehaviorKey($behavior_key); $plan_value = $plan->getPlanProperty($storage_key); if (isset($this->options[$plan_value])) { return $this->options[$plan_value]; } return idx($this->options, $this->defaultKey); } public static function getTransactionMetadataKey() { return 'behavior-key'; } public static function getStorageKeyForBehaviorKey($behavior_key) { return sprintf('behavior.%s', $behavior_key); } public static function getBehavior($key) { $behaviors = self::newPlanBehaviors(); if (!isset($behaviors[$key])) { throw new Exception( pht( 'No build plan behavior with key "%s" exists.', $key)); } return $behaviors[$key]; } public static function newPlanBehaviors() { $draft_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::DRAFTS_ALWAYS) ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( 'Revisions are not sent for review until the build completes, '. 'and are returned to the author for updates if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('building') + ->setKey(self::DRAFTS_IF_BUILDING) ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( 'Revisions are not sent for review until the build completes, '. 'but they will be sent for review even if it fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::DRAFTS_NEVER) ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( 'Revisions are sent for review regardless of the status of the '. 'build.')), ); $land_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('always') ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( '"arc land" warns if the build is still running or has '. 'failed.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('building') ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( '"arc land" warns if the build is still running, but ignores '. 'the build if it has failed.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('complete') ->setIcon('fa-dot-circle-o yellow') ->setName(pht('If Complete')) ->setDescription( pht( '"arc land" warns if the build has failed, but ignores the '. 'build if it is still running.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('never') ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( '"arc land" never warns that the build is still running or '. 'has failed.')), ); $aggregate_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('always') ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( 'The buildable waits for the build, and fails if the '. 'build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('building') ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( 'The buildable waits for the build, but does not fail '. 'if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('never') ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( 'The buildable does not wait for the build.')), ); $restart_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RESTARTABLE_ALWAYS) ->setIcon('fa-repeat green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht('The build may be restarted.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RESTARTABLE_NEVER) ->setIcon('fa-times red') ->setName(pht('Never')) ->setDescription( pht('The build may not be restarted.')), ); $run_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RUNNABLE_IF_EDITABLE) ->setIcon('fa-pencil green') ->setName(pht('If Editable')) ->setIsDefault(true) ->setDescription( pht('Only users who can edit the plan can run it manually.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RUNNABLE_IF_VIEWABLE) ->setIcon('fa-exclamation-triangle yellow') ->setName(pht('If Viewable')) ->setDescription( pht( 'Any user who can view the plan can run it manually.')), ); $behaviors = array( id(new self()) - ->setKey('hold-drafts') + ->setKey(self::BEHAVIOR_DRAFTS) ->setName(pht('Hold Drafts')) ->setEditInstructions( pht( 'When users create revisions in Differential, the default '. 'behavior is to hold them in the "Draft" state until all builds '. 'pass. Once builds pass, the revisions promote and are sent for '. 'review, which notifies reviewers.'. "\n\n". 'The general intent of this workflow is to make sure reviewers '. 'are only spending time on review once changes survive automated '. 'tests. If a change does not pass tests, it usually is not '. 'really ready for review.'. "\n\n". 'If you want to promote revisions out of "Draft" before builds '. 'pass, or promote revisions even when builds fail, you can '. 'change the promotion behavior. This may be useful if you have '. 'very long-running builds, or some builds which are not very '. 'important.'. "\n\n". 'Users may always use "Request Review" to promote a "Draft" '. 'revision, even if builds have failed or are still in progress.')) ->setOptions($draft_options), id(new self()) ->setKey('arc-land') ->setName(pht('Warn When Landing')) ->setEditInstructions( pht( 'When a user attempts to `arc land` a revision and that revision '. 'has ongoing or failed builds, the default behavior of `arc` is '. 'to warn them about those builds and give them a chance to '. 'reconsider: they may want to wait for ongoing builds to '. 'complete, or fix failed builds before landing the change.'. "\n\n". 'If you do not want to warn users about this build, you can '. 'change the warning behavior. This may be useful if the build '. 'takes a long time to run (so you do not expect users to wait '. 'for it) or the outcome is not important.'. "\n\n". 'This warning is only advisory. Users may always elect to ignore '. 'this warning and continue, even if builds have failed.')) ->setOptions($land_options), id(new self()) ->setKey('buildable') ->setEditInstructions( pht( 'The overall state of a buildable (like a commit or revision) is '. 'normally the aggregation of the individual states of all builds '. 'that have run against it.'. "\n\n". 'Buildables are "building" until all builds pass (which changes '. 'them to "pass"), or any build fails (which changes them to '. '"fail").'. "\n\n". 'You can change this behavior if you do not want to wait for this '. 'build, or do not care if it fails.')) ->setName(pht('Affects Buildable')) ->setOptions($aggregate_options), id(new self()) ->setKey(self::BEHAVIOR_RESTARTABLE) ->setEditInstructions( pht( 'Usually, builds may be restarted. This may be useful if you '. 'suspect a build has failed for environmental or circumstantial '. 'reasons unrelated to the actual code, and want to give it '. 'another chance at glory.'. "\n\n". 'If you want to prevent a build from being restarted, you can '. 'change the behavior here. This may be useful to prevent '. 'accidents where a build with a dangerous side effect (like '. 'deployment) is restarted improperly.')) ->setName(pht('Restartable')) ->setOptions($restart_options), id(new self()) ->setKey(self::BEHAVIOR_RUNNABLE) ->setEditInstructions( pht( 'To run a build manually, you normally must have permission to '. 'edit the related build plan. If you would prefer that anyone who '. 'can see the build plan be able to run and restart the build, you '. 'can change the behavior here.'. "\n\n". 'Note that this controls access to all build management actions: '. '"Run Plan Manually", "Restart", "Abort", "Pause", and "Resume".'. "\n\n". 'WARNING: This may be unsafe, particularly if the build has '. 'side effects like deployment.'. "\n\n". 'If you weaken this policy, an attacker with control of an '. 'account that has "Can View" permission but not "Can Edit" '. 'permission can manually run this build against any old version '. 'of the code, including versions with known security issues.'. "\n\n". 'If running the build has a side effect like deploying code, '. 'they can force deployment of a vulnerable version and then '. 'escalate into an attack against the deployed service.')) ->setName(pht('Runnable')) ->setOptions($run_options), ); return mpull($behaviors, null, 'getKey'); } }