diff --git a/resources/sql/autopatches/20150605.diviner.edges.sql b/resources/sql/autopatches/20150605.diviner.edges.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150605.diviner.edges.sql @@ -0,0 +1,17 @@ +CREATE TABLE {$NAMESPACE}_diviner.edge ( + src VARBINARY(64) NOT NULL, + type INT UNSIGNED NOT NULL, + dst VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + + PRIMARY KEY (src, type, dst), + KEY src (src, type, dateCreated, seq), + UNIQUE KEY key_dst (dst, type, src) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_diviner.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150605.diviner.editPolicy.sql b/resources/sql/autopatches/20150605.diviner.editPolicy.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150605.diviner.editPolicy.sql @@ -0,0 +1,6 @@ +ALTER TABLE {$NAMESPACE}_diviner.diviner_livebook + ADD COLUMN editPolicy VARBINARY(64) NOT NULL AFTER viewPolicy; + +UPDATE {$NAMESPACE}_diviner.diviner_livebook + SET editPolicy = 'admin' + WHERE editPolicy = ''; diff --git a/resources/sql/autopatches/20150605.diviner.xaction.sql b/resources/sql/autopatches/20150605.diviner.xaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150605.diviner.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_diviner.diviner_livebooktransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY key_phid (phid), + KEY key_object (objectPHID) +) 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 @@ -649,19 +649,25 @@ 'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php', 'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php', 'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php', + 'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php', 'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php', 'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php', 'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php', 'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.php', 'DivinerController' => 'applications/diviner/controller/DivinerController.php', 'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php', + 'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php', 'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php', + 'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php', 'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php', 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php', 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php', 'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php', + 'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php', + 'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php', + 'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php', 'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php', 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php', 'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php', @@ -671,6 +677,7 @@ 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php', + 'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php', 'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php', 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', @@ -4010,13 +4017,16 @@ 'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 'DivinerAtomizer' => 'Phobject', 'DivinerBookController' => 'DivinerController', + 'DivinerBookEditController' => 'DivinerController', 'DivinerBookItemView' => 'AphrontTagView', 'DivinerBookPHIDType' => 'PhabricatorPHIDType', 'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 'DivinerController' => 'PhabricatorController', 'DivinerDAO' => 'PhabricatorLiskDAO', + 'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DivinerDefaultRenderer' => 'DivinerRenderer', + 'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DivinerDiskCache' => 'Phobject', 'DivinerFileAtomizer' => 'DivinerAtomizer', 'DivinerFindController' => 'DivinerController', @@ -4025,8 +4035,13 @@ 'DivinerLiveBook' => array( 'DivinerDAO', 'PhabricatorPolicyInterface', + 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorApplicationTransactionInterface', ), + 'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor', + 'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction', + 'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DivinerLivePublisher' => 'DivinerPublisher', 'DivinerLiveSymbol' => array( 'DivinerDAO', @@ -4041,6 +4056,7 @@ 'DivinerPublisher' => 'Phobject', 'DivinerRenderer' => 'Phobject', 'DivinerReturnTableView' => 'AphrontTagView', + 'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DivinerSectionView' => 'AphrontTagView', 'DivinerStaticPublisher' => 'DivinerPublisher', 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', diff --git a/src/applications/diviner/application/PhabricatorDivinerApplication.php b/src/applications/diviner/application/PhabricatorDivinerApplication.php --- a/src/applications/diviner/application/PhabricatorDivinerApplication.php +++ b/src/applications/diviner/application/PhabricatorDivinerApplication.php @@ -39,6 +39,7 @@ 'find/' => 'DivinerFindController', ), '/book/(?P[^/]+)/' => 'DivinerBookController', + '/book/(?P[^/]+)/edit/' => 'DivinerBookEditController', '/book/'. '(?P[^/]+)/'. '(?P[^/]+)/'. @@ -52,6 +53,18 @@ return self::GROUP_UTILITIES; } + protected function getCustomCapabilities() { + return array( + DivinerDefaultViewCapability::CAPABILITY => array( + 'template' => DivinerBookPHIDType::TYPECONST, + ), + DivinerDefaultEditCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + 'template' => DivinerBookPHIDType::TYPECONST, + ), + ); + } + public function getRemarkupRules() { return array( new DivinerSymbolRemarkupRule(), diff --git a/src/applications/diviner/atomizer/DivinerArticleAtomizer.php b/src/applications/diviner/atomizer/DivinerArticleAtomizer.php --- a/src/applications/diviner/atomizer/DivinerArticleAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerArticleAtomizer.php @@ -20,7 +20,7 @@ $atom->setDocblockMetaValue('title', $title); } - // If the article has no @name, use the filename after stripping any + // If the article has no `@name`, use the filename after stripping any // extension. $name = idx($meta, 'name'); if (!$name) { diff --git a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php --- a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php @@ -151,9 +151,9 @@ if (count($docs) < count($params)) { $atom->addWarning( pht( - 'This call takes %d parameters, but only %d are documented.', - count($params), - count($docs))); + 'This call takes %s parameter(s), but only %s are documented.', + new PhutilNumber(count($params)), + new PhutilNumber(count($docs)))); } } @@ -212,13 +212,13 @@ if (preg_match('/@(return|param|task|author)/', $value, $matches)) { $atom->addWarning( pht( - 'Atom "%s" is preceded by a comment containing "@%s", but the '. - 'comment is not a documentation comment. Documentation '. - 'comments must begin with "%s", followed by a newline. Did '. + 'Atom "%s" is preceded by a comment containing `%s`, but '. + 'the comment is not a documentation comment. Documentation '. + 'comments must begin with `%s`, followed by a newline. Did '. 'you mean to use a documentation comment? (As the comment is '. 'not a documentation comment, it will be ignored.)', $atom->getName(), - $matches[1], + '@'.$matches[1], '/**')); } } @@ -248,8 +248,8 @@ if ($matches[1] !== $name) { $atom->addWarning( pht( - 'Parameter "%s" is named "%s" in the documentation. The '. - 'documentation may be out of date.', + 'Parameter "%s" is named "%s" in the documentation. '. + 'The documentation may be out of date.', $name, $matches[1])); } @@ -292,8 +292,8 @@ if ($return) { $atom->addWarning( pht( - 'Method %s has explicitly documented %s. The %s method always '. - 'returns %s. Diviner documents this implicitly.', + 'Method `%s` has explicitly documented `%s`. The `%s` method '. + 'always returns `%s`. Diviner documents this implicitly.', '__construct()', '@return', '__construct()', diff --git a/src/applications/diviner/cache/DivinerAtomCache.php b/src/applications/diviner/cache/DivinerAtomCache.php --- a/src/applications/diviner/cache/DivinerAtomCache.php +++ b/src/applications/diviner/cache/DivinerAtomCache.php @@ -18,6 +18,7 @@ public function delete() { parent::delete(); + $this->fileHashMap = null; $this->atomMap = null; $this->atoms = array(); diff --git a/src/applications/diviner/cache/DivinerDiskCache.php b/src/applications/diviner/cache/DivinerDiskCache.php --- a/src/applications/diviner/cache/DivinerDiskCache.php +++ b/src/applications/diviner/cache/DivinerDiskCache.php @@ -26,8 +26,8 @@ * Convert a long-form hash key like `ccbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaN` into * a shortened directory form, like `cc/bb/aaaaaaaaN`. In conjunction with * @{class:PhutilDirectoryKeyValueCache}, this gives us nice directories - * inside .divinercache instead of a million hash files with huge names at - * top level. + * inside `.divinercache` instead of a million hash files with huge names at + * the top level. */ protected function getHashKey($hash) { return implode( diff --git a/src/applications/diviner/cache/DivinerPublishCache.php b/src/applications/diviner/cache/DivinerPublishCache.php --- a/src/applications/diviner/cache/DivinerPublishCache.php +++ b/src/applications/diviner/cache/DivinerPublishCache.php @@ -45,6 +45,7 @@ /* -( Index )-------------------------------------------------------------- */ + public function getIndex() { if ($this->index === null) { $this->index = $this->getCache()->getKey('index', array()); diff --git a/src/applications/diviner/capability/DivinerDefaultEditCapability.php b/src/applications/diviner/capability/DivinerDefaultEditCapability.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/capability/DivinerDefaultEditCapability.php @@ -0,0 +1,11 @@ +bookName = $data['book']; - $this->atomType = $data['type']; - $this->atomName = $data['name']; - $this->atomContext = nonempty(idx($data, 'context'), null); - $this->atomIndex = nonempty(idx($data, 'index'), null); - } - - public function processRequest() { - $request = $this->getRequest(); + public function handleRequest(AphrontRequest $request) { $viewer = $request->getUser(); + $book_name = $request->getURIData('book'); + $atom_type = $request->getURIData('type'); + $atom_name = $request->getURIData('name'); + $atom_context = nonempty($request->getURIData('context'), null); + $atom_index = nonempty($request->getURIData('index'), null); + require_celerity_resource('diviner-shared-css'); $book = id(new DivinerBookQuery()) ->setViewer($viewer) - ->withNames(array($this->bookName)) + ->withNames(array($book_name)) ->executeOne(); if (!$book) { @@ -38,10 +29,10 @@ $symbol = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) - ->withTypes(array($this->atomType)) - ->withNames(array($this->atomName)) - ->withContexts(array($this->atomContext)) - ->withIndexes(array($this->atomIndex)) + ->withTypes(array($atom_type)) + ->withNames(array($atom_name)) + ->withContexts(array($atom_context)) + ->withIndexes(array($atom_index)) ->withIsDocumentable(true) ->needAtoms(true) ->needExtends(true) @@ -75,7 +66,7 @@ ->setName(DivinerAtom::getAtomTypeNameString( $atom ? $atom->getType() : $symbol->getType()))); - $properties = id(new PHUIPropertyListView()); + $properties = new PHUIPropertyListView(); $group = $atom ? $atom->getProperty('group') : $symbol->getGroupName(); if ($group) { @@ -134,9 +125,7 @@ $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) - ->appendChild( - pht( - 'This atom no longer exists.'))); + ->appendChild(pht('This atom no longer exists.'))); } if ($atom) { @@ -304,7 +293,6 @@ pht('Implements'), phutil_implode_html(phutil_tag('br'), $items)); } - } private function renderAtomTag(DivinerLiveSymbol $symbol) { @@ -471,6 +459,7 @@ private function renderFullSignature( DivinerLiveSymbol $symbol, $is_link = false) { + switch ($symbol->getType()) { case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_INTERFACE: diff --git a/src/applications/diviner/controller/DivinerAtomListController.php b/src/applications/diviner/controller/DivinerAtomListController.php --- a/src/applications/diviner/controller/DivinerAtomListController.php +++ b/src/applications/diviner/controller/DivinerAtomListController.php @@ -2,20 +2,15 @@ final class DivinerAtomListController extends DivinerController { - private $key; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->key = idx($data, 'key', 'all'); - } + public function handleRequest(AphrontRequest $request) { + $query_key = $request->getURIData('key'); - public function processRequest() { - $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->key) + ->setQueryKey($query_key) ->setSearchEngine(new DivinerAtomSearchEngine()) ->setNavigation($this->buildSideNavView()); diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php --- a/src/applications/diviner/controller/DivinerBookController.php +++ b/src/applications/diviner/controller/DivinerBookController.php @@ -2,41 +2,46 @@ final class DivinerBookController extends DivinerController { - private $bookName; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->bookName = $data['book']; - } + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + $book_name = $request->getURIData('book'); $book = id(new DivinerBookQuery()) ->setViewer($viewer) - ->withNames(array($this->bookName)) + ->withNames(array($book_name)) ->executeOne(); if (!$book) { return new Aphront404Response(); } + $actions = $this->buildActionView($viewer, $book); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); - $crumbs->addTextCrumb( $book->getShortTitle(), '/book/'.$book->getName().'/'); + $action_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Actions')) + ->setHref('#') + ->setIconFont('fa-bars') + ->addClass('phui-mobile-menu') + ->setDropdownMenu($actions); + $header = id(new PHUIHeaderView()) ->setHeader($book->getTitle()) ->setUser($viewer) ->setPolicyObject($book) - ->setEpoch($book->getDateModified()); + ->setEpoch($book->getDateModified()) + ->addActionLink($action_button); $document = new PHUIDocumentView(); $document->setHeader($header); @@ -100,4 +105,28 @@ )); } + private function buildActionView( + PhabricatorUser $user, + DivinerLiveBook $book) { + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $user, + $book, + PhabricatorPolicyCapability::CAN_EDIT); + + $action_view = id(new PhabricatorActionListView()) + ->setUser($user) + ->setObject($book) + ->setObjectURI($this->getRequest()->getRequestURI()); + + $action_view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Book')) + ->setIcon('fa-pencil') + ->setHref('/book/'.$book->getName().'/edit/') + ->setDisabled(!$can_edit)); + + return $action_view; + } + } diff --git a/src/applications/diviner/controller/DivinerBookEditController.php b/src/applications/diviner/controller/DivinerBookEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/controller/DivinerBookEditController.php @@ -0,0 +1,117 @@ +getViewer(); + + $book_name = $request->getURIData('book'); + + $book = id(new DivinerBookQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needProjectPHIDs(true) + ->withNames(array($book_name)) + ->executeOne(); + + if (!$book) { + return new Aphront404Response(); + } + + $view_uri = '/book/'.$book->getName().'/'; + + if ($request->isFormPost()) { + $v_projects = $request->getArr('projectPHIDs'); + $v_view = $request->getStr('viewPolicy'); + $v_edit = $request->getStr('editPolicy'); + + $xactions = array(); + $xactions[] = id(new DivinerLiveBookTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setNewValue( + array( + '=' => array_fuse($v_projects), + )); + $xactions[] = id(new DivinerLiveBookTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($v_view); + $xactions[] = id(new DivinerLiveBookTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit); + + id(new DivinerLiveBookEditor()) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setActor($viewer) + ->applyTransactions($book, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Edit Basics')); + + $title = pht('Edit %s', $book->getTitle()); + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($book) + ->execute(); + $view_capability = PhabricatorPolicyCapability::CAN_VIEW; + $edit_capability = PhabricatorPolicyCapability::CAN_EDIT; + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setDatasource(new PhabricatorProjectDatasource()) + ->setName('projectPHIDs') + ->setLabel(pht('Projects')) + ->setValue($book->getProjectPHIDs())) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($book) + ->setCapability($view_capability) + ->setPolicies($policies) + ->setCaption($book->describeAutomaticCapability($view_capability))) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($book) + ->setCapability($edit_capability) + ->setPolicies($policies) + ->setCaption($book->describeAutomaticCapability($edit_capability))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save')) + ->addCancelButton($view_uri)); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setForm($form); + + $timeline = $this->buildTransactionTimeline( + $book, + new DivinerLiveBookTransactionQuery()); + $timeline->setShouldTerminate(true); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + $timeline, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/diviner/controller/DivinerController.php b/src/applications/diviner/controller/DivinerController.php --- a/src/applications/diviner/controller/DivinerController.php +++ b/src/applications/diviner/controller/DivinerController.php @@ -3,19 +3,15 @@ abstract class DivinerController extends PhabricatorController { protected function buildSideNavView() { - $menu = $this->buildMenu(); + $menu = $this->buildApplicationMenu(); return AphrontSideNavFilterView::newFromMenu($menu); } public function buildApplicationMenu() { - return $this->buildMenu(); - } - - private function buildMenu() { $menu = new PHUIListView(); id(new DivinerAtomSearchEngine()) - ->setViewer($this->getRequest()->getUser()) + ->setViewer($this->getRequest()->getViewer()) ->addNavigationItems($menu); return $menu; @@ -24,12 +20,8 @@ protected function renderAtomList(array $symbols) { assert_instances_of($symbols, 'DivinerLiveSymbol'); - $request = $this->getRequest(); - $user = $request->getUser(); - $list = array(); foreach ($symbols as $symbol) { - switch ($symbol->getType()) { case DivinerAtom::TYPE_FUNCTION: $title = $symbol->getTitle().'()'; @@ -43,8 +35,7 @@ ->setTitle($title) ->setHref($symbol->getURI()) ->setSubtitle($symbol->getSummary()) - ->setType(DivinerAtom::getAtomTypeNameString( - $symbol->getType())); + ->setType(DivinerAtom::getAtomTypeNameString($symbol->getType())); $list[] = $item; } diff --git a/src/applications/diviner/controller/DivinerFindController.php b/src/applications/diviner/controller/DivinerFindController.php --- a/src/applications/diviner/controller/DivinerFindController.php +++ b/src/applications/diviner/controller/DivinerFindController.php @@ -6,11 +6,10 @@ return true; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); - $book_name = $request->getStr('book'); + $book_name = $request->getStr('book'); $query_text = $request->getStr('name'); $book = null; @@ -19,6 +18,7 @@ ->setViewer($viewer) ->withNames(array($book_name)) ->executeOne(); + if (!$book) { return new Aphront404Response(); } @@ -70,8 +70,8 @@ ->setTitle(pht('Documentation Not Found')) ->appendChild( pht( - 'Unable to find the specified documentation. You may have '. - 'followed a bad or outdated link.')) + 'Unable to find the specified documentation. '. + 'You may have followed a bad or outdated link.')) ->addCancelButton($not_found_uri, pht('Read More Documentation')); return id(new AphrontDialogResponse())->setDialog($dialog); diff --git a/src/applications/diviner/controller/DivinerMainController.php b/src/applications/diviner/controller/DivinerMainController.php --- a/src/applications/diviner/controller/DivinerMainController.php +++ b/src/applications/diviner/controller/DivinerMainController.php @@ -6,9 +6,8 @@ return true; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); $books = id(new DivinerBookQuery()) ->setViewer($viewer) @@ -31,10 +30,10 @@ ->setHeader(pht('Documentation Books')) ->addActionLink($query_button); - $document = new PHUIDocumentView(); - $document->setHeader($header); - $document->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS); - $document->addClass('diviner-view'); + $document = id(new PHUIDocumentView()) + ->setHeader($header) + ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) + ->addClass('diviner-view'); if ($books) { $books = msort($books, 'getTitle'); @@ -54,24 +53,20 @@ ->appendChild($list); $document->appendChild($list); - } else { $text = pht( - "(NOTE) **Looking for Phabricator documentation?** If you're looking ". - "for help and information about Phabricator, you can ". - "[[ https://secure.phabricator.com/diviner/ | browse the public ". - "Phabricator documentation ]] on the live site.\n\n". - "Diviner is the documentation generator used to build the Phabricator ". - "documentation.\n\n". + "(NOTE) **Looking for Phabricator documentation?** ". + "If you're looking for help and information about Phabricator, ". + "you can [[https://secure.phabricator.com/diviner/ | ". + "browse the public Phabricator documentation]] on the live site.\n\n". + "Diviner is the documentation generator used to build the ". + "Phabricator documentation.\n\n". "You haven't generated any Diviner documentation books yet, so ". "there's nothing to show here. If you'd like to generate your own ". "local copy of the Phabricator documentation and have it appear ". "here, run this command:\n\n". - " phabricator/ $ ./bin/diviner generate\n\n". - "Right now, Diviner isn't very useful for generating documentation ". - "for projects other than Phabricator. If you're interested in using ". - "it in your own projects, leave feedback for us on ". - "[[ https://secure.phabricator.com/T4558 | T4558 ]]."); + " %s\n\n", + 'phabricator/ $ ./bin/diviner generate'); $text = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($text), diff --git a/src/applications/diviner/editor/DivinerLiveBookEditor.php b/src/applications/diviner/editor/DivinerLiveBookEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/editor/DivinerLiveBookEditor.php @@ -0,0 +1,23 @@ +book) { $book_name = $this->getConfig('name'); - $book = id(new DivinerLiveBook())->loadOneWhere('name = %s', $book_name); + $book = id(new DivinerLiveBook())->loadOneWhere( + 'name = %s', + $book_name); + if (!$book) { $book = id(new DivinerLiveBook()) ->setName($book_name) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->save(); } @@ -144,7 +148,6 @@ ->setContent(null) ->save(); } - } } diff --git a/src/applications/diviner/publisher/DivinerPublisher.php b/src/applications/diviner/publisher/DivinerPublisher.php --- a/src/applications/diviner/publisher/DivinerPublisher.php +++ b/src/applications/diviner/publisher/DivinerPublisher.php @@ -133,10 +133,20 @@ $created = array_keys($created); } - echo pht('Deleting %d documents.', count($deleted))."\n"; + $console = PhutilConsole::getConsole(); + + $console->writeOut( + "%s\n", + pht( + 'Deleting %s document(s).', + new PhutilNumber(count($deleted)))); $this->deleteDocumentsByHash($deleted); - echo pht('Creating %d documents.', count($created))."\n"; + $console->writeOut( + "%s\n", + pht( + 'Creating %s document(s).', + new PhutilNumber(count($created)))); $this->createDocumentsByHash($created); } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -79,7 +79,6 @@ return $this; } - /** * Include or exclude "ghosts", which are symbols which used to exist but do * not exist currently (for example, a function which existed in an older @@ -137,6 +136,7 @@ foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { + $this->didRejectResult($atom); unset($atoms[$key]); continue; } @@ -158,12 +158,9 @@ // Load all of the symbols this symbol extends, recursively. Commonly, // this means all the ancestor classes and interfaces it extends and // implements. - if ($this->needExtends) { - // First, load all the matching symbols by name. This does 99% of the // work in most cases, assuming things are named at all reasonably. - $names = array(); foreach ($atoms as $atom) { if (!$atom->getAtom()) { @@ -303,6 +300,7 @@ if ($this->titles) { $hashes = array(); + foreach ($this->titles as $title) { $slug = DivinerAtomRef::normalizeTitleString($title); $hash = PhabricatorHash::digestForIndex($slug); @@ -318,6 +316,7 @@ if ($this->contexts) { $with_null = false; $contexts = $this->contexts; + foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); @@ -373,10 +372,9 @@ } if ($this->nameContains) { - // NOTE: This CONVERT() call makes queries case-insensitive, since the - // column has binary collation. Eventually, this should move into + // NOTE: This `CONVERT()` call makes queries case-insensitive, since + // the column has binary collation. Eventually, this should move into // fulltext. - $where[] = qsprintf( $conn_r, 'CONVERT(name USING utf8) LIKE %~', @@ -388,7 +386,6 @@ return $this->formatWhereClause($where); } - /** * Walk a list of atoms and collect all the node hashes of the atoms' * children. When recursing, also walk up the tree and collect children of @@ -413,6 +410,7 @@ foreach ($child_hashes as $hash) { $hashes[$hash] = $hash; } + if ($recurse_up) { $hashes += $this->getAllChildHashes($symbol->getExtends(), true); } @@ -421,7 +419,6 @@ return $hashes; } - /** * Attach child atoms to existing atoms. In recursive mode, also attach child * atoms to atoms that these atoms extend. @@ -452,7 +449,9 @@ $symbol_children[] = $children[$hash]; } } + $symbol->attachChildren($symbol_children); + if ($recurse_up) { $this->attachAllChildren($symbol->getExtends(), $children, true); } diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php --- a/src/applications/diviner/query/DivinerBookQuery.php +++ b/src/applications/diviner/query/DivinerBookQuery.php @@ -6,6 +6,8 @@ private $phids; private $names; + private $needProjectPHIDs; + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -21,6 +23,11 @@ return $this; } + public function needProjectPHIDs($need_phids) { + $this->needProjectPHIDs = $need_phids; + return $this; + } + protected function loadPage() { $table = new DivinerLiveBook(); $conn_r = $table->establishConnection('r'); @@ -36,6 +43,30 @@ return $table->loadAllFromArray($data); } + protected function didFilterPage(array $books) { + assert_instances_of($books, 'DivinerLiveBook'); + + if ($this->needProjectPHIDs) { + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(mpull($books, 'getPHID')) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $edge_query->execute(); + + foreach ($books as $book) { + $project_phids = $edge_query->getDestinationPHIDs( + array( + $book->getPHID(), + )); + $book->attachProjectPHIDs($project_phids); + } + } + + return $books; + } + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); diff --git a/src/applications/diviner/query/DivinerLiveBookTransactionQuery.php b/src/applications/diviner/query/DivinerLiveBookTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/query/DivinerLiveBookTransactionQuery.php @@ -0,0 +1,10 @@ +getBook() != $this->getConfig('name')) { - // If the ref is from a different book, we can't normalize it. Just return - // it as-is if it has enough information to resolve. + // If the ref is from a different book, we can't normalize it. + // Just return it as-is if it has enough information to resolve. if ($ref->getName() && $ref->getType()) { return $ref; } else { @@ -260,5 +260,4 @@ $ref->getTitle()); } - } diff --git a/src/applications/diviner/search/DivinerBookSearchIndexer.php b/src/applications/diviner/search/DivinerBookSearchIndexer.php --- a/src/applications/diviner/search/DivinerBookSearchIndexer.php +++ b/src/applications/diviner/search/DivinerBookSearchIndexer.php @@ -18,6 +18,11 @@ PhabricatorSearchDocumentFieldType::FIELD_BODY, $book->getPreface()); + $this->indexTransactions( + $doc, + new DivinerLiveBookTransactionQuery(), + array($phid)); + return $doc; } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -3,12 +3,17 @@ final class DivinerLiveBook extends DivinerDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorProjectInterface, + PhabricatorDestructibleInterface, + PhabricatorApplicationTransactionInterface { protected $name; protected $viewPolicy; + protected $editPolicy; protected $configurationData = array(); + private $projectPHIDs = self::ATTACHABLE; + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -63,28 +68,47 @@ return idx($spec, 'name', $group); } + public function attachProjectPHIDs(array $project_phids) { + $this->projectPHIDs = $project_phids; + return $this; + } + + public function getProjectPHIDs() { + return $this->assertAttached($this->projectPHIDs); + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - return PhabricatorPolicies::getMostOpenPolicy(); + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return false; + return false; } public function describeAutomaticCapability($capability) { return null; } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ + public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { @@ -102,4 +126,27 @@ $this->saveTransaction(); } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new DivinerLiveBookEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new DivinerLiveBookTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + } diff --git a/src/applications/diviner/storage/DivinerLiveBookTransaction.php b/src/applications/diviner/storage/DivinerLiveBookTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/storage/DivinerLiveBookTransaction.php @@ -0,0 +1,18 @@ +identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( @@ -159,14 +158,17 @@ public function getTitle() { $title = parent::getTitle(); + if (!strlen($title)) { $title = $this->getName(); } + return $title; } public function setTitle($value) { $this->writeField('title', $value); + if (strlen($value)) { $slug = DivinerAtomRef::normalizeTitleString($value); $hash = PhabricatorHash::digestForIndex($slug); @@ -174,6 +176,7 @@ } else { $this->titleSlugHash = null; } + return $this; } @@ -200,16 +203,15 @@ /* -( PhabricatorPolicyInterface )----------------------------------------- */ + public function getCapabilities() { return $this->getBook()->getCapabilities(); } - public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } - public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } @@ -219,19 +221,17 @@ } -/* -( Markup Interface )--------------------------------------------------- */ +/* -( PhabricatorMarkupInterface )------------------------------------------ */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } - public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine('diviner'); } - public function getMarkupText($field) { if (!$this->getAtom()) { return; @@ -240,21 +240,18 @@ return $this->getAtom()->getDocblockText(); } - - public function didMarkupText( - $field, - $output, - PhutilMarkupEngine $engine) { + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } - public function shouldUseMarkupCache($field) { return true; } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ + public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { diff --git a/src/applications/diviner/storage/DivinerSchemaSpec.php b/src/applications/diviner/storage/DivinerSchemaSpec.php new file mode 100644 --- /dev/null +++ b/src/applications/diviner/storage/DivinerSchemaSpec.php @@ -0,0 +1,9 @@ +buildEdgeSchemata(new DivinerLiveBook()); + } + +} diff --git a/src/applications/diviner/view/DivinerBookItemView.php b/src/applications/diviner/view/DivinerBookItemView.php --- a/src/applications/diviner/view/DivinerBookItemView.php +++ b/src/applications/diviner/view/DivinerBookItemView.php @@ -43,23 +43,23 @@ $title = phutil_tag( 'span', - array( - 'class' => 'diviner-book-item-title', - ), + array( + 'class' => 'diviner-book-item-title', + ), $this->title); $subtitle = phutil_tag( 'span', - array( - 'class' => 'diviner-book-item-subtitle', - ), + array( + 'class' => 'diviner-book-item-subtitle', + ), $this->subtitle); $type = phutil_tag( 'span', - array( - 'class' => 'diviner-book-item-type', - ), + array( + 'class' => 'diviner-book-item-type', + ), $this->type); return array($title, $type, $subtitle); diff --git a/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php b/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php --- a/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php +++ b/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php @@ -36,8 +36,10 @@ $atomizer_class = $args->getArg('atomizer'); if (!$atomizer_class) { - throw new Exception( - pht('Specify an atomizer class with %s.', '--atomizer')); + throw new PhutilArgumentUsageException( + pht( + 'Specify an atomizer class with %s.', + '--atomizer')); } $symbols = id(new PhutilSymbolLoader()) @@ -46,7 +48,7 @@ ->setAncestorClass('DivinerAtomizer') ->selectAndLoadSymbols(); if (!$symbols) { - throw new Exception( + throw new PhutilArgumentUsageException( pht( "Atomizer class '%s' must be a concrete subclass of %s.", $atomizer_class, diff --git a/src/applications/diviner/workflow/DivinerGenerateWorkflow.php b/src/applications/diviner/workflow/DivinerGenerateWorkflow.php --- a/src/applications/diviner/workflow/DivinerGenerateWorkflow.php +++ b/src/applications/diviner/workflow/DivinerGenerateWorkflow.php @@ -50,6 +50,7 @@ } else { $cwd = getcwd(); $this->log(pht('FINDING DOCUMENTATION BOOKS')); + $books = id(new FileFinder($cwd)) ->withType('f') ->withSuffix('book') @@ -92,7 +93,7 @@ // amount of work we can, so that regenerating documentation after minor // changes is quick. // - // = ATOM CACHE = + // = Atom Cache = // // In the first stage, we find all the direct changes to source code since // the last run. This stage relies on two data structures: @@ -118,7 +119,7 @@ // its methods). The File Hash Map contains an exhaustive list of all atoms // with type "file", but not child atoms of those top-level atoms.) // - // = GRAPH CACHE = + // = Graph Cache = // // We now know which atoms exist, and can compare the Atom Map to some // existing cache to figure out what has changed. However, this isn't @@ -176,8 +177,9 @@ ->setConcreteOnly(true) ->setAncestorClass('DivinerPublisher') ->selectAndLoadSymbols(); + if (!$symbols) { - throw new Exception( + throw new PhutilArgumentUsageException( pht( "Publisher class '%s' must be a concrete subclass of %s.", $publisher_class, @@ -188,22 +190,37 @@ $this->publishDocumentation($args->getArg('clean'), $publisher); } + /* -( Atom Cache )--------------------------------------------------------- */ + private function buildAtomCache() { $this->log(pht('BUILDING ATOM CACHE')); $file_hashes = $this->findFilesInProject(); - $this->log(pht('Found %d file(s) in project.', count($file_hashes))); + $this->log( + pht( + 'Found %s file(s) in project.', + new PhutilNumber(count($file_hashes)))); $this->deleteDeadAtoms($file_hashes); $atomize = $this->getFilesToAtomize($file_hashes); - $this->log(pht('Found %d unatomized, uncached file(s).', count($atomize))); + $this->log( + pht( + 'Found %s unatomized, uncached file(s).', + new PhutilNumber(count($atomize)))); $file_atomizers = $this->getAtomizersForFiles($atomize); - $this->log(pht('Found %d file(s) to atomize.', count($file_atomizers))); + $this->log( + pht( + 'Found %s file(s) to atomize.', + new PhutilNumber(count($file_atomizers)))); + $futures = $this->buildAtomizerFutures($file_atomizers); - $this->log(pht('Atomizing %d file(s).', count($file_atomizers))); + $this->log( + pht( + 'Atomizing %s file(s).', + new PhutilNumber(count($file_atomizers)))); if ($futures) { $this->resolveAtomizerFutures($futures, $file_hashes); @@ -344,6 +361,7 @@ ->setTotal(count($futures)); $futures = id(new FutureIterator($futures)) ->limit(4); + foreach ($futures as $key => $future) { try { $atoms = $future->resolveJSON(); @@ -396,6 +414,7 @@ /* -( Graph Cache )-------------------------------------------------------- */ + private function buildGraphCache() { $this->log(pht('BUILDING GRAPH CACHE')); @@ -407,7 +426,10 @@ $dirty_nhashes = array(); $del_atoms = array_diff_key($symbol_map, $atoms); - $this->log(pht('Found %d obsolete atom(s) in graph.', count($del_atoms))); + $this->log( + pht( + 'Found %s obsolete atom(s) in graph.', + new PhutilNumber(count($del_atoms)))); foreach ($del_atoms as $nhash => $shash) { $atom_cache->deleteSymbol($nhash); @@ -418,7 +440,10 @@ } $new_atoms = array_diff_key($atoms, $symbol_map); - $this->log(pht('Found %d new atom(s) in graph.', count($new_atoms))); + $this->log( + pht( + 'Found %s new atom(s) in graph.', + new PhutilNumber(count($new_atoms)))); foreach ($new_atoms as $nhash => $ignored) { $shash = $this->computeSymbolHash($nhash); @@ -454,7 +479,10 @@ } } - $this->log(pht('Found %d affected atoms.', count($dirty_nhashes))); + $this->log( + pht( + 'Found %s affected atoms.', + new PhutilNumber(count($dirty_nhashes)))); foreach ($dirty_nhashes as $nhash => $ignored) { $atom_cache->addGraph($nhash, $this->computeGraphHash($nhash)); diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1124,6 +1124,52 @@ '%s changed package owners, added: %4$s; removed: %6$s.', ), + 'Found %s book(s).' => array( + 'Found %s book.', + 'Found %s books.', + ), + 'Found %s file(s) in project.' => array( + 'Found %s file in project.', + 'Found %s files in project.', + ), + 'Found %s unatomized, uncached file(s).' => array( + 'Found %s unatomized, uncached file.', + 'Found %s unatomized, uncached files.', + ), + 'Found %s file(s) to atomize.' => array( + 'Found %s file to atomize.', + 'Found %s files to atomize.', + ), + 'Atomizing %s file(s).' => array( + 'Atomizing %s file.', + 'Atomizing %s files.', + ), + 'Creating %s document(s).' => array( + 'Creating %s document.', + 'Creating %s documents.', + ), + 'Deleting %s document(s).' => array( + 'Deleting %s document.', + 'Deleting %s documents.', + ), + 'Found %s obsolete atom(s) in graph.' => array( + 'Found %s obsolete atom in graph.', + 'Found %s obsolete atoms in graph.', + ), + 'Found %s new atom(s) in graph.' => array( + 'Found %s new atom in graph.', + 'Found %s new atoms in graph.', + ), + 'This call takes %s parameter(s), but only %s are documented.' => array( + array( + 'This call takes %s parameter, but only %s is documented.', + 'This call takes %s parameter, but only %s are documented.', + ), + array( + 'This call takes %s parameters, but only %s is documented.', + 'This call takes %s parameters, but only %s are documented.', + ), + ), ); }