diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index 95429e051d..dc2ebb2c7a 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -1,248 +1,250 @@ getViewer(); return DifferentialRevision::initializeNewRevision($viewer); } protected function newObjectQuery() { return id(new DifferentialRevisionQuery()) ->needActiveDiffs(true) ->needReviewerStatus(true) ->needReviewerAuthority(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Revision'); } protected function getObjectEditTitleText($object) { $monogram = $object->getMonogram(); $title = $object->getTitle(); $diff = $this->getDiff(); if ($diff) { return pht('Update Revision %s: %s', $monogram, $title); } else { return pht('Edit Revision %s: %s', $monogram, $title); } } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Revision'); } protected function getObjectName() { return pht('Revision'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('revision/edit/'); } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function getDiff() { return $this->diff; } protected function newCommentActionGroups() { return array( id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_REVIEW) ->setLabel(pht('Review Actions')), id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_REVISION) ->setLabel(pht('Revision Actions')), ); } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $plan_required = PhabricatorEnv::getEnvConfig( 'differential.require-test-plan-field'); $plan_enabled = $this->isCustomFieldEnabled( $object, 'differential:test-plan'); $diff = $this->getDiff(); if ($diff) { $diff_phid = $diff->getPHID(); } else { $diff_phid = null; } $is_create = $this->getIsCreate(); $is_update = ($diff && !$is_create); $fields = array(); $fields[] = id(new PhabricatorHandlesEditField()) ->setKey(self::KEY_UPDATE) ->setLabel(pht('Update Diff')) ->setDescription(pht('New diff to create or update the revision with.')) ->setConduitDescription(pht('Create or update a revision with a diff.')) ->setConduitTypeDescription(pht('PHID of the diff.')) ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) ->setHandleParameterType(new AphrontPHIDListHTTPParameterType()) ->setSingleValue($diff_phid) ->setIsConduitOnly(!$diff) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsInvisible(true) ->setIsLockable(false); if ($is_update) { $fields[] = id(new PhabricatorInstructionsEditField()) ->setKey('update.help') ->setValue(pht('Describe the updates you have made to the diff.')); $fields[] = id(new PhabricatorCommentEditField()) ->setKey('update.comment') ->setLabel(pht('Comment')) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->setIsWebOnly(true) ->setDescription(pht('Comments providing context for the update.')); $fields[] = id(new PhabricatorSubmitEditField()) ->setKey('update.submit') ->setValue($this->getObjectEditButtonText($object)); $fields[] = id(new PhabricatorDividerEditField()) ->setKey('update.note'); } $fields[] = id(new PhabricatorTextEditField()) ->setKey(DifferentialRevisionTitleTransaction::EDITKEY) ->setLabel(pht('Title')) ->setIsRequired(true) ->setTransactionType( DifferentialRevisionTitleTransaction::TRANSACTIONTYPE) ->setDescription(pht('The title of the revision.')) ->setConduitDescription(pht('Retitle the revision.')) ->setConduitTypeDescription(pht('New revision title.')) ->setValue($object->getTitle()); $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey(DifferentialRevisionSummaryTransaction::EDITKEY) ->setLabel(pht('Summary')) ->setTransactionType( DifferentialRevisionSummaryTransaction::TRANSACTIONTYPE) ->setDescription(pht('The summary of the revision.')) ->setConduitDescription(pht('Change the revision summary.')) ->setConduitTypeDescription(pht('New revision summary.')) ->setValue($object->getSummary()); if ($plan_enabled) { $fields[] = id(new PhabricatorRemarkupEditField()) ->setKey(DifferentialRevisionTestPlanTransaction::EDITKEY) ->setLabel(pht('Test Plan')) ->setIsRequired($plan_required) ->setTransactionType( DifferentialRevisionTestPlanTransaction::TRANSACTIONTYPE) ->setDescription( pht('Actions performed to verify the behavior of the change.')) ->setConduitDescription(pht('Update the revision test plan.')) ->setConduitTypeDescription(pht('New test plan.')) ->setValue($object->getTestPlan()); } $fields[] = id(new PhabricatorDatasourceEditField()) ->setKey(DifferentialRevisionReviewersTransaction::EDITKEY) ->setLabel(pht('Reviewers')) ->setDatasource(new DifferentialReviewerDatasource()) ->setUseEdgeTransactions(true) ->setTransactionType( DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE) ->setCommentActionLabel(pht('Change Reviewers')) ->setDescription(pht('Reviewers for this revision.')) ->setConduitDescription(pht('Change the reviewers for this revision.')) ->setConduitTypeDescription(pht('New reviewers.')) ->setValue($object->getReviewerPHIDsForEdit()); $fields[] = id(new PhabricatorDatasourceEditField()) ->setKey('repositoryPHID') ->setLabel(pht('Repository')) ->setDatasource(new DiffusionRepositoryDatasource()) ->setTransactionType( DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository the revision belongs to.')) ->setConduitDescription(pht('Change the repository for this revision.')) ->setConduitTypeDescription(pht('New repository.')) ->setSingleValue($object->getRepositoryPHID()); // This is a little flimsy, but allows "Maniphest Tasks: ..." to continue // working properly in commit messages until we fully sort out T5873. $fields[] = id(new PhabricatorHandlesEditField()) ->setKey('tasks') ->setUseEdgeTransactions(true) ->setIsConduitOnly(true) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', DifferentialRevisionHasTaskEdgeType::EDGECONST) ->setDescription(pht('Tasks associated with this revision.')) ->setConduitDescription(pht('Change associated tasks.')) ->setConduitTypeDescription(pht('List of tasks.')) ->setValue(array()); $actions = DifferentialRevisionActionTransaction::loadAllActions(); + $actions = msortv($actions, 'getRevisionActionOrderVector'); + foreach ($actions as $key => $action) { $fields[] = $action->newEditField($object, $viewer); } return $fields; } private function isCustomFieldEnabled(DifferentialRevision $revision, $key) { $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_VIEW); $fields = $field_list->getFields(); return isset($fields[$key]); } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 9feb1bf228..ab8b8ddc3a 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,729 +1,734 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRelationships(array()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewerStatus(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } 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', 'originalTitle' => '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 setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } 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 loadRelationships() { if (!$this->getID()) { $this->relationships = array(); return; } $data = array(); $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorObjectHasSubscriberEdgeType::EDGECONST); $subscriber_phids = array_reverse($subscriber_phids); foreach ($subscriber_phids as $phid) { $data[] = array( 'relation' => self::RELATION_SUBSCRIBED, 'objectPHID' => $phid, 'reasonPHID' => null, ); } $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), DifferentialRevisionHasReviewerEdgeType::EDGECONST); $reviewer_phids = array_reverse($reviewer_phids); foreach ($reviewer_phids as $phid) { $data[] = array( 'relation' => self::RELATION_REVIEWER, 'objectPHID' => $phid, 'reasonPHID' => null, ); } return $this->attachRelationships($data); } public function attachRelationships(array $relationships) { $this->relationships = igroup($relationships, 'relation'); return $this; } public function getReviewers() { return $this->getRelatedPHIDs(self::RELATION_REVIEWER); } public function getCCPHIDs() { return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); } private function getRelatedPHIDs($relation) { $this->assertAttached($this->relationships); return ipull($this->getRawRelations($relation), 'objectPHID'); } public function getRawRelations($relation) { return idx($this->relationships, $relation, array()); } public function getPrimaryReviewer() { $reviewers = $this->getReviewers(); $last = $this->lastReviewerPHID; if (!$last || !in_array($last, $reviewers)) { return head($this->getReviewers()); } return $last; } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } /* -( 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("A revision's reviewers can always view it."); $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 getReviewerStatus() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewerStatus(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewerProxy'); $this->reviewerStatus = $reviewers; return $this; } public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewerStatus(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getStatus() == $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 isClosed() { return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); } public function isAbandoned() { $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; return ($this->getStatus() == $status_abandoned); } + public function isAccepted() { + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; + return ($this->getStatus() == $status_accepted); + } + public function getStatusIcon() { $map = array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW => 'fa-code grey', ArcanistDifferentialRevisionStatus::NEEDS_REVISION => 'fa-refresh red', ArcanistDifferentialRevisionStatus::CHANGES_PLANNED => 'fa-headphones red', ArcanistDifferentialRevisionStatus::ACCEPTED => 'fa-check green', ArcanistDifferentialRevisionStatus::CLOSED => 'fa-check-square-o black', ArcanistDifferentialRevisionStatus::ABANDONED => 'fa-plane black', ); return idx($map, $this->getStatus()); } public function getStatusDisplayName() { $status = $this->getStatus(); return ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $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 getDrafts(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getPHID()); } public function attachDrafts(PhabricatorUser $viewer, array $drafts) { $this->drafts[$viewer->getPHID()] = $drafts; return $this; } /* -( 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(); } /* -( 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())) ->needReviewerStatus(true) ->executeOne() ->getReviewerStatus(); } else { $reviewers = $this->getReviewerStatus(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() == $phid) { 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 getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $viewer = $request->getViewer(); $render_data = $timeline->getRenderData(); $left = $request->getInt('left', idx($render_data, 'left')); $right = $request->getInt('right', idx($render_data, 'right')); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($left, $right)) ->execute(); $diffs = mpull($diffs, null, 'getID'); $left_diff = $diffs[$left]; $right_diff = $diffs[$right]; $old_ids = $request->getStr('old', idx($render_data, 'old')); $new_ids = $request->getStr('new', idx($render_data, 'new')); $old_ids = array_filter(explode(',', $old_ids)); $new_ids = array_filter(explode(',', $new_ids)); $type_inline = DifferentialTransaction::TYPE_INLINE; $changeset_ids = array_merge($old_ids, $new_ids); $inlines = array(); foreach ($timeline->getTransactions() as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction->getComment(); $changeset_ids[] = $xaction->getComment()->getChangesetID(); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withIDs($changeset_ids) ->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } foreach ($inlines as $key => $inline) { $inlines[$key] = DifferentialInlineComment::newFromModernComment( $inline); } $query = id(new DifferentialInlineCommentQuery()) ->needHidden(true) ->setViewer($viewer); // NOTE: This is a bit sketchy: this method adjusts the inlines as a // side effect, which means it will ultimately adjust the transaction // comments and affect timeline rendering. $query->adjustInlinesForChangesets( $inlines, array_select_keys($changesets, $old_ids), array_select_keys($changesets, $new_ids), $this); return $timeline ->setChangesets($changesets) ->setRevision($this) ->setLeftDiff($left_diff) ->setRightDiff($right_diff); } /* -( 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 differentally 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(); } /* -( 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.')), ); } public function getFieldValuesForConduit() { return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php index 473a47f514..e4fc7f1a5f 100644 --- a/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionAbandonTransaction.php @@ -1,67 +1,71 @@ isAbandoned(); } public function applyInternalEffects($object, $value) { $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not abandon this revision because it has already been '. 'closed. Only open revisions can be abandoned.')); } $config_key = 'differential.always-allow-abandon'; if (!PhabricatorEnv::getEnvConfig($config_key)) { if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not abandon this revision because you are not the '. 'author. You can only abandon revisions you own. You can change '. 'this behavior by adjusting the "%s" setting in Config.', $config_key)); } } } public function getTitle() { return pht( '%s abandoned this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s abandoned %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php index 61178a58cd..c99c663c8d 100644 --- a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php @@ -1,77 +1,81 @@ getActor(); return $this->isViewerAcceptingReviewer($object, $actor); } public function applyExternalEffects($object, $value) { $status = DifferentialReviewerStatus::STATUS_ACCEPTED; $actor = $this->getActor(); $this->applyReviewerEffect($object, $actor, $value, $status); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not accept this revision because it has already been '. 'closed. Only open revisions can be accepted.')); } $config_key = 'differential.allow-self-accept'; if (!PhabricatorEnv::getEnvConfig($config_key)) { if ($this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not accept this revision because you are the revision '. 'author. You can only accept revisions you do not own. You can '. 'change this behavior by adjusting the "%s" setting in Config.', $config_key)); } } if ($this->isViewerAcceptingReviewer($object, $viewer)) { throw new Exception( pht( 'You can not accept this revision because you have already '. 'accepted it.')); } } public function getTitle() { return pht( '%s accepted this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s accepted %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index f61ed8fffc..e495fff8e7 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -1,95 +1,104 @@ getPhobjectClassConstant('ACTIONKEY', 32); } public function isActionAvailable($object, PhabricatorUser $viewer) { try { $this->validateAction($object, $viewer); return true; } catch (Exception $ex) { return false; } } abstract protected function validateAction($object, PhabricatorUser $viewer); abstract protected function getRevisionActionLabel(); + protected function getRevisionActionOrder() { + return 1000; + } + + public function getRevisionActionOrderVector() { + return id(new PhutilSortVector()) + ->addInt($this->getRevisionActionOrder()); + } + protected function getRevisionActionGroupKey() { return DifferentialRevisionEditEngine::ACTIONGROUP_REVISION; } protected function getRevisionActionDescription() { return null; } public static function loadAllActions() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getRevisionActionKey') ->execute(); } protected function isViewerRevisionAuthor( DifferentialRevision $revision, PhabricatorUser $viewer) { if (!$viewer->getPHID()) { return false; } return ($viewer->getPHID() === $revision->getAuthorPHID()); } public function newEditField( DifferentialRevision $revision, PhabricatorUser $viewer) { $field = id(new PhabricatorApplyEditField()) ->setKey($this->getRevisionActionKey()) ->setTransactionType($this->getTransactionTypeConstant()) ->setValue(true); if ($this->isActionAvailable($revision, $viewer)) { $label = $this->getRevisionActionLabel(); if ($label !== null) { $field->setCommentActionLabel($label); $description = $this->getRevisionActionDescription(); $field->setActionDescription($description); $group_key = $this->getRevisionActionGroupKey(); $field->setCommentActionGroupKey($group_key); } } return $field; } public function validateTransactions($object, array $xactions) { $errors = array(); $actor = $this->getActor(); $action_exception = null; try { $this->validateAction($object, $actor); } catch (Exception $ex) { $action_exception = $ex; } foreach ($xactions as $xaction) { if ($action_exception) { $errors[] = $this->newInvalidError( $action_exception->getMessage(), $xaction); } } return $errors; } } diff --git a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php index ebbd0069a1..c2ba6c1bb8 100644 --- a/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionCloseTransaction.php @@ -1,78 +1,89 @@ isClosed(); } public function applyInternalEffects($object, $value) { $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $old_status = $object->getStatus(); $object->setStatus($status_closed); $was_accepted = ($old_status == $status_accepted); $object->setProperty( DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED, $was_accepted); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not close this revision because it has already been '. 'closed. Only open revisions can be closed.')); } + if (!$object->isAccepted()) { + throw new Exception( + pht( + 'You can not close this revision because it has not been accepted. '. + 'Revisions must be accepted before they can be closed.')); + } + $config_key = 'differential.always-allow-close'; if (!PhabricatorEnv::getEnvConfig($config_key)) { if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not close this revision because you are not the '. 'author. You can only close revisions you own. You can change '. 'this behavior by adjusting the "%s" setting in Config.', $config_key)); } } } public function getTitle() { return pht( '%s closed this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s closed %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php index 7690b79fff..fac7ec8b0b 100644 --- a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php @@ -1,67 +1,71 @@ getAuthorPHID(); } public function generateNewValue($object, $value) { $actor = $this->getActor(); return $actor->getPHID(); } public function applyInternalEffects($object, $value) { $object->setAuthorPHID($value); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not commandeer this revision because it has already '. 'been closed. You can only commandeer open revisions.')); } if ($this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not commandeer this revision because you are already '. 'the author.')); } } public function getTitle() { return pht( '%s commandeered this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s commandeered %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php index e08e3e7277..d33992d36e 100644 --- a/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php @@ -1,75 +1,79 @@ getStatus() == $status_planned); } public function applyInternalEffects($object, $value) { $status_planned = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $object->setStatus($status_planned); } protected function validateAction($object, PhabricatorUser $viewer) { $status_planned = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; if ($object->getStatus() == $status_planned) { throw new Exception( pht( 'You can not request review of this revision because this '. 'revision is already under review and the action would have '. 'no effect.')); } if ($object->isClosed()) { throw new Exception( pht( 'You can not plan changes to this this revision because it has '. 'already been closed.')); } if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not plan changes to this revision because you do not '. 'own it. Only the author of a revision can plan changes to it.')); } } public function getTitle() { return pht( '%s planned changes to this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s planned changes to %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php index f583f089ef..ce8367f7c3 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReclaimTransaction.php @@ -1,62 +1,66 @@ isAbandoned(); } public function applyInternalEffects($object, $value) { $object->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } protected function validateAction($object, PhabricatorUser $viewer) { if (!$object->isAbandoned()) { throw new Exception( pht( 'You can not reclaim this revision because it has not been '. 'abandoned. Only abandoned revisions can be reclaimed.')); } if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not reclaim this revision because you are not the '. 'revision author. You can only reclaim revisions you own.')); } } public function getTitle() { return pht( '%s reclaimed this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s reclaimed %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php index a47f420ec6..17ecca0e20 100644 --- a/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionRejectTransaction.php @@ -1,74 +1,78 @@ getActor(); return $this->isViewerRejectingReviewer($object, $actor); } public function applyExternalEffects($object, $value) { $status = DifferentialReviewerStatus::STATUS_REJECTED; $actor = $this->getActor(); $this->applyReviewerEffect($object, $actor, $value, $status); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not request changes to this revision because it has '. 'already been closed. You can only request changes to open '. 'revisions.')); } if ($this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not request changes to this revision because you are the '. 'revision author. You can only request changes to revisions you do '. 'not own.')); } if ($this->isViewerRejectingReviewer($object, $viewer)) { throw new Exception( pht( 'You can not request changes to this revision because you have '. 'already requested changes.')); } } public function getTitle() { return pht( '%s requested changes to this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s requested changes to %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php index f1e77bbb48..5024ab99fa 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReopenTransaction.php @@ -1,68 +1,72 @@ isClosed(); } public function applyInternalEffects($object, $value) { $object->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } protected function validateAction($object, PhabricatorUser $viewer) { // Note that we're testing for "Closed", exactly, not just any closed // status. $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; if ($object->getStatus() != $status_closed) { throw new Exception( pht( 'You can not reopen this revision because it is not closed. '. 'Only closed revisions can be reopened.')); } $config_key = 'differential.allow-reopen'; if (!PhabricatorEnv::getEnvConfig($config_key)) { throw new Exception( pht( 'You can not reopen this revision because configuration prevents '. 'any revision from being reopened. You can change this behavior '. 'by adjusting the "%s" setting in Config.', $config_key)); } } public function getTitle() { return pht( '%s reopened this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s reopened %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php index bd0320624f..cd1c14c66e 100644 --- a/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php @@ -1,70 +1,74 @@ getStatus() == $status_review); } public function applyInternalEffects($object, $value) { $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $object->setStatus($status_review); } protected function validateAction($object, PhabricatorUser $viewer) { $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; if ($object->getStatus() == $status_review) { throw new Exception( pht( 'You can not request review of this revision because this '. 'revision is already under review and the action would have '. 'no effect.')); } if ($object->isClosed()) { throw new Exception( pht( 'You can not request review of this revision because it has '. 'already been closed. You can only request review of open '. 'revisions.')); } if (!$this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( 'You can not request review of this revision because you are not '. 'the author of the revision.')); } } public function getTitle() { return pht( '%s requested review of this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s requested review of %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php index 05717b8946..f994c2f45e 100644 --- a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php @@ -1,66 +1,70 @@ getActor(); return !$this->isViewerAnyReviewer($object, $actor); } public function applyExternalEffects($object, $value) { $status = DifferentialReviewerStatus::STATUS_RESIGNED; $actor = $this->getActor(); $this->applyReviewerEffect($object, $actor, $value, $status); } protected function validateAction($object, PhabricatorUser $viewer) { if ($object->isClosed()) { throw new Exception( pht( 'You can not resign from this revision because it has already '. 'been closed. You can only resign from open revisions.')); } if (!$this->isViewerAnyReviewer($object, $viewer)) { throw new Exception( pht( 'You can not resign from this revision because you are not a '. 'reviewer. You can only resign from revisions where you are a '. 'reviewer.')); } } public function getTitle() { return pht( '%s resigned from this revision.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s resigned from %s.', $this->renderAuthor(), $this->renderObject()); } }