diff --git a/scripts/__init_env__.php b/scripts/__init_env__.php new file mode 100644 index 0000000000..88a278d5e0 --- /dev/null +++ b/scripts/__init_env__.php @@ -0,0 +1,31 @@ +loadAll(); +$count = count($revs); +echo "Reindexing {$count} revisions"; +foreach ($revs as $rev) { + PhabricatorSearchDifferentialIndexer::indexRevision($rev); + echo '.'; +} +echo "\n"; + +echo "Loading tasks...\n"; +$tasks = id(new ManiphestTask())->loadAll(); +$count = count($tasks); +echo "Reindexing {$count} tasks"; +foreach ($tasks as $task) { + PhabricatorSearchManiphestIndexer::indexTask($task); + echo '.'; +} +echo "\n"; +echo "Done.\n"; + diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f7ad64232e..8f89863e8d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -230,6 +230,21 @@ phutil_register_library_map(array( 'PhabricatorRepositoryGitHubNotification' => 'applications/repository/storage/githubnotification', 'PhabricatorRepositoryGitHubPostReceiveController' => 'applications/repository/controller/github-post-receive', 'PhabricatorRepositoryListController' => 'applications/repository/controller/list', + 'PhabricatorSearchAbstractDocument' => 'applications/search/index/abstractdocument', + 'PhabricatorSearchBaseController' => 'applications/search/controller/base', + 'PhabricatorSearchController' => 'applications/search/controller/search', + 'PhabricatorSearchDAO' => 'applications/search/storage/base', + 'PhabricatorSearchDifferentialIndexer' => 'applications/search/index/indexer/differential', + 'PhabricatorSearchDocument' => 'applications/search/storage/document/document', + 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/field', + 'PhabricatorSearchDocumentIndexer' => 'applications/search/index/indexer/base', + 'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/relationship', + 'PhabricatorSearchExecutor' => 'applications/search/execute/base', + 'PhabricatorSearchField' => 'applications/search/constants/field', + 'PhabricatorSearchManiphestIndexer' => 'applications/search/index/indexer/maniphest', + 'PhabricatorSearchMySQLExecutor' => 'applications/search/execute/mysql', + 'PhabricatorSearchQuery' => 'applications/search/storage/query', + 'PhabricatorSearchRelationship' => 'applications/search/constants/relationship', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base', @@ -251,6 +266,7 @@ phutil_register_library_map(array( 'phabricator_format_relative_time' => 'view/utils', 'phabricator_format_timestamp' => 'view/utils', 'phabricator_format_units_generic' => 'view/utils', + 'phabricator_render_form' => 'infrastructure/javelin/markup', 'qsprintf' => 'storage/qsprintf', 'queryfx' => 'storage/queryfx', 'queryfx_all' => 'storage/queryfx', @@ -437,6 +453,16 @@ phutil_register_library_map(array( 'PhabricatorRepositoryGitHubNotification' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryGitHubPostReceiveController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryListController' => 'PhabricatorController', + 'PhabricatorSearchBaseController' => 'PhabricatorController', + 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', + 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', + 'PhabricatorSearchDifferentialIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', + 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', + 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', + 'PhabricatorSearchManiphestIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhabricatorSearchMySQLExecutor' => 'PhabricatorSearchExecutor', + 'PhabricatorSearchQuery' => 'PhabricatorSearchDAO', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 8db208d90e..daa9c14a8a 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -155,6 +155,11 @@ class AphrontDefaultApplicationConfiguration 'edit/(?P\d+)/$' => 'PhabricatorRepositoryEditController', 'delete/(?P\d+)/$' => 'PhabricatorRepositoryDeleteController', ), + + '/search/' => array( + '$' => 'PhabricatorSearchController', + '(?P\d+)/$' => 'PhabricatorSearchController', + ), ); } diff --git a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php index 99badd804d..027716c392 100644 --- a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php @@ -415,6 +415,8 @@ class DifferentialRevisionEditor { 'actor' => $this->getActorPHID(), ); +// TODO: When timelines get implemented, move indexing to them. + PhabricatorSearchDifferentialIndexer::indexRevision($revision); // TODO // id(new ToolsTimelineEvent('difx', fb_json_encode($event)))->record(); diff --git a/src/applications/differential/editor/revision/__init__.php b/src/applications/differential/editor/revision/__init__.php index bc0bf4fae8..314d676c81 100644 --- a/src/applications/differential/editor/revision/__init__.php +++ b/src/applications/differential/editor/revision/__init__.php @@ -12,6 +12,7 @@ phutil_require_module('phabricator', 'applications/differential/mail/newdiff'); phutil_require_module('phabricator', 'applications/differential/storage/comment'); phutil_require_module('phabricator', 'applications/differential/storage/revision'); phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'applications/search/index/indexer/differential'); phutil_require_module('phabricator', 'storage/qsprintf'); phutil_require_module('phabricator', 'storage/queryfx'); diff --git a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php index 182749bef4..59af1858a1 100644 --- a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php @@ -99,6 +99,9 @@ class ManiphestTransactionEditor { $email_cc, $task->getCCPHIDs()); + // TODO: Do this offline via timeline + PhabricatorSearchManiphestIndexer::indexTask($task); + $this->sendEmail($task, $transactions, $email_to, $email_cc); } diff --git a/src/applications/maniphest/editor/transaction/__init__.php b/src/applications/maniphest/editor/transaction/__init__.php index d55c40092f..75f2b7d20e 100644 --- a/src/applications/maniphest/editor/transaction/__init__.php +++ b/src/applications/maniphest/editor/transaction/__init__.php @@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'applications/maniphest/constants/transacti phutil_require_module('phabricator', 'applications/maniphest/view/transactiondetail'); phutil_require_module('phabricator', 'applications/metamta/storage/mail'); phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'applications/search/index/indexer/maniphest'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php index 92fcaefcd2..3b69492b46 100644 --- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php @@ -96,6 +96,52 @@ class PhabricatorObjectHandleData { $handles[$phid] = $handle; } break; + case 'DREV': + $class = 'DifferentialRevision'; + PhutilSymbolLoader::loadClass($class); + $object = newv($class, array()); + + $revs = $object->loadAllWhere('phid in (%Ls)', $phids); + $revs = mpull($revs, null, 'getPHID'); + + foreach ($phids as $phid) { + $handle = new PhabricatorObjectHandle(); + $handle->setPHID($phid); + if (empty($revs[$phid])) { + $handle->setType(self::TYPE_UNKNOWN); + $handle->setName('Unknown Revision'); + } else { + $rev = $revs[$phid]; + $handle->setType($type); + $handle->setName($rev->getTitle()); + $handle->setURI('/D'.$rev->getID()); + } + $handles[$phid] = $handle; + } + break; + case 'TASK': + $class = 'ManiphestTask'; + PhutilSymbolLoader::loadClass($class); + $object = newv($class, array()); + + $tasks = $object->loadAllWhere('phid in (%Ls)', $phids); + $tasks = mpull($tasks, null, 'getPHID'); + + foreach ($phids as $phid) { + $handle = new PhabricatorObjectHandle(); + $handle->setPHID($phid); + if (empty($tasks[$phid])) { + $handle->setType(self::TYPE_UNKNOWN); + $handle->setName('Unknown Revision'); + } else { + $task = $tasks[$phid]; + $handle->setType($type); + $handle->setName($task->getTitle()); + $handle->setURI('/T'.$task->getID()); + } + $handles[$phid] = $handle; + } + break; case 'FILE': $class = 'PhabricatorFile'; PhutilSymbolLoader::loadClass($class); diff --git a/src/applications/search/constants/field/PhabricatorSearchField.php b/src/applications/search/constants/field/PhabricatorSearchField.php new file mode 100644 index 0000000000..25609d9247 --- /dev/null +++ b/src/applications/search/constants/field/PhabricatorSearchField.php @@ -0,0 +1,25 @@ +buildStandardPageView(); + + $page->setApplicationName('Search'); + $page->setBaseURI('/search/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("(?)"); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/search/controller/base/__init__.php b/src/applications/search/controller/base/__init__.php new file mode 100644 index 0000000000..55de27de55 --- /dev/null +++ b/src/applications/search/controller/base/__init__.php @@ -0,0 +1,15 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + if ($this->id) { + $query = id(new PhabricatorSearchQuery())->load($this->id); + if (!$query) { + return new Aphront404Response(); + } + } else { + $query = new PhabricatorSearchQuery(); + + if ($request->isFormPost()) { + $query->setQuery($request->getStr('query')); + $query->save(); + return id(new AphrontRedirectResponse()) + ->setURI('/search/'.$query->getID().'/'); + } + } + + + $search_form = new AphrontFormView(); + $search_form + ->setUser($user) + ->setAction('/search/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Search') + ->setName('query') + ->setValue($query->getQuery())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Search')); + + $search_panel = new AphrontPanelView(); + $search_panel->setHeader('Search Phabricator'); + $search_panel->appendChild($search_form); + + if ($query->getID()) { + $executor = new PhabricatorSearchMySQLExecutor(); + $results = $executor->executeSearch($query); + $results = ipull($results, 'phid'); + $handles = id(new PhabricatorObjectHandleData($results)) + ->loadHandles(); + $results = array(); + foreach ($handles as $handle) { + $results[] = '

'.$handle->renderLink().'

'; + } + $results = + '
'. + implode("\n", $results). + '
'; + } else { + $results = null; + } + + $results = print_r($results, true); + + return $this->buildStandardPageResponse( + array( + $search_panel, + $results, + ), + array( + 'title' => 'Results: what', + )); + } + +} diff --git a/src/applications/search/controller/search/__init__.php b/src/applications/search/controller/search/__init__.php new file mode 100644 index 0000000000..a116849ddf --- /dev/null +++ b/src/applications/search/controller/search/__init__.php @@ -0,0 +1,22 @@ +getTableName(); + $t_field = $dao_field->getTableName(); + + $conn_r = $dao_doc->establishConnection('r'); + + $q = $query->getQuery(); + + if (strlen($q)) { + $join[] = qsprintf( + $conn_r, + "{$t_field} field ON field.phid = document.phid"); + $where[] = qsprintf( + $conn_r, + 'MATCH(corpus) AGAINST (%s)', + $q); +/* + if ($query->getParameter('order') == AdjutantQuery::ORDER_RELEVANCE) { + $order = qsprintf( + $conn_r, + 'ORDER BY MATCH(corpus) AGAINST (%s) DESC', + $q); + } +*/ + $field = $query->getParameter('field'); + if ($field/* && $field != AdjutantQuery::FIELD_ALL*/) { + $where[] = qsprintf( + $conn_r, + 'field.field = %s', + $field); + } + } + + if ($query->getParameter('type')) { + $where[] = qsprintf( + $conn_r, + 'document.documentType = %s', + $query->getParameter('type')); + } + +/* + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'author', + AdjutantRelationship::RELATIONSHIP_AUTHOR); + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'reviewer', + AdjutantRelationship::RELATIONSHIP_REVIEWER); + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'subscriber', + AdjutantRelationship::RELATIONSHIP_SUBSCRIBER); + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'repository', + AdjutantRelationship::RELATIONSHIP_REPOSITORY); +*/ + $join = array_filter($join); + + foreach ($join as $key => $clause) { + $join[$key] = ' JOIN '.$clause; + } + $join = implode(' ', $join); + + if ($where) { + $where = 'WHERE '.implode(' AND ', $where); + } else { + $where = ''; + } + + $hits = queryfx_all( + $conn_r, + 'SELECT DISTINCT + document.phid, + document.documentType, + document.documentCreated FROM %T document %Q %Q %Q + LIMIT 50', + $t_doc, + $join, + $where, + $order); + + return $hits; + } + + protected function joinRelationship($conn, $query, $field, $type) { + $fbids = $query->getParameter($field, array()); + if (!$fbids) { + return null; + } + return qsprintf( + $conn, + 'relationship AS %C ON %C.fbid = data.fbid AND %C.relation = %s + AND %C.relatedFBID in (%Ld)', + $field, + $field, + $field, + $type, + $field, + $fbids); + } + + +} diff --git a/src/applications/search/execute/mysql/__init__.php b/src/applications/search/execute/mysql/__init__.php new file mode 100644 index 0000000000..a356035a92 --- /dev/null +++ b/src/applications/search/execute/mysql/__init__.php @@ -0,0 +1,16 @@ +phid = $phid; + return $this; + } + + public function setDocumentType($document_type) { + $this->documentType = $document_type; + return $this; + } + + public function setDocumentTitle($title) { + $this->documentTitle = $title; + $this->addField(PhabricatorSearchField::FIELD_TITLE, $title); + return $this; + } + + public function addField($field, $corpus, $aux_phid = null) { + $this->fields[] = array($field, $corpus, $aux_phid); + return $this; + } + + public function addRelationship($type, $related_phid) { + $this->relationships[] = array($type, $related_phid); + return $this; + } + + public function setDocumentCreated($date) { + $this->documentCreated = $date; + return $this; + } + + public function setDocumentModified($date) { + $this->documentModified = $date; + return $this; + } + + public function getPHID() { + return $this->phid; + } + + public function getDocumentType() { + return $this->documentType; + } + + public function getDocumentTitle() { + return $this->documentTitle; + } + + public function getDocumentCreated() { + return $this->documentCreated; + } + + public function getDocumentModified() { + return $this->documentModified; + } + + public function getFieldData() { + return $this->fields; + } + + public function getRelationshipData() { + return $this->relationships; + } +} diff --git a/src/applications/search/index/abstractdocument/__init__.php b/src/applications/search/index/abstractdocument/__init__.php new file mode 100644 index 0000000000..355519d478 --- /dev/null +++ b/src/applications/search/index/abstractdocument/__init__.php @@ -0,0 +1,12 @@ +setPHID($rev->getPHID()); + $doc->setDocumentType('DREV'); + $doc->setDocumentTitle($rev->getTitle()); + $doc->setDocumentCreated($rev->getDateCreated()); + $doc->setDocumentModified($rev->getDateModified()); + + $doc->addField( + PhabricatorSearchField::FIELD_BODY, + $rev->getSummary()); + $doc->addField( + PhabricatorSearchField::FIELD_TEST_PLAN, + $rev->getTestPlan()); + + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, + $rev->getAuthorPHID()); + + PhabricatorSearchDocument::reindexAbstractDocument($doc); + } +} diff --git a/src/applications/search/index/indexer/differential/__init__.php b/src/applications/search/index/indexer/differential/__init__.php new file mode 100644 index 0000000000..ba69035939 --- /dev/null +++ b/src/applications/search/index/indexer/differential/__init__.php @@ -0,0 +1,16 @@ +setPHID($task->getPHID()); + $doc->setDocumentType('TASK'); + $doc->setDocumentTitle($task->getTitle()); + $doc->setDocumentCreated($task->getDateCreated()); + $doc->setDocumentModified($task->getDateModified()); + + $doc->addField( + PhabricatorSearchField::FIELD_BODY, + $task->getDescription()); + + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, + $task->getAuthorPHID()); + + PhabricatorSearchDocument::reindexAbstractDocument($doc); + } +} diff --git a/src/applications/search/index/indexer/maniphest/__init__.php b/src/applications/search/index/indexer/maniphest/__init__.php new file mode 100644 index 0000000000..6bc0288b98 --- /dev/null +++ b/src/applications/search/index/indexer/maniphest/__init__.php @@ -0,0 +1,16 @@ + false, + self::CONFIG_IDS => self::IDS_MANUAL, + ) + parent::getConfiguration(); + } + + public function getIDKey() { + return 'phid'; + } + + public static function reindexAbstractDocument( + PhabricatorSearchAbstractDocument $doc) { + + $phid = $doc->getPHID(); + if (!$phid) { + throw new Exception("Document has no PHID!"); + } + + $store = new PhabricatorSearchDocument(); + $store->setPHID($doc->getPHID()); + $store->setDocumentType($doc->getDocumentType()); + $store->setDocumentTitle($doc->getDocumentTitle()); + $store->setDocumentCreated($doc->getDocumentCreated()); + $store->setDocumentModified($doc->getDocumentModified()); + $store->replace(); + + $conn_w = $store->establishConnection('w'); + + $field_dao = new PhabricatorSearchDocumentField(); + queryfx( + $conn_w, + 'DELETE FROM %T WHERE phid = %s', + $field_dao->getTableName(), + $phid); + foreach ($doc->getFieldData() as $field) { + list($ftype, $corpus, $aux_phid) = $field; + queryfx( + $conn_w, + 'INSERT INTO %T (phid, field, auxPHId, corpus) '. + ' VALUES (%s, %s, %ns, %s)', + $field_dao->getTableName(), + $phid, + $ftype, + $aux_phid, + $corpus); + } + + + $sql = array(); + foreach ($doc->getRelationshipData() as $relationship) { + list($rtype, $toPHID) = $relationship; + $sql[] = qsprintf( + $conn_w, + '(%s, %s, %s)', + $phid, + $toPHID, + $rtype); + } + + $rship_dao = new PhabricatorSearchDocumentRelationship(); + queryfx( + $conn_w, + 'DELETE FROM %T WHERE phid = %s', + $rship_dao->getTableName(), + $phid); + if ($sql) { + queryfx( + $conn_w, + 'INSERT INTO %T (phid, relatedPHID, relation) '. + ' VALUES %Q', + $rship_dao->getTableName(), + implode(', ', $sql)); + } + + } + +} diff --git a/src/applications/search/storage/document/document/__init__.php b/src/applications/search/storage/document/document/__init__.php new file mode 100644 index 0000000000..bae6063fde --- /dev/null +++ b/src/applications/search/storage/document/document/__init__.php @@ -0,0 +1,16 @@ + false, + self::CONFIG_IDS => self::IDS_MANUAL, + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/search/storage/document/field/__init__.php b/src/applications/search/storage/document/field/__init__.php new file mode 100644 index 0000000000..b19d220da3 --- /dev/null +++ b/src/applications/search/storage/document/field/__init__.php @@ -0,0 +1,12 @@ + false, + self::CONFIG_IDS => self::IDS_MANUAL, + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/search/storage/document/relationship/__init__.php b/src/applications/search/storage/document/relationship/__init__.php new file mode 100644 index 0000000000..1672a0d325 --- /dev/null +++ b/src/applications/search/storage/document/relationship/__init__.php @@ -0,0 +1,12 @@ + array( + 'parameters' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function setParameter($parameter, $value) { + $this->parameters[$parameter] = $value; + return $this; + } + + public function getParameter($parameter, $default = null) { + return idx($this->parameters, $parameter, $default); + } + +} diff --git a/src/applications/search/storage/query/__init__.php b/src/applications/search/storage/query/__init__.php new file mode 100644 index 0000000000..9fee0d56db --- /dev/null +++ b/src/applications/search/storage/query/__init__.php @@ -0,0 +1,14 @@ + 'hidden', + 'name' => '__csrf__', + 'value' => $user->getCSRFToken(), + )). + phutil_render_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => '__form__', + 'value' => true, + )).$content); +} + diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php index ef0ab6fcc4..b6320fd7f9 100755 --- a/src/view/page/standard/PhabricatorStandardPageView.php +++ b/src/view/page/standard/PhabricatorStandardPageView.php @@ -154,23 +154,15 @@ class PhabricatorStandardPageView extends AphrontPageView { ' · '. 'Settings'. ' · '. - '
'. - phutil_render_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => '__csrf__', - 'value' => $user->getCSRFToken(), - )). - phutil_render_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => '__form__', - 'value' => true, - )). - ''. - '
'; + phabricator_render_form( + $user, + array( + 'action' => '/search/', + 'method' => 'post', + 'style' => 'display: inline', + ), + ''. + ''); } } @@ -200,6 +192,17 @@ class PhabricatorStandardPageView extends AphrontPageView { } $foot_links[] = $link; } + // This ends up very early in tab order at the top of the page and there's + // a bunch of junk up there anyway, just shove it down here. + $foot_links[] = phabricator_render_form( + $user, + array( + 'action' => '/logout/', + 'method' => 'post', + 'style' => 'display: inline', + ), + ''); + $foot_links = implode(' · ', $foot_links); return