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:
commit
2460755603
103 changed files with 2655 additions and 697 deletions
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
|
2
resources/sql/autopatches/20170328.reviewers.01.void.sql
Normal file
2
resources/sql/autopatches/20170328.reviewers.01.void.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_differential.differential_reviewer
|
||||
ADD voidedPHID VARBINARY(64);
|
|
@ -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',
|
||||
|
|
|
@ -69,6 +69,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication {
|
|||
'databases/' => 'PhabricatorConfigClusterDatabasesController',
|
||||
'notifications/' => 'PhabricatorConfigClusterNotificationsController',
|
||||
'repositories/' => 'PhabricatorConfigClusterRepositoriesController',
|
||||
'search/' => 'PhabricatorConfigClusterSearchController',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.'/',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@ final class DifferentialCreateCommentConduitAPIMethod
|
|||
'accept' => DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE,
|
||||
'reject' => DifferentialRevisionRejectTransaction::TRANSACTIONTYPE,
|
||||
'resign' => DifferentialRevisionResignTransaction::TRANSACTIONTYPE,
|
||||
'request_review' =>
|
||||
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE,
|
||||
);
|
||||
|
||||
$action = $request->getValue('action');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -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 '.
|
||||
|
|
|
@ -84,12 +84,19 @@ 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.
|
||||
// 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)) {
|
||||
// If a reviewer is already in a full "accepted" state, don't
|
||||
|
@ -97,19 +104,36 @@ 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) {
|
||||
$is_force = isset($default_unchecked[$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));
|
||||
|
||||
$value[] = $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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
$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) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,10 +15,6 @@ final class PhabricatorOwnersPackageSearchEngine
|
|||
return new PhabricatorOwnersPackageQuery();
|
||||
}
|
||||
|
||||
public function canUseInPanelContext() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function buildCustomSearchFields() {
|
||||
return array(
|
||||
id(new PhabricatorSearchDatasourceField())
|
||||
|
|
|
@ -52,7 +52,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
|
|||
=> 'PhortuneCartListController',
|
||||
),
|
||||
'charge/(?:query/(?P<queryKey>[^/]+)/)?'
|
||||
=> 'PhortuneChargeListController',
|
||||
=> 'PhortuneAccountChargeListController',
|
||||
),
|
||||
'card/(?P<id>\d+)/' => array(
|
||||
'edit/' => 'PhortunePaymentMethodEditController',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneChargeListController
|
||||
final class PhortuneAccountChargeListController
|
||||
extends PhortuneController {
|
||||
|
||||
private $account;
|
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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')),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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}/");
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchEngineTestCase extends PhabricatorTestCase {
|
||||
|
||||
public function testLoadAllEngines() {
|
||||
PhabricatorFulltextStorageEngine::loadAllEngines();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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,66 +214,42 @@ 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()));
|
||||
if (!$q->getClauseCount('must')) {
|
||||
$q->addMustClause(array('match_all' => array('boost' => 1 )));
|
||||
}
|
||||
|
||||
$spec = array(
|
||||
'_source' => false,
|
||||
'query' => array(
|
||||
'filtered' => $spec + $filter,
|
||||
'bool' => $q->toArray(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!$query->getParameter('query')) {
|
||||
$spec['sort'] = array(
|
||||
|
@ -243,8 +257,16 @@ final class PhabricatorElasticFulltextStorageEngine
|
|||
);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
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(
|
||||
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');
|
||||
$fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType');
|
||||
$relationships = $this->getTypeConstants('PhabricatorSearchRelationship');
|
||||
|
||||
// Ensure we have dateCreated since the default query requires it
|
||||
$data['mappings'][$type]['properties']['dateCreated']['type'] = 'string';
|
||||
$doc_types = array_keys(
|
||||
PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes());
|
||||
|
||||
$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
|
||||
$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());
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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('Nothing to do.'));
|
||||
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('Done.'));
|
||||
}
|
||||
|
||||
if (!$work_done) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('No services need initialization.'));
|
||||
return 0;
|
||||
}
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('Service initialization complete.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
===================
|
||||
|
||||
|
|
210
src/docs/user/cluster/cluster_search.diviner
Normal file
210
src/docs/user/cluster/cluster_search.diviner
Normal 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"
|
||||
}
|
||||
]
|
||||
```
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
123
src/infrastructure/cluster/search/PhabricatorSearchHost.php
Normal file
123
src/infrastructure/cluster/search/PhabricatorSearchHost.php
Normal 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();
|
||||
|
||||
}
|
264
src/infrastructure/cluster/search/PhabricatorSearchService.php
Normal file
264
src/infrastructure/cluster/search/PhabricatorSearchService.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue