Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F18867156
D14266.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D14266.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Nov 4 2025, 9:51 PM (9 w, 4 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/dr/3c/em6j2lyfm7dxcugf
Default Alt Text
D14266.diff (33 KB)
Attached To
Mode
D14266: Rough cut of DrydockRepositoryOperation
Attached
Detach File
Event Timeline
Log In to Comment