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

(stable) Promote 2017 Week 13

This commit is contained in:
epriestley 2017-04-03 04:21:45 -07:00
commit 2460755603
103 changed files with 2655 additions and 697 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array(
'conpherence.pkg.css' => '82aca405',
'conpherence.pkg.js' => '6249a1cf',
'core.pkg.css' => 'dc689e29',
'core.pkg.js' => '1fa7c0c5',
'core.pkg.css' => '1bf8fa70',
'core.pkg.js' => '021685f1',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '90b30783',
'differential.pkg.js' => 'ddfeb49b',
@ -26,7 +26,7 @@ return array(
'rsrc/css/aphront/multi-column.css' => '84cc6640',
'rsrc/css/aphront/notification.css' => '3f6c89c9',
'rsrc/css/aphront/panel-view.css' => '8427b78d',
'rsrc/css/aphront/phabricator-nav-view.css' => 'e58a4a30',
'rsrc/css/aphront/phabricator-nav-view.css' => 'faf6a6fc',
'rsrc/css/aphront/table-view.css' => '6ca8e057',
'rsrc/css/aphront/tokenizer.css' => '9a8cb501',
'rsrc/css/aphront/tooltip.css' => '173b9431',
@ -108,8 +108,8 @@ return array(
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => '9f4cb463',
'rsrc/css/core/remarkup.css' => '2d793c5b',
'rsrc/css/core/syntax.css' => '769d3498',
'rsrc/css/core/remarkup.css' => '17c0fb37',
'rsrc/css/core/syntax.css' => 'cae95e89',
'rsrc/css/core/z-index.css' => '5e72c4e0',
'rsrc/css/diviner/diviner-shared.css' => '896f1d43',
'rsrc/css/font/font-awesome.css' => 'e838e088',
@ -136,7 +136,7 @@ return array(
'rsrc/css/phui/phui-button.css' => '14bfba79',
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-cms.css' => '504b4b23',
'rsrc/css/phui/phui-comment-form.css' => '48fbd65d',
'rsrc/css/phui/phui-comment-form.css' => '7d903c2d',
'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad',
'rsrc/css/phui/phui-crumbs-view.css' => '6ece3bbb',
'rsrc/css/phui/phui-curtain-view.css' => '947bf1a4',
@ -503,7 +503,7 @@ return array(
'rsrc/js/core/behavior-object-selector.js' => 'e0ec7f2f',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
'rsrc/js/core/behavior-phabricator-nav.js' => '08675c6d',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '0c61d4e3',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'a0777ea3',
'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207',
'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b',
'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
@ -528,7 +528,7 @@ return array(
'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9',
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
'rsrc/js/phuix/PHUIXActionView.js' => 'b3465b9b',
'rsrc/js/phuix/PHUIXAutocomplete.js' => '7c492cd2',
'rsrc/js/phuix/PHUIXAutocomplete.js' => 'd5b2abf3',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => '8018ee50',
'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
@ -664,7 +664,7 @@ return array(
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
'javelin-behavior-phabricator-object-selector' => 'e0ec7f2f',
'javelin-behavior-phabricator-oncopy' => '2926fff2',
'javelin-behavior-phabricator-remarkup-assist' => '0c61d4e3',
'javelin-behavior-phabricator-remarkup-assist' => 'a0777ea3',
'javelin-behavior-phabricator-reveal-content' => '60821bc7',
'javelin-behavior-phabricator-search-typeahead' => '06c32383',
'javelin-behavior-phabricator-show-older-transactions' => '94c65b72',
@ -786,14 +786,14 @@ return array(
'phabricator-keyboard-shortcut' => '1ae869f2',
'phabricator-keyboard-shortcut-manager' => '4a021c10',
'phabricator-main-menu-view' => '5294060f',
'phabricator-nav-view-css' => 'e58a4a30',
'phabricator-nav-view-css' => 'faf6a6fc',
'phabricator-notification' => 'ccf1cbf8',
'phabricator-notification-css' => '3f6c89c9',
'phabricator-notification-menu-css' => '6a697e43',
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '8d40ae75',
'phabricator-remarkup-css' => '2d793c5b',
'phabricator-remarkup-css' => '17c0fb37',
'phabricator-search-results-css' => '64ad079a',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
@ -836,7 +836,7 @@ return array(
'phui-calendar-month-css' => '8e10e92c',
'phui-chart-css' => '6bf6f78e',
'phui-cms-css' => '504b4b23',
'phui-comment-form-css' => '48fbd65d',
'phui-comment-form-css' => '7d903c2d',
'phui-comment-panel-css' => 'f50152ad',
'phui-crumbs-view-css' => '6ece3bbb',
'phui-curtain-view-css' => '947bf1a4',
@ -885,7 +885,7 @@ return array(
'phui-workpanel-view-css' => 'a3a63478',
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => 'b3465b9b',
'phuix-autocomplete' => '7c492cd2',
'phuix-autocomplete' => 'd5b2abf3',
'phuix-dropdown-menu' => '8018ee50',
'phuix-form-control-view' => '83e03671',
'phuix-icon-view' => 'bff6884b',
@ -903,7 +903,7 @@ return array(
'sprite-login-css' => '587d92d7',
'sprite-tokens-css' => '9cdfd599',
'syntax-default-css' => '9923583c',
'syntax-highlighting-css' => '769d3498',
'syntax-highlighting-css' => 'cae95e89',
'tokens-css' => '3d0f239e',
'typeahead-browse-css' => '8904346a',
'unhandled-exception-css' => '4c96257a',
@ -988,16 +988,6 @@ return array(
'javelin-dom',
'javelin-router',
),
'0c61d4e3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-phtize',
'phabricator-textareautils',
'javelin-workflow',
'javelin-vector',
'phuix-autocomplete',
),
'0f764c35' => array(
'javelin-install',
'javelin-util',
@ -1446,9 +1436,6 @@ return array(
'phabricator-shaped-request',
'conpherence-thread-manager',
),
'769d3498' => array(
'syntax-default-css',
),
'76b9fc3e' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1477,12 +1464,6 @@ return array(
'owners-path-editor',
'javelin-behavior',
),
'7c492cd2' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
'7cbe244b' => array(
'javelin-install',
'javelin-util',
@ -1705,6 +1686,17 @@ return array(
'javelin-dom',
'javelin-vector',
),
'a0777ea3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-phtize',
'phabricator-textareautils',
'javelin-workflow',
'javelin-vector',
'phuix-autocomplete',
'javelin-mask',
),
'a0b57eb8' => array(
'javelin-behavior',
'javelin-dom',
@ -1999,6 +1991,9 @@ return array(
'phabricator-title',
'phabricator-favicon',
),
'cae95e89' => array(
'syntax-default-css',
),
'ccf1cbf8' => array(
'javelin-install',
'javelin-dom',
@ -2052,6 +2047,12 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'd5b2abf3' => array(
'javelin-install',
'javelin-dom',
'phuix-icon-view',
'phabricator-prefab',
),
'd6a7e717' => array(
'multirow-row-manager',
'javelin-install',

View file

@ -1,7 +1,15 @@
<?php
$search_engine = PhabricatorFulltextStorageEngine::loadEngine();
$use_mysql = ($search_engine instanceof PhabricatorMySQLFulltextStorageEngine);
$use_mysql = false;
$services = PhabricatorSearchService::getAllServices();
foreach ($services as $service) {
$engine = $service->getEngine();
if ($engine instanceof PhabricatorMySQLFulltextStorageEngine) {
$use_mysql = true;
}
}
if ($use_mysql) {
$field = new PhabricatorSearchDocumentField();

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_differential.differential_reviewer
ADD voidedPHID VARBINARY(64);

View file

@ -571,6 +571,7 @@ phutil_register_library_map(array(
'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php',
'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php',
'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
@ -2259,12 +2260,15 @@ phutil_register_library_map(array(
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php',
'PhabricatorClusterStrandedException' => 'infrastructure/cluster/PhabricatorClusterStrandedException.php',
'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/exception/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php',
'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php',
'PhabricatorClusterSearchConfigOptionType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php',
'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php',
'PhabricatorClusterStrandedException' => 'infrastructure/cluster/exception/PhabricatorClusterStrandedException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
@ -2310,6 +2314,7 @@ phutil_register_library_map(array(
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
'PhabricatorConfigClusterSearchController' => 'applications/config/controller/PhabricatorConfigClusterSearchController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
@ -2543,7 +2548,6 @@ phutil_register_library_map(array(
'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
'PhabricatorDatabaseHealthRecord' => 'infrastructure/cluster/PhabricatorDatabaseHealthRecord.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
@ -2651,7 +2655,9 @@ phutil_register_library_map(array(
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php',
'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php',
'PhabricatorElasticsearchHost' => 'infrastructure/cluster/search/PhabricatorElasticsearchHost.php',
'PhabricatorElasticsearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php',
'PhabricatorElasticsearchSetupCheck' => 'applications/config/check/PhabricatorElasticsearchSetupCheck.php',
'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
'PhabricatorEmailContentSource' => 'applications/metamta/contentsource/PhabricatorEmailContentSource.php',
'PhabricatorEmailDeliverySettingsPanel' => 'applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php',
@ -3073,6 +3079,7 @@ phutil_register_library_map(array(
'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',
'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
@ -3654,6 +3661,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
'PhabricatorRepositoryFulltextEngine' => 'applications/repository/search/PhabricatorRepositoryFulltextEngine.php',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
'PhabricatorRepositoryGitLFSRef' => 'applications/repository/storage/PhabricatorRepositoryGitLFSRef.php',
@ -3762,7 +3770,6 @@ phutil_register_library_map(array(
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
@ -3783,8 +3790,8 @@ phutil_register_library_map(array(
'PhabricatorSearchEngineAttachment' => 'applications/search/engineextension/PhabricatorSearchEngineAttachment.php',
'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php',
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
'PhabricatorSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php',
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php',
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php',
@ -3804,6 +3811,7 @@ phutil_register_library_map(array(
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
@ -4303,35 +4311,35 @@ phutil_register_library_map(array(
'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
'PhortuneAccountEditController' => 'applications/phortune/controller/PhortuneAccountEditController.php',
'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php',
'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php',
'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
'PhortuneAccountListController' => 'applications/phortune/controller/PhortuneAccountListController.php',
'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php',
'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/cart/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/cart/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/cart/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php',
'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
'PhortuneCartListController' => 'applications/phortune/controller/PhortuneCartListController.php',
'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php',
'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
'PhortuneCartUpdateController' => 'applications/phortune/controller/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php',
'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargeListController' => 'applications/phortune/controller/PhortuneChargeListController.php',
'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
@ -4350,27 +4358,27 @@ phutil_register_library_map(array(
'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php',
'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php',
'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php',
'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/PhortuneMerchantPictureController.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php',
'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
'PhortuneMerchantViewController' => 'applications/phortune/controller/PhortuneMerchantViewController.php',
'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
@ -4383,13 +4391,13 @@ phutil_register_library_map(array(
'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php',
'PhortuneProductListController' => 'applications/phortune/controller/product/PhortuneProductListController.php',
'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php',
'PhortuneProviderActionController' => 'applications/phortune/controller/PhortuneProviderActionController.php',
'PhortuneProviderDisableController' => 'applications/phortune/controller/PhortuneProviderDisableController.php',
'PhortuneProviderEditController' => 'applications/phortune/controller/PhortuneProviderEditController.php',
'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php',
'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php',
'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php',
'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
@ -4397,15 +4405,15 @@ phutil_register_library_map(array(
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php',
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php',
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
@ -5349,6 +5357,7 @@ phutil_register_library_map(array(
'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType',
'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
'DifferentialRevisionViewController' => 'DifferentialController',
'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialStoredCustomField' => 'DifferentialCustomField',
@ -7303,6 +7312,9 @@ phutil_register_library_map(array(
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterNoHostForRoleException' => 'Exception',
'PhabricatorClusterSearchConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorClusterServiceHealthRecord' => 'Phobject',
'PhabricatorClusterStrandedException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
@ -7354,6 +7366,7 @@ phutil_register_library_map(array(
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
@ -7624,7 +7637,6 @@ phutil_register_library_map(array(
'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController',
'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorDataNotAttachedException' => 'Exception',
'PhabricatorDatabaseHealthRecord' => 'Phobject',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
@ -7738,7 +7750,8 @@ phutil_register_library_map(array(
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost',
'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailContentSource' => 'PhabricatorContentSource',
'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
@ -8208,6 +8221,7 @@ phutil_register_library_map(array(
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(
'PhabricatorSearchDAO',
@ -8898,6 +8912,7 @@ phutil_register_library_map(array(
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
),
'PhabricatorRepositoryAuditRequest' => array(
'PhabricatorRepositoryDAO',
@ -8939,6 +8954,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorRepositoryEngine' => 'Phobject',
'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryGitLFSRef' => array(
@ -9074,7 +9090,6 @@ phutil_register_library_map(array(
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
@ -9095,8 +9110,8 @@ phutil_register_library_map(array(
'PhabricatorSearchEngineAttachment' => 'Phobject',
'PhabricatorSearchEngineExtension' => 'Phobject',
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorSearchField' => 'Phobject',
'PhabricatorSearchHost' => 'Phobject',
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
@ -9116,6 +9131,7 @@ phutil_register_library_map(array(
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
'PhabricatorSearchService' => 'Phobject',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
@ -9726,6 +9742,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneAccountChargeListController' => 'PhortuneController',
'PhortuneAccountEditController' => 'PhortuneController',
'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
@ -9761,7 +9778,6 @@ phutil_register_library_map(array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneChargeListController' => 'PhortuneController',
'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',

View file

@ -69,6 +69,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication {
'databases/' => 'PhabricatorConfigClusterDatabasesController',
'notifications/' => 'PhabricatorConfigClusterNotificationsController',
'repositories/' => 'PhabricatorConfigClusterRepositoriesController',
'search/' => 'PhabricatorConfigClusterSearchController',
),
),
);

View file

@ -1,77 +0,0 @@
<?php
final class PhabricatorElasticSearchSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
if (!$this->shouldUseElasticSearchEngine()) {
return;
}
$engine = new PhabricatorElasticFulltextStorageEngine();
$index_exists = null;
$index_sane = null;
try {
$index_exists = $engine->indexExists();
if ($index_exists) {
$index_sane = $engine->indexIsSane();
}
} catch (Exception $ex) {
$summary = pht('Elasticsearch is not reachable as configured.');
$message = pht(
'Elasticsearch is configured (with the %s setting) but Phabricator '.
'encountered an exception when trying to test the index.'.
"\n\n".
'%s',
phutil_tag('tt', array(), 'search.elastic.host'),
phutil_tag('pre', array(), $ex->getMessage()));
$this->newIssue('elastic.misconfigured')
->setName(pht('Elasticsearch Misconfigured'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('search.elastic.host');
return;
}
if (!$index_exists) {
$summary = pht(
'You enabled Elasticsearch but the index does not exist.');
$message = pht(
'You likely enabled search.elastic.host without creating the '.
'index. Run `./bin/search init` to correct the index.');
$this
->newIssue('elastic.missing-index')
->setName(pht('Elasticsearch index Not Found'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('search.elastic.host');
} else if (!$index_sane) {
$summary = pht(
'Elasticsearch index exists but needs correction.');
$message = pht(
'Either the Phabricator schema for Elasticsearch has changed '.
'or Elasticsearch created the index automatically. Run '.
'`./bin/search init` to correct the index.');
$this
->newIssue('elastic.broken-index')
->setName(pht('Elasticsearch index Incorrect'))
->setSummary($summary)
->setMessage($message);
}
}
protected function shouldUseElasticSearchEngine() {
$search_engine = PhabricatorFulltextStorageEngine::loadEngine();
return ($search_engine instanceof PhabricatorElasticFulltextStorageEngine);
}
}

View file

@ -0,0 +1,82 @@
<?php
final class PhabricatorElasticsearchSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$services = PhabricatorSearchService::getAllServices();
foreach ($services as $service) {
try {
$host = $service->getAnyHostForRole('read');
} catch (PhabricatorClusterNoHostForRoleException $e) {
// ignore the error
continue;
}
if ($host instanceof PhabricatorElasticsearchHost) {
$index_exists = null;
$index_sane = null;
try {
$engine = $host->getEngine();
$index_exists = $engine->indexExists();
if ($index_exists) {
$index_sane = $engine->indexIsSane();
}
} catch (Exception $ex) {
$summary = pht('Elasticsearch is not reachable as configured.');
$message = pht(
'Elasticsearch is configured (with the %s setting) but Phabricator'.
' encountered an exception when trying to test the index.'.
"\n\n".
'%s',
phutil_tag('tt', array(), 'cluster.search'),
phutil_tag('pre', array(), $ex->getMessage()));
$this->newIssue('elastic.misconfigured')
->setName(pht('Elasticsearch Misconfigured'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('cluster.search');
return;
}
if (!$index_exists) {
$summary = pht(
'You enabled Elasticsearch but the index does not exist.');
$message = pht(
'You likely enabled cluster.search without creating the '.
'index. Use the following command to create a new index.');
$this
->newIssue('elastic.missing-index')
->setName(pht('Elasticsearch Index Not Found'))
->addCommand('./bin/search init')
->setSummary($summary)
->setMessage($message);
} else if (!$index_sane) {
$summary = pht(
'Elasticsearch index exists but needs correction.');
$message = pht(
'Either the Phabricator schema for Elasticsearch has changed '.
'or Elasticsearch created the index automatically. '.
'Use the following command to rebuild the index.');
$this
->newIssue('elastic.broken-index')
->setName(pht('Elasticsearch Index Schema Mismatch'))
->addCommand('./bin/search init')
->setSummary($summary)
->setMessage($message);
}
}
}
}
}

View file

@ -198,6 +198,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'This option has been removed, you can use Dashboards to provide '.
'homepage customization. See T11533 for more details.');
$elastic_reason = pht(
'Elasticsearch is now configured with "%s".',
'cluster.search');
$ancient_config += array(
'phid.external-loaders' =>
pht(
@ -348,6 +352,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
'search.elastic.host' => $elastic_reason,
'search.elastic.namespace' => $elastic_reason,
);
return $ancient_config;

View file

@ -145,7 +145,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"be able to find search results for common words. You can gain ".
"access to this option by upgrading MySQL to a more recent ".
"version.\n\n".
"You can ignore this warning if you plan to configure ElasticSearch ".
"You can ignore this warning if you plan to configure Elasticsearch ".
"later, or aren't concerned about searching for common words.",
$host_name,
phutil_tag('tt', array(), 'ft_stopword_file'));
@ -180,7 +180,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"To make search more useful, you can use an alternate stopword ".
"file with fewer words. Alternatively, if you aren't concerned ".
"about searching for common words, you can ignore this warning. ".
"If you later plan to configure ElasticSearch, you can also ignore ".
"If you later plan to configure Elasticsearch, you can also ignore ".
"this warning: this stopword file only affects MySQL fulltext ".
"indexes.\n\n".
"To choose a different stopword file, add this to your %s file ".
@ -231,7 +231,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"You can change this setting to 3 to allow these words to be ".
"indexed. Alternatively, you can ignore this warning if you are ".
"not concerned about searching for 3-letter words. If you later ".
"plan to configure ElasticSearch, you can also ignore this warning: ".
"plan to configure Elasticsearch, you can also ignore this warning: ".
"only MySQL fulltext search is affected.\n\n".
"To reduce the minimum word length to 3, add this to your %s file ".
"(in the %s section) and then restart %s:\n\n".
@ -379,8 +379,13 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
}
protected function shouldUseMySQLSearchEngine() {
$search_engine = PhabricatorFulltextStorageEngine::loadEngine();
return ($search_engine instanceof PhabricatorMySQLFulltextStorageEngine);
$services = PhabricatorSearchService::getAllServices();
foreach ($services as $service) {
if ($service instanceof PhabricatorMySQLSearchHost) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,129 @@
<?php
final class PhabricatorConfigClusterSearchController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$nav = $this->buildSideNavView();
$nav->selectFilter('cluster/search/');
$title = pht('Cluster Search');
$doc_href = PhabricatorEnv::getDoclink('Cluster: Search');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setProfileHeader(true)
->addActionLink(
id(new PHUIButtonView())
->setIcon('fa-book')
->setHref($doc_href)
->setTag('a')
->setText(pht('Documentation')));
$crumbs = $this
->buildApplicationCrumbs($nav)
->addTextCrumb($title)
->setBorder(true);
$search_status = $this->buildClusterSearchStatus();
$content = id(new PhabricatorConfigPageView())
->setHeader($header)
->setContent($search_status);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($content)
->addClass('white-background');
}
private function buildClusterSearchStatus() {
$viewer = $this->getViewer();
$services = PhabricatorSearchService::getAllServices();
Javelin::initBehavior('phabricator-tooltips');
$view = array();
foreach ($services as $service) {
$view[] = $this->renderStatusView($service);
}
return $view;
}
private function renderStatusView($service) {
$head = array_merge(
array(pht('Type')),
array_keys($service->getStatusViewColumns()),
array(pht('Status')));
$rows = array();
$status_map = PhabricatorSearchService::getConnectionStatusMap();
$stats = false;
$stats_view = false;
foreach ($service->getHosts() as $host) {
try {
$status = $host->getConnectionStatus();
$status = idx($status_map, $status, array());
} catch (Exception $ex) {
$status['icon'] = 'fa-times';
$status['label'] = pht('Connection Error');
$status['color'] = 'red';
$host->didHealthCheck(false);
}
if (!$stats_view) {
try {
$stats = $host->getEngine()->getIndexStats($host);
$stats_view = $this->renderIndexStats($stats);
} catch (Exception $e) {
$stats_view = false;
}
}
$type_icon = 'fa-search sky';
$type_tip = $host->getDisplayName();
$type_icon = id(new PHUIIconView())
->setIcon($type_icon);
$status_view = array(
id(new PHUIIconView())->setIcon($status['icon'].' '.$status['color']),
' ',
$status['label'],
);
$row = array(array($type_icon, ' ', $type_tip));
$row = array_merge($row, array_values(
$host->getStatusViewColumns()));
$row[] = $status_view;
$rows[] = $row;
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No search servers are configured.'))
->setHeaders($head);
$view = id(new PHUIObjectBoxView())
->setHeaderText($service->getDisplayName())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
if ($stats_view) {
$view->addPropertyList($stats_view);
}
return $view;
}
private function renderIndexStats($stats) {
$view = id(new PHUIPropertyListView());
if ($stats !== false) {
foreach ($stats as $label => $val) {
$view->addProperty($label, $val);
}
}
return $view;
}
}

View file

@ -42,8 +42,11 @@ abstract class PhabricatorConfigController extends PhabricatorController {
pht('Notification Servers'), null, 'fa-bell-o');
$nav->addFilter('cluster/repositories/',
pht('Repository Servers'), null, 'fa-code');
$nav->addFilter('cluster/search/',
pht('Search Servers'), null, 'fa-search');
$nav->addLabel(pht('Modules'));
$modules = PhabricatorConfigModule::getAllModules();
foreach ($modules as $key => $module) {
$nav->addFilter('module/'.$key.'/',

View file

@ -38,6 +38,17 @@ EOTEXT
$intro_href = PhabricatorEnv::getDoclink('Clustering Introduction');
$intro_name = pht('Clustering Introduction');
$search_type = 'custom:PhabricatorClusterSearchConfigOptionType';
$search_help = $this->deformat(pht(<<<EOTEXT
Define one or more fulltext storage services. Here you can configure which
hosts will handle fulltext search queries and indexing. For help with
configuring fulltext search clusters, see **[[ %s | %s ]]** in the
documentation.
EOTEXT
,
PhabricatorEnv::getDoclink('Cluster: Search'),
pht('Cluster: Search')));
return array(
$this->newOption('cluster.addresses', 'list<string>', array())
->setLocked(true)
@ -114,6 +125,21 @@ EOTEXT
->setSummary(
pht('Configure database read replicas.'))
->setDescription($databases_help),
$this->newOption('cluster.search', $search_type, array())
->setLocked(true)
->setSummary(
pht('Configure full-text search services.'))
->setDescription($search_help)
->setDefault(
array(
array(
'type' => 'mysql',
'roles' => array(
'read' => true,
'write' => true,
),
),
)),
);
}

View file

@ -60,6 +60,8 @@ final class DifferentialCreateCommentConduitAPIMethod
'accept' => DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE,
'reject' => DifferentialRevisionRejectTransaction::TRANSACTIONTYPE,
'resign' => DifferentialRevisionResignTransaction::TRANSACTIONTYPE,
'request_review' =>
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE,
);
$action = $request->getValue('action');

View file

@ -54,9 +54,7 @@ abstract class DifferentialController extends PhabricatorController {
$toc_view->setAuthorityPackages($packages);
}
// TODO: For Subversion, we should adjust these paths to be relative to
// the repository root where possible.
$paths = mpull($changesets, 'getFilename');
$paths = mpull($changesets, 'getOwnersFilename');
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
@ -83,7 +81,7 @@ abstract class DifferentialController extends PhabricatorController {
if ($have_owners) {
$packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$changeset->getFilename());
$changeset->getOwnersFilename());
$item->setPackages($packages);
}

View file

@ -12,6 +12,19 @@ final class DifferentialRevisionHasCommitEdgeType extends PhabricatorEdgeType {
return true;
}
public function getConduitKey() {
return 'revision.commit';
}
public function getConduitName() {
return pht('Revision Has Commit');
}
public function getConduitDescription() {
return pht(
'The source revision is associated with the destination commit.');
}
public function getTransactionAddString(
$actor,
$add_count,

View file

@ -12,6 +12,18 @@ final class DifferentialRevisionHasTaskEdgeType extends PhabricatorEdgeType {
return true;
}
public function getConduitKey() {
return 'revision.task';
}
public function getConduitName() {
return pht('Revision Has Task');
}
public function getConduitDescription() {
return pht('The source revision is associated with the destination task.');
}
public function getTransactionAddString(
$actor,
$add_count,

View file

@ -140,8 +140,6 @@ final class DifferentialTransactionEditor
return ($object->getStatus() == $status_closed);
case DifferentialAction::ACTION_RETHINK:
return ($object->getStatus() != $status_plan);
case DifferentialAction::ACTION_REQUEST:
return ($object->getStatus() != $status_review);
case DifferentialAction::ACTION_CLAIM:
return ($actor_phid != $object->getAuthorPHID());
}
@ -200,9 +198,6 @@ final class DifferentialTransactionEditor
case DifferentialAction::ACTION_REOPEN:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_REQUEST:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_CLOSE:
$old_status = $object->getStatus();
$object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
@ -294,19 +289,6 @@ final class DifferentialTransactionEditor
$downgrade_accepts = true;
}
break;
// TODO: Remove this, obsoleted by ModularTransactions above.
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_REQUEST:
$downgrade_rejects = true;
if ((!$is_sticky_accept) ||
($object->getStatus() != $status_plan)) {
$downgrade_accepts = true;
}
break;
}
break;
}
}
@ -357,6 +339,14 @@ final class DifferentialTransactionEditor
}
}
if ($downgrade_accepts || $downgrade_rejects) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
->setNewValue(true);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
@ -669,11 +659,8 @@ final class DifferentialTransactionEditor
$reviewer_status = $reviewer->getReviewerStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$action_phid = $reviewer->getLastActionDiffPHID();
$active_phid = $active_diff->getPHID();
$is_current = ($action_phid == $active_phid);
if ($is_current) {
if ($reviewer->isRejected($active_phid)) {
$has_rejecting_reviewer = true;
}
break;
@ -685,11 +672,8 @@ final class DifferentialTransactionEditor
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$action_phid = $reviewer->getLastActionDiffPHID();
$active_phid = $active_diff->getPHID();
$is_current = ($action_phid == $active_phid);
if ($is_sticky_accept || $is_current) {
if ($reviewer->isAccepted($active_phid)) {
$has_accepting_user = true;
}
}
@ -947,41 +931,6 @@ final class DifferentialTransactionEditor
}
break;
case DifferentialAction::ACTION_REQUEST:
if (!$actor_is_author) {
return pht(
'You can not request review of this revision because you do '.
'not own it. To request review of a revision, you must be its '.
'owner.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
// These are OK.
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// This will be caught as "no effect" later on.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
return pht(
'You can not request review of this revision because it has '.
'been abandoned. Instead, reclaim it.');
case ArcanistDifferentialRevisionStatus::CLOSED:
return pht(
'You can not request review of this revision because it has '.
'already been closed.');
default:
throw new Exception(
pht(
'Encountered unexpected revision status ("%s") when '.
'validating "%s" action.',
$revision_status,
$action));
}
break;
case DifferentialAction::ACTION_CLOSE:
// We force revisions closed when we discover a corresponding commit.
// In this case, revisions are allowed to transition to closed from
@ -1908,6 +1857,4 @@ final class DifferentialTransactionEditor
$acting_phid);
}
}

View file

@ -75,6 +75,23 @@ final class DifferentialChangeset extends DifferentialDAO
return $name;
}
public function getOwnersFilename() {
// TODO: For Subversion, we should adjust these paths to be relative to
// the repository root where possible.
$path = $this->getFilename();
if (!isset($path[0])) {
return '/';
}
if ($path[0] != '/') {
$path = '/'.$path;
}
return $path;
}
public function addUnsavedHunk(DifferentialHunk $hunk) {
if ($this->hunks === self::ATTACHABLE) {
$this->hunks = array();

View file

@ -9,6 +9,7 @@ final class DifferentialReviewer
protected $lastActionDiffPHID;
protected $lastCommentDiffPHID;
protected $lastActorPHID;
protected $voidedPHID;
private $authority = array();
@ -19,6 +20,7 @@ final class DifferentialReviewer
'lastActionDiffPHID' => 'phid?',
'lastCommentDiffPHID' => 'phid?',
'lastActorPHID' => 'phid?',
'voidedPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_revision' => array(
@ -37,6 +39,11 @@ final class DifferentialReviewer
return (phid_get_type($this->getReviewerPHID()) == $user_type);
}
public function isPackage() {
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $package_type);
}
public function attachAuthority(PhabricatorUser $user, $has_authority) {
$this->authority[$user->getCacheFragment()] = $has_authority;
return $this;
@ -52,6 +59,25 @@ final class DifferentialReviewer
return ($this->getReviewerStatus() == $status_resigned);
}
public function isRejected($diff_phid) {
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
if ($this->getReviewerStatus() != $status_rejected) {
return false;
}
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
return false;
}
public function isAccepted($diff_phid) {
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
@ -59,6 +85,28 @@ final class DifferentialReviewer
return false;
}
// If this accept has been voided (for example, but a reviewer using
// "Request Review"), don't count it as a real "Accept" even if it is
// against the current diff PHID.
if ($this->getVoidedPHID()) {
return false;
}
if ($this->isCurrentAction($diff_phid)) {
return true;
}
$sticky_key = 'differential.sticky-accept';
$is_sticky = PhabricatorEnv::getEnvConfig($sticky_key);
if ($is_sticky) {
return true;
}
return false;
}
private function isCurrentAction($diff_phid) {
if (!$diff_phid) {
return true;
}
@ -73,13 +121,6 @@ final class DifferentialReviewer
return true;
}
$sticky_key = 'differential.sticky-accept';
$is_sticky = PhabricatorEnv::getEnvConfig($sticky_key);
if ($is_sticky) {
return true;
}
return false;
}

View file

@ -48,6 +48,7 @@ final class DifferentialRevision extends DifferentialDAO
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
private $forceMap = array();
const TABLE_COMMIT = 'differential_commit';
@ -245,6 +246,243 @@ final class DifferentialRevision extends DifferentialDAO
return $this;
}
public function canReviewerForceAccept(
PhabricatorUser $viewer,
DifferentialReviewer $reviewer) {
if (!$reviewer->isPackage()) {
return false;
}
$map = $this->getReviewerForceAcceptMap($viewer);
if (!$map) {
return false;
}
if (isset($map[$reviewer->getReviewerPHID()])) {
return true;
}
return false;
}
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
$fragment = $viewer->getCacheFragment();
if (!array_key_exists($fragment, $this->forceMap)) {
$map = $this->newReviewerForceAcceptMap($viewer);
$this->forceMap[$fragment] = $map;
}
return $this->forceMap[$fragment];
}
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
if (!$diff) {
return null;
}
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return null;
}
$paths = array();
try {
$changesets = $diff->getChangesets();
} catch (Exception $ex) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs(array($diff))
->execute();
}
foreach ($changesets as $changeset) {
$paths[] = $changeset->getOwnersFilename();
}
if (!$paths) {
return null;
}
$reviewer_phids = array();
foreach ($this->getReviewers() as $reviewer) {
if (!$reviewer->isPackage()) {
continue;
}
$reviewer_phids[] = $reviewer->getReviewerPHID();
}
if (!$reviewer_phids) {
return null;
}
// Load all the reviewing packages which have control over some of the
// paths in the change. These are packages which the actor may be able
// to force-accept on behalf of.
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withPHIDs($reviewer_phids)
->withControl($repository_phid, $paths);
$control_packages = $control_query->execute();
if (!$control_packages) {
return null;
}
// Load all the packages which have potential control over some of the
// paths in the change and are owned by the actor. These are packages
// which the actor may be able to use their authority over to gain the
// ability to force-accept for other packages. This query doesn't apply
// dominion rules yet, and we'll bypass those rules later on.
$authority_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->withControl($repository_phid, $paths);
$authority_packages = $authority_query->execute();
if (!$authority_packages) {
return null;
}
$authority_packages = mpull($authority_packages, null, 'getPHID');
// Build a map from each path in the revision to the reviewer packages
// which control it.
$control_map = array();
foreach ($paths as $path) {
$control_packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$path);
// Remove packages which the viewer has authority over. We don't need
// to check these for force-accept because they can just accept them
// normally.
$control_packages = mpull($control_packages, null, 'getPHID');
foreach ($control_packages as $phid => $control_package) {
if (isset($authority_packages[$phid])) {
unset($control_packages[$phid]);
}
}
if (!$control_packages) {
continue;
}
$control_map[$path] = $control_packages;
}
if (!$control_map) {
return null;
}
// From here on out, we only care about paths which we have at least one
// controlling package for.
$paths = array_keys($control_map);
// Now, build a map from each path to the packages which would control it
// if there were no dominion rules.
$authority_map = array();
foreach ($paths as $path) {
$authority_packages = $authority_query->getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = true);
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
}
// For each path, find the most general package that the viewer has
// authority over. For example, we'll prefer a package that owns "/" to a
// package that owns "/src/".
$force_map = array();
foreach ($authority_map as $path => $package_map) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
// Find the package that we have authority over which has the most
// general match for this path.
$best_match = null;
$best_package = null;
foreach ($package_map as $package_phid => $package) {
$package_paths = $package->getPathsForRepository($repository_phid);
foreach ($package_paths as $package_path) {
// NOTE: A strength of 0 means "no match". A strength of 1 means
// that we matched "/", so we can not possibly find another stronger
// match.
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength < $best_match || !$best_package) {
$best_match = $strength;
$best_package = $package;
if ($strength == 1) {
break 2;
}
}
}
}
if ($best_package) {
$force_map[$path] = array(
'strength' => $best_match,
'package' => $best_package,
);
}
}
// For each path which the viewer owns a package for, find other packages
// which that authority can be used to force-accept. Once we find a way to
// force-accept a package, we don't need to keep loooking.
$has_control = array();
foreach ($force_map as $path => $spec) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$authority_strength = $spec['strength'];
$control_packages = $control_map[$path];
foreach ($control_packages as $control_phid => $control_package) {
if (isset($has_control[$control_phid])) {
continue;
}
$control_paths = $control_package->getPathsForRepository(
$repository_phid);
foreach ($control_paths as $control_path) {
$strength = $control_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength > $authority_strength) {
$authority = $spec['package'];
$has_control[$control_phid] = array(
'authority' => $authority,
'phid' => $authority->getPHID(),
);
break;
}
}
}
}
// Return a map from packages which may be force accepted to the packages
// which permit that forced acceptance.
return ipull($has_control, 'phid');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -607,8 +607,6 @@ final class DifferentialTransaction
'not closed.');
case DifferentialAction::ACTION_RETHINK:
return pht('This revision already requires changes.');
case DifferentialAction::ACTION_REQUEST:
return pht('Review is already requested for this revision.');
case DifferentialAction::ACTION_CLAIM:
return pht(
'You can not commandeer this revision because you already own '.

View file

@ -84,11 +84,18 @@ final class DifferentialRevisionAcceptTransaction
}
}
$default_unchecked = array();
foreach ($reviewers as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
if (!$reviewer->hasAuthority($viewer)) {
// If the viewer doesn't have authority to act on behalf of a reviewer,
// don't include that reviewer as an option.
continue;
// we check if they can accept by force.
if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
$default_unchecked[$reviewer_phid] = true;
} else {
continue;
}
}
if ($reviewer->isAccepted($diff_phid)) {
@ -97,20 +104,37 @@ final class DifferentialRevisionAcceptTransaction
continue;
}
$reviewer_phid = $reviewer->getReviewerPHID();
$reviewer_phids[$reviewer_phid] = $reviewer_phid;
}
$handles = $viewer->loadHandles($reviewer_phids);
$head = array();
$tail = array();
foreach ($reviewer_phids as $reviewer_phid) {
$options[$reviewer_phid] = pht(
'Accept as %s',
$viewer->renderHandle($reviewer_phid));
$is_force = isset($default_unchecked[$reviewer_phid]);
$value[] = $reviewer_phid;
if ($is_force) {
$tail[] = $reviewer_phid;
$options[$reviewer_phid] = pht(
'Force accept as %s',
$viewer->renderHandle($reviewer_phid));
} else {
$head[] = $reviewer_phid;
$value[] = $reviewer_phid;
$options[$reviewer_phid] = pht(
'Accept as %s',
$viewer->renderHandle($reviewer_phid));
}
}
// Reorder reviewers so "force accept" reviewers come at the end.
$options =
array_select_keys($options, $head) +
array_select_keys($options, $tail);
return array($options, $value);
}

View file

@ -122,7 +122,12 @@ abstract class DifferentialRevisionActionTransaction
$field->setActionConflictKey('revision.action');
list($options, $value) = $this->getActionOptions($viewer, $revision);
if (count($options) > 1) {
// Show the options if the user can select on behalf of two or more
// reviewers, or can force-accept on behalf of one or more reviewers.
$can_multi = (count($options) > 1);
$can_force = (count($value) < count($options));
if ($can_multi || $can_force) {
$field->setOptions($options);
$field->setValue($value);
}

View file

@ -50,25 +50,19 @@ abstract class DifferentialRevisionReviewTransaction
protected function isViewerFullyAccepted(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFullyAmong(
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
),
true);
DifferentialReviewerStatus::STATUS_ACCEPTED);
}
protected function isViewerFullyRejected(
DifferentialRevision $revision,
PhabricatorUser $viewer) {
return $this->isViewerReviewerStatusFullyAmong(
return $this->isViewerReviewerStatusFully(
$revision,
$viewer,
array(
DifferentialReviewerStatus::STATUS_REJECTED,
),
true);
DifferentialReviewerStatus::STATUS_REJECTED);
}
protected function getViewerReviewerStatus(
@ -90,11 +84,10 @@ abstract class DifferentialRevisionReviewTransaction
return null;
}
protected function isViewerReviewerStatusFullyAmong(
private function isViewerReviewerStatusFully(
DifferentialRevision $revision,
PhabricatorUser $viewer,
array $status_list,
$require_current) {
$require_status) {
// If the user themselves is not a reviewer, the reviews they have
// authority over can not all be in any set of states since their own
@ -106,24 +99,52 @@ abstract class DifferentialRevisionReviewTransaction
$active_phid = $this->getActiveDiffPHID($revision);
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
$is_accepted = ($require_status == $status_accepted);
$is_rejected = ($require_status == $status_rejected);
// Otherwise, check that all reviews they have authority over are in
// the desired set of states.
$status_map = array_fuse($status_list);
foreach ($revision->getReviewers() as $reviewer) {
if (!$reviewer->hasAuthority($viewer)) {
continue;
$can_force = false;
if ($is_accepted) {
if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
$can_force = true;
}
}
if (!$can_force) {
continue;
}
}
$status = $reviewer->getReviewerStatus();
if (!isset($status_map[$status])) {
if ($status != $require_status) {
return false;
}
if ($require_current) {
if ($reviewer->getLastActionDiffPHID() != $active_phid) {
// Here, we're primarily testing if we can remove a void on the review.
if ($is_accepted) {
if (!$reviewer->isAccepted($active_phid)) {
return false;
}
}
if ($is_rejected) {
if (!$reviewer->isRejected($active_phid)) {
return false;
}
}
// This is a broader check to see if we can update the diff where the
// last action occurred.
if ($reviewer->getLastActionDiffPHID() != $active_phid) {
return false;
}
}
return true;
@ -141,11 +162,21 @@ abstract class DifferentialRevisionReviewTransaction
// reviewers you have authority for. When you resign, you only affect
// yourself.
$with_authority = ($status != DifferentialReviewerStatus::STATUS_RESIGNED);
$with_force = ($status == DifferentialReviewerStatus::STATUS_ACCEPTED);
if ($with_authority) {
foreach ($revision->getReviewers() as $reviewer) {
if ($reviewer->hasAuthority($viewer)) {
$map[$reviewer->getReviewerPHID()] = $status;
if (!$reviewer->hasAuthority($viewer)) {
if (!$with_force) {
continue;
}
if (!$revision->canReviewerForceAccept($viewer, $reviewer)) {
continue;
}
}
$map[$reviewer->getReviewerPHID()] = $status;
}
}
@ -223,6 +254,11 @@ abstract class DifferentialRevisionReviewTransaction
$reviewer->setLastActorPHID($this->getActingAsPHID());
}
// Clear any outstanding void on this reviewer. A void may be placed
// by the author using "Request Review" when a reviewer has already
// accepted.
$reviewer->setVoidedPHID(null);
try {
$reviewer->save();
} catch (AphrontDuplicateKeyQueryException $ex) {

View file

@ -0,0 +1,70 @@
<?php
/**
* This is an internal transaction type used to void reviews.
*
* For example, "Request Review" voids any open accepts, so they no longer
* act as current accepts.
*/
final class DifferentialRevisionVoidTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'differential.revision.void';
public function generateOldValue($object) {
return false;
}
public function generateNewValue($object, $value) {
$table = new DifferentialReviewer();
$table_name = $table->getTableName();
$conn = $table->establishConnection('w');
$rows = queryfx_all(
$conn,
'SELECT reviewerPHID FROM %T
WHERE revisionPHID = %s
AND voidedPHID IS NULL
AND reviewerStatus IN (%Ls)',
$table_name,
$object->getPHID(),
$this->getVoidableStatuses());
return ipull($rows, 'reviewerPHID');
}
public function getTransactionHasEffect($object, $old, $new) {
return (bool)$new;
}
public function applyExternalEffects($object, $value) {
$table = new DifferentialReviewer();
$table_name = $table->getTableName();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET voidedPHID = %s
WHERE revisionPHID = %s
AND voidedPHID IS NULL
AND reviewerStatus IN (%Ls)',
$table_name,
$this->getActingAsPHID(),
$object->getPHID(),
$this->getVoidableStatuses());
}
public function shouldHide() {
// This is an internal transaction, so don't show it in feeds or
// transaction logs.
return true;
}
private function getVoidableStatuses() {
return array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
DifferentialReviewerStatus::STATUS_REJECTED,
);
}
}

View file

@ -12,4 +12,17 @@ final class DiffusionCommitHasRevisionEdgeType extends PhabricatorEdgeType {
return true;
}
public function getConduitKey() {
return 'commit.revision';
}
public function getConduitName() {
return pht('Commit Has Revision');
}
public function getConduitDescription() {
return pht(
'The source commit is associated with the destination revision.');
}
}

View file

@ -12,6 +12,18 @@ final class DiffusionCommitHasTaskEdgeType extends PhabricatorEdgeType {
return ManiphestTaskHasCommitEdgeType::EDGECONST;
}
public function getConduitKey() {
return 'commit.task';
}
public function getConduitName() {
return pht('Commit Has Task');
}
public function getConduitDescription() {
return pht('The source commit is associated with the destination task.');
}
public function getTransactionAddString(
$actor,
$add_count,

View file

@ -12,6 +12,18 @@ final class ManiphestTaskHasCommitEdgeType extends PhabricatorEdgeType {
return DiffusionCommitHasTaskEdgeType::EDGECONST;
}
public function getConduitKey() {
return 'task.commit';
}
public function getConduitName() {
return pht('Task Has Commit');
}
public function getConduitDescription() {
return pht('The source task is associated with the destination commit.');
}
public function getTransactionAddString(
$actor,
$add_count,

View file

@ -12,6 +12,18 @@ final class ManiphestTaskHasRevisionEdgeType extends PhabricatorEdgeType {
return true;
}
public function getConduitKey() {
return 'task.revision';
}
public function getConduitName() {
return pht('Task Has Revision');
}
public function getConduitDescription() {
return pht('The source task is associated with the destination revision.');
}
public function getTransactionAddString(
$actor,
$add_count,

View file

@ -513,14 +513,14 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
->setParameter('query', $this->fullTextSearch);
// NOTE: Setting this to something larger than 2^53 will raise errors in
// ElasticSearch, and billions of results won't fit in memory anyway.
$fulltext_query->setParameter('limit', 100000);
// 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));
$engine = PhabricatorFulltextStorageEngine::loadEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
$fulltext_results = PhabricatorSearchService::executeSearch(
$fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);

View file

@ -473,6 +473,10 @@ final class ManiphestTask extends ManiphestDAO
->setKey('title')
->setType('string')
->setDescription(pht('The title of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('remarkup')
->setDescription(pht('The task description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
@ -501,7 +505,6 @@ final class ManiphestTask extends ManiphestDAO
}
public function getFieldValuesForConduit() {
$status_value = $this->getStatus();
$status_info = array(
'value' => $status_value,
@ -519,6 +522,9 @@ final class ManiphestTask extends ManiphestDAO
return array(
'name' => $this->getTitle(),
'description' => array(
'raw' => $this->getDescription(),
),
'authorPHID' => $this->getAuthorPHID(),
'ownerPHID' => $this->getOwnerPHID(),
'status' => $status_info,

View file

@ -348,7 +348,10 @@ final class PhabricatorOwnersPackageQuery
*
* @return list<PhabricatorOwnersPackage> List of controlling packages.
*/
public function getControllingPackagesForPath($repository_phid, $path) {
public function getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = false) {
$path = (string)$path;
if (!isset($this->controlMap[$repository_phid][$path])) {
@ -382,9 +385,14 @@ final class PhabricatorOwnersPackageQuery
}
if ($best_match && $include) {
if ($ignore_dominion) {
$is_weak = false;
} else {
$is_weak = ($package->getDominion() == $weak_dominion);
}
$matches[$package_id] = array(
'strength' => $best_match,
'weak' => ($package->getDominion() == $weak_dominion),
'weak' => $is_weak,
'package' => $package,
);
}

View file

@ -15,10 +15,6 @@ final class PhabricatorOwnersPackageSearchEngine
return new PhabricatorOwnersPackageQuery();
}
public function canUseInPanelContext() {
return false;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())

View file

@ -52,7 +52,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
=> 'PhortuneCartListController',
),
'charge/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneChargeListController',
=> 'PhortuneAccountChargeListController',
),
'card/(?P<id>\d+)/' => array(
'edit/' => 'PhortunePaymentMethodEditController',

View file

@ -1,6 +1,6 @@
<?php
final class PhortuneChargeListController
final class PhortuneAccountChargeListController
extends PhortuneController {
private $account;

View file

@ -18,6 +18,7 @@ final class PhortuneAccountListController extends PhortuneController {
$merchants = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withMemberPHIDs(array($viewer->getPHID()))
->needProfileImage(true)
->execute();
$title = pht('Accounts');
@ -39,7 +40,7 @@ final class PhortuneAccountListController extends PhortuneController {
->setHeader($account->getName())
->setHref($this->getApplicationURI($account->getID().'/'))
->setObject($account)
->setImageIcon('fa-credit-card');
->setImageIcon('fa-user-circle');
$payment_list->addItem($item);
}
@ -71,7 +72,7 @@ final class PhortuneAccountListController extends PhortuneController {
->setHeader($merchant->getName())
->setHref($this->getApplicationURI('/merchant/'.$merchant->getID().'/'))
->setObject($merchant)
->setImageIcon('fa-bank');
->setImageURI($merchant->getProfileImageURI());
$merchant_list->addItem($item);
}

View file

@ -8,9 +8,26 @@ final class PhabricatorProjectFulltextEngine
$object) {
$project = $object;
$viewer = $this->getViewer();
// Reload the project to get slugs.
$project = id(new PhabricatorProjectQuery())
->withIDs(array($project->getID()))
->setViewer($viewer)
->needSlugs(true)
->executeOne();
$project->updateDatasourceTokens();
$document->setDocumentTitle($project->getName());
$slugs = array();
foreach ($project->getSlugs() as $slug) {
$slugs[] = $slug->getSlug();
}
$body = implode("\n", $slugs);
$document
->setDocumentTitle($project->getDisplayName())
->addField(PhabricatorSearchDocumentFieldType::FIELD_BODY, $body);
$document->addRelationship(
$project->isArchived()

View file

@ -0,0 +1,27 @@
<?php
final class PhabricatorRepositoryFulltextEngine
extends PhabricatorFulltextEngine {
protected function buildAbstractDocument(
PhabricatorSearchAbstractDocument $document,
$object) {
$repo = $object;
$document->setDocumentTitle($repo->getName());
$document->addField(
PhabricatorSearchDocumentFieldType::FIELD_BODY,
$repo->getRepositorySlug()."\n".$repo->getDetail('description'));
$document->setDocumentCreated($repo->getDateCreated());
$document->setDocumentModified($repo->getDateModified());
$document->addRelationship(
$repo->isTracked()
? PhabricatorSearchRelationship::RELATIONSHIP_OPEN
: PhabricatorSearchRelationship::RELATIONSHIP_CLOSED,
$repo->getPHID(),
PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
PhabricatorTime::getNow());
}
}

View file

@ -14,7 +14,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface {
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
@ -2572,4 +2573,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorRepositoryFulltextEngine();
}
}

View file

@ -1,35 +0,0 @@
<?php
final class PhabricatorSearchConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Search');
}
public function getDescription() {
return pht('Options relating to Search.');
}
public function getIcon() {
return 'fa-search';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
return array(
$this->newOption('search.elastic.host', 'string', null)
->setLocked(true)
->setDescription(pht('Elastic Search host.'))
->addExample('http://elastic.example.com:9200/', pht('Valid Setting')),
$this->newOption('search.elastic.namespace', 'string', 'phabricator')
->setLocked(true)
->setDescription(pht('Elastic Search index.'))
->addExample('phabricator2', pht('Valid Setting')),
);
}
}

View file

@ -622,7 +622,7 @@ final class PhabricatorApplicationSearchController
$dashboard_uri = '/dashboard/install/';
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-dashboard')
->setName(pht('Add to Dasbhoard'))
->setName(pht('Add to Dashboard'))
->setWorkflow(true)
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
}

View file

@ -1,10 +0,0 @@
<?php
final class PhabricatorSearchEngineTestCase extends PhabricatorTestCase {
public function testLoadAllEngines() {
PhabricatorFulltextStorageEngine::loadAllEngines();
$this->assertTrue(true);
}
}

View file

@ -5,6 +5,8 @@ final class PhabricatorFulltextIndexEngineExtension
const EXTENSIONKEY = 'fulltext';
private $configurationVersion;
public function getExtensionName() {
return pht('Fulltext Engine');
}
@ -12,6 +14,11 @@ final class PhabricatorFulltextIndexEngineExtension
public function getIndexVersion($object) {
$version = array();
// When "cluster.search" is reconfigured, new indexes which don't have any
// data yet may have been added. We err on the side of caution and assume
// that every document may need to be reindexed.
$version[] = $this->getConfigurationVersion();
if ($object instanceof PhabricatorApplicationTransactionInterface) {
// If this is a normal object with transactions, we only need to
// reindex it if there are new transactions (or comment edits).
@ -88,5 +95,22 @@ final class PhabricatorFulltextIndexEngineExtension
return $comment_row['id'];
}
private function getConfigurationVersion() {
if ($this->configurationVersion === null) {
$this->configurationVersion = $this->newConfigurationVersion();
}
return $this->configurationVersion;
}
private function newConfigurationVersion() {
$raw = array(
'services' => PhabricatorEnv::getEnvConfig('cluster.search'),
);
$json = phutil_json_encode($raw);
return PhabricatorHash::digestForIndex($json);
}
}

View file

@ -1,37 +1,46 @@
<?php
final class PhabricatorElasticFulltextStorageEngine
class PhabricatorElasticFulltextStorageEngine
extends PhabricatorFulltextStorageEngine {
private $uri;
private $index;
private $timeout;
private $version;
public function __construct() {
$this->uri = PhabricatorEnv::getEnvConfig('search.elastic.host');
$this->index = PhabricatorEnv::getEnvConfig('search.elastic.namespace');
public function setService(PhabricatorSearchService $service) {
$this->service = $service;
$config = $service->getConfig();
$index = idx($config, 'path', '/phabricator');
$this->index = str_replace('/', '', $index);
$this->timeout = idx($config, 'timeout', 15);
$this->version = (int)idx($config, 'version', 5);
return $this;
}
public function getEngineIdentifier() {
return 'elasticsearch';
}
public function getEnginePriority() {
return 10;
public function getTimestampField() {
return $this->version < 2 ?
'_timestamp' : 'lastModified';
}
public function isEnabled() {
return (bool)$this->uri;
public function getTextFieldType() {
return $this->version >= 5
? 'text' : 'string';
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
public function getHostType() {
return new PhabricatorElasticsearchHost($this);
}
public function setIndex($index) {
$this->index = $index;
return $this;
public function getHostForRead() {
return $this->getService()->getAnyHostForRole('read');
}
public function getHostForWrite() {
return $this->getService()->getAnyHostForRole('write');
}
public function setTimeout($timeout) {
@ -39,21 +48,21 @@ final class PhabricatorElasticFulltextStorageEngine
return $this;
}
public function getURI() {
return $this->uri;
}
public function getIndex() {
return $this->index;
}
public function getTimeout() {
return $this->timeout;
}
public function getTypeConstants($class) {
$relationship_class = new ReflectionClass($class);
$typeconstants = $relationship_class->getConstants();
return array_unique(array_values($typeconstants));
}
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
$host = $this->getHostForWrite();
$type = $doc->getDocumentType();
$phid = $doc->getPHID();
$handle = id(new PhabricatorHandleQuery())
@ -61,36 +70,45 @@ final class PhabricatorElasticFulltextStorageEngine
->withPHIDs(array($phid))
->executeOne();
// URL is not used internally but it can be useful externally.
$timestamp_key = $this->getTimestampField();
$spec = array(
'title' => $doc->getDocumentTitle(),
'url' => PhabricatorEnv::getProductionURI($handle->getURI()),
'dateCreated' => $doc->getDocumentCreated(),
'_timestamp' => $doc->getDocumentModified(),
'field' => array(),
'relationship' => array(),
$timestamp_key => $doc->getDocumentModified(),
);
foreach ($doc->getFieldData() as $field) {
$spec['field'][] = array_combine(array('type', 'corpus', 'aux'), $field);
list($field_name, $corpus, $aux) = $field;
if (!isset($spec[$field_name])) {
$spec[$field_name] = array($corpus);
} else {
$spec[$field_name][] = $corpus;
}
if ($aux != null) {
$spec[$field_name][] = $aux;
}
}
foreach ($doc->getRelationshipData() as $relationship) {
list($rtype, $to_phid, $to_type, $time) = $relationship;
$spec['relationship'][$rtype][] = array(
'phid' => $to_phid,
'phidType' => $to_type,
'when' => (int)$time,
);
foreach ($doc->getRelationshipData() as $field) {
list($field_name, $related_phid, $rtype, $time) = $field;
if (!isset($spec[$field_name])) {
$spec[$field_name] = array($related_phid);
} else {
$spec[$field_name][] = $related_phid;
}
if ($time) {
$spec[$field_name.'_ts'] = $time;
}
}
$this->executeRequest("/{$type}/{$phid}/", $spec, 'PUT');
$this->executeRequest($host, "/{$type}/{$phid}/", $spec, 'PUT');
}
public function reconstructDocument($phid) {
$type = phid_get_type($phid);
$response = $this->executeRequest("/{$type}/{$phid}", array());
$host = $this->getHostForRead();
$response = $this->executeRequest($host, "/{$type}/{$phid}", array());
if (empty($response['exists'])) {
return null;
@ -103,10 +121,11 @@ final class PhabricatorElasticFulltextStorageEngine
$doc->setDocumentType($response['_type']);
$doc->setDocumentTitle($hit['title']);
$doc->setDocumentCreated($hit['dateCreated']);
$doc->setDocumentModified($hit['_timestamp']);
$doc->setDocumentModified($hit[$this->getTimestampField()]);
foreach ($hit['field'] as $fdef) {
$doc->addField($fdef['type'], $fdef['corpus'], $fdef['aux']);
$field_type = $fdef['type'];
$doc->addField($field_type, $hit[$field_type], $fdef['aux']);
}
foreach ($hit['relationship'] as $rtype => $rships) {
@ -123,35 +142,54 @@ final class PhabricatorElasticFulltextStorageEngine
}
private function buildSpec(PhabricatorSavedQuery $query) {
$spec = array();
$filter = array();
$title_spec = array();
$q = new PhabricatorElasticsearchQueryBuilder('bool');
$query_string = $query->getParameter('query');
if (strlen($query_string)) {
$fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType');
if (strlen($query->getParameter('query'))) {
$spec[] = array(
// Build a simple_query_string query over all fields that must match all
// of the words in the search string.
$q->addMustClause(array(
'simple_query_string' => array(
'query' => $query->getParameter('query'),
'fields' => array('field.corpus'),
'query' => $query_string,
'fields' => array(
PhabricatorSearchDocumentFieldType::FIELD_TITLE.'.*',
PhabricatorSearchDocumentFieldType::FIELD_BODY.'.*',
PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'.*',
),
'default_operator' => 'AND',
),
);
));
$title_spec = array(
// This second query clause is "SHOULD' so it only affects ranking of
// documents which already matched the Must clause. This amplifies the
// score of documents which have an exact match on title, body
// or comments.
$q->addShouldClause(array(
'simple_query_string' => array(
'query' => $query->getParameter('query'),
'fields' => array('title'),
'query' => $query_string,
'fields' => array(
'*.raw',
PhabricatorSearchDocumentFieldType::FIELD_TITLE.'^4',
PhabricatorSearchDocumentFieldType::FIELD_BODY.'^3',
PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'^1.2',
),
'analyzer' => 'english_exact',
'default_operator' => 'and',
),
);
));
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
$filter[] = array(
$q->addFilterClause(array(
'not' => array(
'ids' => array(
'values' => array($exclude),
),
),
);
));
}
$relationship_map = array(
@ -176,75 +214,59 @@ final class PhabricatorElasticFulltextStorageEngine
$include_closed = !empty($statuses[$rel_closed]);
if ($include_open && !$include_closed) {
$relationship_map[$rel_open] = true;
$q->addExistsClause($rel_open);
} else if (!$include_open && $include_closed) {
$relationship_map[$rel_closed] = true;
$q->addExistsClause($rel_closed);
}
if ($query->getParameter('withUnowned')) {
$relationship_map[$rel_unowned] = true;
$q->addExistsClause($rel_unowned);
}
$rel_owner = PhabricatorSearchRelationship::RELATIONSHIP_OWNER;
if ($query->getParameter('withAnyOwner')) {
$relationship_map[$rel_owner] = true;
$q->addExistsClause($rel_owner);
} else {
$owner_phids = $query->getParameter('ownerPHIDs', array());
$relationship_map[$rel_owner] = $owner_phids;
}
foreach ($relationship_map as $field => $param) {
if (is_array($param) && $param) {
$should = array();
foreach ($param as $val) {
$should[] = array(
'match' => array(
"relationship.{$field}.phid" => array(
'query' => $val,
'type' => 'phrase',
),
),
);
}
// We couldn't solve it by minimum_number_should_match because it can
// match multiple owners without matching author.
$spec[] = array('bool' => array('should' => $should));
} else if ($param) {
$filter[] = array(
'exists' => array(
'field' => "relationship.{$field}.phid",
),
);
if (count($owner_phids)) {
$q->addTermsClause($rel_owner, $owner_phids);
}
}
if ($spec) {
$spec = array('query' => array('bool' => array('must' => $spec)));
if ($title_spec) {
$spec['query']['bool']['should'] = $title_spec;
foreach ($relationship_map as $field => $phids) {
if (is_array($phids) && !empty($phids)) {
$q->addTermsClause($field, $phids);
}
}
if ($filter) {
$filter = array('filter' => array('and' => $filter));
if (!$spec) {
$spec = array('query' => array('match_all' => new stdClass()));
}
$spec = array(
'query' => array(
'filtered' => $spec + $filter,
),
);
if (!$q->getClauseCount('must')) {
$q->addMustClause(array('match_all' => array('boost' => 1 )));
}
$spec = array(
'_source' => false,
'query' => array(
'bool' => $q->toArray(),
),
);
if (!$query->getParameter('query')) {
$spec['sort'] = array(
array('dateCreated' => 'desc'),
);
}
$spec['from'] = (int)$query->getParameter('offset', 0);
$spec['size'] = (int)$query->getParameter('limit', 25);
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 101);
if ($offset + $limit > 10000) {
throw new Exception(pht(
'Query offset is too large. offset+limit=%s (max=%s)',
$offset + $limit,
10000));
}
$spec['from'] = $offset;
$spec['size'] = $limit;
return $spec;
}
@ -261,30 +283,37 @@ final class PhabricatorElasticFulltextStorageEngine
// some bigger index). Use '/$types/_search' instead.
$uri = '/'.implode(',', $types).'/_search';
try {
$response = $this->executeRequest($uri, $this->buildSpec($query));
} catch (HTTPFutureHTTPResponseStatus $ex) {
// elasticsearch probably uses Lucene query syntax:
// http://lucene.apache.org/core/3_6_1/queryparsersyntax.html
// Try literal search if operator search fails.
if (!strlen($query->getParameter('query'))) {
throw $ex;
}
$query = clone $query;
$query->setParameter(
'query',
addcslashes(
$query->getParameter('query'), '+-&|!(){}[]^"~*?:\\'));
$response = $this->executeRequest($uri, $this->buildSpec($query));
}
$spec = $this->buildSpec($query);
$exceptions = array();
$phids = ipull($response['hits']['hits'], '_id');
return $phids;
foreach ($this->service->getAllHostsForRole('read') as $host) {
try {
$response = $this->executeRequest($host, $uri, $spec);
$phids = ipull($response['hits']['hits'], '_id');
return $phids;
} catch (Exception $e) {
$exceptions[] = $e;
}
}
throw new PhutilAggregateException(pht('All Fulltext Search hosts failed:'),
$exceptions);
}
public function indexExists() {
public function indexExists(PhabricatorElasticsearchHost $host = null) {
if (!$host) {
$host = $this->getHostForRead();
}
try {
return (bool)$this->executeRequest('/_status/', array());
if ($this->version >= 5) {
$uri = '/_stats/';
$res = $this->executeRequest($host, $uri, array());
return isset($res['indices']['phabricator']);
} else if ($this->version >= 2) {
$uri = '';
} else {
$uri = '/_status/';
}
return (bool)$this->executeRequest($host, $uri, array());
} catch (HTTPFutureHTTPResponseStatus $e) {
if ($e->getStatusCode() == 404) {
return false;
@ -300,52 +329,124 @@ final class PhabricatorElasticFulltextStorageEngine
'auto_expand_replicas' => '0-2',
'analysis' => array(
'filter' => array(
'trigrams_filter' => array(
'min_gram' => 3,
'type' => 'ngram',
'max_gram' => 3,
'english_stop' => array(
'type' => 'stop',
'stopwords' => '_english_',
),
'english_stemmer' => array(
'type' => 'stemmer',
'language' => 'english',
),
'english_possessive_stemmer' => array(
'type' => 'stemmer',
'language' => 'possessive_english',
),
),
'analyzer' => array(
'custom_trigrams' => array(
'type' => 'custom',
'filter' => array(
'lowercase',
'kstem',
'trigrams_filter',
),
'english_exact' => array(
'tokenizer' => 'standard',
'filter' => array('lowercase'),
),
'letter_stop' => array(
'tokenizer' => 'letter',
'filter' => array('lowercase', 'english_stop'),
),
'english_stem' => array(
'tokenizer' => 'standard',
'filter' => array(
'english_possessive_stemmer',
'lowercase',
'english_stop',
'english_stemmer',
),
),
),
),
),
);
$types = array_keys(
$fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType');
$relationships = $this->getTypeConstants('PhabricatorSearchRelationship');
$doc_types = array_keys(
PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes());
foreach ($types as $type) {
// Use the custom trigram analyzer for the corpus of text
$data['mappings'][$type]['properties']['field']['properties']['corpus'] =
array('type' => 'string', 'analyzer' => 'custom_trigrams');
$text_type = $this->getTextFieldType();
foreach ($doc_types as $type) {
$properties = array();
foreach ($fields as $field) {
// Use the custom analyzer for the corpus of text
$properties[$field] = array(
'type' => $text_type,
'fields' => array(
'raw' => array(
'type' => $text_type,
'analyzer' => 'english_exact',
'search_analyzer' => 'english',
'search_quote_analyzer' => 'english_exact',
),
'keywords' => array(
'type' => $text_type,
'analyzer' => 'letter_stop',
),
'stems' => array(
'type' => $text_type,
'analyzer' => 'english_stem',
),
),
);
}
if ($this->version < 5) {
foreach ($relationships as $rel) {
$properties[$rel] = array(
'type' => 'string',
'index' => 'not_analyzed',
'include_in_all' => false,
);
$properties[$rel.'_ts'] = array(
'type' => 'date',
'include_in_all' => false,
);
}
} else {
foreach ($relationships as $rel) {
$properties[$rel] = array(
'type' => 'keyword',
'include_in_all' => false,
'doc_values' => false,
);
$properties[$rel.'_ts'] = array(
'type' => 'date',
'include_in_all' => false,
);
}
}
// Ensure we have dateCreated since the default query requires it
$data['mappings'][$type]['properties']['dateCreated']['type'] = 'string';
}
$properties['dateCreated']['type'] = 'date';
$properties['lastModified']['type'] = 'date';
$data['mappings'][$type]['properties'] = $properties;
}
return $data;
}
public function indexIsSane() {
if (!$this->indexExists()) {
public function indexIsSane(PhabricatorElasticsearchHost $host = null) {
if (!$host) {
$host = $this->getHostForRead();
}
if (!$this->indexExists($host)) {
return false;
}
$cur_mapping = $this->executeRequest('/_mapping/', array());
$cur_settings = $this->executeRequest('/_settings/', array());
$cur_mapping = $this->executeRequest($host, '/_mapping/', array());
$cur_settings = $this->executeRequest($host, '/_settings/', array());
$actual = array_merge($cur_settings[$this->index],
$cur_mapping[$this->index]);
return $this->check($actual, $this->getIndexConfiguration());
$res = $this->check($actual, $this->getIndexConfiguration());
return $res;
}
/**
@ -355,7 +456,7 @@ final class PhabricatorElasticFulltextStorageEngine
* @param $required array
* @return bool
*/
private function check($actual, $required) {
private function check($actual, $required, $path = '') {
foreach ($required as $key => $value) {
if (!array_key_exists($key, $actual)) {
if ($key === '_all') {
@ -369,7 +470,7 @@ final class PhabricatorElasticFulltextStorageEngine
if (!is_array($actual[$key])) {
return false;
}
if (!$this->check($actual[$key], $value)) {
if (!$this->check($actual[$key], $value, $path.'.'.$key)) {
return false;
}
continue;
@ -403,39 +504,76 @@ final class PhabricatorElasticFulltextStorageEngine
}
public function initIndex() {
$host = $this->getHostForWrite();
if ($this->indexExists()) {
$this->executeRequest('/', array(), 'DELETE');
$this->executeRequest($host, '/', array(), 'DELETE');
}
$data = $this->getIndexConfiguration();
$this->executeRequest('/', $data, 'PUT');
$this->executeRequest($host, '/', $data, 'PUT');
}
private function executeRequest($path, array $data, $method = 'GET') {
$uri = new PhutilURI($this->uri);
$uri->setPath($this->index);
$uri->appendPath($path);
$data = json_encode($data);
public function getIndexStats(PhabricatorElasticsearchHost $host = null) {
if ($this->version < 2) {
return false;
}
if (!$host) {
$host = $this->getHostForRead();
}
$uri = '/_stats/';
$res = $this->executeRequest($host, $uri, array());
$stats = $res['indices'][$this->index];
return array(
pht('Queries') =>
idxv($stats, array('primaries', 'search', 'query_total')),
pht('Documents') =>
idxv($stats, array('total', 'docs', 'count')),
pht('Deleted') =>
idxv($stats, array('total', 'docs', 'deleted')),
pht('Storage Used') =>
phutil_format_bytes(idxv($stats,
array('total', 'store', 'size_in_bytes'))),
);
}
private function executeRequest(PhabricatorElasticsearchHost $host, $path,
array $data, $method = 'GET') {
$uri = $host->getURI($path);
$data = phutil_json_encode($data);
$future = new HTTPSFuture($uri, $data);
$future->addHeader('Content-Type', 'application/json');
if ($method != 'GET') {
$future->setMethod($method);
}
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
list($body) = $future->resolvex();
try {
list($body) = $future->resolvex();
} catch (HTTPFutureResponseStatus $ex) {
if ($ex->isTimeout() || (int)$ex->getStatusCode() > 499) {
$host->didHealthCheck(false);
}
throw $ex;
}
if ($method != 'GET') {
return null;
}
try {
return phutil_json_decode($body);
$data = phutil_json_decode($body);
$host->didHealthCheck(true);
return $data;
} catch (PhutilJSONParserException $ex) {
$host->didHealthCheck(false);
throw new PhutilProxyException(
pht('ElasticSearch server returned invalid JSON!'),
pht('Elasticsearch server returned invalid JSON!'),
$ex);
}
}
}

View file

@ -0,0 +1,78 @@
<?php
class PhabricatorElasticsearchQueryBuilder {
protected $name;
protected $clauses = array();
public function getClauses($termkey = null) {
$clauses = $this->clauses;
if ($termkey == null) {
return $clauses;
}
if (isset($clauses[$termkey])) {
return $clauses[$termkey];
}
return array();
}
public function getClauseCount($clausekey) {
if (isset($this->clauses[$clausekey])) {
return count($this->clauses[$clausekey]);
} else {
return 0;
}
}
public function addExistsClause($field) {
return $this->addClause('filter', array(
'exists' => array(
'field' => $field,
),
));
}
public function addTermsClause($field, $values) {
return $this->addClause('filter', array(
'terms' => array(
$field => array_values($values),
),
));
}
public function addMustClause($clause) {
return $this->addClause('must', $clause);
}
public function addFilterClause($clause) {
return $this->addClause('filter', $clause);
}
public function addShouldClause($clause) {
return $this->addClause('should', $clause);
}
public function addMustNotClause($clause) {
return $this->addClause('must_not', $clause);
}
public function addClause($clause, $terms) {
$this->clauses[$clause][] = $terms;
return $this;
}
public function toArray() {
$clauses = $this->getClauses();
return $clauses;
$cleaned = array();
foreach ($clauses as $clause => $subclauses) {
if (is_array($subclauses) && count($subclauses) == 1) {
$cleaned[$clause] = array_shift($subclauses);
} else {
$cleaned[$clause] = $subclauses;
}
}
return $cleaned;
}
}

View file

@ -7,6 +7,31 @@
*/
abstract class PhabricatorFulltextStorageEngine extends Phobject {
protected $service;
public function getHosts() {
return $this->service->getHosts();
}
public function setService(PhabricatorSearchService $service) {
$this->service = $service;
return $this;
}
/**
* @return PhabricatorSearchService
*/
public function getService() {
return $this->service;
}
/**
* Implementations must return a prototype host instance which is cloned
* by the PhabricatorSearchService infrastructure to configure each engine.
* @return PhabricatorSearchHost
*/
abstract public function getHostType();
/* -( Engine Metadata )---------------------------------------------------- */
/**
@ -17,37 +42,6 @@ abstract class PhabricatorFulltextStorageEngine extends Phobject {
*/
abstract public function getEngineIdentifier();
/**
* Prioritize this engine relative to other engines.
*
* Engines with a smaller priority number get an opportunity to write files
* first. Generally, lower-latency filestores should have lower priority
* numbers, and higher-latency filestores should have higher priority
* numbers. Setting priority to approximately the number of milliseconds of
* read latency will generally produce reasonable results.
*
* In conjunction with filesize limits, the goal is to store small files like
* profile images, thumbnails, and text snippets in lower-latency engines,
* and store large files in higher-capacity engines.
*
* @return float Engine priority.
* @task meta
*/
abstract public function getEnginePriority();
/**
* Return `true` if the engine is currently writable.
*
* Engines that are disabled or missing configuration should return `false`
* to prevent new writes. If writes were made with this engine in the past,
* the application may still try to perform reads.
*
* @return bool True if this engine can support new writes.
* @task meta
*/
abstract public function isEnabled();
/* -( Managing Documents )------------------------------------------------- */
/**
@ -83,6 +77,13 @@ abstract class PhabricatorFulltextStorageEngine extends Phobject {
*/
abstract public function indexExists();
/**
* Implementations should override this method to return a dictionary of
* stats which are suitable for display in the admin UI.
*/
abstract public function getIndexStats();
/**
* Is the index in a usable state?
*
@ -100,39 +101,4 @@ abstract class PhabricatorFulltextStorageEngine extends Phobject {
public function initIndex() {}
/* -( Loading Storage Engines )-------------------------------------------- */
/**
* @task load
*/
public static function loadAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineIdentifier')
->setSortMethod('getEnginePriority')
->execute();
}
/**
* @task load
*/
public static function loadActiveEngines() {
$engines = self::loadAllEngines();
$active = array();
foreach ($engines as $key => $engine) {
if (!$engine->isEnabled()) {
continue;
}
$active[$key] = $engine;
}
return $active;
}
public static function loadEngine() {
return head(self::loadActiveEngines());
}
}

View file

@ -7,12 +7,8 @@ final class PhabricatorMySQLFulltextStorageEngine
return 'mysql';
}
public function getEnginePriority() {
return 100;
}
public function isEnabled() {
return true;
public function getHostType() {
return new PhabricatorMySQLSearchHost($this);
}
public function reindexAbstractDocument(
@ -415,4 +411,9 @@ final class PhabricatorMySQLFulltextStorageEngine
public function indexExists() {
return true;
}
public function getIndexStats() {
return false;
}
}

View file

@ -40,8 +40,7 @@ abstract class PhabricatorFulltextEngine
$extension->indexFulltextObject($object, $document);
}
$storage_engine = PhabricatorFulltextStorageEngine::loadEngine();
$storage_engine->reindexAbstractDocument($document);
PhabricatorSearchService::reindexAbstractDocument($document);
}
protected function newAbstractDocument($object) {

View file

@ -45,6 +45,8 @@ final class PhabricatorSearchManagementIndexWorkflow
}
public function execute(PhutilArgumentParser $args) {
$this->validateClusterSearchConfig();
$console = PhutilConsole::getConsole();
$is_all = $args->getArg('all');
@ -85,8 +87,9 @@ final class PhabricatorSearchManagementIndexWorkflow
}
if (!$is_background) {
$console->writeOut(
"%s\n",
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'),
pht(
'Run this workflow with "%s" to queue tasks for the daemon workers.',
'--background'));
@ -107,9 +110,32 @@ final class PhabricatorSearchManagementIndexWorkflow
);
$any_success = false;
// If we aren't using "--background" or "--force", track how many objects
// we're skipping so we can print this information for the user and give
// them a hint that they might want to use "--force".
$track_skips = (!$is_background && !$is_force);
$count_updated = 0;
$count_skipped = 0;
foreach ($phids as $phid) {
try {
if ($track_skips) {
$old_versions = $this->loadIndexVersions($phid);
}
PhabricatorSearchWorker::queueDocumentForIndexing($phid, $parameters);
if ($track_skips) {
$new_versions = $this->loadIndexVersions($phid);
if ($old_versions !== $new_versions) {
$count_updated++;
} else {
$count_skipped++;
}
}
$any_success = true;
} catch (Exception $ex) {
phlog($ex);
@ -125,6 +151,45 @@ final class PhabricatorSearchManagementIndexWorkflow
pht('Failed to rebuild search index for any documents.'));
}
if ($track_skips) {
if ($count_updated) {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Updated search indexes for %s document(s).',
new PhutilNumber($count_updated)));
}
if ($count_skipped) {
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('SKIP'),
pht(
'Skipped %s documents(s) which have not updated since they were '.
'last indexed.',
new PhutilNumber($count_skipped)));
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'),
pht(
'Use "--force" to force the index to update these documents.'));
}
} else if ($is_background) {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Queued %s document(s) for background indexing.',
new PhutilNumber(count($phids))));
} else {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Forced search index updates for %s document(s).',
new PhutilNumber(count($phids))));
}
}
private function loadPHIDsByNames(array $names) {
@ -158,9 +223,15 @@ final class PhabricatorSearchManagementIndexWorkflow
$object_class = get_class($object);
$normalized_class = phutil_utf8_strtolower($object_class);
if ($normalized_class === $normalized_type) {
$matches = array($object_class => $object);
break;
}
if (!strlen($type) ||
strpos($normalized_class, $normalized_type) !== false) {
$matches[$object_class] = $object;
}
}
@ -198,5 +269,16 @@ final class PhabricatorSearchManagementIndexWorkflow
return $phids;
}
private function loadIndexVersions($phid) {
$table = new PhabricatorSearchIndexVersion();
$conn = $table->establishConnection('r');
return queryfx_all(
$conn,
'SELECT extensionKey, version FROM %T WHERE objectPHID = %s
ORDER BY extensionKey, version',
$table->getTableName(),
$phid);
}
}

View file

@ -6,46 +6,66 @@ final class PhabricatorSearchManagementInitWorkflow
protected function didConstruct() {
$this
->setName('init')
->setSynopsis(pht('Initialize or repair an index.'))
->setSynopsis(pht('Initialize or repair a search service.'))
->setExamples('**init**');
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engine = PhabricatorFulltextStorageEngine::loadEngine();
$this->validateClusterSearchConfig();
$work_done = false;
if (!$engine->indexExists()) {
$console->writeOut(
'%s',
pht('Index does not exist, creating...'));
$engine->initIndex();
$console->writeOut(
"%s\n",
pht('done.'));
$work_done = true;
} else if (!$engine->indexIsSane()) {
$console->writeOut(
'%s',
pht('Index exists but is incorrect, fixing...'));
$engine->initIndex();
$console->writeOut(
"%s\n",
pht('done.'));
$work_done = true;
}
if ($work_done) {
$console->writeOut(
foreach (PhabricatorSearchService::getAllServices() as $service) {
echo tsprintf(
"%s\n",
pht(
'Index maintenance complete. Run `%s` to reindex documents',
'./bin/search index'));
} else {
$console->writeOut(
'Initializing search service "%s".',
$service->getDisplayName()));
if (!$service->isWritable()) {
echo tsprintf(
"%s\n",
pht(
'Skipping service "%s" because it is not writable.',
$service->getDisplayName()));
continue;
}
$engine = $service->getEngine();
if (!$engine->indexExists()) {
echo tsprintf(
"%s\n",
pht('Service index does not exist, creating...'));
$engine->initIndex();
$work_done = true;
} else if (!$engine->indexIsSane()) {
echo tsprintf(
"%s\n",
pht('Service index is out of date, repairing...'));
$engine->initIndex();
$work_done = true;
} else {
echo tsprintf(
"%s\n",
pht('Service index is already up to date.'));
}
echo tsprintf(
"%s\n",
pht('Nothing to do.'));
pht('Done.'));
}
if (!$work_done) {
echo tsprintf(
"%s\n",
pht('No services need initialization.'));
return 0;
}
echo tsprintf(
"%s\n",
pht('Service initialization complete.'));
}
}

View file

@ -1,4 +1,26 @@
<?php
abstract class PhabricatorSearchManagementWorkflow
extends PhabricatorManagementWorkflow {}
extends PhabricatorManagementWorkflow {
protected function validateClusterSearchConfig() {
// Configuration is normally validated by setup self-checks on the web
// workflow, but users may reasonsably run `bin/search` commands after
// making manual edits to "local.json". Re-verify configuration here before
// continuing.
$config_key = 'cluster.search';
$config_value = PhabricatorEnv::getEnvConfig($config_key);
try {
PhabricatorClusterSearchConfigOptionType::validateValue($config_value);
} catch (Exception $ex) {
throw new PhutilArgumentUsageException(
pht(
'Setting "%s" is misconfigured: %s',
$config_key,
$ex->getMessage()));
}
}
}

View file

@ -73,10 +73,7 @@ final class PhabricatorSearchDocumentQuery
$query = id(clone($this->savedQuery))
->setParameter('offset', $this->getOffset())
->setParameter('limit', $this->getRawResultLimit());
$engine = PhabricatorFulltextStorageEngine::loadEngine();
return $engine->executeSearch($query);
return PhabricatorSearchService::executeSearch($query);
}
public function getQueryApplicationClass() {

View file

@ -47,6 +47,7 @@ will have on availability, resistance to data loss, and scalability.
| **SSH Servers** | Minimal | Low | No Risk | Low
| **Web Servers** | Minimal | **High** | No Risk | Moderate
| **Notifications** | Minimal | Low | No Risk | Low
| **Fulltext Search** | Minimal | Low | No Risk | Low
See below for a walkthrough of these services in greater detail.
@ -237,6 +238,21 @@ hosts is unlikely to have much impact on scalability.
For details, see @{article:Cluster: Notifications}.
Cluster: Fulltext Search
========================
Configuring search services is relatively simple and has no pre-requisites.
By default, Phabricator uses MySQL as a fulltext search engine, so deploying
multiple database hosts will effectively also deploy multiple fulltext search
hosts.
Search indexes can be completely rebuilt from the database, so there is no
risk of data loss no matter how fulltext search is configured.
For details, see @{article:Cluster: Search}.
Overlaying Services
===================

View file

@ -0,0 +1,210 @@
@title Cluster: Search
@group cluster
Overview
========
You can configure Phabricator to connect to one or more fulltext search
services.
By default, Phabricator will use MySQL for fulltext search. This is suitable
for most installs. However, alternate engines are supported.
Configuring Search Services
===========================
To configure search services, adjust the `cluster.search` configuration
option. This option contains a list of one or more fulltext search services,
like this:
```lang=json
[
{
"type": "...",
"hosts": [
...
],
"roles": {
"read": true,
"write": true
}
}
]
```
When a user makes a change to a document, Phabricator writes the updated
document into every configured, writable fulltext service.
When a user issues a query, Phabricator tries configured, readable services
in order until it is able to execute the query successfully.
These options are supported by all service types:
| Key | Description |
|---|---|
| `type` | Constant identifying the service type, like `mysql`.
| `roles` | Dictionary of role settings, for enabling reads and writes.
| `hosts` | List of hosts for this service.
Some service types support additional options.
Available Service Types
=======================
These service types are supported:
| Service | Key | Description |
|---|---|---|
| MySQL | `mysql` | Default MySQL fulltext index.
| Elasticsearch | `elasticsearch` | Use an external Elasticsearch service
Fulltext Service Roles
======================
These roles are supported:
| Role | Key | Description
|---|---|---|
| Read | `read` | Allows the service to be queried when users search.
| Write | `write` | Allows documents to be published to the service.
Specifying Hosts
================
The `hosts` key should contain a list of dictionaries, each specifying the
details of a host. A service should normally have one or more hosts.
When an option is set at the service level, it serves as a default for all
hosts. It may be overridden by changing the value for a particular host.
Service Type: MySQL
==============
The `mysql` service type does not require any configuration, and does not
need to have hosts specified. This service uses the builtin database to
index and search documents.
A typical `mysql` service configuration looks like this:
```lang=json
{
"type": "mysql"
}
```
Service Type: Elasticsearch
======================
The `elasticsearch` sevice type supports these options:
| Key | Description |
|---|---|
| `protocol` | Either `"http"` (default) or `"https"`.
| `port` | Elasticsearch TCP port.
| `version` | Elasticsearch version, either `2` or `5` (default).
| `path` | Path for the index. Defaults to `/phabriator`. Advanced.
A typical `elasticsearch` service configuration looks like this:
```lang=json
{
"type": "elasticsearch",
"hosts": [
{
"protocol": "http",
"host": "127.0.0.1",
"port": 9200
}
]
}
```
Monitoring Search Services
==========================
You can monitor fulltext search in {nav Config > Search Servers}. This
interface shows you a quick overview of services and their health.
The table on this page shows some basic stats for each configured service,
followed by the configuration and current status of each host.
Rebuilding Indexes
==================
After adding new search services, you will need to rebuild document indexes
on them. To do this, first initialize the services:
```
phabricator/ $ ./bin/search init
```
This will perform index setup steps and other one-time configuration.
To populate documents in all indexes, run this command:
```
phabricator/ $ ./bin/search index --force --background --type all
```
This initiates an exhaustive rebuild of the document indexes. To get a more
detailed list of indexing options available, run:
```
phabricator/ $ ./bin/search help index
```
Advanced Example
================
This is a more advanced example which shows a configuration with multiple
different services in different roles. In this example:
- Phabricator is using an Elasticsearch 2 service as its primary fulltext
service.
- An Elasticsearch 5 service is online, but only receiving writes.
- The MySQL service is serving as a backup if Elasticsearch fails.
This particular configuration may not be very useful. It is primarily
intended to show how to configure many different options.
```lang=json
[
{
"type": "elasticsearch",
"version": 2,
"hosts": [
{
"host": "elastic2.mycompany.com",
"port": 9200,
"protocol": "http"
}
]
},
{
"type": "elasticsearch",
"version": 5,
"hosts": [
{
"host": "elastic5.mycompany.com",
"port": 9789,
"protocol": "https"
"roles": {
"read": false,
"write": true
}
}
]
},
{
"type": "mysql"
}
]
```

View file

@ -1,20 +1,19 @@
<?php
final class PhabricatorDatabaseHealthRecord
class PhabricatorClusterServiceHealthRecord
extends Phobject {
private $ref;
private $cacheKey;
private $shouldCheck;
private $isHealthy;
private $upEventCount;
private $downEventCount;
public function __construct(PhabricatorDatabaseRef $ref) {
$this->ref = $ref;
public function __construct($cache_key) {
$this->cacheKey = $cache_key;
$this->readState();
}
/**
* Is the database currently healthy?
*/
@ -153,18 +152,13 @@ final class PhabricatorDatabaseHealthRecord
}
}
private function getHealthRecordCacheKey() {
$ref = $this->ref;
$host = $ref->getHost();
$port = $ref->getPort();
return "cluster.db.health({$host}, {$port})";
public function getCacheKey() {
return $this->cacheKey;
}
private function readHealthRecord() {
$cache = PhabricatorCaches::getSetupCache();
$cache_key = $this->getHealthRecordCacheKey();
$cache_key = $this->getCacheKey();
$health_record = $cache->getKey($cache_key);
if (!is_array($health_record)) {
@ -180,7 +174,7 @@ final class PhabricatorDatabaseHealthRecord
private function writeHealthRecord(array $record) {
$cache = PhabricatorCaches::getSetupCache();
$cache_key = $this->getHealthRecordCacheKey();
$cache_key = $this->getCacheKey();
$cache->setKey($cache_key, $record);
}

View file

@ -14,6 +14,7 @@ final class PhabricatorDatabaseRef
const REPLICATION_SLOW = 'replica-slow';
const REPLICATION_NOT_REPLICATING = 'not-replicating';
const KEY_HEALTH = 'cluster.db.health';
const KEY_REFS = 'cluster.db.refs';
const KEY_INDIVIDUAL = 'cluster.db.individual';
@ -489,9 +490,18 @@ final class PhabricatorDatabaseRef
return $this;
}
private function getHealthRecordCacheKey() {
$host = $this->getHost();
$port = $this->getPort();
$key = self::KEY_HEALTH;
return "{$key}({$host}, {$port})";
}
public function getHealthRecord() {
if (!$this->healthRecord) {
$this->healthRecord = new PhabricatorDatabaseHealthRecord($this);
$this->healthRecord = new PhabricatorClusterServiceHealthRecord(
$this->getHealthRecordCacheKey());
}
return $this->healthRecord;
}

View file

@ -0,0 +1,84 @@
<?php
final class PhabricatorClusterSearchConfigOptionType
extends PhabricatorConfigJSONOptionType {
public function validateOption(PhabricatorConfigOption $option, $value) {
self::validateValue($value);
}
public static function validateValue($value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Search cluster configuration is not valid: value must be a '.
'list of search hosts.'));
}
$engines = PhabricatorSearchService::loadAllFulltextStorageEngines();
foreach ($value as $index => $spec) {
if (!is_array($spec)) {
throw new Exception(
pht(
'Search cluster configuration is not valid: each entry in the '.
'list must be a dictionary describing a search service, but '.
'the value with index "%s" is not a dictionary.',
$index));
}
try {
PhutilTypeSpec::checkMap(
$spec,
array(
'type' => 'string',
'hosts' => 'optional list<map<string, wild>>',
'roles' => 'optional map<string, wild>',
'port' => 'optional int',
'protocol' => 'optional string',
'path' => 'optional string',
'version' => 'optional int',
));
} catch (Exception $ex) {
throw new Exception(
pht(
'Search engine configuration has an invalid service '.
'specification (at index "%s"): %s.',
$index,
$ex->getMessage()));
}
if (!array_key_exists($spec['type'], $engines)) {
throw new Exception(
pht(
'Invalid search engine type: %s. Valid types are: %s.',
$spec['type'],
implode(', ', array_keys($engines))));
}
if (isset($spec['hosts'])) {
foreach ($spec['hosts'] as $hostindex => $host) {
try {
PhutilTypeSpec::checkMap(
$host,
array(
'host' => 'string',
'roles' => 'optional map<string, wild>',
'port' => 'optional int',
'protocol' => 'optional string',
'path' => 'optional string',
'version' => 'optional int',
));
} catch (Exception $ex) {
throw new Exception(
pht(
'Search cluster configuration has an invalid host '.
'specification (at index "%s"): %s.',
$hostindex,
$ex->getMessage()));
}
}
}
}
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorClusterNoHostForRoleException
extends Exception {
public function __construct($role) {
parent::__construct(pht('Search cluster has no hosts for role "%s".',
$role));
}
}

View file

@ -0,0 +1,82 @@
<?php
final class PhabricatorElasticsearchHost
extends PhabricatorSearchHost {
private $version = 5;
private $path = 'phabricator/';
private $protocol = 'http';
const KEY_REFS = 'search.elastic.refs';
public function setConfig($config) {
$this->setRoles(idx($config, 'roles', $this->getRoles()))
->setHost(idx($config, 'host', $this->host))
->setPort(idx($config, 'port', $this->port))
->setProtocol(idx($config, 'protocol', $this->protocol))
->setPath(idx($config, 'path', $this->path))
->setVersion(idx($config, 'version', $this->version));
return $this;
}
public function getDisplayName() {
return pht('Elasticsearch');
}
public function getStatusViewColumns() {
return array(
pht('Protocol') => $this->getProtocol(),
pht('Host') => $this->getHost(),
pht('Port') => $this->getPort(),
pht('Index Path') => $this->getPath(),
pht('Elastic Version') => $this->getVersion(),
pht('Roles') => implode(', ', array_keys($this->getRoles())),
);
}
public function setProtocol($protocol) {
$this->protocol = $protocol;
return $this;
}
public function getProtocol() {
return $this->protocol;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function setVersion($version) {
$this->version = $version;
return $this;
}
public function getVersion() {
return $this->version;
}
public function getURI($to_path = null) {
$uri = id(new PhutilURI('http://'.$this->getHost()))
->setProtocol($this->getProtocol())
->setPort($this->getPort())
->setPath($this->getPath());
if ($to_path) {
$uri->appendPath($to_path);
}
return $uri;
}
public function getConnectionStatus() {
$status = $this->getEngine()->indexIsSane($this);
return $status ? parent::STATUS_OKAY : parent::STATUS_FAIL;
}
}

View file

@ -0,0 +1,43 @@
<?php
final class PhabricatorMySQLSearchHost
extends PhabricatorSearchHost {
public function setConfig($config) {
$this->setRoles(idx($config, 'roles',
array('read' => true, 'write' => true)));
return $this;
}
public function getDisplayName() {
return 'MySQL';
}
public function getStatusViewColumns() {
return array(
pht('Protocol') => 'mysql',
pht('Roles') => implode(', ', array_keys($this->getRoles())),
);
}
public function getProtocol() {
return 'mysql';
}
public function getHealthRecord() {
if (!$this->healthRecord) {
$ref = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
'search');
$this->healthRecord = $ref->getHealthRecord();
}
return $this->healthRecord;
}
public function getConnectionStatus() {
PhabricatorDatabaseRef::queryAll();
$ref = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication('search');
$status = $ref->getConnectionStatus();
return $status;
}
}

View file

@ -0,0 +1,123 @@
<?php
abstract class PhabricatorSearchHost
extends Phobject {
const KEY_REFS = 'cluster.search.refs';
const KEY_HEALTH = 'cluster.search.health';
protected $engine;
protected $healthRecord;
protected $roles = array();
protected $disabled;
protected $host;
protected $port;
const STATUS_OKAY = 'okay';
const STATUS_FAIL = 'fail';
public function __construct(PhabricatorFulltextStorageEngine $engine) {
$this->engine = $engine;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
/**
* @return PhabricatorFulltextStorageEngine
*/
public function getEngine() {
return $this->engine;
}
public function isWritable() {
return $this->hasRole('write');
}
public function isReadable() {
return $this->hasRole('read');
}
public function hasRole($role) {
return isset($this->roles[$role]) && $this->roles[$role] === true;
}
public function setRoles(array $roles) {
foreach ($roles as $role => $val) {
$this->roles[$role] = $val;
}
return $this;
}
public function getRoles() {
$roles = array();
foreach ($this->roles as $key => $val) {
if ($val) {
$roles[$key] = $val;
}
}
return $roles;
}
public function setPort($value) {
$this->port = $value;
return $this;
}
public function getPort() {
return $this->port;
}
public function setHost($value) {
$this->host = $value;
return $this;
}
public function getHost() {
return $this->host;
}
public function getHealthRecordCacheKey() {
$host = $this->getHost();
$port = $this->getPort();
$key = self::KEY_HEALTH;
return "{$key}({$host}, {$port})";
}
/**
* @return PhabricatorClusterServiceHealthRecord
*/
public function getHealthRecord() {
if (!$this->healthRecord) {
$this->healthRecord = new PhabricatorClusterServiceHealthRecord(
$this->getHealthRecordCacheKey());
}
return $this->healthRecord;
}
public function didHealthCheck($reachable) {
$record = $this->getHealthRecord();
$should_check = $record->getShouldCheck();
if ($should_check) {
$record->didHealthCheck($reachable);
}
}
/**
* @return string[] Get a list of fields to show in the status overview UI
*/
abstract public function getStatusViewColumns();
abstract public function getConnectionStatus();
}

View file

@ -0,0 +1,264 @@
<?php
class PhabricatorSearchService
extends Phobject {
const KEY_REFS = 'cluster.search.refs';
protected $config;
protected $disabled;
protected $engine;
protected $hosts = array();
protected $hostsConfig;
protected $hostType;
protected $roles = array();
const STATUS_OKAY = 'okay';
const STATUS_FAIL = 'fail';
const ROLE_WRITE = 'write';
const ROLE_READ = 'read';
public function __construct(PhabricatorFulltextStorageEngine $engine) {
$this->engine = $engine;
$this->hostType = $engine->getHostType();
}
/**
* @throws Exception
*/
public function newHost($config) {
$host = clone($this->hostType);
$host_config = $this->config + $config;
$host->setConfig($host_config);
$this->hosts[] = $host;
return $host;
}
public function getEngine() {
return $this->engine;
}
public function getDisplayName() {
return $this->hostType->getDisplayName();
}
public function getStatusViewColumns() {
return $this->hostType->getStatusViewColumns();
}
public function setConfig($config) {
$this->config = $config;
if (!isset($config['hosts'])) {
$config['hosts'] = array(
array(
'host' => idx($config, 'host'),
'port' => idx($config, 'port'),
'protocol' => idx($config, 'protocol'),
'roles' => idx($config, 'roles'),
),
);
}
foreach ($config['hosts'] as $host) {
$this->newHost($host);
}
}
public function getConfig() {
return $this->config;
}
public static function getConnectionStatusMap() {
return array(
self::STATUS_OKAY => array(
'icon' => 'fa-exchange',
'color' => 'green',
'label' => pht('Okay'),
),
self::STATUS_FAIL => array(
'icon' => 'fa-times',
'color' => 'red',
'label' => pht('Failed'),
),
);
}
public function isWritable() {
return (bool)$this->getAllHostsForRole(self::ROLE_WRITE);
}
public function isReadable() {
return (bool)$this->getAllHostsForRole(self::ROLE_READ);
}
public function getPort() {
return idx($this->config, 'port');
}
public function getProtocol() {
return idx($this->config, 'protocol');
}
public function getVersion() {
return idx($this->config, 'version');
}
public function getHosts() {
return $this->hosts;
}
/**
* Get a random host reference with the specified role, skipping hosts which
* failed recent health checks.
* @throws PhabricatorClusterNoHostForRoleException if no healthy hosts match.
* @return PhabricatorSearchHost
*/
public function getAnyHostForRole($role) {
$hosts = $this->getAllHostsForRole($role);
shuffle($hosts);
foreach ($hosts as $host) {
$health = $host->getHealthRecord();
if ($health->getIsHealthy()) {
return $host;
}
}
throw new PhabricatorClusterNoHostForRoleException($role);
}
/**
* Get all configured hosts for this service which have the specified role.
* @return PhabricatorSearchHost[]
*/
public function getAllHostsForRole($role) {
// if the role is explicitly set to false at the top level, then all hosts
// have the role disabled.
if (idx($this->config, $role) === false) {
return array();
}
$hosts = array();
foreach ($this->hosts as $host) {
if ($host->hasRole($role)) {
$hosts[] = $host;
}
}
return $hosts;
}
/**
* Get a reference to all configured fulltext search cluster services
* @return PhabricatorSearchService[]
*/
public static function getAllServices() {
$cache = PhabricatorCaches::getRequestCache();
$refs = $cache->getKey(self::KEY_REFS);
if (!$refs) {
$refs = self::newRefs();
$cache->setKey(self::KEY_REFS, $refs);
}
return $refs;
}
/**
* Load all valid PhabricatorFulltextStorageEngine subclasses
*/
public static function loadAllFulltextStorageEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFulltextStorageEngine')
->setUniqueMethod('getEngineIdentifier')
->execute();
}
/**
* Create instances of PhabricatorSearchService based on configuration
* @return PhabricatorSearchService[]
*/
public static function newRefs() {
$services = PhabricatorEnv::getEnvConfig('cluster.search');
$engines = self::loadAllFulltextStorageEngines();
$refs = array();
foreach ($services as $config) {
// Normally, we've validated configuration before we get this far, but
// make sure we don't fatal if we end up here with a bogus configuration.
if (!isset($engines[$config['type']])) {
throw new Exception(
pht(
'Configured search engine type "%s" is unknown. Valid engines '.
'are: %s.',
$config['type'],
implode(', ', array_keys($engines))));
}
$engine = clone($engines[$config['type']]);
$cluster = new self($engine);
$cluster->setConfig($config);
$engine->setService($cluster);
$refs[] = $cluster;
}
return $refs;
}
/**
* (re)index the document: attempt to pass the document to all writable
* fulltext search hosts
*/
public static function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$exceptions = array();
foreach (self::getAllServices() as $service) {
if (!$service->isWritable()) {
continue;
}
$engine = $service->getEngine();
try {
$engine->reindexAbstractDocument($document);
} catch (Exception $ex) {
$exceptions[] = $ex;
}
}
if ($exceptions) {
throw new PhutilAggregateException(
pht(
'Writes to search services failed while reindexing document "%s".',
$document->getPHID()),
$exceptions);
}
}
/**
* Execute a full-text query and return a list of PHIDs of matching objects.
* @return string[]
* @throws PhutilAggregateException
*/
public static function executeSearch(PhabricatorSavedQuery $query) {
$exceptions = array();
// try all services until one succeeds
foreach (self::getAllServices() as $service) {
try {
$engine = $service->getEngine();
$res = $engine->executeSearch($query);
// return immediately if we get results
return $res;
} catch (Exception $ex) {
$exceptions[] = $ex;
}
}
$msg = pht('All of the configured Fulltext Search services failed.');
throw new PhutilAggregateException($msg, $exceptions);
}
}

View file

@ -14,6 +14,7 @@ abstract class PhabricatorStandardCustomFieldTokenizer
->setName($this->getFieldKey())
->setDatasource($this->getDatasource())
->setCaption($this->getCaption())
->setError($this->getFieldError())
->setValue(nonempty($value, array()));
$limit = $this->getFieldConfigValue('limit');

View file

@ -128,6 +128,10 @@ abstract class PhabricatorTestCase extends PhutilTestCase {
$this->env->overrideEnvConfig('phabricator.silent', false);
$this->env->overrideEnvConfig('cluster.read-only', false);
$this->env->overrideEnvConfig(
'maniphest.custom-field-definitions',
array());
}
protected function didRunTests() {

View file

@ -97,6 +97,15 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
'headerIcon' => 'fa-smile-o',
'headerText' => pht('Find Emoji:'),
'hintText' => $emoji_datasource->getPlaceholderText(),
// Cancel on emoticons like ":3".
'ignore' => array(
'3',
')',
'(',
'-',
'/',
),
),
),
));
@ -172,11 +181,6 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
'align' => 'right',
);
$actions[] = array(
'spacer' => true,
'align' => 'right',
);
$actions['fa-book'] = array(
'tip' => pht('Help'),
'align' => 'right',
@ -200,10 +204,6 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
}
if ($mode_actions) {
$actions[] = array(
'spacer' => true,
'align' => 'right',
);
$actions += $mode_actions;
}

View file

@ -69,6 +69,11 @@
margin-left: 212px;
}
.device-desktop .phabricator-standard-page-body .has-drag-nav
.phabricator-nav-local {
max-width: none;
}
.has-drag-nav ul.phui-list-view {
height: 100%;
overflow-y: auto;

View file

@ -558,9 +558,13 @@ var.remarkup-assist-textarea {
.remarkup-assist-button {
display: block;
margin-top: 2px;
padding: 4px 5px;
margin-top: 4px;
height: 20px;
padding: 2px 5px 3px;
line-height: 18px;
width: 16px;
float: left;
border-radius: 3px;
}
.remarkup-assist-button:hover .phui-icon-view.phui-font-fa {
@ -617,37 +621,6 @@ var.remarkup-assist-textarea {
opacity: 1.0;
}
.remarkup-control-fullscreen-mode {
position: fixed;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.remarkup-control-fullscreen-mode textarea.remarkup-assist-textarea {
position: absolute;
top: 32px;
left: 0;
right: 0;
bottom: 0;
/* NOTE: This doesn't work in Firefox, there's a JS behavior to correct it. */
height: auto;
border-width: 1px 0 0 0;
outline: none;
resize: none;
background: #fff !important;
}
.remarkup-control-fullscreen-mode textarea.remarkup-assist-textarea:focus {
border-color: none;
box-shadow: none;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button .fa-arrows-alt {
color: {$sky};
}
.phabricator-image-macro-hero {
margin: auto;
max-width: 95%;
@ -673,42 +646,12 @@ var.remarkup-assist-textarea {
padding: 0 4px;
}
.remarkup-inline-preview {
display: block;
position: relative;
background: #fff;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
border: 1px solid {$sky};
resize: vertical;
padding: 4px 6px;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
resize: none;
}
.remarkup-inline-preview * {
resize: none;
}
.remarkup-assist-button.preview-active {
background: {$sky};
}
.remarkup-assist-button.preview-active .phui-icon-view {
color: #ffffff;
}
.remarkup-assist-button.preview-active:hover .phui-icon-view {
color: {$lightsky};
}
.device .remarkup-assist-nodevice {
display: none;
}
/* - Autocomplete ----------------------------------------------------------- */
.phuix-autocomplete {
position: absolute;
width: 300px;
@ -764,6 +707,9 @@ var.remarkup-assist-textarea {
color: #000;
}
/* - Pinned ----------------------------------------------------------------- */
.phui-box.phui-object-box.phui-comment-form-view.remarkup-assist-pinned {
position: fixed;
background-color: #ffffff;
@ -783,3 +729,144 @@ var.remarkup-assist-textarea {
.remarkup-assist-pinned-spacer {
position: relative;
}
/* - Preview ---------------------------------------------------------------- */
.remarkup-inline-preview {
display: block;
position: relative;
background: #fff;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
resize: vertical;
padding: 8px;
border: 1px solid {$lightblueborder};
border-top: none;
-webkit-font-smoothing: antialiased;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
resize: none;
}
.remarkup-inline-preview * {
resize: none;
}
.remarkup-assist-button.preview-active {
background: {$sky};
}
.remarkup-assist-button.preview-active .phui-icon-view {
color: #fff;
}
.remarkup-assist-button.preview-active:hover {
text-decoration: none;
}
.remarkup-assist-button.preview-active:hover .phui-icon-view {
color: #fff;
}
.remarkup-preview-active .remarkup-assist,
.remarkup-preview-active .remarkup-assist-separator {
opacity: .2;
transition: all 100ms cubic-bezier(0.250, 0.250, 0.750, 0.750);
transition-timing-function: cubic-bezier(0.250, 0.250, 0.750, 0.750);
}
.remarkup-preview-active .remarkup-assist-button {
pointer-events: none;
cursor: default;
}
.remarkup-preview-active .remarkup-assist-button.preview-active {
pointer-events: inherit;
cursor: pointer;
}
.remarkup-preview-active .remarkup-assist.fa-eye {
opacity: 1;
transition: all 100ms cubic-bezier(0.250, 0.250, 0.750, 0.750);
transition-timing-function: cubic-bezier(0.250, 0.250, 0.750, 0.750);
}
/* - Fullscreen ------------------------------------------------------------- */
.remarkup-fullscreen-mode {
overflow: hidden;
}
.remarkup-control-fullscreen-mode {
position: fixed;
border: none;
top: 32px;
bottom: 32px;
left: 64px;
right: 64px;
border-radius: 3px;
box-shadow: 0px 4px 32px #555;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button {
padding: 1px 6px 4px;
font-size: 15px;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button .remarkup-assist {
height: 16px;
width: 16px;
}
.aphront-form-input .remarkup-control-fullscreen-mode .remarkup-assist-bar {
border: none;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
height: 32px;
padding: 4px 8px;
background: {$bluebackground};
}
.aphront-form-control .remarkup-control-fullscreen-mode
textarea.remarkup-assist-textarea {
position: absolute;
top: 39px;
left: 0;
right: 0;
height: calc(100% - 36px) !important;
padding: 16px;
font-size: {$biggerfontsize};
line-height: 1.51em;
border-width: 1px 0 0 0;
outline: none;
resize: none;
background: #fff !important;
}
.remarkup-control-fullscreen-mode textarea.remarkup-assist-textarea:focus {
border-color: {$thinblueborder};
box-shadow: none;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
font-size: {$biggerfontsize};
border: none;
padding: 16px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.remarkup-control-fullscreen-mode .remarkup-assist-button .fa-arrows-alt {
color: {$sky};
}
.device-phone .remarkup-control-fullscreen-mode {
top: 0;
bottom: 0;
left: 0;
right: 0;
}

View file

@ -11,7 +11,7 @@
margin-right: 1px;
}
.remarkup-code td span {
.remarkup-code td > span {
display: inline;
word-break: break-all;
}

Some files were not shown because too many files have changed in this diff Show more