@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rough cut of DrydockRepositoryOperation

Summary:
Ref T182. This doesn't do anything interesting yet and is mostly scaffolding, but here's roughly the workflow. From previous revision, you can configure "Repository Automation" for a repository:

{F875741}

If it's configured, a new "Land Revision" button shows up:

{F875743}

Once you click it you get a big warning dialog that it won't work, and then this shows up at the top of the revision (completely temporary/placeholder UI, some day a nice progress bar or whatever):

{F875747}

If you're lucky, the operation eventually sort of works:

{F875750}

It only runs `git show` right now, doesn't actually do any writes or anything.

Test Plan:
- Clicked "Land Revision".
- Watched `phd debug task`.
- Saw it log `git show` to output.
- Verified operation success in UI (by fiddling URL, no way to get there normally yet).

Reviewers: chad

Reviewed By: chad

Subscribers: revi

Maniphest Tasks: T182

Differential Revision: https://secure.phabricator.com/D14266

+845
+16
resources/sql/autopatches/20151013.drydock.op.1.sql
··· 1 + CREATE TABLE {$NAMESPACE}_drydock.drydock_repositoryoperation ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + objectPHID VARBINARY(64) NOT NULL, 6 + repositoryPHID VARBINARY(64) NOT NULL, 7 + repositoryTarget LONGBLOB NOT NULL, 8 + operationType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, 9 + operationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, 10 + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 11 + dateCreated INT UNSIGNED NOT NULL, 12 + dateModified INT UNSIGNED NOT NULL, 13 + UNIQUE KEY `key_phid` (phid), 14 + KEY `key_object` (objectPHID), 15 + KEY `key_repository` (repositoryPHID, operationState) 16 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+19
src/__phutil_library_map__.php
··· 467 467 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 468 468 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 469 469 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', 470 + 'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php', 470 471 'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php', 471 472 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 472 473 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', ··· 839 840 'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php', 840 841 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 841 842 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 843 + 'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php', 842 844 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 843 845 'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php', 844 846 'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php', ··· 878 880 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 879 881 'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php', 880 882 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 883 + 'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php', 884 + 'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php', 885 + 'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php', 886 + 'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php', 887 + 'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php', 888 + 'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php', 881 889 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 882 890 'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php', 883 891 'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php', ··· 4205 4213 'DifferentialRevisionListController' => 'DifferentialController', 4206 4214 'DifferentialRevisionListView' => 'AphrontView', 4207 4215 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', 4216 + 'DifferentialRevisionOperationController' => 'DifferentialController', 4208 4217 'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType', 4209 4218 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 4210 4219 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', ··· 4605 4614 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', 4606 4615 'DrydockFilesystemInterface' => 'DrydockInterface', 4607 4616 'DrydockInterface' => 'Phobject', 4617 + 'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType', 4608 4618 'DrydockLease' => array( 4609 4619 'DrydockDAO', 4610 4620 'PhabricatorPolicyInterface', ··· 4650 4660 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 4651 4661 'DrydockObjectAuthorizationView' => 'AphrontView', 4652 4662 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4663 + 'DrydockRepositoryOperation' => array( 4664 + 'DrydockDAO', 4665 + 'PhabricatorPolicyInterface', 4666 + ), 4667 + 'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType', 4668 + 'DrydockRepositoryOperationQuery' => 'DrydockQuery', 4669 + 'DrydockRepositoryOperationType' => 'Phobject', 4670 + 'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker', 4671 + 'DrydockRepositoryOperationViewController' => 'DrydockController', 4653 4672 'DrydockResource' => array( 4654 4673 'DrydockDAO', 4655 4674 'PhabricatorPolicyInterface',
+2
src/applications/differential/application/PhabricatorDifferentialApplication.php
··· 75 75 => 'DifferentialRevisionCloseDetailsController', 76 76 'update/(?P<revisionID>[1-9]\d*)/' 77 77 => 'DifferentialDiffCreateController', 78 + 'operation/(?P<id>[1-9]\d*)/' 79 + => 'DifferentialRevisionOperationController', 78 80 ), 79 81 'comment/' => array( 80 82 'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
+105
src/applications/differential/controller/DifferentialRevisionOperationController.php
··· 1 + <?php 2 + 3 + final class DifferentialRevisionOperationController 4 + extends DifferentialController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + $id = $request->getURIData('id'); 9 + 10 + $revision = id(new DifferentialRevisionQuery()) 11 + ->withIDs(array($id)) 12 + ->setViewer($viewer) 13 + ->executeOne(); 14 + if (!$revision) { 15 + return new Aphront404Response(); 16 + } 17 + 18 + $detail_uri = "/D{$id}"; 19 + 20 + $repository = $revision->getRepository(); 21 + if (!$repository) { 22 + return $this->rejectOperation( 23 + $revision, 24 + pht('No Repository'), 25 + pht( 26 + 'This revision is not associated with a known repository. Only '. 27 + 'revisions associated with a tracked repository can be landed '. 28 + 'automatically.')); 29 + } 30 + 31 + if (!$repository->canPerformAutomation()) { 32 + return $this->rejectOperation( 33 + $revision, 34 + pht('No Repository Automation'), 35 + pht( 36 + 'The repository this revision is associated with ("%s") is not '. 37 + 'configured to support automation. Configure automation for the '. 38 + 'repository to enable revisions to be landed automatically.', 39 + $repository->getMonogram())); 40 + } 41 + 42 + // TODO: At some point we should allow installs to give "land reviewed 43 + // code" permission to more users than "push any commit", because it is 44 + // a much less powerful operation. For now, just require push so this 45 + // doesn't do anything users can't do on their own. 46 + $can_push = PhabricatorPolicyFilter::hasCapability( 47 + $viewer, 48 + $repository, 49 + DiffusionPushCapability::CAPABILITY); 50 + if (!$can_push) { 51 + return $this->rejectOperation( 52 + $revision, 53 + pht('Unable to Push'), 54 + pht( 55 + 'You do not have permission to push to the repository this '. 56 + 'revision is associated with ("%s"), so you can not land it.', 57 + $repository->getMonogram())); 58 + } 59 + 60 + if ($request->isFormPost()) { 61 + $op = new DrydockLandRepositoryOperation(); 62 + 63 + $operation = DrydockRepositoryOperation::initializeNewOperation($op) 64 + ->setAuthorPHID($viewer->getPHID()) 65 + ->setObjectPHID($revision->getPHID()) 66 + ->setRepositoryPHID($repository->getPHID()) 67 + ->setRepositoryTarget('branch:master'); 68 + 69 + $operation->save(); 70 + $operation->scheduleUpdate(); 71 + 72 + return id(new AphrontRedirectResponse()) 73 + ->setURI($detail_uri); 74 + } 75 + 76 + return $this->newDialog() 77 + ->setTitle(pht('Land Revision')) 78 + ->appendParagraph( 79 + pht( 80 + 'In theory, this will do approximately what `arc land` would do. '. 81 + 'In practice, that is almost certainly not what it will actually '. 82 + 'do.')) 83 + ->appendParagraph( 84 + pht( 85 + 'THIS FEATURE IS EXPERIMENTAL AND DANGEROUS! USE IT AT YOUR '. 86 + 'OWN RISK!')) 87 + ->addCancelButton($detail_uri) 88 + ->addSubmitButton(pht('Mutate Repository Unpredictably')); 89 + } 90 + 91 + private function rejectOperation( 92 + DifferentialRevision $revision, 93 + $title, 94 + $body) { 95 + 96 + $id = $revision->getID(); 97 + $detail_uri = "/D{$id}"; 98 + 99 + return $this->newDialog() 100 + ->setTitle($title) 101 + ->appendParagraph($body) 102 + ->addCancelButton($detail_uri); 103 + } 104 + 105 + }
+54
src/applications/differential/controller/DifferentialRevisionViewController.php
··· 463 463 464 464 $object_id = 'D'.$revision->getID(); 465 465 466 + $operations_box = $this->buildOperationsBox($revision); 467 + 466 468 $content = array( 469 + $operations_box, 467 470 $revision_detail_box, 468 471 $diff_detail_box, 469 472 $page_pane, ··· 1030 1033 } 1031 1034 1032 1035 return $view; 1036 + } 1037 + 1038 + private function buildOperationsBox(DifferentialRevision $revision) { 1039 + $viewer = $this->getViewer(); 1040 + 1041 + // Save a query if we can't possibly have pending operations. 1042 + $repository = $revision->getRepository(); 1043 + if (!$repository || !$repository->canPerformAutomation()) { 1044 + return null; 1045 + } 1046 + 1047 + $operations = id(new DrydockRepositoryOperationQuery()) 1048 + ->setViewer($viewer) 1049 + ->withObjectPHIDs(array($revision->getPHID())) 1050 + ->withOperationStates( 1051 + array( 1052 + DrydockRepositoryOperation::STATE_WAIT, 1053 + DrydockRepositoryOperation::STATE_WORK, 1054 + DrydockRepositoryOperation::STATE_FAIL, 1055 + )) 1056 + ->execute(); 1057 + if (!$operations) { 1058 + return null; 1059 + } 1060 + 1061 + $operation = head(msort($operations, 'getID')); 1062 + 1063 + // TODO: This is completely made up for now, give it useful information and 1064 + // a sweet progress bar. 1065 + 1066 + switch ($operation->getOperationState()) { 1067 + case DrydockRepositoryOperation::STATE_WAIT: 1068 + case DrydockRepositoryOperation::STATE_WORK: 1069 + $severity = PHUIInfoView::SEVERITY_NOTICE; 1070 + $text = pht( 1071 + 'Some sort of repository operation is currently running.'); 1072 + break; 1073 + default: 1074 + $severity = PHUIInfoView::SEVERITY_ERROR; 1075 + $text = pht( 1076 + 'Some sort of repository operation failed.'); 1077 + break; 1078 + } 1079 + 1080 + $info_view = id(new PHUIInfoView()) 1081 + ->setSeverity($severity) 1082 + ->appendChild($text); 1083 + 1084 + return id(new PHUIObjectBoxView()) 1085 + ->setHeaderText(pht('Active Operations (EXPERIMENTAL!)')) 1086 + ->setInfoView($info_view); 1033 1087 } 1034 1088 1035 1089 }
+12
src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
··· 37 37 return null; 38 38 } 39 39 40 + if ($repository->canPerformAutomation()) { 41 + $revision_id = $revision->getID(); 42 + 43 + $action = id(new PhabricatorActionView()) 44 + ->setWorkflow(true) 45 + ->setName(pht('Land Revision')) 46 + ->setIcon('fa-fighter-jet') 47 + ->setHref("/differential/revision/operation/{$revision_id}/"); 48 + 49 + $this->addActionMenuItems($event, $action); 50 + } 51 + 40 52 $strategies = id(new PhutilClassMapQuery()) 41 53 ->setAncestorClass('DifferentialLandingStrategy') 42 54 ->execute();
+5
src/applications/drydock/application/PhabricatorDrydockApplication.php
··· 90 90 'DrydockAuthorizationAuthorizeController', 91 91 ), 92 92 ), 93 + '(?P<type>operation)/' => array( 94 + '(?P<id>[1-9]\d*)/' => array( 95 + '' => 'DrydockRepositoryOperationViewController', 96 + ), 97 + ), 93 98 ), 94 99 ); 95 100 }
+92
src/applications/drydock/controller/DrydockRepositoryOperationViewController.php
··· 1 + <?php 2 + 3 + final class DrydockRepositoryOperationViewController 4 + extends DrydockController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + $id = $request->getURIData('id'); 9 + 10 + $operation = id(new DrydockRepositoryOperationQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->executeOne(); 14 + if (!$operation) { 15 + return new Aphront404Response(); 16 + } 17 + 18 + $id = $operation->getID(); 19 + $title = pht('Repository Operation %d', $id); 20 + 21 + $header = id(new PHUIHeaderView()) 22 + ->setHeader($title) 23 + ->setUser($viewer) 24 + ->setPolicyObject($operation); 25 + 26 + $state = $operation->getOperationState(); 27 + $icon = DrydockRepositoryOperation::getOperationStateIcon($state); 28 + $name = DrydockRepositoryOperation::getOperationStateName($state); 29 + $header->setStatus($icon, null, $name); 30 + 31 + $actions = $this->buildActionListView($operation); 32 + $properties = $this->buildPropertyListView($operation); 33 + $properties->setActionList($actions); 34 + 35 + $crumbs = $this->buildApplicationCrumbs(); 36 + $crumbs->addTextCrumb($title); 37 + 38 + $object_box = id(new PHUIObjectBoxView()) 39 + ->setHeader($header) 40 + ->addPropertyList($properties); 41 + 42 + return $this->buildApplicationPage( 43 + array( 44 + $crumbs, 45 + $object_box, 46 + ), 47 + array( 48 + 'title' => $title, 49 + )); 50 + 51 + } 52 + 53 + private function buildActionListView(DrydockRepositoryOperation $operation) { 54 + $viewer = $this->getViewer(); 55 + $id = $operation->getID(); 56 + 57 + $view = id(new PhabricatorActionListView()) 58 + ->setUser($viewer) 59 + ->setObjectURI($this->getRequest()->getRequestURI()) 60 + ->setObject($operation); 61 + 62 + return $view; 63 + } 64 + 65 + private function buildPropertyListView( 66 + DrydockRepositoryOperation $operation) { 67 + 68 + $viewer = $this->getViewer(); 69 + 70 + $view = new PHUIPropertyListView(); 71 + $view->addProperty( 72 + pht('Repository'), 73 + $viewer->renderHandle($operation->getRepositoryPHID())); 74 + 75 + $view->addProperty( 76 + pht('Object'), 77 + $viewer->renderHandle($operation->getObjectPHID())); 78 + 79 + return $view; 80 + } 81 + 82 + public function buildSideNavView() { 83 + // TODO: Get rid of this, but it's currently required by DrydockController. 84 + return null; 85 + } 86 + 87 + public function buildApplicationMenu() { 88 + // TODO: As above. 89 + return null; 90 + } 91 + 92 + }
+8
src/applications/drydock/operation/DrydockLandRepositoryOperation.php
··· 1 + <?php 2 + 3 + final class DrydockLandRepositoryOperation 4 + extends DrydockRepositoryOperationType { 5 + 6 + const OPCONST = 'land'; 7 + 8 + }
+16
src/applications/drydock/operation/DrydockRepositoryOperationType.php
··· 1 + <?php 2 + 3 + abstract class DrydockRepositoryOperationType extends Phobject { 4 + 5 + final public function getOperationConstant() { 6 + return $this->getPhobjectClassConstant('OPCONST', 32); 7 + } 8 + 9 + final public static function getAllOperationTypes() { 10 + return id(new PhutilClassMapQuery()) 11 + ->setAncestorClass(__CLASS__) 12 + ->setUniqueMethod('getOperationConstant') 13 + ->execute(); 14 + } 15 + 16 + }
+37
src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php
··· 1 + <?php 2 + 3 + final class DrydockRepositoryOperationPHIDType extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'DRYO'; 6 + 7 + public function getTypeName() { 8 + return pht('Drydock Repository Operation'); 9 + } 10 + 11 + public function newObject() { 12 + return new DrydockRepositoryOperation(); 13 + } 14 + 15 + protected function buildQueryForObjects( 16 + PhabricatorObjectQuery $query, 17 + array $phids) { 18 + 19 + return id(new DrydockRepositoryOperationQuery()) 20 + ->withPHIDs($phids); 21 + } 22 + 23 + public function loadHandles( 24 + PhabricatorHandleQuery $query, 25 + array $handles, 26 + array $objects) { 27 + 28 + foreach ($handles as $phid => $handle) { 29 + $operation = $objects[$phid]; 30 + $id = $operation->getID(); 31 + 32 + $handle->setName(pht('Drydock Repository Operation %d', $id)); 33 + $handle->setURI("/drydock/operation/{$id}/"); 34 + } 35 + } 36 + 37 + }
+132
src/applications/drydock/query/DrydockRepositoryOperationQuery.php
··· 1 + <?php 2 + 3 + final class DrydockRepositoryOperationQuery extends DrydockQuery { 4 + 5 + private $ids; 6 + private $phids; 7 + private $objectPHIDs; 8 + private $repositoryPHIDs; 9 + private $operationStates; 10 + 11 + public function withIDs(array $ids) { 12 + $this->ids = $ids; 13 + return $this; 14 + } 15 + 16 + public function withPHIDs(array $phids) { 17 + $this->phids = $phids; 18 + return $this; 19 + } 20 + 21 + public function withObjectPHIDs(array $object_phids) { 22 + $this->objectPHIDs = $object_phids; 23 + return $this; 24 + } 25 + 26 + public function withRepositoryPHIDs(array $repository_phids) { 27 + $this->repositoryPHIDs = $repository_phids; 28 + return $this; 29 + } 30 + 31 + public function withOperationStates(array $states) { 32 + $this->operationStates = $states; 33 + return $this; 34 + } 35 + 36 + public function newResultObject() { 37 + return new DrydockRepositoryOperation(); 38 + } 39 + 40 + protected function loadPage() { 41 + return $this->loadStandardPage($this->newResultObject()); 42 + } 43 + 44 + protected function willFilterPage(array $operations) { 45 + $repository_phids = mpull($operations, 'getRepositoryPHID'); 46 + if ($repository_phids) { 47 + $repositories = id(new PhabricatorRepositoryQuery()) 48 + ->setViewer($this->getViewer()) 49 + ->setParentQuery($this) 50 + ->withPHIDs($repository_phids) 51 + ->execute(); 52 + $repositories = mpull($repositories, null, 'getPHID'); 53 + } else { 54 + $repositories = array(); 55 + } 56 + 57 + foreach ($operations as $key => $operation) { 58 + $repository = idx($repositories, $operation->getRepositoryPHID()); 59 + if (!$repository) { 60 + $this->didRejectResult($operation); 61 + unset($operations[$key]); 62 + continue; 63 + } 64 + $operation->attachRepository($repository); 65 + } 66 + 67 + return $operations; 68 + } 69 + 70 + protected function didFilterPage(array $operations) { 71 + $object_phids = mpull($operations, 'getObjectPHID'); 72 + if ($object_phids) { 73 + $objects = id(new PhabricatorObjectQuery()) 74 + ->setViewer($this->getViewer()) 75 + ->setParentQuery($this) 76 + ->withPHIDs($object_phids) 77 + ->execute(); 78 + $objects = mpull($objects, 'getPHID'); 79 + } else { 80 + $objects = array(); 81 + } 82 + 83 + foreach ($operations as $key => $operation) { 84 + $object = idx($objects, $operation->getObjectPHID()); 85 + $operation->attachObject($object); 86 + } 87 + 88 + return $operations; 89 + } 90 + 91 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 92 + $where = parent::buildWhereClauseParts($conn); 93 + 94 + if ($this->ids !== null) { 95 + $where[] = qsprintf( 96 + $conn, 97 + 'id IN (%Ld)', 98 + $this->ids); 99 + } 100 + 101 + if ($this->phids !== null) { 102 + $where[] = qsprintf( 103 + $conn, 104 + 'phid IN (%Ls)', 105 + $this->phids); 106 + } 107 + 108 + if ($this->objectPHIDs !== null) { 109 + $where[] = qsprintf( 110 + $conn, 111 + 'objectPHID IN (%Ls)', 112 + $this->objectPHIDs); 113 + } 114 + 115 + if ($this->repositoryPHIDs !== null) { 116 + $where[] = qsprintf( 117 + $conn, 118 + 'repositoryPHID IN (%Ls)', 119 + $this->repositoryPHIDs); 120 + } 121 + 122 + if ($this->operationStates !== null) { 123 + $where[] = qsprintf( 124 + $conn, 125 + 'operationState IN (%Ls)', 126 + $this->operationStates); 127 + } 128 + 129 + return $where; 130 + } 131 + 132 + }
+148
src/applications/drydock/storage/DrydockRepositoryOperation.php
··· 1 + <?php 2 + 3 + /** 4 + * Represents a request to perform a repository operation like a merge or 5 + * cherry-pick. 6 + */ 7 + final class DrydockRepositoryOperation extends DrydockDAO 8 + implements 9 + PhabricatorPolicyInterface { 10 + 11 + const STATE_WAIT = 'wait'; 12 + const STATE_WORK = 'work'; 13 + const STATE_DONE = 'done'; 14 + const STATE_FAIL = 'fail'; 15 + 16 + protected $authorPHID; 17 + protected $objectPHID; 18 + protected $repositoryPHID; 19 + protected $repositoryTarget; 20 + protected $operationType; 21 + protected $operationState; 22 + protected $properties = array(); 23 + 24 + private $repository = self::ATTACHABLE; 25 + private $object = self::ATTACHABLE; 26 + 27 + public static function initializeNewOperation( 28 + DrydockRepositoryOperationType $op) { 29 + 30 + return id(new DrydockRepositoryOperation()) 31 + ->setOperationState(self::STATE_WAIT) 32 + ->setOperationType($op->getOperationConstant()); 33 + } 34 + 35 + protected function getConfiguration() { 36 + return array( 37 + self::CONFIG_AUX_PHID => true, 38 + self::CONFIG_SERIALIZATION => array( 39 + 'properties' => self::SERIALIZATION_JSON, 40 + ), 41 + self::CONFIG_COLUMN_SCHEMA => array( 42 + 'repositoryTarget' => 'bytes', 43 + 'operationType' => 'text32', 44 + 'operationState' => 'text32', 45 + ), 46 + self::CONFIG_KEY_SCHEMA => array( 47 + 'key_object' => array( 48 + 'columns' => array('objectPHID'), 49 + ), 50 + 'key_repository' => array( 51 + 'columns' => array('repositoryPHID', 'operationState'), 52 + ), 53 + ), 54 + ) + parent::getConfiguration(); 55 + } 56 + 57 + public function generatePHID() { 58 + return PhabricatorPHID::generateNewPHID( 59 + DrydockRepositoryOperationPHIDType::TYPECONST); 60 + } 61 + 62 + public function attachRepository(PhabricatorRepository $repository) { 63 + $this->repository = $repository; 64 + return $this; 65 + } 66 + 67 + public function getRepository() { 68 + return $this->assertAttached($this->repository); 69 + } 70 + 71 + public function attachObject($object) { 72 + $this->object = $object; 73 + return $this; 74 + } 75 + 76 + public function getObject() { 77 + return $this->assertAttached($this->object); 78 + } 79 + 80 + public function getProperty($key, $default = null) { 81 + return idx($this->properties, $key, $default); 82 + } 83 + 84 + public function setProperty($key, $value) { 85 + $this->properties[$key] = $value; 86 + return $this; 87 + } 88 + 89 + public static function getOperationStateIcon($state) { 90 + $map = array( 91 + self::STATE_WAIT => 'fa-clock-o', 92 + self::STATE_WORK => 'fa-refresh blue', 93 + self::STATE_DONE => 'fa-check green', 94 + self::STATE_FAIL => 'fa-times red', 95 + ); 96 + 97 + return idx($map, $state, null); 98 + } 99 + 100 + public static function getOperationStateName($state) { 101 + $map = array( 102 + self::STATE_WAIT => pht('Waiting'), 103 + self::STATE_WORK => pht('Working'), 104 + self::STATE_DONE => pht('Done'), 105 + self::STATE_FAIL => pht('Failed'), 106 + ); 107 + 108 + return idx($map, $state, pht('<Unknown: %s>', $state)); 109 + } 110 + 111 + public function scheduleUpdate() { 112 + PhabricatorWorker::scheduleTask( 113 + 'DrydockRepositoryOperationUpdateWorker', 114 + array( 115 + 'operationPHID' => $this->getPHID(), 116 + ), 117 + array( 118 + 'objectPHID' => $this->getPHID(), 119 + 'priority' => PhabricatorWorker::PRIORITY_ALERTS, 120 + )); 121 + } 122 + 123 + 124 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 125 + 126 + 127 + public function getCapabilities() { 128 + return array( 129 + PhabricatorPolicyCapability::CAN_VIEW, 130 + PhabricatorPolicyCapability::CAN_EDIT, 131 + ); 132 + } 133 + 134 + public function getPolicy($capability) { 135 + return $this->getRepository()->getPolicy($capability); 136 + } 137 + 138 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 139 + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); 140 + } 141 + 142 + public function describeAutomaticCapability($capability) { 143 + return pht( 144 + 'A repository operation inherits the policies of the repository it '. 145 + 'affects.'); 146 + } 147 + 148 + }
+173
src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php
··· 1 + <?php 2 + 3 + final class DrydockRepositoryOperationUpdateWorker 4 + extends DrydockWorker { 5 + 6 + protected function doWork() { 7 + $operation_phid = $this->getTaskDataValue('operationPHID'); 8 + 9 + $hash = PhabricatorHash::digestForIndex($operation_phid); 10 + $lock_key = 'drydock.operation:'.$hash; 11 + 12 + $lock = PhabricatorGlobalLock::newLock($lock_key) 13 + ->lock(1); 14 + 15 + try { 16 + $operation = $this->loadOperation($operation_phid); 17 + $this->handleUpdate($operation); 18 + } catch (Exception $ex) { 19 + $lock->unlock(); 20 + throw $ex; 21 + } 22 + 23 + $lock->unlock(); 24 + } 25 + 26 + 27 + private function handleUpdate(DrydockRepositoryOperation $operation) { 28 + $operation_state = $operation->getOperationState(); 29 + 30 + switch ($operation_state) { 31 + case DrydockRepositoryOperation::STATE_WAIT: 32 + $operation 33 + ->setOperationState(DrydockRepositoryOperation::STATE_WORK) 34 + ->save(); 35 + break; 36 + case DrydockRepositoryOperation::STATE_WORK: 37 + break; 38 + case DrydockRepositoryOperation::STATE_DONE: 39 + case DrydockRepositoryOperation::STATE_FAIL: 40 + // No more processing for these requests. 41 + return; 42 + } 43 + 44 + // TODO: We should probably check for other running operations with lower 45 + // IDs and the same repository target and yield to them here? That is, 46 + // enforce sequential evaluation of operations against the same target so 47 + // that if you land "A" and then land "B", we always finish "A" first. 48 + // For now, just let stuff happen in any order. We can't lease until 49 + // we know we're good to move forward because we might deadlock if we do: 50 + // we're waiting for another operation to complete, and that operation is 51 + // waiting for a lease we're holding. 52 + 53 + try { 54 + $lease = $this->loadWorkingCopyLease($operation); 55 + 56 + $interface = $lease->getInterface( 57 + DrydockCommandInterface::INTERFACE_TYPE); 58 + 59 + // No matter what happens here, destroy the lease away once we're done. 60 + $lease->releaseOnDestruction(true); 61 + 62 + // TODO: Some day, do useful things instead of running `git show`. 63 + list($stdout) = $interface->execx('git show'); 64 + phlog($stdout); 65 + } catch (PhabricatorWorkerYieldException $ex) { 66 + throw $ex; 67 + } catch (Exception $ex) { 68 + $operation 69 + ->setOperationState(DrydockRepositoryOperation::STATE_FAIL) 70 + ->save(); 71 + throw $ex; 72 + } 73 + 74 + $operation 75 + ->setOperationState(DrydockRepositoryOperation::STATE_DONE) 76 + ->save(); 77 + 78 + // TODO: Once we have sequencing, we could awaken the next operation 79 + // against this target after finishing or failing. 80 + } 81 + 82 + private function loadWorkingCopyLease( 83 + DrydockRepositoryOperation $operation) { 84 + $viewer = $this->getViewer(); 85 + 86 + // TODO: This is very similar to leasing in Harbormaster, maybe we can 87 + // share some of the logic? 88 + 89 + $lease_phid = $operation->getProperty('exec.leasePHID'); 90 + if ($lease_phid) { 91 + $lease = id(new DrydockLeaseQuery()) 92 + ->setViewer($viewer) 93 + ->withPHIDs(array($lease_phid)) 94 + ->executeOne(); 95 + if (!$lease) { 96 + throw new PhabricatorWorkerPermanentFailureException( 97 + pht( 98 + 'Lease "%s" could not be loaded.', 99 + $lease_phid)); 100 + } 101 + } else { 102 + $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) 103 + ->getType(); 104 + 105 + $repository = $operation->getRepository(); 106 + 107 + $allowed_phids = $repository->getAutomationBlueprintPHIDs(); 108 + $authorizing_phid = $repository->getPHID(); 109 + 110 + $lease = DrydockLease::initializeNewLease() 111 + ->setResourceType($working_copy_type) 112 + ->setOwnerPHID($operation->getPHID()) 113 + ->setAuthorizingPHID($authorizing_phid) 114 + ->setAllowedBlueprintPHIDs($allowed_phids); 115 + 116 + $map = $this->buildRepositoryMap($operation); 117 + 118 + $lease->setAttribute('repositories.map', $map); 119 + 120 + $task_id = $this->getCurrentWorkerTaskID(); 121 + if ($task_id) { 122 + $lease->setAwakenTaskIDs(array($task_id)); 123 + } 124 + 125 + $operation 126 + ->setProperty('exec.leasePHID', $lease->getPHID()) 127 + ->save(); 128 + 129 + $lease->queueForActivation(); 130 + } 131 + 132 + if ($lease->isActivating()) { 133 + throw new PhabricatorWorkerYieldException(15); 134 + } 135 + 136 + if (!$lease->isActive()) { 137 + throw new PhabricatorWorkerPermanentFailureException( 138 + pht( 139 + 'Lease "%s" never activated.', 140 + $lease->getPHID())); 141 + } 142 + 143 + return $lease; 144 + } 145 + 146 + private function buildRepositoryMap(DrydockRepositoryOperation $operation) { 147 + $repository = $operation->getRepository(); 148 + 149 + $target = $operation->getRepositoryTarget(); 150 + list($type, $name) = explode(':', $target, 2); 151 + switch ($type) { 152 + case 'branch': 153 + $spec = array( 154 + 'branch' => $name, 155 + ); 156 + break; 157 + default: 158 + throw new Exception( 159 + pht( 160 + 'Unknown repository operation target type "%s" (in target "%s").', 161 + $type, 162 + $target)); 163 + } 164 + 165 + $map = array(); 166 + $map[$repository->getCloneName()] = array( 167 + 'phid' => $repository->getPHID(), 168 + 'default' => true, 169 + ) + $spec; 170 + 171 + return $map; 172 + } 173 + }
+15
src/applications/drydock/worker/DrydockWorker.php
··· 36 36 return $resource; 37 37 } 38 38 39 + protected function loadOperation($operation_phid) { 40 + $viewer = $this->getViewer(); 41 + 42 + $operation = id(new DrydockRepositoryOperationQuery()) 43 + ->setViewer($viewer) 44 + ->withPHIDs(array($operation_phid)) 45 + ->executeOne(); 46 + if (!$operation) { 47 + throw new PhabricatorWorkerPermanentFailureException( 48 + pht('No such operation "%s"!', $operation_phid)); 49 + } 50 + 51 + return $operation; 52 + } 53 + 39 54 protected function loadCommands($target_phid) { 40 55 $viewer = $this->getViewer(); 41 56
+11
src/applications/repository/storage/PhabricatorRepository.php
··· 1822 1822 return $this->isGit(); 1823 1823 } 1824 1824 1825 + public function canPerformAutomation() { 1826 + if (!$this->supportsAutomation()) { 1827 + return false; 1828 + } 1829 + 1830 + if (!$this->getAutomationBlueprintPHIDs()) { 1831 + return false; 1832 + } 1833 + 1834 + return true; 1835 + } 1825 1836 1826 1837 public function getAutomationBlueprintPHIDs() { 1827 1838 if (!$this->supportsAutomation()) {