Page MenuHomePhabricator

D14266.diff
No OneTemporary

D14266.diff

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<revisionID>[1-9]\d*)/'
=> 'DifferentialDiffCreateController',
+ 'operation/(?P<id>[1-9]\d*)/'
+ => 'DifferentialRevisionOperationController',
),
'comment/' => array(
'preview/(?P<id>[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 @@
+<?php
+
+final class DifferentialRevisionOperationController
+ extends DifferentialController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->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',
),
),
+ '(?P<type>operation)/' => array(
+ '(?P<id>[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 @@
+<?php
+
+final class DrydockRepositoryOperationViewController
+ extends DrydockController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->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 @@
+<?php
+
+final class DrydockLandRepositoryOperation
+ extends DrydockRepositoryOperationType {
+
+ const OPCONST = 'land';
+
+}
diff --git a/src/applications/drydock/operation/DrydockRepositoryOperationType.php b/src/applications/drydock/operation/DrydockRepositoryOperationType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/operation/DrydockRepositoryOperationType.php
@@ -0,0 +1,16 @@
+<?php
+
+abstract class DrydockRepositoryOperationType extends Phobject {
+
+ final public function getOperationConstant() {
+ return $this->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 @@
+<?php
+
+final class DrydockRepositoryOperationPHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'DRYO';
+
+ public function getTypeName() {
+ return pht('Drydock Repository Operation');
+ }
+
+ public function newObject() {
+ return new DrydockRepositoryOperation();
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new DrydockRepositoryOperationQuery())
+ ->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 @@
+<?php
+
+final class DrydockRepositoryOperationQuery extends DrydockQuery {
+
+ private $ids;
+ private $phids;
+ private $objectPHIDs;
+ private $repositoryPHIDs;
+ private $operationStates;
+
+ public function withIDs(array $ids) {
+ $this->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 @@
+<?php
+
+/**
+ * Represents a request to perform a repository operation like a merge or
+ * cherry-pick.
+ */
+final class DrydockRepositoryOperation extends DrydockDAO
+ implements
+ PhabricatorPolicyInterface {
+
+ const STATE_WAIT = 'wait';
+ const STATE_WORK = 'work';
+ const STATE_DONE = 'done';
+ const STATE_FAIL = 'fail';
+
+ protected $authorPHID;
+ protected $objectPHID;
+ protected $repositoryPHID;
+ protected $repositoryTarget;
+ protected $operationType;
+ protected $operationState;
+ protected $properties = array();
+
+ private $repository = self::ATTACHABLE;
+ private $object = self::ATTACHABLE;
+
+ public static function initializeNewOperation(
+ DrydockRepositoryOperationType $op) {
+
+ return id(new DrydockRepositoryOperation())
+ ->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('<Unknown: %s>', $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 @@
+<?php
+
+final class DrydockRepositoryOperationUpdateWorker
+ extends DrydockWorker {
+
+ protected function doWork() {
+ $operation_phid = $this->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()) {

File Metadata

Mime Type
text/plain
Expires
Mon, May 13, 12:49 PM (3 w, 3 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/ex/pf/62b4mcmwo22bfh5j
Default Alt Text
D14266.diff (33 KB)

Event Timeline