diff --git a/resources/sql/autopatches/20142805.chronicle.1.sql b/resources/sql/autopatches/20142805.chronicle.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20142805.chronicle.1.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_chronicle.chronicle_trigger ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(254) NOT NULL COLLATE utf8_bin, + epoch INT UNSIGNED NOT NULL, + epochRepeat INT UNSIGNED NOT NULL, + actionClass VARCHAR(254) NOT NULL COLLATE utf8_bin, + actionData VARCHAR(4000) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; 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 @@ -112,6 +112,18 @@ 'CelerityResourcesOnDisk' => 'infrastructure/celerity/resources/CelerityResourcesOnDisk.php', 'CeleritySpriteGenerator' => 'infrastructure/celerity/CeleritySpriteGenerator.php', 'CelerityStaticResourceResponse' => 'infrastructure/celerity/CelerityStaticResourceResponse.php', + 'ChronicleAction' => 'applications/chronicle/action/ChronicleAction.php', + 'ChronicleCapabilityManagePlans' => 'applications/chronicle/capability/ChronicleCapabilityManageTriggers.php', + 'ChronicleController' => 'applications/chronicle/controller/ChronicleController.php', + 'ChronicleDAO' => 'applications/chronicle/storage/ChronicleDAO.php', + 'ChroniclePHIDTypeTrigger' => 'applications/chronicle/phid/ChroniclePHIDTypeTrigger.php', + 'ChronicleStartHarbormasterBuildAction' => 'applications/chronicle/action/ChronicleStartHarbormasterBuildAction.php', + 'ChronicleTrigger' => 'applications/chronicle/storage/ChronicleTrigger.php', + 'ChronicleTriggerAddController' => 'applications/chronicle/controller/ChronicleTriggerAddController.php', + 'ChronicleTriggerController' => 'applications/chronicle/controller/ChronicleTriggerController.php', + 'ChronicleTriggerListController' => 'applications/chronicle/controller/ChronicleTriggerListController.php', + 'ChronicleTriggerQuery' => 'applications/chronicle/query/ChronicleTriggerQuery.php', + 'ChronicleTriggerSearchEngine' => 'applications/chronicle/query/ChronicleTriggerSearchEngine.php', 'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php', 'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php', 'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php', @@ -1111,6 +1123,7 @@ 'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php', 'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php', 'PhabricatorApplicationChatLog' => 'applications/chatlog/applications/PhabricatorApplicationChatLog.php', + 'PhabricatorApplicationChronicle' => 'applications/chronicle/application/PhabricatorApplicationChronicle.php', 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 'PhabricatorApplicationConfig' => 'applications/config/application/PhabricatorApplicationConfig.php', 'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php', @@ -2786,6 +2799,21 @@ 'CelerityResourceGraph' => 'AbstractDirectedGraph', 'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase', 'CelerityResourcesOnDisk' => 'CelerityPhysicalResources', + 'ChronicleCapabilityManagePlans' => 'PhabricatorPolicyCapability', + 'ChronicleController' => 'PhabricatorController', + 'ChronicleDAO' => 'PhabricatorLiskDAO', + 'ChroniclePHIDTypeTrigger' => 'PhabricatorPHIDType', + 'ChronicleStartHarbormasterBuildAction' => 'ChronicleAction', + 'ChronicleTrigger' => + array( + 0 => 'ChronicleDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'ChronicleTriggerAddController' => 'ChronicleTriggerController', + 'ChronicleTriggerController' => 'ChronicleController', + 'ChronicleTriggerListController' => 'ChronicleTriggerController', + 'ChronicleTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'ChronicleTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ConduitAPIMethod' => array( 0 => 'Phobject', @@ -3866,6 +3894,7 @@ 'PhabricatorApplicationAuth' => 'PhabricatorApplication', 'PhabricatorApplicationCalendar' => 'PhabricatorApplication', 'PhabricatorApplicationChatLog' => 'PhabricatorApplication', + 'PhabricatorApplicationChronicle' => 'PhabricatorApplication', 'PhabricatorApplicationConduit' => 'PhabricatorApplication', 'PhabricatorApplicationConfig' => 'PhabricatorApplication', 'PhabricatorApplicationConfigOptions' => 'Phobject', diff --git a/src/applications/chronicle/action/ChronicleAction.php b/src/applications/chronicle/action/ChronicleAction.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/action/ChronicleAction.php @@ -0,0 +1,75 @@ +setAncestorClass('ChronicleAction') + ->loadObjects(); + } + + public static function getImplementation($class) { + $base = idx(self::getImplementations(), $class); + + if ($base) { + return (clone $base); + } + + return null; + } + + public static function requireImplementation($class) { + if (!$class) { + throw new Exception(pht('No implementation is specified!')); + } + + $implementation = self::getImplementation($class); + if (!$implementation) { + throw new Exception(pht('No such implementation "%s" exists!', $class)); + } + + return $implementation; + } + + /** + * The name of the implementation. + */ + abstract public function getName(); + + /** + * The description of the implementation. + */ + public function getDescription() { + return ''; + } + + /** + * Run the build target against the specified build. + */ + abstract public function execute(); + + /** + * Gets the settings for this build step. + */ + public function getSettings() { + return $this->settings; + } + + public function getSetting($key, $default = null) { + return idx($this->settings, $key, $default); + } + + /** + * Loads the settings for this build step implementation from a build + * step or target. + */ + public final function loadSettings($action_data) { + $this->settings = $action_data; + return $this; + } + + public function getFieldSpecifications() { + return array(); + } + +} diff --git a/src/applications/chronicle/action/ChronicleStartHarbormasterBuildAction.php b/src/applications/chronicle/action/ChronicleStartHarbormasterBuildAction.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/action/ChronicleStartHarbormasterBuildAction.php @@ -0,0 +1,38 @@ + array( + 'name' => pht('Repository Callsign'), + 'type' => 'text', + 'required' => true, + ), + 'branch' => array( + 'name' => pht('Branch or Reference'), + 'type' => 'text', + 'required' => true + ), + 'planid' => array( + 'name' => pht('Build Plan ID'), + 'type' => 'text', + 'required' => true, + ), + ); + } + +} diff --git a/src/applications/chronicle/application/PhabricatorApplicationChronicle.php b/src/applications/chronicle/application/PhabricatorApplicationChronicle.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/application/PhabricatorApplicationChronicle.php @@ -0,0 +1,59 @@ + array( + '(?:query/(?P[^/]+)/)?' + => 'ChronicleTriggerListController', + 'trigger/' => array( + 'add/(?:(?P\d+)/)?' => 'ChronicleTriggerAddController', + // 'new/(?P\d+)/(?P[^/]+)/' + // => 'ChronicleTriggerEditController', + // 'edit/(?:(?P\d+)/)?' => 'ChronicleTriggerEditController', + // 'view/(?:(?P\d+)/)?' => 'ChronicleTriggerViewController', + // 'delete/(?:(?P\d+)/)?' => 'ChronicleTriggerDeleteController', + ), + ), + ); + } + + public function getCustomCapabilities() { + return array( + ChronicleCapabilityManagePlans::CAPABILITY => array( + 'caption' => pht('Can create and manage triggers.'), + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), + ); + } + + public function shouldAppearInLaunchView() { + return false; + } + +} diff --git a/src/applications/chronicle/capability/ChronicleCapabilityManageTriggers.php b/src/applications/chronicle/capability/ChronicleCapabilityManageTriggers.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/capability/ChronicleCapabilityManageTriggers.php @@ -0,0 +1,21 @@ +addAction( + id(new PHUIListItemView()) + ->setName(pht('New Trigger')) + ->setHref($this->getApplicationURI('trigger/add/')) + ->setIcon('fa-plus-square') + ->setWorkflow(true)); + + return $crumbs; + } + +} diff --git a/src/applications/chronicle/controller/ChronicleTriggerAddController.php b/src/applications/chronicle/controller/ChronicleTriggerAddController.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/controller/ChronicleTriggerAddController.php @@ -0,0 +1,57 @@ +getRequest(); + $viewer = $request->getUser(); + + $this->requireApplicationCapability( + ChronicleCapabilityManagePlans::CAPABILITY); + + $cancel_uri = $this->getApplicationURI('/'); + + $errors = array(); + if ($request->isFormPost()) { + + $errors[] = pht( + 'Not yet implemented.'); + + $class = $request->getStr('class'); + if (!ChronicleAction::getImplementation($class)) { + $errors[] = pht( + 'Choose the type of build step you want to add.'); + } + if (!$errors) { + $new_uri = $this->getApplicationURI("trigger/new/{$class}/"); + return id(new AphrontRedirectResponse())->setURI($new_uri); + } + } + + $control = id(new AphrontFormRadioButtonControl()) + ->setName('class'); + + $all = ChronicleAction::getImplementations(); + foreach ($all as $class => $implementation) { + $control->addButton( + $class, + $implementation->getName(), + $implementation->getDescription()); + } + + if ($errors) { + $errors = id(new AphrontErrorView()) + ->setErrors($errors); + } + + return $this->newDialog() + ->setTitle(pht('Create New Trigger')) + ->addSubmitButton(pht('Create New Trigger')) + ->addCancelButton($cancel_uri) + ->appendChild($errors) + ->appendParagraph(pht('Choose a type of build step to add:')) + ->appendChild($control); + } + +} diff --git a/src/applications/chronicle/controller/ChronicleTriggerController.php b/src/applications/chronicle/controller/ChronicleTriggerController.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/controller/ChronicleTriggerController.php @@ -0,0 +1,5 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new ChronicleTriggerSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView($for_app = false) { + $user = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new ChronicleTriggerSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationMenu() { + return $this->buildSideNavView(true)->getMenu(); + } + +} diff --git a/src/applications/chronicle/phid/ChroniclePHIDTypeTrigger.php b/src/applications/chronicle/phid/ChroniclePHIDTypeTrigger.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/phid/ChroniclePHIDTypeTrigger.php @@ -0,0 +1,45 @@ +withPHIDs($phids) + ->needBuildableHandles(true); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $trigger = $objects[$phid]; + + $id = $trigger->getID(); + $name = $trigger->getName(); + + $handle->setURI("/chronicle/trigger/view/{$id}"); + $handle->setName("Chronicle Trigger {$id}"); + $handle->setFullName("Chronicle Trigger {$id}: ".$name); + } + } + +} diff --git a/src/applications/chronicle/query/ChronicleTriggerQuery.php b/src/applications/chronicle/query/ChronicleTriggerQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/query/ChronicleTriggerQuery.php @@ -0,0 +1,60 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + $table = new ChronicleTrigger(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationChronicle'; + } + +} diff --git a/src/applications/chronicle/query/ChronicleTriggerSearchEngine.php b/src/applications/chronicle/query/ChronicleTriggerSearchEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/query/ChronicleTriggerSearchEngine.php @@ -0,0 +1,80 @@ + pht('All Triggers'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $triggers, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($triggers, 'ChronicleTrigger'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setCards(true); + foreach ($triggers as $trigger) { + $id = $trigger->getID(); + + $item = id(new PHUIObjectItemView()) + ->setHeader(pht('Chronicle Trigger %d', $id)); + + if ($id) { + $item->setHref("/chronicle/trigger/view/{$id}"); + } + + $list->addItem($item); + + } + + return $list; + } +} diff --git a/src/applications/chronicle/storage/ChronicleDAO.php b/src/applications/chronicle/storage/ChronicleDAO.php new file mode 100644 --- /dev/null +++ b/src/applications/chronicle/storage/ChronicleDAO.php @@ -0,0 +1,9 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'actionData' => self::SERIALIZATION_JSON, + ) + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + ChroniclePHIDTypeTrigger::TYPECONST); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -119,6 +119,7 @@ 'db.phragment' => array(), 'db.dashboard' => array(), 'db.system' => array(), + 'db.chronicle' => array(), '0000.legacy.sql' => array( 'legacy' => 0, ),