diff --git a/resources/sql/autopatches/20151013.drydock.op.1.sql b/resources/sql/autopatches/20151013.drydock.op.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20151013.drydock.op.1.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_repositoryoperation ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + repositoryPHID VARBINARY(64) NOT NULL, + repositoryTarget LONGBLOB NOT NULL, + operationType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + operationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID), + KEY `key_repository` (repositoryPHID, operationState) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -467,6 +467,7 @@ 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', + 'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php', 'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php', 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', @@ -839,6 +840,7 @@ 'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php', 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', + 'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php', 'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php', @@ -878,6 +880,12 @@ 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', + 'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php', + 'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php', + 'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php', + 'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php', + 'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php', + 'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php', 'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php', @@ -4205,6 +4213,7 @@ 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', + 'DifferentialRevisionOperationController' => 'DifferentialController', 'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType', 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', @@ -4605,6 +4614,7 @@ 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockInterface' => 'Phobject', + 'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType', 'DrydockLease' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', @@ -4650,6 +4660,15 @@ 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'DrydockObjectAuthorizationView' => 'AphrontView', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DrydockRepositoryOperation' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), + 'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType', + 'DrydockRepositoryOperationQuery' => 'DrydockQuery', + 'DrydockRepositoryOperationType' => 'Phobject', + 'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker', + 'DrydockRepositoryOperationViewController' => 'DrydockController', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php --- a/src/applications/differential/application/PhabricatorDifferentialApplication.php +++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php @@ -75,6 +75,8 @@ => 'DifferentialRevisionCloseDetailsController', 'update/(?P[1-9]\d*)/' => 'DifferentialDiffCreateController', + 'operation/(?P[1-9]\d*)/' + => 'DifferentialRevisionOperationController', ), 'comment/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController', diff --git a/src/applications/differential/controller/DifferentialRevisionOperationController.php b/src/applications/differential/controller/DifferentialRevisionOperationController.php new file mode 100644 --- /dev/null +++ b/src/applications/differential/controller/DifferentialRevisionOperationController.php @@ -0,0 +1,105 @@ +getViewer(); + $id = $request->getURIData('id'); + + $revision = id(new DifferentialRevisionQuery()) + ->withIDs(array($id)) + ->setViewer($viewer) + ->executeOne(); + if (!$revision) { + return new Aphront404Response(); + } + + $detail_uri = "/D{$id}"; + + $repository = $revision->getRepository(); + if (!$repository) { + return $this->rejectOperation( + $revision, + pht('No Repository'), + pht( + 'This revision is not associated with a known repository. Only '. + 'revisions associated with a tracked repository can be landed '. + 'automatically.')); + } + + if (!$repository->canPerformAutomation()) { + return $this->rejectOperation( + $revision, + pht('No Repository Automation'), + pht( + 'The repository this revision is associated with ("%s") is not '. + 'configured to support automation. Configure automation for the '. + 'repository to enable revisions to be landed automatically.', + $repository->getMonogram())); + } + + // TODO: At some point we should allow installs to give "land reviewed + // code" permission to more users than "push any commit", because it is + // a much less powerful operation. For now, just require push so this + // doesn't do anything users can't do on their own. + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionPushCapability::CAPABILITY); + if (!$can_push) { + return $this->rejectOperation( + $revision, + pht('Unable to Push'), + pht( + 'You do not have permission to push to the repository this '. + 'revision is associated with ("%s"), so you can not land it.', + $repository->getMonogram())); + } + + if ($request->isFormPost()) { + $op = new DrydockLandRepositoryOperation(); + + $operation = DrydockRepositoryOperation::initializeNewOperation($op) + ->setAuthorPHID($viewer->getPHID()) + ->setObjectPHID($revision->getPHID()) + ->setRepositoryPHID($repository->getPHID()) + ->setRepositoryTarget('branch:master'); + + $operation->save(); + $operation->scheduleUpdate(); + + return id(new AphrontRedirectResponse()) + ->setURI($detail_uri); + } + + return $this->newDialog() + ->setTitle(pht('Land Revision')) + ->appendParagraph( + pht( + 'In theory, this will do approximately what `arc land` would do. '. + 'In practice, that is almost certainly not what it will actually '. + 'do.')) + ->appendParagraph( + pht( + 'THIS FEATURE IS EXPERIMENTAL AND DANGEROUS! USE IT AT YOUR '. + 'OWN RISK!')) + ->addCancelButton($detail_uri) + ->addSubmitButton(pht('Mutate Repository Unpredictably')); + } + + private function rejectOperation( + DifferentialRevision $revision, + $title, + $body) { + + $id = $revision->getID(); + $detail_uri = "/D{$id}"; + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($detail_uri); + } + +} diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -463,7 +463,10 @@ $object_id = 'D'.$revision->getID(); + $operations_box = $this->buildOperationsBox($revision); + $content = array( + $operations_box, $revision_detail_box, $diff_detail_box, $page_pane, @@ -1032,4 +1035,55 @@ return $view; } + private function buildOperationsBox(DifferentialRevision $revision) { + $viewer = $this->getViewer(); + + // Save a query if we can't possibly have pending operations. + $repository = $revision->getRepository(); + if (!$repository || !$repository->canPerformAutomation()) { + return null; + } + + $operations = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($revision->getPHID())) + ->withOperationStates( + array( + DrydockRepositoryOperation::STATE_WAIT, + DrydockRepositoryOperation::STATE_WORK, + DrydockRepositoryOperation::STATE_FAIL, + )) + ->execute(); + if (!$operations) { + return null; + } + + $operation = head(msort($operations, 'getID')); + + // TODO: This is completely made up for now, give it useful information and + // a sweet progress bar. + + switch ($operation->getOperationState()) { + case DrydockRepositoryOperation::STATE_WAIT: + case DrydockRepositoryOperation::STATE_WORK: + $severity = PHUIInfoView::SEVERITY_NOTICE; + $text = pht( + 'Some sort of repository operation is currently running.'); + break; + default: + $severity = PHUIInfoView::SEVERITY_ERROR; + $text = pht( + 'Some sort of repository operation failed.'); + break; + } + + $info_view = id(new PHUIInfoView()) + ->setSeverity($severity) + ->appendChild($text); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Active Operations (EXPERIMENTAL!)')) + ->setInfoView($info_view); + } + } diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php --- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php +++ b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php @@ -37,6 +37,18 @@ return null; } + if ($repository->canPerformAutomation()) { + $revision_id = $revision->getID(); + + $action = id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setName(pht('Land Revision')) + ->setIcon('fa-fighter-jet') + ->setHref("/differential/revision/operation/{$revision_id}/"); + + $this->addActionMenuItems($event, $action); + } + $strategies = id(new PhutilClassMapQuery()) ->setAncestorClass('DifferentialLandingStrategy') ->execute(); diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -90,6 +90,11 @@ 'DrydockAuthorizationAuthorizeController', ), ), + '(?Poperation)/' => array( + '(?P[1-9]\d*)/' => array( + '' => 'DrydockRepositoryOperationViewController', + ), + ), ), ); } diff --git a/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php b/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php @@ -0,0 +1,92 @@ +getViewer(); + $id = $request->getURIData('id'); + + $operation = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$operation) { + return new Aphront404Response(); + } + + $id = $operation->getID(); + $title = pht('Repository Operation %d', $id); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($operation); + + $state = $operation->getOperationState(); + $icon = DrydockRepositoryOperation::getOperationStateIcon($state); + $name = DrydockRepositoryOperation::getOperationStateName($state); + $header->setStatus($icon, null, $name); + + $actions = $this->buildActionListView($operation); + $properties = $this->buildPropertyListView($operation); + $properties->setActionList($actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($title); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $title, + )); + + } + + private function buildActionListView(DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + $id = $operation->getID(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setObject($operation); + + return $view; + } + + private function buildPropertyListView( + DrydockRepositoryOperation $operation) { + + $viewer = $this->getViewer(); + + $view = new PHUIPropertyListView(); + $view->addProperty( + pht('Repository'), + $viewer->renderHandle($operation->getRepositoryPHID())); + + $view->addProperty( + pht('Object'), + $viewer->renderHandle($operation->getObjectPHID())); + + return $view; + } + + public function buildSideNavView() { + // TODO: Get rid of this, but it's currently required by DrydockController. + return null; + } + + public function buildApplicationMenu() { + // TODO: As above. + return null; + } + +} diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -0,0 +1,8 @@ +getPhobjectClassConstant('OPCONST', 32); + } + + final public static function getAllOperationTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getOperationConstant') + ->execute(); + } + +} diff --git a/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php b/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $operation = $objects[$phid]; + $id = $operation->getID(); + + $handle->setName(pht('Drydock Repository Operation %d', $id)); + $handle->setURI("/drydock/operation/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php @@ -0,0 +1,132 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withRepositoryPHIDs(array $repository_phids) { + $this->repositoryPHIDs = $repository_phids; + return $this; + } + + public function withOperationStates(array $states) { + $this->operationStates = $states; + return $this; + } + + public function newResultObject() { + return new DrydockRepositoryOperation(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $operations) { + $repository_phids = mpull($operations, 'getRepositoryPHID'); + if ($repository_phids) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + } else { + $repositories = array(); + } + + foreach ($operations as $key => $operation) { + $repository = idx($repositories, $operation->getRepositoryPHID()); + if (!$repository) { + $this->didRejectResult($operation); + unset($operations[$key]); + continue; + } + $operation->attachRepository($repository); + } + + return $operations; + } + + protected function didFilterPage(array $operations) { + $object_phids = mpull($operations, 'getObjectPHID'); + if ($object_phids) { + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, 'getPHID'); + } else { + $objects = array(); + } + + foreach ($operations as $key => $operation) { + $object = idx($objects, $operation->getObjectPHID()); + $operation->attachObject($object); + } + + return $operations; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->repositoryPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'repositoryPHID IN (%Ls)', + $this->repositoryPHIDs); + } + + if ($this->operationStates !== null) { + $where[] = qsprintf( + $conn, + 'operationState IN (%Ls)', + $this->operationStates); + } + + return $where; + } + +} diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php @@ -0,0 +1,148 @@ +setOperationState(self::STATE_WAIT) + ->setOperationType($op->getOperationConstant()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'repositoryTarget' => 'bytes', + 'operationType' => 'text32', + 'operationState' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_object' => array( + 'columns' => array('objectPHID'), + ), + 'key_repository' => array( + 'columns' => array('repositoryPHID', 'operationState'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + DrydockRepositoryOperationPHIDType::TYPECONST); + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public static function getOperationStateIcon($state) { + $map = array( + self::STATE_WAIT => 'fa-clock-o', + self::STATE_WORK => 'fa-refresh blue', + self::STATE_DONE => 'fa-check green', + self::STATE_FAIL => 'fa-times red', + ); + + return idx($map, $state, null); + } + + public static function getOperationStateName($state) { + $map = array( + self::STATE_WAIT => pht('Waiting'), + self::STATE_WORK => pht('Working'), + self::STATE_DONE => pht('Done'), + self::STATE_FAIL => pht('Failed'), + ); + + return idx($map, $state, pht('', $state)); + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockRepositoryOperationUpdateWorker', + array( + 'operationPHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + 'priority' => PhabricatorWorker::PRIORITY_ALERTS, + )); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getRepository()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'A repository operation inherits the policies of the repository it '. + 'affects.'); + } + +} diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -0,0 +1,173 @@ +getTaskDataValue('operationPHID'); + + $hash = PhabricatorHash::digestForIndex($operation_phid); + $lock_key = 'drydock.operation:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + try { + $operation = $this->loadOperation($operation_phid); + $this->handleUpdate($operation); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + } + + + private function handleUpdate(DrydockRepositoryOperation $operation) { + $operation_state = $operation->getOperationState(); + + switch ($operation_state) { + case DrydockRepositoryOperation::STATE_WAIT: + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_WORK) + ->save(); + break; + case DrydockRepositoryOperation::STATE_WORK: + break; + case DrydockRepositoryOperation::STATE_DONE: + case DrydockRepositoryOperation::STATE_FAIL: + // No more processing for these requests. + return; + } + + // TODO: We should probably check for other running operations with lower + // IDs and the same repository target and yield to them here? That is, + // enforce sequential evaluation of operations against the same target so + // that if you land "A" and then land "B", we always finish "A" first. + // For now, just let stuff happen in any order. We can't lease until + // we know we're good to move forward because we might deadlock if we do: + // we're waiting for another operation to complete, and that operation is + // waiting for a lease we're holding. + + try { + $lease = $this->loadWorkingCopyLease($operation); + + $interface = $lease->getInterface( + DrydockCommandInterface::INTERFACE_TYPE); + + // No matter what happens here, destroy the lease away once we're done. + $lease->releaseOnDestruction(true); + + // TODO: Some day, do useful things instead of running `git show`. + list($stdout) = $interface->execx('git show'); + phlog($stdout); + } catch (PhabricatorWorkerYieldException $ex) { + throw $ex; + } catch (Exception $ex) { + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_FAIL) + ->save(); + throw $ex; + } + + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_DONE) + ->save(); + + // TODO: Once we have sequencing, we could awaken the next operation + // against this target after finishing or failing. + } + + private function loadWorkingCopyLease( + DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + + // TODO: This is very similar to leasing in Harbormaster, maybe we can + // share some of the logic? + + $lease_phid = $operation->getProperty('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(); + + $repository = $operation->getRepository(); + + $allowed_phids = $repository->getAutomationBlueprintPHIDs(); + $authorizing_phid = $repository->getPHID(); + + $lease = DrydockLease::initializeNewLease() + ->setResourceType($working_copy_type) + ->setOwnerPHID($operation->getPHID()) + ->setAuthorizingPHID($authorizing_phid) + ->setAllowedBlueprintPHIDs($allowed_phids); + + $map = $this->buildRepositoryMap($operation); + + $lease->setAttribute('repositories.map', $map); + + $task_id = $this->getCurrentWorkerTaskID(); + if ($task_id) { + $lease->setAwakenTaskIDs(array($task_id)); + } + + $operation + ->setProperty('exec.leasePHID', $lease->getPHID()) + ->save(); + + $lease->queueForActivation(); + } + + if ($lease->isActivating()) { + throw new PhabricatorWorkerYieldException(15); + } + + if (!$lease->isActive()) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Lease "%s" never activated.', + $lease->getPHID())); + } + + return $lease; + } + + private function buildRepositoryMap(DrydockRepositoryOperation $operation) { + $repository = $operation->getRepository(); + + $target = $operation->getRepositoryTarget(); + list($type, $name) = explode(':', $target, 2); + switch ($type) { + case 'branch': + $spec = array( + 'branch' => $name, + ); + break; + default: + throw new Exception( + pht( + 'Unknown repository operation target type "%s" (in target "%s").', + $type, + $target)); + } + + $map = array(); + $map[$repository->getCloneName()] = array( + 'phid' => $repository->getPHID(), + 'default' => true, + ) + $spec; + + return $map; + } +} diff --git a/src/applications/drydock/worker/DrydockWorker.php b/src/applications/drydock/worker/DrydockWorker.php --- a/src/applications/drydock/worker/DrydockWorker.php +++ b/src/applications/drydock/worker/DrydockWorker.php @@ -36,6 +36,21 @@ return $resource; } + protected function loadOperation($operation_phid) { + $viewer = $this->getViewer(); + + $operation = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withPHIDs(array($operation_phid)) + ->executeOne(); + if (!$operation) { + throw new PhabricatorWorkerPermanentFailureException( + pht('No such operation "%s"!', $operation_phid)); + } + + return $operation; + } + protected function loadCommands($target_phid) { $viewer = $this->getViewer(); diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1822,6 +1822,17 @@ return $this->isGit(); } + public function canPerformAutomation() { + if (!$this->supportsAutomation()) { + return false; + } + + if (!$this->getAutomationBlueprintPHIDs()) { + return false; + } + + return true; + } public function getAutomationBlueprintPHIDs() { if (!$this->supportsAutomation()) {