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.css' => 'e68cf1fa',
'conpherence.pkg.js' => 'b5b51108', 'conpherence.pkg.js' => 'b5b51108',
'core.pkg.css' => 'e9473020', 'core.pkg.css' => 'e9473020',
'core.pkg.js' => '6c085267', 'core.pkg.js' => '28552e58',
'darkconsole.pkg.js' => '1f9a31bc', 'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '45951e9e', 'differential.pkg.css' => '45951e9e',
'differential.pkg.js' => 'b71b8c5d', 'differential.pkg.js' => 'b71b8c5d',
@ -374,7 +374,7 @@ return array(
'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f',
'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a', 'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2', '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-aphlict-status.js' => '5e2634b9',
'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289',
'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443', 'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443',
@ -467,7 +467,7 @@ return array(
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9',
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', '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/Prefab.js' => 'c5af80a2',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '320810c8', 'rsrc/js/core/TextAreaUtils.js' => '320810c8',
@ -585,7 +585,7 @@ return array(
'javelin-aphlict' => 'e1d4b11a', 'javelin-aphlict' => 'e1d4b11a',
'javelin-behavior' => '61cbc29a', 'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => 'caade6f2', 'javelin-behavior-aphlict-dropdown' => 'caade6f2',
'javelin-behavior-aphlict-listen' => 'a14cbdfc', 'javelin-behavior-aphlict-listen' => '4cc4f460',
'javelin-behavior-aphlict-status' => '5e2634b9', 'javelin-behavior-aphlict-status' => '5e2634b9',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22', 'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22',
@ -789,7 +789,7 @@ return array(
'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9',
'phabricator-main-menu-view' => '1802a242', 'phabricator-main-menu-view' => '1802a242',
'phabricator-nav-view-css' => 'faf6a6fc', 'phabricator-nav-view-css' => 'faf6a6fc',
'phabricator-notification' => '5c3349b2', 'phabricator-notification' => '008faf9c',
'phabricator-notification-css' => '457861ec', 'phabricator-notification-css' => '457861ec',
'phabricator-notification-menu-css' => '10685bd4', 'phabricator-notification-menu-css' => '10685bd4',
'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-object-selector-css' => '85ee8ce6',
@ -904,6 +904,13 @@ return array(
'unhandled-exception-css' => '4c96257a', 'unhandled-exception-css' => '4c96257a',
), ),
'requires' => array( 'requires' => array(
'008faf9c' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'013ffff9' => array( '013ffff9' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1236,6 +1243,20 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-notification', '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( '4d863052' => array(
'javelin-dom', 'javelin-dom',
'javelin-util', 'javelin-util',
@ -1326,13 +1347,6 @@ return array(
'javelin-vector', 'javelin-vector',
'javelin-dom', 'javelin-dom',
), ),
'5c3349b2' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'5c54cbf3' => array( '5c54cbf3' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -1684,20 +1698,6 @@ return array(
'javelin-util', 'javelin-util',
'phabricator-keyboard-shortcut', '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( 'a3a63478' => array(
'phui-workcard-view-css', '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( phutil_register_library_map(array(
'__library_version__' => 2, '__library_version__' => 2,
'class' => array( 'class' => array(
'AlamancServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlamancServiceEditConduitAPIMethod.php',
'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php',
@ -36,6 +37,7 @@ phutil_register_library_map(array(
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php', 'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php',
'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php', 'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php',
'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
@ -2667,6 +2669,7 @@ phutil_register_library_map(array(
'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php', 'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php', 'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php',
'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php', 'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php', 'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php', 'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php',
@ -3196,7 +3199,6 @@ phutil_register_library_map(array(
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php', 'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php', 'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php', 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php', 'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php', 'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
@ -4930,6 +4932,7 @@ phutil_register_library_map(array(
'require_celerity_resource' => 'applications/celerity/api.php', 'require_celerity_resource' => 'applications/celerity/api.php',
), ),
'xmap' => array( 'xmap' => array(
'AlamancServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacAddress' => 'Phobject', 'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array( 'AlmanacBinding' => array(
'AlmanacDAO', 'AlmanacDAO',
@ -4975,6 +4978,7 @@ phutil_register_library_map(array(
'PhabricatorExtendedPolicyInterface', 'PhabricatorExtendedPolicyInterface',
), ),
'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceController' => 'AlmanacController',
'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacDeviceEditController' => 'AlmanacDeviceController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController',
'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine', 'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine',
'AlmanacDeviceEditor' => 'AlmanacEditor', 'AlmanacDeviceEditor' => 'AlmanacEditor',
@ -5367,6 +5371,7 @@ phutil_register_library_map(array(
'DifferentialChangeset' => array( 'DifferentialChangeset' => array(
'DifferentialDAO', 'DifferentialDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
), ),
'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject', 'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
@ -5461,6 +5466,7 @@ phutil_register_library_map(array(
'DifferentialHunk' => array( 'DifferentialHunk' => array(
'DifferentialDAO', 'DifferentialDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
), ),
'DifferentialHunkParser' => 'Phobject', 'DifferentialHunkParser' => 'Phobject',
'DifferentialHunkParserTestCase' => 'PhabricatorTestCase', 'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
@ -7999,6 +8005,7 @@ phutil_register_library_map(array(
'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow', 'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication', 'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting', 'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
@ -8584,7 +8591,6 @@ phutil_register_library_map(array(
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array( '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) { if ($config_attach) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID()); // See T12033, T11767, and PHI55. This is a crude fix to stop the
$mime_type = 'text/x-patch; charset=utf-8'; // major concrete problems that lackluster email size limits cause.
$body->addAttachment( if (strlen($patch) < $body_limit) {
new PhabricatorMetaMTAAttachment($patch, $name, $mime_type)); $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 <?php
final class DifferentialChangeset extends DifferentialDAO final class DifferentialChangeset
implements PhabricatorPolicyInterface { extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID; protected $diffID;
protected $oldFile; protected $oldFile;
@ -236,4 +239,25 @@ final class DifferentialChangeset extends DifferentialDAO
return $this->getDiff()->hasAutomaticCapability($capability, $viewer); 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(); $this->delete();
foreach ($this->loadChangesets() as $changeset) { foreach ($this->loadChangesets() as $changeset) {
$changeset->delete(); $engine->destroyObject($changeset);
} }
$properties = id(new DifferentialDiffProperty())->loadAllWhere( $properties = id(new DifferentialDiffProperty())->loadAllWhere(

View file

@ -1,7 +1,10 @@
<?php <?php
abstract class DifferentialHunk extends DifferentialDAO abstract class DifferentialHunk
implements PhabricatorPolicyInterface { extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $changesetID; protected $changesetID;
protected $oldOffset; protected $oldOffset;
@ -228,4 +231,14 @@ abstract class DifferentialHunk extends DifferentialDAO
return $this->getChangeset()->hasAutomaticCapability($capability, $viewer); 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 $rawData;
private $forcedEncoding; private $forcedEncoding;
private $fileData;
public function getTableName() { public function getTableName() {
return 'differential_hunk_modern'; return 'differential_hunk_modern';
@ -87,6 +88,57 @@ final class DifferentialModernHunk extends DifferentialHunk {
return parent::save(); 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() { private function getRawData() {
if ($this->rawData === null) { if ($this->rawData === null) {
$type = $this->getDataType(); $type = $this->getDataType();
@ -98,6 +150,8 @@ final class DifferentialModernHunk extends DifferentialHunk {
$data = $data; $data = $data;
break; break;
case self::DATATYPE_FILE: case self::DATATYPE_FILE:
$data = $this->loadFileData();
break;
default: default:
throw new Exception( throw new Exception(
pht('Hunk has unsupported data type "%s"!', $type)); pht('Hunk has unsupported data type "%s"!', $type));
@ -123,4 +177,75 @@ final class DifferentialModernHunk extends DifferentialHunk {
return $this->rawData; 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) { foreach ($changesets as $changeset_id => $changeset) {
$path = $changeset->getFilename(); $path = $changeset->getFilename();
$anchor = substr(md5($path), 0, 8); $anchor = $changeset->getAnchorName();
$history_link = $diffusion_view->linkHistory($path); $history_link = $diffusion_view->linkHistory($path);
$browse_link = $diffusion_view->linkBrowse( $browse_link = $diffusion_view->linkBrowse(

View file

@ -768,7 +768,10 @@ final class DiffusionServeController extends DiffusionController {
$input = strlen($input)."\n".$input."0\n"; $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); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $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'); $full_text = $request->getValue('fullText');
if ($full_text) { 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'); $status = $request->getValue('status');

View file

@ -88,23 +88,40 @@ final class ManiphestReportController extends ManiphestController {
$data = queryfx_all( $data = queryfx_all(
$conn, $conn,
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q 'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated
WHERE transactionType = %s FROM %T x %Q
WHERE transactionType IN (%Ls)
ORDER BY x.dateCreated ASC', ORDER BY x.dateCreated ASC',
$table->getTableName(), $table->getTableName(),
$joins, $joins,
ManiphestTaskStatusTransaction::TRANSACTIONTYPE); array(
ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE,
));
$stats = array(); $stats = array();
$day_buckets = array(); $day_buckets = array();
$open_tasks = array(); $open_tasks = array();
$default_status = ManiphestTaskStatus::getDefaultStatus();
$duplicate_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($data as $key => $row) { foreach ($data as $key => $row) {
switch ($row['transactionType']) {
// NOTE: Hack to avoid json_decode(). case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$oldv = trim($row['oldValue'], '"'); // NOTE: Hack to avoid json_decode().
$newv = trim($row['newValue'], '"'); $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') { if ($oldv == 'null') {
$old_is_open = false; $old_is_open = false;

View file

@ -24,8 +24,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $subtaskIDs; private $subtaskIDs;
private $subtypes; private $subtypes;
private $fullTextSearch = '';
private $status = 'status-any'; private $status = 'status-any';
const STATUS_ANY = 'status-any'; const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open'; const STATUS_OPEN = 'status-open';
@ -115,11 +113,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this; return $this;
} }
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) { public function setGroupBy($group) {
$this->groupBy = $group; $this->groupBy = $group;
@ -329,7 +322,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->taskIDs !== null) { if ($this->taskIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
@ -481,36 +473,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return '('.implode(') OR (', $subclause).')'; 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) { protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$open_statuses = ManiphestTaskStatus::getOpenStatusConstants(); $open_statuses = ManiphestTaskStatus::getOpenStatusConstants();
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;

View file

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

View file

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

View file

@ -38,15 +38,9 @@ final class PhabricatorNotificationIndividualController
$dict = $builder->buildDict(); $dict = $builder->buildDict();
$data = $dict[0]; $data = $dict[0];
$response = array( $response = $data + array(
'pertinent' => true, 'pertinent' => true,
'primaryObjectPHID' => $story->getPrimaryObjectPHID(), '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), 'content' => hsprintf('%s', $content),
'uniqueID' => 'story/'.$story->getChronologicalKey(), 'uniqueID' => 'story/'.$story->getChronologicalKey(),
); );

View file

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

View file

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

View file

@ -324,6 +324,9 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$result = $head + $body + $tail; $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) { foreach ($this->getHiddenFields() as $hidden_key) {
unset($result[$hidden_key]); unset($result[$hidden_key]);
} }

View file

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

View file

@ -7,7 +7,7 @@ final class PhabricatorFerretFulltextStorageEngine
private $engineLimits; private $engineLimits;
public function getEngineIdentifier() { public function getEngineIdentifier() {
return 'ferret'; return 'mysql';
} }
public function getHostType() { public function getHostType() {
@ -86,6 +86,10 @@ final class PhabricatorFerretFulltextStorageEngine
$type_results[$type] = $results; $type_results[$type] = $results;
$metadata += $engine_query->getFerretMetadata(); $metadata += $engine_query->getFerretMetadata();
if (!$this->fulltextTokens) {
$this->fulltextTokens = $engine_query->getFerretTokens();
}
} }
$list = array(); $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 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. to someone else, it will show results for //their// username when they run it.
This can be particularly useful when creating dashboard panels. 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; 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( public function withFerretConstraint(
PhabricatorFerretEngine $engine, PhabricatorFerretEngine $engine,
array $fulltext_tokens) { array $fulltext_tokens) {
@ -1672,6 +1683,9 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
// If this is a stemmed term, only look for ngrams present in both the // If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations. // unstemmed and stemmed variations.
if ($is_stemmed) { 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_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value); $stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams); $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, {}); JX.Stratcom.invoke('notification-panel-update', null, {});
var response = e.getData(); var response = e.getData();
if (!response.showAnyNotification) {
return;
}
// Show the notification itself. // Show the notification itself.
new JX.Notification() new JX.Notification()
.setContent(JX.$H(response.content)) .setContent(JX.$H(response.content))
.setDesktopReady(response.desktopReady)
.setWebReady(response.webReady)
.setKey(response.primaryObjectPHID) .setKey(response.primaryObjectPHID)
.setShowAsDesktopNotification(response.showDesktopNotification)
.setTitle(response.title) .setTitle(response.title)
.setBody(response.body) .setBody(response.body)
.setHref(response.href) .setHref(response.href)

View file

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