1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-15 10:00:55 +01:00

(stable) Promote 2017 Week 37

This commit is contained in:
epriestley 2017-09-15 10:06:22 -07:00
commit dc22aba9df
30 changed files with 1003 additions and 670 deletions

View file

@ -10,7 +10,7 @@ return array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => 'b5b51108',
'core.pkg.css' => 'e9473020',
'core.pkg.js' => '6c085267',
'core.pkg.js' => '28552e58',
'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '45951e9e',
'differential.pkg.js' => 'b71b8c5d',
@ -374,7 +374,7 @@ return array(
'rsrc/image/texture/table_header_tall.png' => 'd56b434f',
'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'a14cbdfc',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '4cc4f460',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9',
'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289',
'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443',
@ -467,7 +467,7 @@ return array(
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9',
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
'rsrc/js/core/Notification.js' => '5c3349b2',
'rsrc/js/core/Notification.js' => '008faf9c',
'rsrc/js/core/Prefab.js' => 'c5af80a2',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '320810c8',
@ -585,7 +585,7 @@ return array(
'javelin-aphlict' => 'e1d4b11a',
'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => 'caade6f2',
'javelin-behavior-aphlict-listen' => 'a14cbdfc',
'javelin-behavior-aphlict-listen' => '4cc4f460',
'javelin-behavior-aphlict-status' => '5e2634b9',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22',
@ -789,7 +789,7 @@ return array(
'phabricator-keyboard-shortcut-manager' => 'c19dd9b9',
'phabricator-main-menu-view' => '1802a242',
'phabricator-nav-view-css' => 'faf6a6fc',
'phabricator-notification' => '5c3349b2',
'phabricator-notification' => '008faf9c',
'phabricator-notification-css' => '457861ec',
'phabricator-notification-menu-css' => '10685bd4',
'phabricator-object-selector-css' => '85ee8ce6',
@ -904,6 +904,13 @@ return array(
'unhandled-exception-css' => '4c96257a',
),
'requires' => array(
'008faf9c' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'013ffff9' => array(
'javelin-install',
'javelin-util',
@ -1236,6 +1243,20 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'4cc4f460' => array(
'javelin-behavior',
'javelin-aphlict',
'javelin-stratcom',
'javelin-request',
'javelin-uri',
'javelin-dom',
'javelin-json',
'javelin-router',
'javelin-util',
'javelin-leader',
'javelin-sound',
'phabricator-notification',
),
'4d863052' => array(
'javelin-dom',
'javelin-util',
@ -1326,13 +1347,6 @@ return array(
'javelin-vector',
'javelin-dom',
),
'5c3349b2' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'5c54cbf3' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1684,20 +1698,6 @@ return array(
'javelin-util',
'phabricator-keyboard-shortcut',
),
'a14cbdfc' => array(
'javelin-behavior',
'javelin-aphlict',
'javelin-stratcom',
'javelin-request',
'javelin-uri',
'javelin-dom',
'javelin-json',
'javelin-router',
'javelin-util',
'javelin-leader',
'javelin-sound',
'phabricator-notification',
),
'a3a63478' => array(
'phui-workcard-view-css',
),

View file

@ -0,0 +1,19 @@
<?php
// Advise installs to perform a reindex in order to rebuild the Ferret engine
// indexes.
// If the install is completely empty with no user accounts, don't require
// a rebuild. In particular, this happens when rebuilding the quickstart file.
$users = id(new PhabricatorUser())->loadAllWhere('1 = 1 LIMIT 1');
if (!$users) {
return;
}
try {
id(new PhabricatorConfigManualActivity())
->setActivityType(PhabricatorConfigManualActivity::TYPE_REINDEX)
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// If we've already noted that this activity is required, just move on.
}

File diff suppressed because one or more lines are too long

View file

@ -9,6 +9,7 @@
phutil_register_library_map(array(
'__library_version__' => 2,
'class' => array(
'AlamancServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlamancServiceEditConduitAPIMethod.php',
'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php',
@ -36,6 +37,7 @@ phutil_register_library_map(array(
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php',
'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php',
'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
@ -2667,6 +2669,7 @@ phutil_register_library_map(array(
'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php',
'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php',
@ -3196,7 +3199,6 @@ phutil_register_library_map(array(
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
@ -4930,6 +4932,7 @@ phutil_register_library_map(array(
'require_celerity_resource' => 'applications/celerity/api.php',
),
'xmap' => array(
'AlamancServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array(
'AlmanacDAO',
@ -4975,6 +4978,7 @@ phutil_register_library_map(array(
'PhabricatorExtendedPolicyInterface',
),
'AlmanacDeviceController' => 'AlmanacController',
'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacDeviceEditController' => 'AlmanacDeviceController',
'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine',
'AlmanacDeviceEditor' => 'AlmanacEditor',
@ -5367,6 +5371,7 @@ phutil_register_library_map(array(
'DifferentialChangeset' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
@ -5461,6 +5466,7 @@ phutil_register_library_map(array(
'DifferentialHunk' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialHunkParser' => 'Phobject',
'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
@ -7999,6 +8005,7 @@ phutil_register_library_map(array(
'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
@ -8584,7 +8591,6 @@ phutil_register_library_map(array(
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(

View file

@ -0,0 +1,19 @@
<?php
final class AlamancServiceEditConduitAPIMethod
extends PhabricatorEditEngineAPIMethod {
public function getAPIMethodName() {
return 'almanac.service.edit';
}
public function newEditEngine() {
return new AlmanacServiceEditEngine();
}
public function getMethodSummary() {
return pht(
'Apply transactions to create a new service or edit an existing one.');
}
}

View file

@ -0,0 +1,19 @@
<?php
final class AlmanacDeviceEditConduitAPIMethod
extends PhabricatorEditEngineAPIMethod {
public function getAPIMethodName() {
return 'almanac.device.edit';
}
public function newEditEngine() {
return new AlmanacDeviceEditEngine();
}
public function getMethodSummary() {
return pht(
'Apply transactions to create a new device or edit an existing one.');
}
}

View file

@ -766,10 +766,14 @@ final class DifferentialTransactionEditor
}
if ($config_attach) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
// See T12033, T11767, and PHI55. This is a crude fix to stop the
// major concrete problems that lackluster email size limits cause.
if (strlen($patch) < $body_limit) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
}
}
}
}

View file

@ -0,0 +1,86 @@
<?php
final class PhabricatorDifferentialMigrateHunkWorkflow
extends PhabricatorDifferentialManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate-hunk')
->setExamples('**migrate-hunk** --id __hunk__ --to __storage__')
->setSynopsis(pht('Migrate storage engines for a hunk.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Hunk ID to migrate.'),
),
array(
'name' => 'to',
'param' => 'storage',
'help' => pht('Storage engine to migrate to.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht('Specify a hunk to migrate with --id.'));
}
$storage = $args->getArg('to');
switch ($storage) {
case DifferentialModernHunk::DATATYPE_TEXT:
case DifferentialModernHunk::DATATYPE_FILE:
break;
default:
throw new PhutilArgumentUsageException(
pht('Specify a hunk storage engine with --to.'));
}
$hunk = $this->loadHunk($id);
$old_data = $hunk->getChanges();
switch ($storage) {
case DifferentialModernHunk::DATATYPE_TEXT:
$hunk->saveAsText();
$this->logOkay(
pht('TEXT'),
pht('Convereted hunk to text storage.'));
break;
case DifferentialModernHunk::DATATYPE_FILE:
$hunk->saveAsFile();
$this->logOkay(
pht('FILE'),
pht('Convereted hunk to file storage.'));
break;
}
$hunk = $this->loadHunk($id);
$new_data = $hunk->getChanges();
if ($old_data !== $new_data) {
throw new Exception(
pht(
'Integrity check failed: new file data differs fom old data!'));
}
return 0;
}
private function loadHunk($id) {
$hunk = id(new DifferentialModernHunk())->load($id);
if (!$hunk) {
throw new PhutilArgumentUsageException(
pht(
'No hunk exists with ID "%s".',
$id));
}
return $hunk;
}
}

View file

@ -1,7 +1,10 @@
<?php
final class DifferentialChangeset extends DifferentialDAO
implements PhabricatorPolicyInterface {
final class DifferentialChangeset
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID;
protected $oldFile;
@ -236,4 +239,25 @@ final class DifferentialChangeset extends DifferentialDAO
return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$hunks = id(new DifferentialModernHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$engine->destroyObject($hunk);
}
$this->delete();
$this->saveTransaction();
}
}

View file

@ -727,7 +727,7 @@ final class DifferentialDiff
$this->delete();
foreach ($this->loadChangesets() as $changeset) {
$changeset->delete();
$engine->destroyObject($changeset);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(

View file

@ -1,7 +1,10 @@
<?php
abstract class DifferentialHunk extends DifferentialDAO
implements PhabricatorPolicyInterface {
abstract class DifferentialHunk
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $changesetID;
protected $oldOffset;
@ -228,4 +231,14 @@ abstract class DifferentialHunk extends DifferentialDAO
return $this->getChangeset()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}

View file

@ -15,6 +15,7 @@ final class DifferentialModernHunk extends DifferentialHunk {
private $rawData;
private $forcedEncoding;
private $fileData;
public function getTableName() {
return 'differential_hunk_modern';
@ -87,6 +88,57 @@ final class DifferentialModernHunk extends DifferentialHunk {
return parent::save();
}
public function saveAsText() {
$old_type = $this->getDataType();
$old_data = $this->getData();
if ($old_type == self::DATATYPE_TEXT) {
return $this;
}
$raw_data = $this->getRawData();
$this->setDataType(self::DATATYPE_TEXT);
$this->setData($raw_data);
$result = $this->save();
$this->destroyData($old_type, $old_data);
return $result;
}
public function saveAsFile() {
$old_type = $this->getDataType();
$old_data = $this->getData();
if ($old_type == self::DATATYPE_FILE) {
return $this;
}
$raw_data = $this->getRawData();
$file = PhabricatorFile::newFromFileData(
$raw_data,
array(
'name' => 'differential-hunk',
'mime-type' => 'application/octet-stream',
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$this->setDataType(self::DATATYPE_FILE);
$this->setData($file->getPHID());
// NOTE: Because hunks don't have a PHID and we just load hunk data with
// the ominipotent viewer, we do not need to attach the file to anything.
$result = $this->save();
$this->destroyData($old_type, $old_data);
return $result;
}
private function getRawData() {
if ($this->rawData === null) {
$type = $this->getDataType();
@ -98,6 +150,8 @@ final class DifferentialModernHunk extends DifferentialHunk {
$data = $data;
break;
case self::DATATYPE_FILE:
$data = $this->loadFileData();
break;
default:
throw new Exception(
pht('Hunk has unsupported data type "%s"!', $type));
@ -123,4 +177,75 @@ final class DifferentialModernHunk extends DifferentialHunk {
return $this->rawData;
}
private function loadFileData() {
if ($this->fileData === null) {
$type = $this->getDataType();
if ($type !== self::DATATYPE_FILE) {
throw new Exception(
pht(
'Unable to load file data for hunk with wrong data type ("%s").',
$type));
}
$file_phid = $this->getData();
$file = $this->loadRawFile($file_phid);
$data = $file->loadFileData();
$this->fileData = $data;
}
return $this->fileData;
}
private function loadRawFile($file_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->execute();
if (!$files) {
throw new Exception(
pht(
'Failed to load file ("%s") with hunk data.',
$file_phid));
}
$file = head($files);
return $file;
}
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$type = $this->getDataType();
$data = $this->getData();
$this->destroyData($type, $data, $engine);
return parent::destroyObjectPermanently($engine);
}
private function destroyData(
$type,
$data,
PhabricatorDestructionEngine $engine = null) {
if (!$engine) {
$engine = new PhabricatorDestructionEngine();
}
switch ($type) {
case self::DATATYPE_FILE:
$file = $this->loadRawFile($data);
$engine->destroyObject($file);
break;
}
}
}

View file

@ -946,7 +946,7 @@ final class DiffusionCommitController extends DiffusionController {
foreach ($changesets as $changeset_id => $changeset) {
$path = $changeset->getFilename();
$anchor = substr(md5($path), 0, 8);
$anchor = $changeset->getAnchorName();
$history_link = $diffusion_view->linkHistory($path);
$browse_link = $diffusion_view->linkBrowse(

View file

@ -768,7 +768,10 @@ final class DiffusionServeController extends DiffusionController {
$input = strlen($input)."\n".$input."0\n";
}
$command = csprintf('%s serve --stdio', $bin);
$command = csprintf(
'%s serve -R %s --stdio',
$bin,
$repository->getLocalPath());
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))

View file

@ -102,7 +102,10 @@ final class ManiphestQueryConduitAPIMethod extends ManiphestConduitAPIMethod {
$full_text = $request->getValue('fullText');
if ($full_text) {
$query->withFullTextSearch($full_text);
throw new Exception(
pht(
'Parameter "fullText" is no longer supported. Use method '.
'"maniphest.search" with the "query" constraint instead.'));
}
$status = $request->getValue('status');

View file

@ -88,23 +88,40 @@ final class ManiphestReportController extends ManiphestController {
$data = queryfx_all(
$conn,
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q
WHERE transactionType = %s
'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated
FROM %T x %Q
WHERE transactionType IN (%Ls)
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
ManiphestTaskStatusTransaction::TRANSACTIONTYPE);
array(
ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE,
));
$stats = array();
$day_buckets = array();
$open_tasks = array();
$default_status = ManiphestTaskStatus::getDefaultStatus();
$duplicate_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($data as $key => $row) {
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
switch ($row['transactionType']) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
break;
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
// NOTE: Merging a task does not generate a "status" transaction.
// We pretend it did. Note that this is not always accurate: it is
// possble to merge a task which was previously closed, but this
// fake transaction always counts a merge as a closure.
$oldv = $default_status;
$newv = $duplicate_status;
break;
}
if ($oldv == 'null') {
$old_is_open = false;

View file

@ -24,8 +24,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $subtaskIDs;
private $subtypes;
private $fullTextSearch = '';
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
@ -115,11 +113,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
@ -329,7 +322,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->taskIDs !== null) {
$where[] = qsprintf(
@ -481,36 +473,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return '('.implode(') OR (', $subclause).')';
}
private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!strlen($this->fullTextSearch)) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = id(new PhabricatorSavedQuery())
->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
->setParameter('query', $this->fullTextSearch);
// NOTE: Setting this to something larger than 10,000 will raise errors in
// Elasticsearch, and billions of results won't fit in memory anyway.
$fulltext_query->setParameter('limit', 10000);
$fulltext_query->setParameter('types',
array(ManiphestTaskPHIDType::TYPECONST));
$fulltext_results = PhabricatorSearchService::executeSearch(
$fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'task.phid IN (%Ls)',
$fulltext_results);
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$open_statuses = ManiphestTaskStatus::getOpenStatusConstants();
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;

View file

@ -86,9 +86,6 @@ final class ManiphestTaskSearchEngine
pht('Search for tasks with given subtypes.'))
->setDatasource(new ManiphestTaskSubtypeDatasource())
->setIsHidden($hide_subtypes),
id(new PhabricatorSearchTextField())
->setLabel(pht('Contains Words'))
->setKey('fulltext'),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Open Parents'))
->setKey('hasParents')
@ -144,7 +141,6 @@ final class ManiphestTaskSearchEngine
'statuses',
'priorities',
'subtypes',
'fulltext',
'hasParents',
'hasSubtasks',
'parentIDs',
@ -220,10 +216,6 @@ final class ManiphestTaskSearchEngine
$query->withOpenSubtasks($map['hasSubtasks']);
}
if (strlen($map['fulltext'])) {
$query->withFullTextSearch($map['fulltext']);
}
if ($map['parentIDs']) {
$query->withParentTaskIDs($map['parentIDs']);
}

View file

@ -153,8 +153,8 @@ final class PhabricatorNotificationBuilder extends Phobject {
foreach ($stories as $story) {
if ($story instanceof PhabricatorApplicationTransactionFeedStory) {
$dict[] = array(
'desktopReady' => $desktop_ready,
'webReady' => $web_ready,
'showAnyNotification' => $web_ready,
'showDesktopNotification' => $desktop_ready,
'title' => $story->renderText(),
'body' => $story->renderTextBody(),
'href' => $story->getURI(),
@ -162,8 +162,8 @@ final class PhabricatorNotificationBuilder extends Phobject {
);
} else if ($story instanceof PhabricatorNotificationTestFeedStory) {
$dict[] = array(
'desktopReady' => $desktop_ready,
'webReady' => $web_ready,
'showAnyNotification' => $web_ready,
'showDesktopNotification' => $desktop_ready,
'title' => pht('Test Notification'),
'body' => $story->renderText(),
'href' => null,
@ -171,8 +171,8 @@ final class PhabricatorNotificationBuilder extends Phobject {
);
} else {
$dict[] = array(
'desktopReady' => false,
'webReady' => false,
'showWebNotification' => false,
'showDesktopNotification' => false,
'title' => null,
'body' => null,
'href' => null,

View file

@ -38,15 +38,9 @@ final class PhabricatorNotificationIndividualController
$dict = $builder->buildDict();
$data = $dict[0];
$response = array(
$response = $data + array(
'pertinent' => true,
'primaryObjectPHID' => $story->getPrimaryObjectPHID(),
'desktopReady' => $data['desktopReady'],
'webReady' => $data['webReady'],
'href' => $data['href'],
'icon' => $data['icon'],
'title' => $data['title'],
'body' => $data['body'],
'content' => hsprintf('%s', $content),
'uniqueID' => 'story/'.$story->getChronologicalKey(),
);

View file

@ -8,7 +8,6 @@ final class PhabricatorRepositoryQuery
private $callsigns;
private $types;
private $uuids;
private $nameContains;
private $uris;
private $datasourceQuery;
private $slugs;
@ -116,11 +115,6 @@ final class PhabricatorRepositoryQuery
return $this;
}
public function withNameContains($contains) {
$this->nameContains = $contains;
return $this;
}
public function withURIs(array $uris) {
$this->uris = $uris;
return $this;
@ -661,13 +655,6 @@ final class PhabricatorRepositoryQuery
$this->uuids);
}
if (strlen($this->nameContains)) {
$where[] = qsprintf(
$conn,
'r.name LIKE %~',
$this->nameContains);
}
if (strlen($this->datasourceQuery)) {
// This handles having "rP" match callsigns starting with "P...".
$query = trim($this->datasourceQuery);

View file

@ -24,9 +24,6 @@ final class PhabricatorRepositorySearchEngine
id(new PhabricatorSearchStringListField())
->setLabel(pht('Callsigns'))
->setKey('callsigns'),
id(new PhabricatorSearchTextField())
->setLabel(pht('Name Contains'))
->setKey('name'),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Status'))
->setKey('status')
@ -72,10 +69,6 @@ final class PhabricatorRepositorySearchEngine
$query->withTypes($map['types']);
}
if (strlen($map['name'])) {
$query->withNameContains($map['name']);
}
if ($map['uris']) {
$query->withURIs($map['uris']);
}

View file

@ -324,6 +324,9 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$result = $head + $body + $tail;
// Force the fulltext "query" field to the top unconditionally.
$result = array_select_keys($result, array('query')) + $result;
foreach ($this->getHiddenFields() as $hidden_key) {
unset($result[$hidden_key]);
}

View file

@ -6,7 +6,7 @@ final class PhabricatorFerretSearchEngineExtension
const EXTENSIONKEY = 'ferret';
public function isExtensionEnabled() {
return PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
return true;
}
public function getExtensionName() {
@ -56,7 +56,7 @@ final class PhabricatorFerretSearchEngineExtension
$fields[] = id(new PhabricatorSearchTextField())
->setKey('query')
->setLabel(pht('Query (Prototype)'))
->setLabel(pht('Query'))
->setDescription(pht('Fulltext search.'));
return $fields;

View file

@ -7,7 +7,7 @@ final class PhabricatorFerretFulltextStorageEngine
private $engineLimits;
public function getEngineIdentifier() {
return 'ferret';
return 'mysql';
}
public function getHostType() {
@ -86,6 +86,10 @@ final class PhabricatorFerretFulltextStorageEngine
$type_results[$type] = $results;
$metadata += $engine_query->getFerretMetadata();
if (!$this->fulltextTokens) {
$this->fulltextTokens = $engine_query->getFerretTokens();
}
}
$list = array();

View file

@ -1,504 +0,0 @@
<?php
final class PhabricatorMySQLFulltextStorageEngine
extends PhabricatorFulltextStorageEngine {
private $fulltextTokens = array();
private $engineLimits;
public function getEngineIdentifier() {
return 'mysql';
}
public function getHostType() {
return new PhabricatorMySQLSearchHost($this);
}
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
$phid = $doc->getPHID();
if (!$phid) {
throw new Exception(pht('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');
$stemmer = new PhutilSearchStemmer();
$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;
$stemmed_corpus = $stemmer->stemCorpus($corpus);
queryfx(
$conn_w,
'INSERT INTO %T
(phid, phidType, field, auxPHID, corpus, stemmedCorpus) '.
'VALUES (%s, %s, %s, %ns, %s, %s)',
$field_dao->getTableName(),
$phid,
$doc->getDocumentType(),
$ftype,
$aux_phid,
$corpus,
$stemmed_corpus);
}
$sql = array();
foreach ($doc->getRelationshipData() as $relationship) {
list($rtype, $to_phid, $to_type, $time) = $relationship;
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %s, %d)',
$phid,
$to_phid,
$rtype,
$to_type,
$time);
}
$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, relatedType, relatedTime) '.
'VALUES %Q',
$rship_dao->getTableName(),
implode(', ', $sql));
}
}
public function executeSearch(PhabricatorSavedQuery $query) {
$table = new PhabricatorSearchDocument();
$document_table = $table->getTableName();
$conn = $table->establishConnection('r');
$subquery = $this->newFulltextSubquery($query, $conn);
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 25);
// NOTE: We must JOIN the subquery in order to apply a limit.
$results = queryfx_all(
$conn,
'SELECT
documentPHID,
MAX(fieldScore) AS documentScore
FROM (%Q) query
JOIN %T root ON query.documentPHID = root.phid
GROUP BY documentPHID
ORDER BY documentScore DESC
LIMIT %d, %d',
$subquery,
$document_table,
$offset,
$limit);
return ipull($results, 'documentPHID');
}
private function newFulltextSubquery(
PhabricatorSavedQuery $query,
AphrontDatabaseConnection $conn) {
$field = new PhabricatorSearchDocumentField();
$field_table = $field->getTableName();
$document = new PhabricatorSearchDocument();
$document_table = $document->getTableName();
$select = array();
$select[] = 'document.phid AS documentPHID';
$join = array();
$where = array();
$title_field = PhabricatorSearchDocumentFieldType::FIELD_TITLE;
$title_boost = 1024;
$stemmer = new PhutilSearchStemmer();
$raw_query = $query->getParameter('query');
$raw_query = trim($raw_query);
if (strlen($raw_query)) {
$compiler = PhabricatorSearchDocument::newQueryCompiler()
->setStemmer($stemmer);
$tokens = $compiler->newTokens($raw_query);
list($min_length, $stopword_list) = $this->getEngineLimits($conn);
// Process all the parts of the user's query so we can show them which
// parts we searched for and which ones we ignored.
$fulltext_tokens = array();
foreach ($tokens as $key => $token) {
$fulltext_token = id(new PhabricatorFulltextToken())
->setToken($token);
$fulltext_tokens[$key] = $fulltext_token;
$value = $token->getValue();
// If the value is unquoted, we'll stem it in the query, so stem it
// here before performing filtering tests. See T12596.
if (!$token->isQuoted()) {
$value = $stemmer->stemToken($value);
}
if ($this->isShortToken($value, $min_length)) {
$fulltext_token->setIsShort(true);
continue;
}
if (isset($stopword_list[phutil_utf8_strtolower($value)])) {
$fulltext_token->setIsStopword(true);
continue;
}
}
$this->fulltextTokens = $fulltext_tokens;
// Remove tokens which aren't queryable from the query. This is mostly
// a workaround for the peculiar behaviors described in T12137.
foreach ($this->fulltextTokens as $key => $fulltext_token) {
if (!$fulltext_token->isQueryable()) {
unset($tokens[$key]);
}
}
if (!$tokens) {
throw new PhutilSearchQueryCompilerSyntaxException(
pht(
'All of your search terms are too short or too common to '.
'appear in the search index. Search for longer or more '.
'distinctive terms.'));
}
$queries = array();
$queries[] = $compiler->compileLiteralQuery($tokens);
$queries[] = $compiler->compileStemmedQuery($tokens);
$compiled_query = implode(' ', array_filter($queries));
} else {
$compiled_query = null;
}
if (strlen($compiled_query)) {
$select[] = qsprintf(
$conn,
'IF(field.field = %s, %d, 0) +
MATCH(corpus, stemmedCorpus) AGAINST (%s IN BOOLEAN MODE)
AS fieldScore',
$title_field,
$title_boost,
$compiled_query);
$join[] = qsprintf(
$conn,
'%T field ON field.phid = document.phid',
$field_table);
$where[] = qsprintf(
$conn,
'MATCH(corpus, stemmedCorpus) AGAINST (%s IN BOOLEAN MODE)',
$compiled_query);
if ($query->getParameter('field')) {
$where[] = qsprintf(
$conn,
'field.field = %s',
$field);
}
} else {
$select[] = qsprintf(
$conn,
'document.documentCreated AS fieldScore');
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
$where[] = qsprintf(
$conn,
'document.phid != %s',
$exclude);
}
$types = $query->getParameter('types');
if ($types) {
if (strlen($compiled_query)) {
$where[] = qsprintf(
$conn,
'field.phidType IN (%Ls)',
$types);
}
$where[] = qsprintf(
$conn,
'document.documentType IN (%Ls)',
$types);
}
$join[] = $this->joinRelationship(
$conn,
$query,
'authorPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR);
$statuses = $query->getParameter('statuses', array());
$statuses = array_fuse($statuses);
$open_rel = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$closed_rel = PhabricatorSearchRelationship::RELATIONSHIP_CLOSED;
$include_open = !empty($statuses[$open_rel]);
$include_closed = !empty($statuses[$closed_rel]);
if ($include_open && !$include_closed) {
$join[] = $this->joinRelationship(
$conn,
$query,
'statuses',
$open_rel,
true);
} else if ($include_closed && !$include_open) {
$join[] = $this->joinRelationship(
$conn,
$query,
'statuses',
$closed_rel,
true);
}
if ($query->getParameter('withAnyOwner')) {
$join[] = $this->joinRelationship(
$conn,
$query,
'withAnyOwner',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
true);
} else if ($query->getParameter('withUnowned')) {
$join[] = $this->joinRelationship(
$conn,
$query,
'withUnowned',
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
true);
} else {
$join[] = $this->joinRelationship(
$conn,
$query,
'ownerPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER);
}
$join[] = $this->joinRelationship(
$conn,
$query,
'subscriberPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER);
$join[] = $this->joinRelationship(
$conn,
$query,
'projectPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT);
$join[] = $this->joinRelationship(
$conn,
$query,
'repository',
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY);
$select = implode(', ', $select);
$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 = '';
}
if (strlen($compiled_query)) {
$order = '';
} else {
// When not executing a query, order by document creation date. This
// is the default view in object browser dialogs, like "Close Duplicate".
$order = qsprintf(
$conn,
'ORDER BY document.documentCreated DESC');
}
return qsprintf(
$conn,
'SELECT %Q FROM %T document %Q %Q %Q LIMIT 1000',
$select,
$document_table,
$join,
$where,
$order);
}
protected function joinRelationship(
AphrontDatabaseConnection $conn,
PhabricatorSavedQuery $query,
$field,
$type,
$is_existence = false) {
$sql = qsprintf(
$conn,
'%T AS %C ON %C.phid = document.phid AND %C.relation = %s',
id(new PhabricatorSearchDocumentRelationship())->getTableName(),
$field,
$field,
$field,
$type);
if (!$is_existence) {
$phids = $query->getParameter($field, array());
if (!$phids) {
return null;
}
$sql .= qsprintf(
$conn,
' AND %C.relatedPHID in (%Ls)',
$field,
$phids);
}
return $sql;
}
public function indexExists() {
return true;
}
public function getIndexStats() {
return false;
}
public function getFulltextTokens() {
return $this->fulltextTokens;
}
private function getEngineLimits(AphrontDatabaseConnection $conn) {
if ($this->engineLimits === null) {
$this->engineLimits = $this->newEngineLimits($conn);
}
return $this->engineLimits;
}
private function newEngineLimits(AphrontDatabaseConnection $conn) {
// First, try InnoDB. Some database may not have both table engines, so
// selecting variables from missing table engines can fail and throw.
try {
$result = queryfx_one(
$conn,
'SELECT @@innodb_ft_min_token_size innodb_max,
@@innodb_ft_server_stopword_table innodb_stopword_config');
} catch (AphrontQueryException $ex) {
$result = null;
}
if ($result) {
$min_len = $result['innodb_max'];
$stopword_config = $result['innodb_stopword_config'];
if (preg_match('(/)', $stopword_config)) {
// If the setting is nonempty and contains a slash, query the
// table the user has configured.
$parts = explode('/', $stopword_config);
list($stopword_database, $stopword_table) = $parts;
} else {
// Otherwise, query the InnoDB default stopword table.
$stopword_database = 'INFORMATION_SCHEMA';
$stopword_table = 'INNODB_FT_DEFAULT_STOPWORD';
}
$stopwords = queryfx_all(
$conn,
'SELECT * FROM %T.%T',
$stopword_database,
$stopword_table);
$stopwords = ipull($stopwords, 'value');
$stopwords = array_fuse($stopwords);
return array($min_len, $stopwords);
}
// If InnoDB fails, try MyISAM.
$result = queryfx_one(
$conn,
'SELECT
@@ft_min_word_len myisam_max,
@@ft_stopword_file myisam_stopwords');
$min_len = $result['myisam_max'];
$file = $result['myisam_stopwords'];
if (preg_match('(/resources/sql/stopwords\.txt\z)', $file)) {
// If this is set to something that looks like the Phabricator
// stopword file, read that.
$file = 'stopwords.txt';
} else {
// Otherwise, just use the default stopwords. This might be wrong
// but we can't read the actual value dynamically and reading
// whatever file the variable is set to could be a big headache
// to get right from a security perspective.
$file = 'stopwords_myisam.txt';
}
$root = dirname(phutil_get_library_root('phabricator'));
$data = Filesystem::readFile($root.'/resources/sql/'.$file);
$stopwords = explode("\n", $data);
$stopwords = array_filter($stopwords);
$stopwords = array_fuse($stopwords);
return array($min_len, $stopwords);
}
private function isShortToken($value, $min_length) {
// NOTE: The engine tokenizes internally on periods, so terms in the form
// "ab.cd", where short substrings are separated by periods, do not produce
// any queryable tokens. These terms are meaningful if at least one
// substring is longer than the minimum length, like "example.py". See
// T12928. This also applies to words with intermediate apostrophes, like
// "to's".
$parts = preg_split('/[.\']+/', $value);
foreach ($parts as $part) {
if (phutil_utf8_strlen($part) >= $min_length) {
return false;
}
}
return true;
}
}

View file

@ -123,3 +123,53 @@ Another useful function is the `viewer()` function, which works as though you'd
typed your own username when you run the query. However, if you send the query
to someone else, it will show results for //their// username when they run it.
This can be particularly useful when creating dashboard panels.
Fulltext Search
===============
Global search and some applications provide **fulltext search**. In
applications, this is a field called {nav Query}.
Fulltext search allows you to search the text content of objects and supports
some special syntax. These features are supported:
- Substring search with `~platypus`.
- Field search with `title:platypus`.
- Filtering out matches with `-platypus`.
- Quoted terms with `"platypus attorney"`.
- Combining features with `title:~"platypus attorney"`.
See below for more detail.
**Substrings**: Normally, query terms are searched for as words, so searching
for `read` won't find documents which only contain the word `threaded`, even
though "read" is a substring of "threaded". With the substring operator, `~`,
you can search for substrings instead: the query `~read` will match documents
which contain that text anywhere, even in the middle of a word.
**Quoted Terms**: When you search for multiple terms, documents which match
each term will be returned, even if the terms are not adjacent in the document.
For example, the query `void star` will match a document titled `A star in the
void`, because it matches both `void` and `star`. To search for an exact
sequence of terms, quote them: `"void star"`. This query will only match
documents which use those terms as written.
**Stemming**: Searching for a term like `rearming` will find documents which
contain variations of the word, like `rearm`, `rearms`, and `rearmed`. To
search for an an exact word, quote the term: `"rearming"`.
**Field Search**: By default, query terms are searched for in the title, body,
and comments. If you only want to search for a term in titles, use `title:`.
For example, `title:platypus` only finds documents with that term in the
title. This can be combined with other operators, for example `title:~platypus`
or `title:"platypus attorney"`. These scopes are also supported:
- `title:...` searches titles.
- `body:...` searches bodies (descriptions or summaries).
- `core:...` searches titles and bodies, but not comments.
- `comments:...` searches only comments.
**Filtering Matches**: You can remove documents which match certain terms from
the result set with `-`. For example: `platypus -mammal`. Documents which match
negated terms will be filtered out of the result set.

View file

@ -1469,6 +1469,17 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
return $this;
}
public function getFerretTokens() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
return $this->ferretTokens;
}
public function withFerretConstraint(
PhabricatorFerretEngine $engine,
array $fulltext_tokens) {
@ -1672,6 +1683,9 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
// If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations.
if ($is_stemmed) {
// Trim the boundary space characters so the stemmer recognizes this
// is (or, at least, may be) a normal word and activates.
$terms_value = trim($terms_value, ' ');
$stem_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams);

View file

@ -78,12 +78,15 @@ JX.behavior('aphlict-listen', function(config) {
JX.Stratcom.invoke('notification-panel-update', null, {});
var response = e.getData();
if (!response.showAnyNotification) {
return;
}
// Show the notification itself.
new JX.Notification()
.setContent(JX.$H(response.content))
.setDesktopReady(response.desktopReady)
.setWebReady(response.webReady)
.setKey(response.primaryObjectPHID)
.setShowAsDesktopNotification(response.showDesktopNotification)
.setTitle(response.title)
.setBody(response.body)
.setHref(response.href)

View file

@ -26,8 +26,7 @@ JX.install('Notification', {
_visible : false,
_hideTimer : null,
_duration : 12000,
_desktopReady : false,
_webReady : false,
_asDesktop : false,
_key : null,
_title : null,
_body : null,
@ -37,11 +36,6 @@ JX.install('Notification', {
show : function() {
var self = JX.Notification;
// This person doesn't like any real-time notification
if (!this._desktopReady && !this._webReady) {
return;
}
if (!this._visible) {
this._visible = true;
@ -51,7 +45,7 @@ JX.install('Notification', {
if (self.supportsDesktopNotifications() &&
self.desktopNotificationsEnabled() &&
this._desktopReady) {
this._asDesktop) {
// Note: specifying "tag" means that notifications with matching
// keys will aggregate.
var n = new window.Notification(this._title, {
@ -94,13 +88,8 @@ JX.install('Notification', {
return this;
},
setDesktopReady : function(ready) {
this._desktopReady = ready;
return this;
},
setWebReady : function(ready) {
this._webReady = ready;
setShowAsDesktopNotification : function(mode) {
this._asDesktop = mode;
return this;
},