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:
commit
dc22aba9df
30 changed files with 1003 additions and 670 deletions
|
@ -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',
|
||||
),
|
||||
|
|
19
resources/sql/autopatches/20170912.ferret.01.activity.php
Normal file
19
resources/sql/autopatches/20170912.ferret.01.activity.php
Normal 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
|
@ -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(
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -766,6 +766,9 @@ final class DifferentialTransactionEditor
|
|||
}
|
||||
|
||||
if ($config_attach) {
|
||||
// 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(
|
||||
|
@ -773,6 +776,7 @@ final class DifferentialTransactionEditor
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -727,7 +727,7 @@ final class DifferentialDiff
|
|||
$this->delete();
|
||||
|
||||
foreach ($this->loadChangesets() as $changeset) {
|
||||
$changeset->delete();
|
||||
$engine->destroyObject($changeset);
|
||||
}
|
||||
|
||||
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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) {
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue