diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 9d742740d1..089a801220 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -1,242 +1,244 @@ getViewer(); $this->requireApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $id = $request->getURIData('id'); if ($id) { $step = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$step) { return new Aphront404Response(); } $plan = $step->getBuildPlan(); $is_new = false; } else { $plan_id = $request->getURIData('plan'); $class = $request->getURIData('class'); $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($plan_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $impl = HarbormasterBuildStepImplementation::getImplementation($class); if (!$impl) { return new Aphront404Response(); } if ($impl->shouldRequireAutotargeting()) { // No manual creation of autotarget steps. return new Aphront404Response(); } $step = HarbormasterBuildStep::initializeNewStep($viewer) ->setBuildPlanPHID($plan->getPHID()) ->setClassName($class); $is_new = true; } $plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); $implementation = $step->getStepImplementation(); $field_list = PhabricatorCustomField::getObjectFields( $step, PhabricatorCustomField::ROLE_EDIT); $field_list ->setViewer($viewer) ->readFieldsFromStorage($step); $e_name = true; $v_name = $step->getName(); - $e_description = true; + $e_description = null; $v_description = $step->getDescription(); - $e_depends_on = true; + $e_depends_on = null; $v_depends_on = $step->getDetail('dependsOn', array()); $errors = array(); $validation_exception = null; if ($request->isFormPost()) { $e_name = null; $v_name = $request->getStr('name'); - $e_description = null; $v_description = $request->getStr('description'); - $e_depends_on = null; $v_depends_on = $request->getArr('dependsOn'); $xactions = $field_list->buildFieldTransactionsFromRequest( new HarbormasterBuildStepTransaction(), $request); $editor = id(new HarbormasterBuildStepEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $name_xaction = id(new HarbormasterBuildStepTransaction()) ->setTransactionType(HarbormasterBuildStepTransaction::TYPE_NAME) ->setNewValue($v_name); array_unshift($xactions, $name_xaction); $depends_on_xaction = id(new HarbormasterBuildStepTransaction()) ->setTransactionType( HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON) ->setNewValue($v_depends_on); array_unshift($xactions, $depends_on_xaction); $description_xaction = id(new HarbormasterBuildStepTransaction()) ->setTransactionType( HarbormasterBuildStepTransaction::TYPE_DESCRIPTION) ->setNewValue($v_description); array_unshift($xactions, $description_xaction); if ($is_new) { // When creating a new step, make sure we have a create transaction // so we'll apply the transactions even if the step has no // configurable options. $create_xaction = id(new HarbormasterBuildStepTransaction()) ->setTransactionType(HarbormasterBuildStepTransaction::TYPE_CREATE); array_unshift($xactions, $create_xaction); } try { $editor->applyTransactions($step, $xactions); return id(new AphrontRedirectResponse())->setURI($plan_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setError($e_name) ->setValue($v_name)); + $form->appendChild(id(new AphrontFormDividerControl())); + + $field_list->appendFieldsToForm($form); + + $form->appendChild(id(new AphrontFormDividerControl())); + $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(id(new HarbormasterBuildDependencyDatasource()) ->setParameters(array( 'planPHID' => $plan->getPHID(), 'stepPHID' => $is_new ? null : $step->getPHID(), ))) ->setName('dependsOn') ->setLabel(pht('Depends On')) ->setError($e_depends_on) ->setValue($v_depends_on)); - $field_list->appendFieldsToForm($form); - $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('description') ->setLabel(pht('Description')) ->setError($e_description) ->setValue($v_description)); if ($is_new) { $submit = pht('Create Build Step'); $header = pht('New Step: %s', $implementation->getName()); $crumb = pht('Add Step'); } else { $submit = pht('Save Build Step'); $header = pht('Edit Step: %s', $implementation->getName()); $crumb = pht('Edit Step'); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue($submit) ->addCancelButton($plan_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $plan->getID(); $crumbs->addTextCrumb(pht('Plan %d', $id), $plan_uri); $crumbs->addTextCrumb($crumb); $variables = $this->renderBuildVariablesTable(); if ($is_new) { $xaction_view = null; $timeline = null; } else { $timeline = $this->buildTransactionTimeline( $step, new HarbormasterBuildStepTransactionQuery()); $timeline->setShouldTerminate(true); } return $this->buildApplicationPage( array( $crumbs, $box, $variables, $timeline, ), array( 'title' => $implementation->getName(), )); } private function renderBuildVariablesTable() { $viewer = $this->getRequest()->getUser(); $variables = HarbormasterBuild::getAvailableBuildVariables(); ksort($variables); $rows = array(); $rows[] = pht( 'The following variables can be used in most fields. '. 'To reference a variable, use `%s` in a field.', '${name}'); $rows[] = pht('| Variable | Description |'); $rows[] = '|---|---|'; foreach ($variables as $name => $description) { $rows[] = '| `'.$name.'` | '.$description.' |'; } $rows = implode("\n", $rows); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions($rows); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Build Variables')) ->appendChild($form); } } diff --git a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php index 03703a48f4..0ad8f960cf 100644 --- a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php +++ b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php @@ -1,75 +1,79 @@ getStepImplementation(); $specs = $impl->getFieldSpecifications(); if ($impl->supportsWaitForMessage()) { $specs['builtin.next-steps-header'] = array( 'type' => 'header', 'name' => pht('Next Steps'), ); $specs['builtin.wait-for-message'] = array( 'type' => 'select', 'name' => pht('When Complete'), 'instructions' => pht( 'After completing this build step Harbormaster can continue the '. 'build normally, or it can pause the build and wait for a message. '. 'If you are using this build step to trigger some work in an '. 'external system, you may want to have Phabricator wait for that '. 'system to perform the work and report results back.'. "\n\n". 'If you select **Continue Build Normally**, the build plan will '. 'proceed once this step finishes.'. "\n\n". 'If you select **Wait For Message**, the build plan will pause '. 'indefinitely once this step finishes. To resume the build, an '. 'external system must call `harbormaster.sendmessage` with the '. 'build target PHID, and either `"pass"` or `"fail"` to indicate '. 'the result for this step. After the result is recorded, the build '. 'plan will resume.'), 'options' => array( '' => pht('Continue Build Normally'), 'wait' => pht('Wait For Message'), ), ); } return PhabricatorStandardCustomField::buildStandardFields($this, $specs); } public function shouldUseStorage() { return false; } public function readValueFromObject(PhabricatorCustomFieldInterface $object) { $key = $this->getProxy()->getRawStandardFieldKey(); $this->setValueFromStorage($object->getDetail($key)); } public function applyApplicationTransactionInternalEffects( PhabricatorApplicationTransaction $xaction) { $object = $this->getObject(); $key = $this->getProxy()->getRawStandardFieldKey(); $this->setValueFromApplicationTransactions($xaction->getNewValue()); $value = $this->getValueForStorage(); $object->setDetail($key, $value); } public function applyApplicationTransactionExternalEffects( PhabricatorApplicationTransaction $xaction) { return; } + public function getBuildTargetFieldValue() { + return $this->getProxy()->getFieldValue(); + } + } diff --git a/src/applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php b/src/applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php index abc442cb20..4ff6ba799d 100644 --- a/src/applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php +++ b/src/applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php @@ -1,4 +1,8 @@ getSettings(); // TODO: We should probably have a separate temporary storage area for // execution stuff that doesn't step on configuration state? $lease_phid = $build_target->getDetail('exec.leasePHID'); if ($lease_phid) { $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withPHIDs(array($lease_phid)) ->executeOne(); if (!$lease) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Lease "%s" could not be loaded.', $lease_phid)); } } else { $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) ->getType(); $lease = id(new DrydockLease()) ->setResourceType($working_copy_type) ->setOwnerPHID($build_target->getPHID()); $map = $this->buildRepositoryMap($build_target); $lease->setAttribute('repositories.map', $map); $task_id = $this->getCurrentWorkerTaskID(); if ($task_id) { $lease->setAwakenTaskIDs(array($task_id)); } $lease->queueForActivation(); $build_target ->setDetail('exec.leasePHID', $lease->getPHID()) ->save(); } if ($lease->isActivating()) { // TODO: Smart backoff? throw new PhabricatorWorkerYieldException(15); } if (!$lease->isActive()) { // TODO: We could just forget about this lease and retry? throw new PhabricatorWorkerPermanentFailureException( pht( 'Lease "%s" never activated.', $lease->getPHID())); } $artifact = $build_target->createArtifact( $viewer, $settings['name'], HarbormasterWorkingCopyArtifact::ARTIFACTCONST, array( 'drydockLeasePHID' => $lease->getPHID(), )); } public function getArtifactOutputs() { return array( array( 'name' => pht('Working Copy'), 'key' => $this->getSetting('name'), 'type' => HarbormasterWorkingCopyArtifact::ARTIFACTCONST, ), ); } public function getFieldSpecifications() { return array( 'name' => array( 'name' => pht('Artifact Name'), 'type' => 'text', 'required' => true, ), + 'repositoryPHIDs' => array( + 'name' => pht('Also Clone'), + 'type' => 'datasource', + 'datasource.class' => 'DiffusionRepositoryDatasource', + ), ); } private function buildRepositoryMap(HarbormasterBuildTarget $build_target) { $viewer = PhabricatorUser::getOmnipotentUser(); $variables = $build_target->getVariables(); $repository_phid = idx($variables, 'repository.phid'); + $also_phids = $build_target->getFieldValue('repositoryPHIDs'); + + $all_phids = $also_phids; + $all_phids[] = $repository_phid; - $repository = id(new PhabricatorRepositoryQuery()) + $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) - ->withPHIDs(array($repository_phid)) - ->executeOne(); - if (!$repository) { - throw new PhabricatorWorkerPermanentFailureException( - pht( - 'Unable to load repository with PHID "%s".', - $repository_phid)); + ->withPHIDs($all_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + + foreach ($all_phids as $phid) { + if (empty($repositories[$phid])) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Unable to load repository with PHID "%s".', + $phid)); + } } $commit = idx($variables, 'repository.commit'); $map = array(); + foreach ($also_phids as $also_phid) { + $also_repo = $repositories[$also_phid]; + $map[$also_repo->getCloneName()] = array( + 'phid' => $also_repo->getPHID(), + 'branch' => 'master', + ); + } + + $repository = $repositories[$repository_phid]; + $directory = $repository->getCloneName(); $map[$directory] = array( 'phid' => $repository->getPHID(), 'commit' => $commit, 'default' => true, ); return $map; } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index fffa30a883..27655189e6 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,336 +1,358 @@ setName($build_step->getName()) ->setBuildPHID($build->getPHID()) ->setBuildStepPHID($build_step->getPHID()) ->setClassName($build_step->getClassName()) ->setDetails($build_step->getDetails()) ->setTargetStatus(self::STATUS_PENDING) ->setVariables($variables) ->setBuildGeneration($build->getBuildGeneration()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'variables' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'targetStatus' => 'text64', 'dateStarted' => 'epoch?', 'dateCompleted' => 'epoch?', 'buildGeneration' => 'uint32', // T6203/NULLABILITY // This should not be nullable. 'name' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_build' => array( 'columns' => array('buildPHID', 'buildStepPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildTargetPHIDType::TYPECONST); } public function attachBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->assertAttached($this->build); } public function attachBuildStep(HarbormasterBuildStep $step = null) { $this->buildStep = $step; return $this; } public function getBuildStep() { return $this->assertAttached($this->buildStep); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getVariables() { return parent::getVariables() + $this->getBuildTargetVariables(); } public function getVariable($key, $default = null) { return idx($this->variables, $key, $default); } public function setVariable($key, $value) { $this->variables[$key] = $value; return $this; } public function getImplementation() { if ($this->implementation === null) { $obj = HarbormasterBuildStepImplementation::requireImplementation( $this->className); $obj->loadSettings($this); $this->implementation = $obj; } return $this->implementation; } public function isAutotarget() { try { return (bool)$this->getImplementation()->getBuildStepAutotargetPlanKey(); } catch (Exception $e) { return false; } } public function getName() { if (strlen($this->name) && !$this->isAutotarget()) { return $this->name; } try { return $this->getImplementation()->getName(); } catch (Exception $e) { return $this->getClassName(); } } private function getBuildTargetVariables() { return array( 'target.phid' => $this->getPHID(), ); } public function createArtifact( PhabricatorUser $actor, $artifact_key, $artifact_type, array $artifact_data) { $impl = HarbormasterArtifact::getArtifactType($artifact_type); if (!$impl) { throw new Exception( pht( 'There is no implementation available for artifacts of type "%s".', $artifact_type)); } $impl->validateArtifactData($artifact_data); $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($this) ->setArtifactKey($artifact_key) ->setArtifactType($artifact_type) ->setArtifactData($artifact_data); $impl = $artifact->getArtifactImplementation(); $impl->willCreateArtifact($actor); return $artifact->save(); } public function loadArtifact($artifact_key) { $indexes = array(); $indexes[] = HarbormasterBuildArtifact::getArtifactIndex( $this, $artifact_key); $artifact = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withArtifactIndexes($indexes) ->executeOne(); if ($artifact === null) { throw new Exception( pht( 'Artifact "%s" not found!', $artifact_key)); } return $artifact; } public function newLog($log_source, $log_type) { $log_source = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->truncateString($log_source); $log = HarbormasterBuildLog::initializeNewBuildLog($this) ->setLogSource($log_source) ->setLogType($log_type); $log->start(); return $log; } + public function getFieldValue($key) { + $field_list = PhabricatorCustomField::getObjectFields( + $this->getBuildStep(), + PhabricatorCustomField::ROLE_VIEW); + + $fields = $field_list->getFields(); + $full_key = "std:harbormaster:core:{$key}"; + + $field = idx($fields, $full_key); + if (!$field) { + throw new Exception( + pht( + 'Unknown build step field "%s"!', + $key)); + } + + $field = clone $field; + $field->setValueFromStorage($this->getDetail($key)); + return $field->getBuildTargetFieldValue(); + } + + /* -( Status )------------------------------------------------------------- */ public function isComplete() { switch ($this->getTargetStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ABORTED: return true; } return false; } public function isFailed() { switch ($this->getTargetStatus()) { case self::STATUS_FAILED: case self::STATUS_ABORTED: return true; } return false; } public function isWaiting() { switch ($this->getTargetStatus()) { case self::STATUS_WAITING: return true; } return false; } public function isUnderway() { switch ($this->getTargetStatus()) { case self::STATUS_PENDING: case self::STATUS_BUILDING: return true; } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuild()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuild()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a build to view its build targets.'); } } diff --git a/src/applications/harbormaster/worker/HarbormasterTargetWorker.php b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php index 0f4d4092fa..ac3014dc29 100644 --- a/src/applications/harbormaster/worker/HarbormasterTargetWorker.php +++ b/src/applications/harbormaster/worker/HarbormasterTargetWorker.php @@ -1,114 +1,115 @@ loadBuildTarget(); } catch (Exception $ex) { return null; } return $viewer->renderHandle($target->getPHID()); } private function loadBuildTarget() { $data = $this->getTaskData(); $id = idx($data, 'targetID'); $target = id(new HarbormasterBuildTargetQuery()) ->withIDs(array($id)) ->setViewer($this->getViewer()) + ->needBuildSteps(true) ->executeOne(); if (!$target) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Bad build target ID "%d".', $id)); } return $target; } protected function doWork() { $target = $this->loadBuildTarget(); $build = $target->getBuild(); $viewer = $this->getViewer(); $target->setDateStarted(time()); try { if ($target->getBuildGeneration() !== $build->getBuildGeneration()) { throw new HarbormasterBuildAbortedException(); } $status_pending = HarbormasterBuildTarget::STATUS_PENDING; if ($target->getTargetStatus() == $status_pending) { $target->setTargetStatus(HarbormasterBuildTarget::STATUS_BUILDING); $target->save(); } $implementation = $target->getImplementation(); $implementation->setCurrentWorkerTaskID($this->getCurrentWorkerTaskID()); $implementation->execute($build, $target); $next_status = HarbormasterBuildTarget::STATUS_PASSED; if ($implementation->shouldWaitForMessage($target)) { $next_status = HarbormasterBuildTarget::STATUS_WAITING; } $target->setTargetStatus($next_status); if ($target->isComplete()) { $target->setDateCompleted(PhabricatorTime::getNow()); } $target->save(); } catch (PhabricatorWorkerYieldException $ex) { // If the target wants to yield, let that escape without further // processing. We'll resume after the task retries. throw $ex; } catch (HarbormasterBuildFailureException $ex) { // A build step wants to fail explicitly. $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); $target->setDateCompleted(PhabricatorTime::getNow()); $target->save(); } catch (HarbormasterBuildAbortedException $ex) { // A build step is aborting because the build has been restarted. $target->setTargetStatus(HarbormasterBuildTarget::STATUS_ABORTED); $target->setDateCompleted(PhabricatorTime::getNow()); $target->save(); } catch (Exception $ex) { phlog($ex); try { $log = $build->createLog($target, 'core', 'exception'); $start = $log->start(); $log->append((string)$ex); $log->finalize($start); } catch (Exception $log_ex) { phlog($log_ex); } $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); $target->setDateCompleted(time()); $target->save(); } id(new HarbormasterBuildEngine()) ->setViewer($viewer) ->setBuild($build) ->continueBuild(); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index d900d41eb3..491c780667 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -1,179 +1,234 @@ getFieldValue(); if (is_array($value)) { foreach ($value as $phid) { $indexes[] = $this->newStringIndex($phid); } } return $indexes; } public function readValueFromRequest(AphrontRequest $request) { $value = $request->getArr($this->getFieldKey()); $this->setFieldValue($value); } public function getValueForStorage() { $value = $this->getFieldValue(); if (!$value) { return null; } return json_encode(array_values($value)); } public function setValueFromStorage($value) { $result = array(); if ($value) { $value = json_decode($value, true); if (is_array($value)) { $result = array_values($value); } } $this->setFieldValue($value); return $this; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getArr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($value) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function getRequiredHandlePHIDsForPropertyView() { $value = $this->getFieldValue(); if ($value) { return $value; } return array(); } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!$value) { return null; } $handles = mpull($handles, 'renderLink'); $handles = phutil_implode_html(', ', $handles); return $handles; } public function getRequiredHandlePHIDsForEdit() { $value = $this->getFieldValue(); if ($value) { return $value; } else { return array(); } } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); return array_merge($add, $rem); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && !$rem) { return pht( '%s updated %s, added %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add)); } else if ($rem && !$add) { return pht( '%s updated %s, removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } else { return pht( '%s updated %s, added %d: %s; removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } } + public function validateApplicationTransactions( + PhabricatorApplicationTransactionEditor $editor, + $type, + array $xactions) { + + $errors = parent::validateApplicationTransactions( + $editor, + $type, + $xactions); + + // If the user is adding PHIDs, make sure the new PHIDs are valid and + // visible to the actor. It's OK for a user to edit a field which includes + // some invalid or restricted values, but they can't add new ones. + + foreach ($xactions as $xaction) { + $old = phutil_json_decode($xaction->getOldValue()); + $new = phutil_json_decode($xaction->getNewValue()); + + $add = array_diff($new, $old); + + if (!$add) { + continue; + } + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($editor->getActor()) + ->withPHIDs($add) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + $invalid = array(); + foreach ($add as $phid) { + if (empty($objects[$phid])) { + $invalid[] = $phid; + } + } + + if ($invalid) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'Some of the selected PHIDs in field "%s" are invalid or '. + 'restricted: %s.', + $this->getFieldName(), + implode(', ', $invalid)), + $xaction); + $errors[] = $error; + $this->setFieldError(pht('Invalid')); + } + } + + return $errors; + } + public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_INCLUDE_ALL, HeraldAdapter::CONDITION_INCLUDE_ANY, HeraldAdapter::CONDITION_INCLUDE_NONE, HeraldAdapter::CONDITION_EXISTS, HeraldAdapter::CONDITION_NOT_EXISTS, ); } public function getHeraldFieldValue() { // If the field has a `null` value, make sure we hand an `array()` to // Herald. $value = parent::getHeraldFieldValue(); if ($value) { return $value; } return array(); } } diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php index 6357ebc5d3..be2408e54a 100644 --- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php @@ -1,56 +1,58 @@ 'id', 'param' => 'id', 'repeat' => true, 'help' => pht('Select one or more tasks by ID.'), ), ); } protected function loadTasks(PhutilArgumentParser $args) { $ids = $args->getArg('id'); if (!$ids) { throw new PhutilArgumentUsageException( pht('Use --id to select tasks by ID.')); } $active_tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere( 'id IN (%Ls)', $ids); $archive_tasks = id(new PhabricatorWorkerArchiveTaskQuery()) ->withIDs($ids) ->execute(); $tasks = mpull($active_tasks, null, 'getID') + mpull($archive_tasks, null, 'getID'); foreach ($ids as $id) { if (empty($tasks[$id])) { throw new PhutilArgumentUsageException( pht('No task exists with id "%s"!', $id)); } } // When we lock tasks properly, this gets populated as a side effect. Just // fake it when doing manual CLI stuff. This makes sure CLI yields have // their expires times set properly. foreach ($tasks as $task) { - $task->setServerTime(PhabricatorTime::getNow()); + if ($task instanceof PhabricatorWorkerActiveTask) { + $task->setServerTime(PhabricatorTime::getNow()); + } } return $tasks; } protected function describeTask(PhabricatorWorkerTask $task) { return pht('Task %d (%s)', $task->getID(), $task->getTaskClass()); } }