diff --git a/resources/sql/autopatches/20150605.diviner.edges.sql b/resources/sql/autopatches/20150605.diviner.edges.sql new file mode 100644 index 0000000000..b0e1d61705 --- /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 index 0000000000..8c960a4f07 --- /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 index 0000000000..33f9b5d313 --- /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 index 6a45078f42..fc465aeea6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -649,19 +649,25 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( 'DivinerLiveBook' => array( 'DivinerDAO', 'PhabricatorPolicyInterface', + 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorApplicationTransactionInterface', ), + 'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor', + 'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction', + 'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DivinerLivePublisher' => 'DivinerPublisher', 'DivinerLiveSymbol' => array( 'DivinerDAO', @@ -4041,6 +4056,7 @@ phutil_register_library_map(array( '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 index 68b569213a..4d0691d990 100644 --- a/src/applications/diviner/application/PhabricatorDivinerApplication.php +++ b/src/applications/diviner/application/PhabricatorDivinerApplication.php @@ -39,6 +39,7 @@ final class PhabricatorDivinerApplication extends PhabricatorApplication { 'find/' => 'DivinerFindController', ), '/book/(?P[^/]+)/' => 'DivinerBookController', + '/book/(?P[^/]+)/edit/' => 'DivinerBookEditController', '/book/'. '(?P[^/]+)/'. '(?P[^/]+)/'. @@ -52,6 +53,18 @@ final class PhabricatorDivinerApplication extends PhabricatorApplication { 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 index e973a49447..bf375640f6 100644 --- a/src/applications/diviner/atomizer/DivinerArticleAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerArticleAtomizer.php @@ -20,7 +20,7 @@ final class DivinerArticleAtomizer extends DivinerAtomizer { $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 index b964f7961f..c22cb4bcc3 100644 --- a/src/applications/diviner/atomizer/DivinerPHPAtomizer.php +++ b/src/applications/diviner/atomizer/DivinerPHPAtomizer.php @@ -151,9 +151,9 @@ final class DivinerPHPAtomizer extends DivinerAtomizer { 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 @@ final class DivinerPHPAtomizer extends DivinerAtomizer { 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 @@ final class DivinerPHPAtomizer extends DivinerAtomizer { 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 @@ final class DivinerPHPAtomizer extends DivinerAtomizer { 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 index 4b3ca32b79..b37bd71419 100644 --- a/src/applications/diviner/cache/DivinerAtomCache.php +++ b/src/applications/diviner/cache/DivinerAtomCache.php @@ -18,6 +18,7 @@ final class DivinerAtomCache extends DivinerDiskCache { 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 index 9f8cc60c9d..f605796c0c 100644 --- a/src/applications/diviner/cache/DivinerDiskCache.php +++ b/src/applications/diviner/cache/DivinerDiskCache.php @@ -26,8 +26,8 @@ abstract class DivinerDiskCache extends Phobject { * 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 index 38ee1aa3b5..e1bff354dd 100644 --- a/src/applications/diviner/cache/DivinerPublishCache.php +++ b/src/applications/diviner/cache/DivinerPublishCache.php @@ -45,6 +45,7 @@ final class DivinerPublishCache extends DivinerDiskCache { /* -( 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 index 0000000000..fcb93c0323 --- /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 @@ final class DivinerAtomController extends DivinerController { $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 @@ final class DivinerAtomController extends DivinerController { ->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 @@ final class DivinerAtomController extends DivinerController { $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 @@ final class DivinerAtomController extends DivinerController { pht('Implements'), phutil_implode_html(phutil_tag('br'), $items)); } - } private function renderAtomTag(DivinerLiveSymbol $symbol) { @@ -471,6 +459,7 @@ final class DivinerAtomController extends DivinerController { 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 index 68d1f6b652..209fdfb9ea 100644 --- 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 index 125edbb6ab..f8d222b116 100644 --- 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 @@ final class DivinerBookController extends DivinerController { )); } + 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 index 0000000000..3ac8c4fe2e --- /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 index cf4c9e197c..e9678c1b4b 100644 --- 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 @@ abstract class DivinerController extends PhabricatorController { 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 @@ abstract class DivinerController extends PhabricatorController { ->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 index 061e357b9f..b9e165fc1d 100644 --- a/src/applications/diviner/controller/DivinerFindController.php +++ b/src/applications/diviner/controller/DivinerFindController.php @@ -6,11 +6,10 @@ final class DivinerFindController extends DivinerController { 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 @@ final class DivinerFindController extends DivinerController { ->setViewer($viewer) ->withNames(array($book_name)) ->executeOne(); + if (!$book) { return new Aphront404Response(); } @@ -70,8 +70,8 @@ final class DivinerFindController extends DivinerController { ->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 index 34647dc7cf..49fc323733 100644 --- a/src/applications/diviner/controller/DivinerMainController.php +++ b/src/applications/diviner/controller/DivinerMainController.php @@ -6,9 +6,8 @@ final class DivinerMainController extends DivinerController { 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 @@ final class DivinerMainController extends DivinerController { ->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 @@ final class DivinerMainController extends DivinerController { ->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 index 0000000000..3f392391bb --- /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 @@ final class DivinerLivePublisher extends DivinerPublisher { ->setContent(null) ->save(); } - } } diff --git a/src/applications/diviner/publisher/DivinerPublisher.php b/src/applications/diviner/publisher/DivinerPublisher.php index 591897f0e5..4b6490639d 100644 --- a/src/applications/diviner/publisher/DivinerPublisher.php +++ b/src/applications/diviner/publisher/DivinerPublisher.php @@ -133,10 +133,20 @@ abstract class DivinerPublisher extends Phobject { $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 index 2daa9bc3a7..b8856141a5 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -79,7 +79,6 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { 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 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { + $this->didRejectResult($atom); unset($atoms[$key]); continue; } @@ -158,12 +158,9 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { // 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 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { if ($this->titles) { $hashes = array(); + foreach ($this->titles as $title) { $slug = DivinerAtomRef::normalizeTitleString($title); $hash = PhabricatorHash::digestForIndex($slug); @@ -318,6 +316,7 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { if ($this->contexts) { $with_null = false; $contexts = $this->contexts; + foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); @@ -373,10 +372,9 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { } 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 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { 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 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { foreach ($child_hashes as $hash) { $hashes[$hash] = $hash; } + if ($recurse_up) { $hashes += $this->getAllChildHashes($symbol->getExtends(), true); } @@ -421,7 +419,6 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { 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 @@ final class DivinerAtomQuery extends PhabricatorCursorPagedPolicyAwareQuery { $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 index 90fed52a23..b1f5f0a995 100644 --- a/src/applications/diviner/query/DivinerBookQuery.php +++ b/src/applications/diviner/query/DivinerBookQuery.php @@ -6,6 +6,8 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $phids; private $names; + private $needProjectPHIDs; + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -21,6 +23,11 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery { 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 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery { 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 index 0000000000..e3ce6d41d7 --- /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 @@ final class DivinerDefaultRenderer extends DivinerRenderer { $ref->getTitle()); } - } diff --git a/src/applications/diviner/search/DivinerBookSearchIndexer.php b/src/applications/diviner/search/DivinerBookSearchIndexer.php index b08042a4f8..106ae9e389 100644 --- a/src/applications/diviner/search/DivinerBookSearchIndexer.php +++ b/src/applications/diviner/search/DivinerBookSearchIndexer.php @@ -18,6 +18,11 @@ final class DivinerBookSearchIndexer extends PhabricatorSearchDocumentIndexer { 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 index ddb03e9d14..e689955171 100644 --- 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 @@ final class DivinerLiveBook extends DivinerDAO 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 @@ final class DivinerLiveBook extends DivinerDAO $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 index 0000000000..ae461e751a --- /dev/null +++ b/src/applications/diviner/storage/DivinerLiveBookTransaction.php @@ -0,0 +1,18 @@ +identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( @@ -159,14 +158,17 @@ final class DivinerLiveSymbol extends DivinerDAO 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 @@ final class DivinerLiveSymbol extends DivinerDAO } else { $this->titleSlugHash = null; } + return $this; } @@ -200,16 +203,15 @@ final class DivinerLiveSymbol extends DivinerDAO /* -( 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 @@ final class DivinerLiveSymbol extends DivinerDAO } -/* -( 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 @@ final class DivinerLiveSymbol extends DivinerDAO 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 index 0000000000..e57408b89f --- /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 index 227a791308..5abf43638f 100644 --- a/src/applications/diviner/view/DivinerBookItemView.php +++ b/src/applications/diviner/view/DivinerBookItemView.php @@ -43,23 +43,23 @@ final class DivinerBookItemView extends AphrontTagView { $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 index c16cb24c30..7a1d5edfaf 100644 --- a/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php +++ b/src/applications/diviner/workflow/DivinerAtomizeWorkflow.php @@ -36,8 +36,10 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow { $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 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow { ->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 index 2b0e4ed3ae..a08180e4a1 100644 --- a/src/applications/diviner/workflow/DivinerGenerateWorkflow.php +++ b/src/applications/diviner/workflow/DivinerGenerateWorkflow.php @@ -50,6 +50,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { } else { $cwd = getcwd(); $this->log(pht('FINDING DOCUMENTATION BOOKS')); + $books = id(new FileFinder($cwd)) ->withType('f') ->withSuffix('book') @@ -92,7 +93,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { // 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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { // 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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { ->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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { $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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { ->setTotal(count($futures)); $futures = id(new FutureIterator($futures)) ->limit(4); + foreach ($futures as $key => $future) { try { $atoms = $future->resolveJSON(); @@ -396,6 +414,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { /* -( Graph Cache )-------------------------------------------------------- */ + private function buildGraphCache() { $this->log(pht('BUILDING GRAPH CACHE')); @@ -407,7 +426,10 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { $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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { } $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 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow { } } - $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 index 320665fd73..b13dfe1b50 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1124,6 +1124,52 @@ final class PhabricatorUSEnglishTranslation '%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.', + ), + ), ); }