From 85d9a009a9d31413572df19537c0f485cd73c94a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 07:10:05 -0700 Subject: [PATCH 001/239] Remove dead link from "External Editors" documentation Summary: Fixes T12418. This is a fairly advanced feature and I think users can reasonably consult the documentation for their own editors to figure out how to do this. Test Plan: Saw no more text. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12418 Differential Revision: https://secure.phabricator.com/D17510 --- src/docs/user/userguide/external_editor.diviner | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/docs/user/userguide/external_editor.diviner b/src/docs/user/userguide/external_editor.diviner index 1139a28e0e..16a34d90fb 100644 --- a/src/docs/user/userguide/external_editor.diviner +++ b/src/docs/user/userguide/external_editor.diviner @@ -42,10 +42,3 @@ Then set your "Editor Link" to: lang=uri txmt://open/?url=file:///Users/alincoln/editor_links/%r/%f&line=%l - -== Configuring: Other Editors == - -General instructions for configuring some other editors and environments can be -found here: - -http://wiki.nette.org/en/howto-editor-link From 2921bad1ff082066416890e00edd6a0a8f4c834c Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 20 Mar 2017 14:15:16 -0700 Subject: [PATCH 002/239] Add an action to adding Panels from ApplicationSearch Summary: Ref T5307. This adds an additional action to Use Results for creating a panel from the query. Test Plan: Navigate to Maniphest, select dropdown for Use Results. Try any of the following: - Try to set a panel without a name (fail) - Muck up query or engine (fail) - Set a fake Dashboard ID (fail) Give panel a name and select a dashboard I have edit permissions to, get taken to dashboard. Reviewers: epriestley Subscribers: Korvin Maniphest Tasks: T5307 Differential Revision: https://secure.phabricator.com/D17516 --- src/__phutil_library_map__.php | 2 + .../PhabricatorDashboardApplication.php | 2 + ...orDashboardQueryPanelInstallController.php | 159 ++++++++++++++++++ ...PhabricatorApplicationSearchController.php | 36 +++- 4 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 29bf2e418f..645a9ee6b2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2530,6 +2530,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', 'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php', 'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php', + 'PhabricatorDashboardQueryPanelInstallController' => 'applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php', 'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php', 'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php', 'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php', @@ -7610,6 +7611,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardProfileController' => 'PhabricatorController', 'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDashboardQueryPanelInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index c0ce2ff09e..29024cbcb8 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -36,6 +36,8 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { 'removepanel/(?P\d+)/' => 'PhabricatorDashboardRemovePanelController', 'panel/' => array( + 'install/(?P[^/]+)/(?:(?P[^/]+)/)?' => + 'PhabricatorDashboardQueryPanelInstallController', '(?:query/(?P[^/]+)/)?' => 'PhabricatorDashboardPanelListController', 'create/' => 'PhabricatorDashboardPanelEditController', diff --git a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php new file mode 100644 index 0000000000..6d0d8c2e74 --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php @@ -0,0 +1,159 @@ +getViewer(); + + $v_dashboard = null; + $v_name = null; + $v_column = 0; + $v_engine = $request->getURIData('engineKey'); + $v_query = $request->getURIData('queryKey'); + + // Validate Engines + $engines = PhabricatorApplicationSearchEngine::getAllEngines(); + foreach ($engines as $name => $engine) { + if (!$engine->canUseInPanelContext()) { + unset($engines[$name]); + } + } + if (!in_array($v_engine, array_keys($engines))) { + return new Aphront404Response(); + } + + // Validate Queries + $engine = $engines[$v_engine]; + $engine->setViewer($viewer); + $queries = array_keys($engine->loadEnabledNamedQueries()); + if (!in_array($v_query, $queries)) { + return new Aphront404Response(); + } + + $errors = array(); + + if ($request->isFormPost()) { + $v_dashboard = $request->getInt('dashboardID'); + $v_name = $request->getStr('name'); + if (!$v_name) { + $errors[] = pht('You must provide a name for this panel.'); + } + + $dashboard = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withIDs(array($v_dashboard)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + if (!$dashboard) { + $errors[] = pht('Please select a valid dashboard.'); + } + + if (!$errors) { + $redirect_uri = "/dashboard/arrange/{$v_dashboard}/"; + + $panel_type = id(new PhabricatorDashboardQueryPanelType()) + ->getPanelTypeKey(); + $panel = PhabricatorDashboardPanel::initializeNewPanel($viewer); + $panel->setPanelType($panel_type); + + $field_list = PhabricatorCustomField::getObjectFields( + $panel, + PhabricatorCustomField::ROLE_EDIT); + + $field_list + ->setViewer($viewer) + ->readFieldsFromStorage($panel); + + $panel->requireImplementation()->initializeFieldsFromRequest( + $panel, + $field_list, + $request); + + $xactions = array(); + + $xactions[] = id(new PhabricatorDashboardPanelTransaction()) + ->setTransactionType(PhabricatorDashboardPanelTransaction::TYPE_NAME) + ->setNewValue($v_name); + + $xactions[] = id(new PhabricatorDashboardPanelTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) + ->setMetadataValue('customfield:key', 'std:dashboard:core:class') + ->setOldValue(null) + ->setNewValue($v_engine); + + $xactions[] = id(new PhabricatorDashboardPanelTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) + ->setMetadataValue('customfield:key', 'std:dashboard:core:key') + ->setOldValue(null) + ->setNewValue($v_query); + + $editor = id(new PhabricatorDashboardPanelTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($panel, $xactions); + + PhabricatorDashboardTransactionEditor::addPanelToDashboard( + $viewer, + PhabricatorContentSource::newFromRequest($request), + $panel, + $dashboard, + $request->getInt('column', 0)); + + return id(new AphrontRedirectResponse())->setURI($redirect_uri); + } + } + + // Make this a select for now, as we don't expect someone to have + // edit access to a vast number of dashboards. + // Can add optiongroup if needed down the road. + $dashboards = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withStatuses(array( + PhabricatorDashboard::STATUS_ACTIVE, + )) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + $options = mpull($dashboards, 'getName', 'getID'); + asort($options); + + $redirect_uri = '#'; // ?? + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('engine', $v_engine) + ->addHiddenInput('query', $v_query) + ->addHiddenInput('column', $v_column) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($v_name)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setUser($this->getViewer()) + ->setValue($v_dashboard) + ->setName('dashboardID') + ->setOptions($options) + ->setLabel(pht('Dashboard'))); + + return $this->newDialog() + ->setTitle(pht('Add Panel to Dashboard')) + ->setErrors($errors) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($form->buildLayoutView()) + ->addCancelButton($redirect_uri) + ->addSubmitButton(pht('Add Panel')); + } + +} diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index faca9991ea..dd3508e6bb 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -555,8 +555,9 @@ final class PhabricatorApplicationSearchController ->setTag('a') ->setHref('#') ->setText(pht('Use Results...')) - ->setIcon('fa-road') - ->setDropdownMenu($action_list); + ->setIcon('fa-bars') + ->setDropdownMenu($action_list) + ->addClass('dropdown'); } private function newOverflowingView() { @@ -600,9 +601,32 @@ final class PhabricatorApplicationSearchController private function newBuiltinUseActions() { $actions = array(); + $request = $this->getRequest(); + $viewer = $request->getUser(); $is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + $engine = $this->getSearchEngine(); + $engine_class = get_class($engine); + $query_key = $this->getQueryKey(); + if (!$query_key) { + $query_key = head_key($engine->loadEnabledNamedQueries()); + } + + $can_use = $engine->canUseInPanelContext(); + $is_installed = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorDashboardApplication', + $viewer); + + if ($can_use && $is_installed) { + $dashboard_uri = '/dashboard/install/'; + $actions[] = id(new PhabricatorActionView()) + ->setIcon('fa-dashboard') + ->setName(pht('Add to Dasbhoard')) + ->setWorkflow(true) + ->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/"); + } + if ($is_dev) { $engine = $this->getSearchEngine(); $nux_uri = $engine->getQueryBaseURI(); @@ -610,8 +634,8 @@ final class PhabricatorApplicationSearchController ->setQueryParam('nux', true); $actions[] = id(new PhabricatorActionView()) - ->setIcon('fa-bug') - ->setName(pht('Developer: Show New User State')) + ->setIcon('fa-user-plus') + ->setName(pht('DEV: New User State')) ->setHref($nux_uri); } @@ -620,8 +644,8 @@ final class PhabricatorApplicationSearchController ->setQueryParam('overheated', true); $actions[] = id(new PhabricatorActionView()) - ->setIcon('fa-bug') - ->setName(pht('Developer: Show Overheated State')) + ->setIcon('fa-fire') + ->setName(pht('DEV: Overheated State')) ->setHref($overheated_uri); } From 9b07adb8dad30385df152f2cac4ec1a94c92b10d Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 20 Mar 2017 14:45:30 -0700 Subject: [PATCH 003/239] Add better error checking to 'Add to Dashboard' Summary: Ref T5307. Adds a better query check query, sets required for the name, adds the correct URI for cancelling. Test Plan: Test a form without a name, fake a query string, test cancel button. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T5307 Differential Revision: https://secure.phabricator.com/D17520 --- ...orDashboardQueryPanelInstallController.php | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php index 6d0d8c2e74..846ada7d05 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php @@ -12,6 +12,8 @@ final class PhabricatorDashboardQueryPanelInstallController $v_engine = $request->getURIData('engineKey'); $v_query = $request->getURIData('queryKey'); + $e_name = true; + // Validate Engines $engines = PhabricatorApplicationSearchEngine::getAllEngines(); foreach ($engines as $name => $engine) { @@ -26,8 +28,20 @@ final class PhabricatorDashboardQueryPanelInstallController // Validate Queries $engine = $engines[$v_engine]; $engine->setViewer($viewer); - $queries = array_keys($engine->loadEnabledNamedQueries()); - if (!in_array($v_query, $queries)) { + $good_query = false; + if ($engine->isBuiltinQuery($v_engine)) { + $good_query = true; + } else { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withEngineClassNames(array($v_engine)) + ->withQueryKeys(array($v_query)) + ->executeOne(); + if ($saved_query) { + $good_query = true; + } + } + if (!$good_query) { return new Aphront404Response(); } @@ -38,6 +52,7 @@ final class PhabricatorDashboardQueryPanelInstallController $v_name = $request->getStr('name'); if (!$v_name) { $errors[] = pht('You must provide a name for this panel.'); + $e_name = pht('Required'); } $dashboard = id(new PhabricatorDashboardQuery()) @@ -127,7 +142,7 @@ final class PhabricatorDashboardQueryPanelInstallController $options = mpull($dashboards, 'getName', 'getID'); asort($options); - $redirect_uri = '#'; // ?? + $redirect_uri = $engine->getQueryResultsPageURI($v_query); $form = id(new AphrontFormView()) ->setUser($viewer) @@ -138,7 +153,8 @@ final class PhabricatorDashboardQueryPanelInstallController id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') - ->setValue($v_name)) + ->setValue($v_name) + ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setUser($this->getViewer()) From e69f8f717b80b157dd757f861667654db8abef69 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 20 Mar 2017 15:02:52 -0700 Subject: [PATCH 004/239] Fix 'Add to Dashboard' issue with builtins Summary: Ref T5307. Actually check the built in query with query, not engine. Test Plan: Try a builtin query, and a custom query when making a dashboard panel. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T5307 Differential Revision: https://secure.phabricator.com/D17521 --- .../PhabricatorDashboardQueryPanelInstallController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php index 846ada7d05..ecc635966e 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php @@ -29,7 +29,7 @@ final class PhabricatorDashboardQueryPanelInstallController $engine = $engines[$v_engine]; $engine->setViewer($viewer); $good_query = false; - if ($engine->isBuiltinQuery($v_engine)) { + if ($engine->isBuiltinQuery($v_query)) { $good_query = true; } else { $saved_query = id(new PhabricatorSavedQueryQuery()) From 216052baf982bfe3cca22d992fdf59efcf4ef424 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 08:33:43 -0700 Subject: [PATCH 005/239] Apply reviewer changes from Herald via ModularTransactions Summary: Ref T10967. This converts the reviewer update action in Herald from an older edge write to a newer ModularTransactions write. The major value from this is that we get a double-write to the new reviewers table. Test Plan: - Wrote a Herald rule to add a reviewer and a blocking reviewer. - Saw them added properly to a revision with: no reviewers; both as blocking; A as blocking, B as nonblocking; A as nonblocking, B as blocking. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17511 --- .../herald/DifferentialReviewersHeraldAction.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php index 3e98fdd92f..6574455465 100644 --- a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php +++ b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php @@ -81,18 +81,17 @@ abstract class DifferentialReviewersHeraldAction $value = array(); foreach ($phids as $phid) { - $value[$phid] = array( - 'data' => array( - 'status' => $new_status, - ), - ); + if ($is_blocking) { + $value[] = 'blocking('.$phid.')'; + } else { + $value[] = $phid; + } } - $edgetype_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; + $reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; $xaction = $adapter->newTransaction() - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $edgetype_reviewer) + ->setTransactionType($reviewers_type) ->setNewValue( array( '+' => $value, From a9cbbf3e5e151d345318a66d6b0dd0de804fb64c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 09:13:07 -0700 Subject: [PATCH 006/239] Apply Owners reviewers using ModularTransactions Summary: Ref T10967. See that task for some discussion. This lets us do double writes on this pathway. Test Plan: Set an Owners package to auto-review. Created revisions which triggered it: one with no reviewers (autoreview added); one with the package as a blocking reviewer explicitly (no automatic stuff happened, as expected). Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17512 --- .../editor/DifferentialTransactionEditor.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 85a44669c8..19c2de8ec0 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1687,22 +1687,21 @@ final class DifferentialTransactionEditor $value = array(); foreach ($phids as $phid) { - $value[$phid] = array( - 'data' => array( - 'status' => $new_status, - ), - ); + if ($is_blocking) { + $value[] = 'blocking('.$phid.')'; + } else { + $value[] = $phid; + } } - $edgetype_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - $owners_phid = id(new PhabricatorOwnersApplication()) ->getPHID(); + $reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; + return $object->getApplicationTransactionTemplate() ->setAuthorPHID($owners_phid) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $edgetype_reviewer) + ->setTransactionType($reviewers_type) ->setNewValue( array( '+' => $value, From 77b3efafbd4ee902194a25843055b36679767f8c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 09:29:10 -0700 Subject: [PATCH 007/239] Use ModularTransactions for accept/reject/resign in "differential.createcomment" Summary: Ref T10967. `differential.createcomment` is a frozen API method which has been obsoleted by `differential.revision.edit`. It is the only remaining way to apply an "accept", "reject", or "resign" action using the old "ACTION" code. Instead of using the old code, sneakly apply a new type of transaction in these cases instead. Then, remove all the remaining old code for this stuff on the write pathways. Test Plan: - Used "differential.createcomment" to accept, reject, and resign from a revision. - Grepped for all removed ACTION_X constants, found them only in rendering code. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17513 --- ...ferentialCreateCommentConduitAPIMethod.php | 23 ++- .../constants/DifferentialAction.php | 33 ---- .../editor/DifferentialTransactionEditor.php | 144 ------------------ 3 files changed, 20 insertions(+), 180 deletions(-) diff --git a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php index 459298c54f..23b8f770cd 100644 --- a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php @@ -56,11 +56,28 @@ final class DifferentialCreateCommentConduitAPIMethod $xactions = array(); + $modular_map = array( + 'accept' => DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE, + 'reject' => DifferentialRevisionRejectTransaction::TRANSACTIONTYPE, + 'resign' => DifferentialRevisionResignTransaction::TRANSACTIONTYPE, + ); + $action = $request->getValue('action'); - if ($action && ($action != 'comment') && ($action != 'none')) { + if (isset($modular_map[$action])) { $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(DifferentialTransaction::TYPE_ACTION) - ->setNewValue($action); + ->setTransactionType($modular_map[$action]) + ->setNewValue(true); + } else if ($action) { + switch ($action) { + case 'comment': + case 'none': + break; + default: + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType(DifferentialTransaction::TYPE_ACTION) + ->setNewValue($action); + break; + } } $content = $request->getValue('message'); diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php index 6227d61dd0..a955ad9dd2 100644 --- a/src/applications/differential/constants/DifferentialAction.php +++ b/src/applications/differential/constants/DifferentialAction.php @@ -119,37 +119,4 @@ final class DifferentialAction extends Phobject { return $title; } - public static function getActionVerb($action) { - $verbs = array( - self::ACTION_COMMENT => pht('Comment'), - self::ACTION_ACCEPT => pht("Accept Revision \xE2\x9C\x94"), - self::ACTION_REJECT => pht("Request Changes \xE2\x9C\x98"), - self::ACTION_RETHINK => pht("Plan Changes \xE2\x9C\x98"), - self::ACTION_ABANDON => pht('Abandon Revision'), - self::ACTION_REQUEST => pht('Request Review'), - self::ACTION_RECLAIM => pht('Reclaim Revision'), - self::ACTION_RESIGN => pht('Resign as Reviewer'), - self::ACTION_ADDREVIEWERS => pht('Add Reviewers'), - self::ACTION_ADDCCS => pht('Add Subscribers'), - self::ACTION_CLOSE => pht('Close Revision'), - self::ACTION_CLAIM => pht('Commandeer Revision'), - self::ACTION_REOPEN => pht('Reopen'), - ); - - if (!empty($verbs[$action])) { - return $verbs[$action]; - } else { - return pht('brazenly %s', $action); - } - } - - public static function allowReviewers($action) { - if ($action == self::ACTION_ADDREVIEWERS || - $action == self::ACTION_REQUEST || - $action == self::ACTION_RESIGN) { - return true; - } - return false; - } - } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 19c2de8ec0..59bd746ccd 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -130,33 +130,6 @@ final class DifferentialTransactionEditor $action_type = $xaction->getNewValue(); switch ($action_type) { - case DifferentialAction::ACTION_ACCEPT: - case DifferentialAction::ACTION_REJECT: - if ($action_type == DifferentialAction::ACTION_ACCEPT) { - $new_status = DifferentialReviewerStatus::STATUS_ACCEPTED; - } else { - $new_status = DifferentialReviewerStatus::STATUS_REJECTED; - } - - $actor = $this->getActor(); - - // These transactions can cause effects in two ways: by altering the - // status of an existing reviewer; or by adding the actor as a new - // reviewer. - - $will_add_reviewer = true; - foreach ($object->getReviewerStatus() as $reviewer) { - if ($reviewer->hasAuthority($actor)) { - if ($reviewer->getStatus() != $new_status) { - return true; - } - } - if ($reviewer->getReviewerPHID() == $actor_phid) { - $will_add_reviwer = false; - } - } - - return $will_add_reviewer; case DifferentialAction::ACTION_CLOSE: return ($object->getStatus() != $status_closed); case DifferentialAction::ACTION_ABANDON: @@ -169,13 +142,6 @@ final class DifferentialTransactionEditor return ($object->getStatus() != $status_plan); case DifferentialAction::ACTION_REQUEST: return ($object->getStatus() != $status_review); - case DifferentialAction::ACTION_RESIGN: - foreach ($object->getReviewerStatus() as $reviewer) { - if ($reviewer->getReviewerPHID() == $actor_phid) { - return true; - } - } - return false; case DifferentialAction::ACTION_CLAIM: return ($actor_phid != $object->getAuthorPHID()); } @@ -222,12 +188,6 @@ final class DifferentialTransactionEditor return; case DifferentialTransaction::TYPE_ACTION: switch ($xaction->getNewValue()) { - case DifferentialAction::ACTION_RESIGN: - case DifferentialAction::ACTION_ACCEPT: - case DifferentialAction::ACTION_REJECT: - // These have no direct effects, and affect review status only - // indirectly by altering reviewers with TYPE_EDGE transactions. - return; case DifferentialAction::ACTION_ABANDON: $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); return; @@ -482,59 +442,9 @@ final class DifferentialTransactionEditor $action_type = $xaction->getNewValue(); switch ($action_type) { - case DifferentialAction::ACTION_ACCEPT: - case DifferentialAction::ACTION_REJECT: - if ($action_type == DifferentialAction::ACTION_ACCEPT) { - $data = array( - 'status' => DifferentialReviewerStatus::STATUS_ACCEPTED, - ); - } else { - $data = array( - 'status' => DifferentialReviewerStatus::STATUS_REJECTED, - ); - } - - $edits = array(); - - foreach ($object->getReviewerStatus() as $reviewer) { - if ($reviewer->hasAuthority($actor)) { - $edits[$reviewer->getReviewerPHID()] = array( - 'data' => $data, - ); - } - } - - // Also either update or add the actor themselves as a reviewer. - $edits[$actor_phid] = array( - 'data' => $data, - ); - - $results[] = id(new DifferentialTransaction()) - ->setTransactionType($type_edge) - ->setMetadataValue('edge:type', $edge_reviewer) - ->setIgnoreOnNoEffect(true) - ->setNewValue(array('+' => $edits)); - break; - case DifferentialAction::ACTION_CLAIM: $is_commandeer = true; break; - case DifferentialAction::ACTION_RESIGN: - // If the user is resigning, add a separate reviewer edit - // transaction which removes them as a reviewer. - - $results[] = id(new DifferentialTransaction()) - ->setTransactionType($type_edge) - ->setMetadataValue('edge:type', $edge_reviewer) - ->setIgnoreOnNoEffect(true) - ->setNewValue( - array( - '-' => array( - $actor_phid => $actor_phid, - ), - )); - - break; } break; } @@ -925,60 +835,6 @@ final class DifferentialTransactionEditor $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; switch ($action) { - case DifferentialAction::ACTION_ACCEPT: - if ($actor_is_author && !$allow_self_accept) { - return pht( - 'You can not accept this revision because you are the owner.'); - } - - if ($revision_status == $status_abandoned) { - return pht( - 'You can not accept this revision because it has been '. - 'abandoned.'); - } - - if ($revision_status == $status_closed) { - return pht( - 'You can not accept this revision because it has already been '. - 'closed.'); - } - - // TODO: It would be nice to make this generic at some point. - $signatures = DifferentialRequiredSignaturesField::loadForRevision( - $revision); - foreach ($signatures as $phid => $signed) { - if (!$signed) { - return pht( - 'You can not accept this revision because the author has '. - 'not signed all of the required legal documents.'); - } - } - - break; - - case DifferentialAction::ACTION_REJECT: - if ($actor_is_author) { - return pht('You can not request changes to your own revision.'); - } - - if ($revision_status == $status_abandoned) { - return pht( - 'You can not request changes to this revision because it has been '. - 'abandoned.'); - } - - if ($revision_status == $status_closed) { - return pht( - 'You can not request changes to this revision because it has '. - 'already been closed.'); - } - break; - - case DifferentialAction::ACTION_RESIGN: - // You can always resign from a revision if you're a reviewer. If you - // aren't, this is a no-op rather than invalid. - break; - case DifferentialAction::ACTION_CLAIM: // You can claim a revision if you're not the owner. If you are, this // is a no-op rather than invalid. From 794b456530bc67dd6bb88c50eaac1ca23042fa16 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 09:54:48 -0700 Subject: [PATCH 008/239] Store "last comment" and "last action" diffs on reviewers Summary: Ref T10967. We have a "commented" state to help reviewers get a better sense of who is part of a discussion, and a "last action" state to help distinguish between "accept" and "accepted an older version", for the purposes of sticky accepts and as a UI hint. Currently, these are first-class states, partly beacuse we were somewhat limited in what we could do with edges. However, a more flexible way to represent them is as flags separate from the primary state flag. In the new storage, write them as separate state information: `lastActionDiffPHID` stores the Diff PHID of the last review action (accept, reject, etc). `lastCommentDiffPHID` stores the Diff PHID of the last comment (top-level or inline). Test Plan: Applied storage changes, commented and acted on a revision. Saw appropriate state reflected in the database. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17514 --- .../20170320.reviewers.01.lastaction.sql | 2 + .../20170320.reviewers.02.lastcomment.sql | 2 + .../editor/DifferentialTransactionEditor.php | 58 +++++++++++++++++++ .../storage/DifferentialReviewer.php | 5 ++ .../DifferentialRevisionReviewTransaction.php | 11 ++++ 5 files changed, 78 insertions(+) create mode 100644 resources/sql/autopatches/20170320.reviewers.01.lastaction.sql create mode 100644 resources/sql/autopatches/20170320.reviewers.02.lastcomment.sql diff --git a/resources/sql/autopatches/20170320.reviewers.01.lastaction.sql b/resources/sql/autopatches/20170320.reviewers.01.lastaction.sql new file mode 100644 index 0000000000..41b8051275 --- /dev/null +++ b/resources/sql/autopatches/20170320.reviewers.01.lastaction.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_reviewer + ADD lastActionDiffPHID VARBINARY(64); diff --git a/resources/sql/autopatches/20170320.reviewers.02.lastcomment.sql b/resources/sql/autopatches/20170320.reviewers.02.lastcomment.sql new file mode 100644 index 0000000000..c430d86064 --- /dev/null +++ b/resources/sql/autopatches/20170320.reviewers.02.lastcomment.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_reviewer + ADD lastCommentDiffPHID VARBINARY(64); diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 59bd746ccd..cfaff0cbef 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -718,6 +718,9 @@ final class DifferentialTransactionEditor break; } + + $this->markReviewerComments($object, $xactions); + return $xactions; } @@ -1836,4 +1839,59 @@ final class DifferentialTransactionEditor ->setNewValue($edits); } + public function getActiveDiff($object) { + if ($this->getIsNewObject()) { + return null; + } else { + return $object->getActiveDiff(); + } + } + + /** + * When a reviewer makes a comment, mark the last revision they commented + * on. + * + * This allows us to show a hint to help authors and other reviewers quickly + * distinguish between reviewers who have participated in the discussion and + * reviewers who haven't been part of it. + */ + private function markReviewerComments($object, array $xactions) { + $acting_phid = $this->getActingAsPHID(); + if (!$acting_phid) { + return; + } + + $diff = $this->getActiveDiff($object); + if (!$diff) { + return; + } + + $has_comment = false; + foreach ($xactions as $xaction) { + if ($xaction->hasComment()) { + $has_comment = true; + break; + } + } + + if (!$has_comment) { + return; + } + + $reviewer_table = new DifferentialReviewer(); + $conn = $reviewer_table->establishConnection('w'); + + queryfx( + $conn, + 'UPDATE %T SET lastCommentDiffPHID = %s + WHERE revisionPHID = %s + AND reviewerPHID = %s', + $reviewer_table->getTableName(), + $diff->getPHID(), + $object->getPHID(), + $acting_phid); + } + + + } diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 72317a0a4c..731fc9be14 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -7,10 +7,15 @@ final class DifferentialReviewer protected $reviewerPHID; protected $reviewerStatus; + protected $lastActionDiffPHID; + protected $lastCommentDiffPHID; + protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'reviewerStatus' => 'text64', + 'lastActionDiffPHID' => 'phid?', + 'lastCommentDiffPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_revision' => array( diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index 9ac731fea9..de38f67bc0 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -138,6 +138,13 @@ abstract class DifferentialRevisionReviewTransaction // Now, do the new write. if ($map) { + $diff = $this->getEditor()->getActiveDiff($revision); + if ($diff) { + $diff_phid = $diff->getPHID(); + } else { + $diff_phid = null; + } + $table = new DifferentialReviewer(); $reviewers = $table->loadAllWhere( @@ -156,6 +163,10 @@ abstract class DifferentialRevisionReviewTransaction $reviewer->setReviewerStatus($status); + if ($diff_phid) { + $reviewer->setLastActionDiffPHID($diff_phid); + } + if ($status == DifferentialReviewerStatus::STATUS_RESIGNED) { if ($reviewer->getID()) { $reviewer->delete(); From 8ad5d28686a424e325ebd1bdaab7874529f436a6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 10:21:01 -0700 Subject: [PATCH 009/239] Migrate old reviewer edges to new storage Summary: Ref T10967. We still have double writes, so all reviewers are being written to both old and new storage. This migrates all the data in the old storage to the new storage, so both storage tables should have a complete set of data and be getting identical updates as we move forward. After this, I can move readers over one at a time and eventually get rid of the old writes and old storage. This loads all of the edge data into memory in a big chunk. I reached out to one install to get some more information about their data size. Ours is quite manageable and I think even large installs will probably fit into memory, but we can do this in chunks if not. However, because the Edge table doesn't have an `id` column, we can't use either the `RawMigrationIterator` or the `MigrationIterator`, and would need to write a new `EdgeMigrationIterator`. This isn't tons of work but might not be necessary. Test Plan: Ran the migration locally, spot-checked the results in the database for sanity and correctness. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17515 --- .../20170320.reviewers.03.migrate.php | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 resources/sql/autopatches/20170320.reviewers.03.migrate.php diff --git a/resources/sql/autopatches/20170320.reviewers.03.migrate.php b/resources/sql/autopatches/20170320.reviewers.03.migrate.php new file mode 100644 index 0000000000..04abb5c141 --- /dev/null +++ b/resources/sql/autopatches/20170320.reviewers.03.migrate.php @@ -0,0 +1,125 @@ +establishConnection('w'); + +// Previously "DifferentialRevisionHasReviewerEdgeType::EDGECONST". +$edge_type = 35; + +// NOTE: We can't use normal migration iterators for edges because they don't +// have an "id" column. For now, try just loading the whole result set: the +// actual size of the rows is small. If we run into issues, we could write an +// EdgeIterator. +$every_edge = queryfx_all( + $conn, + 'SELECT * FROM %T edge LEFT JOIN %T data ON edge.dataID = data.id + WHERE edge.type = %d', + $table_name, + $data_name, + $edge_type); + +foreach ($every_edge as $edge) { + if ($edge['type'] != $edge_type) { + // Ignore edges which aren't "reviewers", like subscribers. + continue; + } + + try { + $data = phutil_json_decode($edge['data']); + $data = idx($data, 'data'); + } catch (Exception $ex) { + // Just ignore any kind of issue with the edge data, we'll use a default + // below. + $data = null; + } + + if (!$data) { + $data = array( + 'status' => 'added', + ); + } + + $status = idx($data, 'status'); + + $diff_phid = null; + + // NOTE: At one point, the code to populate "diffID" worked correctly, but + // it seems to have later been broken. Salvage it if we can, and look up + // the corresponding diff PHID. + $diff_id = idx($data, 'diffID'); + if ($diff_id) { + $row = queryfx_one( + $conn, + 'SELECT phid FROM %T WHERE id = %d', + $diff_table->getTableName(), + $diff_id); + if ($row) { + $diff_phid = $row['phid']; + } + } + + if (!$diff_phid) { + // If the status is "accepted" or "rejected", look up the current diff + // PHID so we can distinguish between "accepted" and "accepted older". + switch ($status) { + case 'accepted': + case 'rejected': + case 'commented': + $row = queryfx_one( + $conn, + 'SELECT diff.phid FROM %T diff JOIN %T revision + ON diff.revisionID = revision.id + WHERE revision.phid = %s + ORDER BY diff.id DESC LIMIT 1', + $diff_table->getTableName(), + $table->getTableName(), + $edge['src']); + if ($row) { + $diff_phid = $row['phid']; + } + break; + } + } + + // We now represent some states (like "Commented" and "Accepted Older") as + // a primary state plus an extra flag, instead of making "Commented" a + // primary state. Map old states to new states and flags. + + if ($status == 'commented') { + $status = 'added'; + $comment_phid = $diff_phid; + $action_phid = null; + } else { + $comment_phid = null; + $action_phid = $diff_phid; + } + + if ($status == 'accepted-older') { + $status = 'accepted'; + } + + if ($status == 'rejected-older') { + $status = 'rejected'; + } + + queryfx( + $conn, + 'INSERT INTO %T (revisionPHID, reviewerPHID, reviewerStatus, + lastActionDiffPHID, lastCommentDiffPHID, dateCreated, dateModified) + VALUES (%s, %s, %s, %ns, %ns, %d, %d) + ON DUPLICATE KEY UPDATE dateCreated = VALUES(dateCreated)', + $reviewer_table->getTableName(), + $edge['src'], + $edge['dst'], + $status, + $action_phid, + $comment_phid, + $edge['dateCreated'], + $edge['dateCreated']); +} From dccd799b1b44852d928a91347d6925a1efaf21e2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 12:35:30 -0700 Subject: [PATCH 010/239] Move many "reviewers" readers to new storage Summary: Ref T10967. When we query for revisions with particular reviewers, use the new table to drive the query. When we load revisions for use in the application, also use the new table to drive the query. This doesn't convert everything: there's some old `loadRelationships()` stuff still using the old table. But this moves the major stuff over. (This also changes the icon for "commented" from a question mark to a speech bubble.) Test Plan: - Viewed revision lists and detail views on old and new code, saw identical outcomes. - Updated revisions, accepted/rejected/commented on revisions. - Hit the "Accepted Older" and "Commented Older" states by taking an action and then updating. - Grepped for removed methods (like `getEdgeData()` and `getDiffID()`). Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17517 --- src/__phutil_library_map__.php | 2 - .../customfield/DifferentialCustomField.php | 9 ++ .../DifferentialProjectReviewersField.php | 5 +- .../DifferentialReviewersField.php | 5 +- .../query/DifferentialRevisionQuery.php | 60 ++++++------ .../storage/DifferentialReviewer.php | 24 ++++- .../storage/DifferentialReviewerProxy.php | 56 ----------- .../storage/DifferentialRevision.php | 12 +-- .../storage/DifferentialTransaction.php | 23 ----- .../view/DifferentialReviewersView.php | 94 ++++++++++--------- 10 files changed, 123 insertions(+), 167 deletions(-) delete mode 100644 src/applications/differential/storage/DifferentialReviewerProxy.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 645a9ee6b2..66dfda8a65 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -494,7 +494,6 @@ phutil_register_library_map(array( 'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php', 'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php', 'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php', - 'DifferentialReviewerProxy' => 'applications/differential/storage/DifferentialReviewerProxy.php', 'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php', @@ -5255,7 +5254,6 @@ phutil_register_library_map(array( 'DifferentialReviewer' => 'DifferentialDAO', 'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType', - 'DifferentialReviewerProxy' => 'Phobject', 'DifferentialReviewerStatus' => 'Phobject', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction', diff --git a/src/applications/differential/customfield/DifferentialCustomField.php b/src/applications/differential/customfield/DifferentialCustomField.php index 0382034a0e..0b2d330775 100644 --- a/src/applications/differential/customfield/DifferentialCustomField.php +++ b/src/applications/differential/customfield/DifferentialCustomField.php @@ -70,6 +70,15 @@ abstract class DifferentialCustomField return array(); } + protected function getActiveDiff() { + $object = $this->getObject(); + try { + return $object->getActiveDiff(); + } catch (Exception $ex) { + return null; + } + } + public function getRequiredHandlePHIDsForRevisionHeaderWarnings() { return array(); } diff --git a/src/applications/differential/customfield/DifferentialProjectReviewersField.php b/src/applications/differential/customfield/DifferentialProjectReviewersField.php index 87ea0c34a3..5fe5258cdb 100644 --- a/src/applications/differential/customfield/DifferentialProjectReviewersField.php +++ b/src/applications/differential/customfield/DifferentialProjectReviewersField.php @@ -42,7 +42,10 @@ final class DifferentialProjectReviewersField ->setReviewers($reviewers) ->setHandles($handles); - // TODO: Active diff stuff. + $diff = $this->getActiveDiff(); + if ($diff) { + $view->setActiveDiff($diff); + } return $view; } diff --git a/src/applications/differential/customfield/DifferentialReviewersField.php b/src/applications/differential/customfield/DifferentialReviewersField.php index ad96a22b88..4d7ebfc691 100644 --- a/src/applications/differential/customfield/DifferentialReviewersField.php +++ b/src/applications/differential/customfield/DifferentialReviewersField.php @@ -43,7 +43,10 @@ final class DifferentialReviewersField ->setReviewers($reviewers) ->setHandles($handles); - // TODO: Active diff stuff. + $diff = $this->getActiveDiff(); + if ($diff) { + $view->setActiveDiff($diff); + } return $view; } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index 5dff433e9c..c3fb98e607 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -605,11 +605,11 @@ final class DifferentialRevisionQuery if ($this->reviewers) { $joins[] = qsprintf( $conn_r, - 'JOIN %T e_reviewers ON e_reviewers.src = r.phid '. - 'AND e_reviewers.type = %s '. - 'AND e_reviewers.dst in (%Ls)', - PhabricatorEdgeConfig::TABLE_NAME_EDGE, - DifferentialRevisionHasReviewerEdgeType::EDGECONST, + 'JOIN %T reviewer ON reviewer.revisionPHID = r.phid + AND reviewer.reviewerStatus != %s + AND reviewer.reviewerPHID in (%Ls)', + id(new DifferentialReviewer())->getTableName(), + DifferentialReviewerStatus::STATUS_RESIGNED, $this->reviewers); } @@ -972,21 +972,28 @@ final class DifferentialRevisionQuery } private function loadReviewers( - AphrontDatabaseConnection $conn_r, + AphrontDatabaseConnection $conn, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); - $edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($revisions, 'getPHID')) - ->withEdgeTypes(array($edge_type)) - ->needEdgeData(true) - ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) - ->execute(); + $reviewer_table = new DifferentialReviewer(); + $reviewer_rows = queryfx_all( + $conn, + 'SELECT * FROM %T WHERE revisionPHID IN (%Ls) + ORDER BY id ASC', + $reviewer_table->getTableName(), + mpull($revisions, 'getPHID')); + $reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows); + $reviewer_map = mgroup($reviewer_list, 'getRevisionPHID'); + + foreach ($reviewer_map as $key => $reviewers) { + $reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID'); + } $viewer = $this->getViewer(); $viewer_phid = $viewer->getPHID(); + $allow_key = 'differential.allow-self-accept'; $allow_self = PhabricatorEnv::getEnvConfig($allow_key); @@ -994,18 +1001,13 @@ final class DifferentialRevisionQuery if ($this->needReviewerAuthority && $viewer_phid) { $authority = $this->loadReviewerAuthority( $revisions, - $edges, + $reviewer_map, $allow_self); } foreach ($revisions as $revision) { - $revision_edges = $edges[$revision->getPHID()][$edge_type]; - $reviewers = array(); - foreach ($revision_edges as $reviewer_phid => $edge) { - $reviewer = new DifferentialReviewerProxy( - $reviewer_phid, - $edge['data']); - + $reviewers = idx($reviewer_map, $revision->getPHID(), array()); + foreach ($reviewers as $reviewer_phid => $reviewer) { if ($this->needReviewerAuthority) { if (!$viewer_phid) { // Logged-out users never have authority. @@ -1031,7 +1033,7 @@ final class DifferentialRevisionQuery private function loadReviewerAuthority( array $revisions, - array $edges, + array $reviewers, $allow_self) { $revision_map = mpull($revisions, null, 'getPHID'); @@ -1045,9 +1047,9 @@ final class DifferentialRevisionQuery $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; $edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - foreach ($edges as $src => $types) { + foreach ($reviewers as $revision_phid => $reviewer_list) { if (!$allow_self) { - if ($revision_map[$src]->getAuthorPHID() == $viewer_phid) { + if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) { // If self-review isn't permitted, the user will never have // authority over projects on revisions they authored because you // can't accept your own revisions, so we don't need to load any @@ -1055,14 +1057,14 @@ final class DifferentialRevisionQuery continue; } } - $edge_data = idx($types, $edge_type, array()); - foreach ($edge_data as $dst => $data) { - $phid_type = phid_get_type($dst); + + foreach ($reviewer_list as $reviewer_phid => $reviewer) { + $phid_type = phid_get_type($reviewer_phid); if ($phid_type == $project_type) { - $project_phids[] = $dst; + $project_phids[] = $reviewer_phid; } if ($phid_type == $package_type) { - $package_phids[] = $dst; + $package_phids[] = $reviewer_phid; } } } diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 731fc9be14..5904ae2d7f 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -6,10 +6,11 @@ final class DifferentialReviewer protected $revisionPHID; protected $reviewerPHID; protected $reviewerStatus; - protected $lastActionDiffPHID; protected $lastCommentDiffPHID; + private $authority = array(); + protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( @@ -26,4 +27,25 @@ final class DifferentialReviewer ) + parent::getConfiguration(); } + public function getStatus() { + // TODO: This is an older method for compatibility with some callers + // which have not yet been cleaned up. + return $this->getReviewerStatus(); + } + + public function isUser() { + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; + return (phid_get_type($this->getReviewerPHID()) == $user_type); + } + + public function attachAuthority(PhabricatorUser $user, $has_authority) { + $this->authority[$user->getCacheFragment()] = $has_authority; + return $this; + } + + public function hasAuthority(PhabricatorUser $viewer) { + $cache_fragment = $viewer->getCacheFragment(); + return $this->assertAttachedKey($this->authority, $cache_fragment); + } + } diff --git a/src/applications/differential/storage/DifferentialReviewerProxy.php b/src/applications/differential/storage/DifferentialReviewerProxy.php deleted file mode 100644 index bf38ee27e3..0000000000 --- a/src/applications/differential/storage/DifferentialReviewerProxy.php +++ /dev/null @@ -1,56 +0,0 @@ -reviewerPHID = $reviewer_phid; - $this->status = idx($edge_data, 'status'); - $this->diffID = idx($edge_data, 'diff'); - } - - public function getReviewerPHID() { - return $this->reviewerPHID; - } - - public function getStatus() { - return $this->status; - } - - public function getDiffID() { - return $this->diffID; - } - - public function isUser() { - $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; - return (phid_get_type($this->getReviewerPHID()) == $user_type); - } - - public function attachAuthority(PhabricatorUser $user, $has_authority) { - $this->authority[$user->getPHID()] = $has_authority; - return $this; - } - - public function hasAuthority(PhabricatorUser $viewer) { - // It would be nice to use assertAttachedKey() here, but we don't extend - // PhabricatorLiskDAO, and faking that seems sketchy. - - $viewer_phid = $viewer->getPHID(); - if (!array_key_exists($viewer_phid, $this->authority)) { - throw new Exception(pht('You must %s first!', 'attachAuthority()')); - } - return $this->authority[$viewer_phid]; - } - - public function getEdgeData() { - return array( - 'status' => $this->status, - 'diffID' => $this->diffID, - ); - } - -} diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index df43043d6d..ff826a36b7 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -296,15 +296,6 @@ final class DifferentialRevision extends DifferentialDAO return idx($this->relationships, $relation, array()); } - public function getPrimaryReviewer() { - $reviewers = $this->getReviewers(); - $last = $this->lastReviewerPHID; - if (!$last || !in_array($last, $reviewers)) { - return head($this->getReviewers()); - } - return $last; - } - public function getHashes() { return $this->assertAttached($this->hashes); } @@ -406,8 +397,7 @@ final class DifferentialRevision extends DifferentialDAO } public function attachReviewerStatus(array $reviewers) { - assert_instances_of($reviewers, 'DifferentialReviewerProxy'); - + assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewerStatus = $reviewers; return $this; } diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 7342f35498..868a24ebb0 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -212,13 +212,6 @@ final class DifferentialTransaction $tags[] = self::MAILTAG_UPDATED; } break; - case PhabricatorTransactions::TYPE_EDGE: - switch ($this->getMetadataValue('edge:type')) { - case DifferentialRevisionHasReviewerEdgeType::EDGECONST: - $tags[] = self::MAILTAG_REVIEWERS; - break; - } - break; case PhabricatorTransactions::TYPE_COMMENT: case self::TYPE_INLINE: $tags[] = self::MAILTAG_COMMENT; @@ -598,14 +591,6 @@ final class DifferentialTransaction public function getNoEffectDescription() { switch ($this->getTransactionType()) { - case PhabricatorTransactions::TYPE_EDGE: - switch ($this->getMetadataValue('edge:type')) { - case DifferentialRevisionHasReviewerEdgeType::EDGECONST: - return pht( - 'The reviewers you are trying to add are already reviewing '. - 'this revision.'); - } - break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: @@ -624,18 +609,10 @@ final class DifferentialTransaction return pht('This revision already requires changes.'); case DifferentialAction::ACTION_REQUEST: return pht('Review is already requested for this revision.'); - case DifferentialAction::ACTION_RESIGN: - return pht( - 'You can not resign from this revision because you are not '. - 'a reviewer.'); case DifferentialAction::ACTION_CLAIM: return pht( 'You can not commandeer this revision because you already own '. 'it.'); - case DifferentialAction::ACTION_ACCEPT: - return pht('You have already accepted this revision.'); - case DifferentialAction::ACTION_REJECT: - return pht('You have already requested changes to this revision.'); } break; } diff --git a/src/applications/differential/view/DifferentialReviewersView.php b/src/applications/differential/view/DifferentialReviewersView.php index fd558d85f1..54b7e35257 100644 --- a/src/applications/differential/view/DifferentialReviewersView.php +++ b/src/applications/differential/view/DifferentialReviewersView.php @@ -7,7 +7,7 @@ final class DifferentialReviewersView extends AphrontView { private $diff; public function setReviewers(array $reviewers) { - assert_instances_of($reviewers, 'DifferentialReviewerProxy'); + assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewers = $reviewers; return $this; } @@ -31,47 +31,54 @@ final class DifferentialReviewersView extends AphrontView { $phid = $reviewer->getReviewerPHID(); $handle = $this->handles[$phid]; - // If we're missing either the diff or action information for the - // reviewer, render information as current. - $is_current = (!$this->diff) || - (!$reviewer->getDiffID()) || - ($this->diff->getID() == $reviewer->getDiffID()); + $action_phid = $reviewer->getLastActionDiffPHID(); + $is_current_action = $this->isCurrent($action_phid); + + $comment_phid = $reviewer->getLastCommentDiffPHID(); + $is_current_comment = $this->isCurrent($comment_phid); $item = new PHUIStatusItemView(); $item->setHighlighted($reviewer->hasAuthority($viewer)); - switch ($reviewer->getStatus()) { + switch ($reviewer->getReviewerStatus()) { case DifferentialReviewerStatus::STATUS_ADDED: - $item->setIcon( - PHUIStatusItemView::ICON_OPEN, - 'bluegrey', - pht('Review Requested')); + if ($comment_phid) { + if ($is_current_comment) { + $item->setIcon( + 'fa-comment', + 'blue', + pht('Commented')); + } else { + $item->setIcon( + 'fa-comment-o', + 'bluegrey', + pht('Commented Previously')); + } + } else { + $item->setIcon( + PHUIStatusItemView::ICON_OPEN, + 'bluegrey', + pht('Review Requested')); + } break; case DifferentialReviewerStatus::STATUS_ACCEPTED: - if ($is_current) { + if ($is_current_action) { $item->setIcon( PHUIStatusItemView::ICON_ACCEPT, 'green', pht('Accepted')); } else { $item->setIcon( - PHUIStatusItemView::ICON_ACCEPT, + 'fa-check-circle-o', 'bluegrey', pht('Accepted Prior Diff')); } break; - case DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER: - $item->setIcon( - 'fa-check-circle-o', - 'bluegrey', - pht('Accepted Prior Diff')); - break; - case DifferentialReviewerStatus::STATUS_REJECTED: - if ($is_current) { + if ($is_current_action) { $item->setIcon( PHUIStatusItemView::ICON_REJECT, 'red', @@ -84,27 +91,6 @@ final class DifferentialReviewersView extends AphrontView { } break; - case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: - $item->setIcon( - 'fa-times-circle-o', - 'bluegrey', - pht('Rejected Prior Diff')); - break; - - case DifferentialReviewerStatus::STATUS_COMMENTED: - if ($is_current) { - $item->setIcon( - 'fa-question-circle', - 'blue', - pht('Commented')); - } else { - $item->setIcon( - 'fa-question-circle-o', - 'bluegrey', - pht('Commented Previously')); - } - break; - case DifferentialReviewerStatus::STATUS_BLOCKING: $item->setIcon( PHUIStatusItemView::ICON_MINUS, @@ -116,7 +102,7 @@ final class DifferentialReviewersView extends AphrontView { $item->setIcon( PHUIStatusItemView::ICON_QUESTION, 'bluegrey', - pht('%s?', $reviewer->getStatus())); + pht('%s?', $reviewer->getReviewerStatus())); break; } @@ -128,4 +114,26 @@ final class DifferentialReviewersView extends AphrontView { return $view; } + private function isCurrent($action_phid) { + if (!$this->diff) { + echo "A\n"; + return true; + } + + if (!$action_phid) { + return true; + } + + $diff_phid = $this->diff->getPHID(); + if (!$diff_phid) { + return true; + } + + if ($diff_phid == $action_phid) { + return true; + } + + return false; + } + } From d179d0150c4ce5e980e6d19eca21f2aaaf38f700 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 14:02:08 -0700 Subject: [PATCH 011/239] Remove obsolete "relationships" code from Differential Summary: Ref T10967. There have been two different ways to load reviewers for a while: `needReviewerStatus()` and `needRelationships()`. The `needRelationships()` stuff was a false start along time ago that didn't really go anywhere. I believe the idea was that we might want to load several different types of edges (subscribers, reviewers, etc) on lots of different types of objects. However, all that stuff pretty much ended up modularizing so that main `Query` classes did not need to know about it, so `needRelationships()` never got generalized or went anywhere. A handful of things still use it, but get rid of them: they should either `needReviewerStatus()` to get reviewer info, or the ~3 callsites that care about subscribers can just load them directly. Test Plan: - Grepped for removed methods (`needRelationships()`, `getReviewers()`, `getCCPHIDs()`, etc). - Browsed Diffusion, Differential. - Called `differential.query`. It's possible I missed some stuff, but it should mostly show up as super obvious fatals ("call needReviewerStatus() before getReviewerStatus()!"). Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17518 --- ...ifferentialGetRevisionConduitAPIMethod.php | 3 +- .../DifferentialQueryConduitAPIMethod.php | 14 +++- .../DifferentialRevisionViewController.php | 10 +-- ...alDoorkeeperRevisionFeedStoryPublisher.php | 9 +-- .../HeraldDifferentialRevisionAdapter.php | 1 - .../query/DifferentialRevisionQuery.php | 53 --------------- .../DifferentialRevisionSearchEngine.php | 1 - .../storage/DifferentialRevision.php | 65 ++----------------- .../view/DifferentialRevisionListView.php | 8 +-- .../controller/DiffusionBrowseController.php | 2 +- ...sionCommitRevisionReviewersHeraldField.php | 2 +- ...mitContentRevisionReviewersHeraldField.php | 4 +- .../diffusion/herald/HeraldCommitAdapter.php | 1 - .../herald/HeraldPreCommitContentAdapter.php | 2 +- 14 files changed, 35 insertions(+), 140 deletions(-) diff --git a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php index a158d86d9f..3b4d9a6a63 100644 --- a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php @@ -42,7 +42,6 @@ final class DifferentialGetRevisionConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($request->getUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); @@ -50,7 +49,7 @@ final class DifferentialGetRevisionConduitAPIMethod throw new ConduitException('ERR_BAD_REVISION'); } - $reviewer_phids = array_values($revision->getReviewers()); + $reviewer_phids = $revision->getReviewerPHIDs(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) diff --git a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php index 720361367a..c9f90d0877 100644 --- a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php @@ -182,7 +182,7 @@ final class DifferentialQueryConduitAPIMethod $query->withBranches($branches); } - $query->needRelationships(true); + $query->needReviewerStatus(true); $query->needCommitPHIDs(true); $query->needDiffIDs(true); $query->needActiveDiffs(true); @@ -194,6 +194,14 @@ final class DifferentialQueryConduitAPIMethod $request->getUser(), $revisions); + if ($revisions) { + $ccs = id(new PhabricatorSubscribersQuery()) + ->withObjectPHIDs(mpull($revisions, 'getPHID')) + ->execute(); + } else { + $ccs = array(); + } + $results = array(); foreach ($revisions as $revision) { $diff = $revision->getActiveDiff(); @@ -224,8 +232,8 @@ final class DifferentialQueryConduitAPIMethod 'activeDiffPHID' => $diff->getPHID(), 'diffs' => $revision->getDiffIDs(), 'commits' => $revision->getCommitPHIDs(), - 'reviewers' => array_values($revision->getReviewers()), - 'ccs' => array_values($revision->getCCPHIDs()), + 'reviewers' => $revision->getReviewerPHIDs(), + 'ccs' => idx($ccs, $phid, array()), 'hashes' => $revision->getHashes(), 'auxiliary' => idx($field_data, $phid, array()), 'repositoryPHID' => $diff->getRepositoryPHID(), diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index d31da8fefc..9b87d1163d 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -17,7 +17,6 @@ final class DifferentialRevisionViewController extends DifferentialController { $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($viewer) - ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); @@ -103,9 +102,12 @@ final class DifferentialRevisionViewController extends DifferentialController { $this->loadDiffProperties($diffs); $props = $target_manual->getDiffProperties(); + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $revision->getPHID()); + $object_phids = array_merge( - $revision->getReviewers(), - $revision->getCCPHIDs(), + $revision->getReviewerPHIDs(), + $subscriber_phids, $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), @@ -782,7 +784,7 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setLimit(10) ->needFlags(true) ->needDrafts(true) - ->needRelationships(true); + ->needReviewerStatus(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 23ad165ca9..135e9b560d 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -26,7 +26,7 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher return id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) - ->needRelationships(true) + ->needReviewerStatus(true) ->executeOne(); } @@ -37,7 +37,7 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher public function getActiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { - return $object->getReviewers(); + return $object->getReviewerPHIDs(); } else { return array(); } @@ -48,12 +48,13 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return array(); } else { - return $object->getReviewers(); + return $object->getReviewerPHIDs(); } } public function getCCUserPHIDs($object) { - return $object->getCCPHIDs(); + return PhabricatorSubscribersQuery::loadSubscribersForPHID( + $object->getPHID()); } public function getObjectTitle($object) { diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index 53fd62fe23..19d15c5c3f 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -85,7 +85,6 @@ final class HeraldDifferentialRevisionAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision->getID())) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index c3fb98e607..1b60df777a 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -43,7 +43,6 @@ final class DifferentialRevisionQuery const ORDER_MODIFIED = 'order-modified'; const ORDER_CREATED = 'order-created'; - private $needRelationships = false; private $needActiveDiffs = false; private $needDiffIDs = false; private $needCommitPHIDs = false; @@ -227,20 +226,6 @@ final class DifferentialRevisionQuery } - - /** - * Set whether or not the query will load and attach relationships. - * - * @param bool True to load and attach relationships. - * @return this - * @task config - */ - public function needRelationships($need_relationships) { - $this->needRelationships = $need_relationships; - return $this; - } - - /** * Set whether or not the query should load the active diff for each * revision. @@ -425,10 +410,6 @@ final class DifferentialRevisionQuery $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); - if ($this->needRelationships) { - $this->loadRelationships($conn_r, $revisions); - } - if ($this->needCommitPHIDs) { $this->loadCommitPHIDs($conn_r, $revisions); } @@ -854,40 +835,6 @@ final class DifferentialRevisionQuery ); } - private function loadRelationships($conn_r, array $revisions) { - assert_instances_of($revisions, 'DifferentialRevision'); - - $type_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST; - $type_subscriber = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; - - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($revisions, 'getPHID')) - ->withEdgeTypes(array($type_reviewer, $type_subscriber)) - ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) - ->execute(); - - $type_map = array( - DifferentialRevision::RELATION_REVIEWER => $type_reviewer, - DifferentialRevision::RELATION_SUBSCRIBED => $type_subscriber, - ); - - foreach ($revisions as $revision) { - $data = array(); - foreach ($type_map as $rel_type => $edge_type) { - $revision_edges = $edges[$revision->getPHID()][$edge_type]; - foreach ($revision_edges as $dst_phid => $edge_data) { - $data[] = array( - 'relation' => $rel_type, - 'objectPHID' => $dst_phid, - 'reasonPHID' => null, - ); - } - } - - $revision->attachRelationships($data); - } - } - private function loadCommitPHIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $commit_phids = queryfx_all( diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 9212860292..2c40be4813 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -19,7 +19,6 @@ final class DifferentialRevisionSearchEngine return id(new DifferentialRevisionQuery()) ->needFlags(true) ->needDrafts(true) - ->needRelationships(true) ->needReviewerStatus(true); } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index ff826a36b7..062503451f 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -38,7 +38,6 @@ final class DifferentialRevision extends DifferentialDAO protected $editPolicy = PhabricatorPolicies::POLICY_USER; protected $properties = array(); - private $relationships = self::ATTACHABLE; private $commits = self::ATTACHABLE; private $activeDiff = self::ATTACHABLE; private $diffIDs = self::ATTACHABLE; @@ -69,7 +68,6 @@ final class DifferentialRevision extends DifferentialDAO return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) - ->attachRelationships(array()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewerStatus(array()) @@ -238,64 +236,6 @@ final class DifferentialRevision extends DifferentialDAO return parent::save(); } - public function loadRelationships() { - if (!$this->getID()) { - $this->relationships = array(); - return; - } - - $data = array(); - - $subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getPHID(), - PhabricatorObjectHasSubscriberEdgeType::EDGECONST); - $subscriber_phids = array_reverse($subscriber_phids); - foreach ($subscriber_phids as $phid) { - $data[] = array( - 'relation' => self::RELATION_SUBSCRIBED, - 'objectPHID' => $phid, - 'reasonPHID' => null, - ); - } - - $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getPHID(), - DifferentialRevisionHasReviewerEdgeType::EDGECONST); - $reviewer_phids = array_reverse($reviewer_phids); - foreach ($reviewer_phids as $phid) { - $data[] = array( - 'relation' => self::RELATION_REVIEWER, - 'objectPHID' => $phid, - 'reasonPHID' => null, - ); - } - - return $this->attachRelationships($data); - } - - public function attachRelationships(array $relationships) { - $this->relationships = igroup($relationships, 'relation'); - return $this; - } - - public function getReviewers() { - return $this->getRelatedPHIDs(self::RELATION_REVIEWER); - } - - public function getCCPHIDs() { - return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); - } - - private function getRelatedPHIDs($relation) { - $this->assertAttached($this->relationships); - - return ipull($this->getRawRelations($relation), 'objectPHID'); - } - - public function getRawRelations($relation) { - return idx($this->relationships, $relation, array()); - } - public function getHashes() { return $this->assertAttached($this->hashes); } @@ -402,6 +342,11 @@ final class DifferentialRevision extends DifferentialDAO return $this; } + public function getReviewerPHIDs() { + $reviewers = $this->getReviewerStatus(); + return mpull($reviewers, 'getReviewerPHID'); + } + public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewerStatus(); diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 811e74fd65..5aacafbb69 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -52,10 +52,7 @@ final class DifferentialRevisionListView extends AphrontView { $phids = array(); foreach ($this->revisions as $revision) { $phids[] = array($revision->getAuthorPHID()); - - // TODO: Switch to getReviewerStatus(), but not all callers pass us - // revisions with this data loaded. - $phids[] = $revision->getReviewers(); + $phids[] = $revision->getReviewerPHIDs(); } return array_mergev($phids); } @@ -132,8 +129,7 @@ final class DifferentialRevisionListView extends AphrontView { } $reviewers = array(); - // TODO: As above, this should be based on `getReviewerStatus()`. - foreach ($revision->getReviewers() as $reviewer) { + foreach ($revision->getReviewerPHIDs() as $reviewer) { $reviewers[] = $this->handles[$reviewer]->renderLink(); } if (!$reviewers) { diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index aa8e370e25..dc0131356d 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1765,7 +1765,7 @@ final class DiffusionBrowseController extends DiffusionController { ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) - ->needRelationships(true) + ->needReviewerStatus(true) ->needFlags(true) ->needDrafts(true) ->execute(); diff --git a/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php index 684f5d75d6..50900ede78 100644 --- a/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php @@ -20,7 +20,7 @@ final class DiffusionCommitRevisionReviewersHeraldField return array(); } - return $revision->getReviewers(); + return $revision->getReviewerPHIDs(); } protected function getHeraldFieldStandardType() { diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php index bce6b5b517..936126ba89 100644 --- a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php @@ -20,8 +20,8 @@ final class DiffusionPreCommitContentRevisionReviewersHeraldField return array(); } - return $revision->getReviewers(); - } + return $revision->getReviewerPHIDs(); + } protected function getHeraldFieldStandardType() { return self::STANDARD_PHID_LIST; diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index 9d63b9db3d..3465924f92 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -190,7 +190,6 @@ final class HeraldCommitAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if ($revision) { diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php index 13c2695fed..5fbc970dfa 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -190,7 +190,7 @@ final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter { $this->revision = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($revision_id)) - ->needRelationships(true) + ->needReviewerStatus(true) ->executeOne(); } } From a15df4f8d58021f03dba8d08c3d3c75e876a2a39 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 14:37:24 -0700 Subject: [PATCH 012/239] Rename "needReviewerStatus()" into "needReviewers()" Summary: Ref T10967. The old name was because we had a `getReviewers()` tied to `needRelationships()`, rename this method to use a simpler and more clear name. Test Plan: `grep`, browsed around. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17519 --- .../conduit/DifferentialCloseConduitAPIMethod.php | 2 +- .../DifferentialCreateCommentConduitAPIMethod.php | 2 +- .../DifferentialGetCommitMessageConduitAPIMethod.php | 2 +- .../DifferentialGetRevisionConduitAPIMethod.php | 2 +- .../conduit/DifferentialQueryConduitAPIMethod.php | 2 +- .../DifferentialUpdateRevisionConduitAPIMethod.php | 2 +- .../controller/DifferentialRevisionViewController.php | 4 ++-- ...ifferentialDoorkeeperRevisionFeedStoryPublisher.php | 2 +- .../editor/DifferentialRevisionEditEngine.php | 2 +- .../editor/DifferentialTransactionEditor.php | 6 +++--- .../DifferentialHovercardEngineExtension.php | 2 +- .../herald/HeraldDifferentialRevisionAdapter.php | 2 +- .../mail/DifferentialRevisionMailReceiver.php | 2 +- .../differential/query/DifferentialRevisionQuery.php | 10 +++++----- .../query/DifferentialRevisionSearchEngine.php | 2 +- .../search/DifferentialRevisionFulltextEngine.php | 2 +- .../differential/storage/DifferentialRevision.php | 2 +- .../diffusion/controller/DiffusionBrowseController.php | 2 +- .../diffusion/herald/HeraldCommitAdapter.php | 2 +- .../diffusion/herald/HeraldPreCommitContentAdapter.php | 2 +- .../worker/PhabricatorRepositoryCommitOwnersWorker.php | 2 +- .../PhabricatorRepositoryCommitMessageParserWorker.php | 2 +- 22 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php index d71876961e..30f8a5e5b7 100644 --- a/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php @@ -44,7 +44,7 @@ final class DifferentialCloseConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($id)) ->setViewer($viewer) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); if (!$revision) { throw new ConduitException('ERR_NOT_FOUND'); diff --git a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php index 23b8f770cd..ee70644537 100644 --- a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php @@ -47,7 +47,7 @@ final class DifferentialCreateCommentConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($request->getValue('revision_id'))) - ->needReviewerStatus(true) + ->needReviewers(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { diff --git a/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php index d45fc22fd3..51225023ff 100644 --- a/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php @@ -39,7 +39,7 @@ final class DifferentialGetCommitMessageConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($id)) ->setViewer($viewer) - ->needReviewerStatus(true) + ->needReviewers(true) ->needActiveDiffs(true) ->executeOne(); if (!$revision) { diff --git a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php index 3b4d9a6a63..b05efb5c15 100644 --- a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php @@ -42,7 +42,7 @@ final class DifferentialGetRevisionConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($request->getUser()) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); if (!$revision) { diff --git a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php index c9f90d0877..3b88087a67 100644 --- a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php @@ -182,7 +182,7 @@ final class DifferentialQueryConduitAPIMethod $query->withBranches($branches); } - $query->needReviewerStatus(true); + $query->needReviewers(true); $query->needCommitPHIDs(true); $query->needDiffIDs(true); $query->needActiveDiffs(true); diff --git a/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php index d45dc9749e..3ad5b07564 100644 --- a/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php @@ -57,7 +57,7 @@ final class DifferentialUpdateRevisionConduitAPIMethod $revision = id(new DifferentialRevisionQuery()) ->setViewer($request->getUser()) ->withIDs(array($request->getValue('id'))) - ->needReviewerStatus(true) + ->needReviewers(true) ->needActiveDiffs(true) ->requireCapabilities( array( diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 9b87d1163d..e7d0a1cc87 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -17,7 +17,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($viewer) - ->needReviewerStatus(true) + ->needReviewers(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { @@ -784,7 +784,7 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setLimit(10) ->needFlags(true) ->needDrafts(true) - ->needReviewerStatus(true); + ->needReviewers(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 135e9b560d..9583486c98 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -26,7 +26,7 @@ final class DifferentialDoorkeeperRevisionFeedStoryPublisher return id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); } diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index 5c2fef275d..9aad031a7c 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -41,7 +41,7 @@ final class DifferentialRevisionEditEngine protected function newObjectQuery() { return id(new DifferentialRevisionQuery()) ->needActiveDiffs(true) - ->needReviewerStatus(true) + ->needReviewers(true) ->needReviewerAuthority(true); } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index cfaff0cbef..340a4e6735 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -614,7 +614,7 @@ final class DifferentialTransactionEditor $new_revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) - ->needReviewerStatus(true) + ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); @@ -1575,7 +1575,7 @@ final class DifferentialTransactionEditor ->setViewer($this->getActor()) ->withPHIDs(array($object->getPHID())) ->needActiveDiffs(true) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); if (!$revision) { throw new Exception( @@ -1791,7 +1791,7 @@ final class DifferentialTransactionEditor // Reload to pick up the active diff and reviewer status. return id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) - ->needReviewerStatus(true) + ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); diff --git a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php index 6c178f44e0..ef7ad9cbc6 100644 --- a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php +++ b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php @@ -25,7 +25,7 @@ final class DifferentialHovercardEngineExtension $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPHIDs($phids) - ->needReviewerStatus(true) + ->needReviewers(true) ->execute(); $revisions = mpull($revisions, null, 'getPHID'); diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index 19d15c5c3f..509ad37564 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -85,7 +85,7 @@ final class HeraldDifferentialRevisionAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision->getID())) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); $object->revision = $revision; diff --git a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php index 6b27c5a371..929ee72647 100644 --- a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php +++ b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php @@ -18,7 +18,7 @@ final class DifferentialRevisionMailReceiver return id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($id)) - ->needReviewerStatus(true) + ->needReviewers(true) ->needReviewerAuthority(true) ->needActiveDiffs(true) ->executeOne(); diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index 1b60df777a..dd74862b58 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -47,7 +47,7 @@ final class DifferentialRevisionQuery private $needDiffIDs = false; private $needCommitPHIDs = false; private $needHashes = false; - private $needReviewerStatus = false; + private $needReviewers = false; private $needReviewerAuthority; private $needDrafts; private $needFlags; @@ -283,14 +283,14 @@ final class DifferentialRevisionQuery /** - * Set whether or not the query should load associated reviewer status. + * Set whether or not the query should load associated reviewers. * * @param bool True to load and attach reviewers. * @return this * @task config */ - public function needReviewerStatus($need_reviewer_status) { - $this->needReviewerStatus = $need_reviewer_status; + public function needReviewers($need_reviewers) { + $this->needReviewers = $need_reviewers; return $this; } @@ -429,7 +429,7 @@ final class DifferentialRevisionQuery $this->loadHashes($conn_r, $revisions); } - if ($this->needReviewerStatus || $this->needReviewerAuthority) { + if ($this->needReviewers || $this->needReviewerAuthority) { $this->loadReviewers($conn_r, $revisions); } diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 2c40be4813..a925878bda 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -19,7 +19,7 @@ final class DifferentialRevisionSearchEngine return id(new DifferentialRevisionQuery()) ->needFlags(true) ->needDrafts(true) - ->needReviewerStatus(true); + ->needReviewers(true); } protected function buildQueryFromParameters(array $map) { diff --git a/src/applications/differential/search/DifferentialRevisionFulltextEngine.php b/src/applications/differential/search/DifferentialRevisionFulltextEngine.php index 60d267fe71..f012c59fbe 100644 --- a/src/applications/differential/search/DifferentialRevisionFulltextEngine.php +++ b/src/applications/differential/search/DifferentialRevisionFulltextEngine.php @@ -10,7 +10,7 @@ final class DifferentialRevisionFulltextEngine $revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($object->getPHID())) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); // TODO: This isn't very clean, but custom fields currently rely on it. diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 062503451f..61210ac647 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -478,7 +478,7 @@ final class DifferentialRevision extends DifferentialDAO $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne() ->getReviewerStatus(); } else { diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index dc0131356d..b29dcf3d1e 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1765,7 +1765,7 @@ final class DiffusionBrowseController extends DiffusionController { ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) - ->needReviewerStatus(true) + ->needReviewers(true) ->needFlags(true) ->needDrafts(true) ->execute(); diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index 3465924f92..4687028418 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -190,7 +190,7 @@ final class HeraldCommitAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php index 5fbc970dfa..f4d7e794a2 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -190,7 +190,7 @@ final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter { $this->revision = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($revision_id)) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 5deb975fa9..1192ea379b 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -67,7 +67,7 @@ final class PhabricatorRepositoryCommitOwnersWorker $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision_id)) - ->needReviewerStatus(true) + ->needReviewers(true) ->executeOne(); } else { $revision = null; diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index f340fbf91f..fec7c1f8af 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -178,7 +178,7 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $revision_query = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($actor) - ->needReviewerStatus(true) + ->needReviewers(true) ->needActiveDiffs(true); $revision = $revision_query->executeOne(); From 0ceab7d36f914f1c7ae45b733a1ab1ae7b7afbad Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 Mar 2017 15:04:23 -0700 Subject: [PATCH 013/239] Rename "getReviewerStatus()" to "getReviewers()" Summary: Ref T10967. Improves some method names: - `Revision->getReviewerStatus()` -> `Revision->getReviewers()` - `Revision->attachReviewerStatus()` -> `Revision->attachReviewers()` - `Reviewer->getStatus()` -> `Reviewer->getReviewerStatus()` (this is mostly to make this more greppable) Test Plan: - bunch o' `grep` - Browsed around. - If I missed anything, it should fatal in an obvious way. We have a lot of other `getStatus()` calls and it's hard to be sure I got them all. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17522 --- ...erentialCreateRevisionConduitAPIMethod.php | 2 +- .../DifferentialProjectReviewersField.php | 2 +- .../DifferentialReviewersField.php | 4 +- .../editor/DifferentialTransactionEditor.php | 42 +++++++++++++------ .../DifferentialHovercardEngineExtension.php | 3 +- ...fferentialReviewedByCommitMessageField.php | 5 +-- ...ifferentialReviewersCommitMessageField.php | 4 +- .../DifferentialReviewersHeraldAction.php | 3 +- .../HeraldDifferentialRevisionAdapter.php | 3 +- ...rDifferentialRevisionTestDataGenerator.php | 2 +- .../query/DifferentialRevisionQuery.php | 3 +- .../DifferentialRevisionResultBucket.php | 4 +- .../DifferentialRevisionFulltextEngine.php | 7 ++-- .../storage/DifferentialReviewer.php | 6 --- .../storage/DifferentialRevision.php | 17 ++++---- .../DifferentialRevisionReviewTransaction.php | 27 ++++++++---- ...fferentialRevisionReviewersTransaction.php | 4 +- .../DifferentialRevisionTransactionType.php | 9 ++++ ...habricatorRepositoryCommitOwnersWorker.php | 4 +- 19 files changed, 89 insertions(+), 62 deletions(-) diff --git a/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php index 532d63680b..7039089b06 100644 --- a/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php @@ -53,7 +53,7 @@ final class DifferentialCreateRevisionConduitAPIMethod } $revision = DifferentialRevision::initializeNewRevision($viewer); - $revision->attachReviewerStatus(array()); + $revision->attachReviewers(array()); $result = $this->applyFieldEdit( $request, diff --git a/src/applications/differential/customfield/DifferentialProjectReviewersField.php b/src/applications/differential/customfield/DifferentialProjectReviewersField.php index 5fe5258cdb..4e0bb13986 100644 --- a/src/applications/differential/customfield/DifferentialProjectReviewersField.php +++ b/src/applications/differential/customfield/DifferentialProjectReviewersField.php @@ -52,7 +52,7 @@ final class DifferentialProjectReviewersField private function getProjectReviewers() { $reviewers = array(); - foreach ($this->getObject()->getReviewerStatus() as $reviewer) { + foreach ($this->getObject()->getReviewers() as $reviewer) { if (!$reviewer->isUser()) { $reviewers[] = $reviewer; } diff --git a/src/applications/differential/customfield/DifferentialReviewersField.php b/src/applications/differential/customfield/DifferentialReviewersField.php index 4d7ebfc691..7633bfb492 100644 --- a/src/applications/differential/customfield/DifferentialReviewersField.php +++ b/src/applications/differential/customfield/DifferentialReviewersField.php @@ -17,7 +17,7 @@ final class DifferentialReviewersField protected function readValueFromRevision( DifferentialRevision $revision) { - return $revision->getReviewerStatus(); + return $revision->getReviewers(); } public function shouldAppearInPropertyView() { @@ -53,7 +53,7 @@ final class DifferentialReviewersField private function getUserReviewers() { $reviewers = array(); - foreach ($this->getObject()->getReviewerStatus() as $reviewer) { + foreach ($this->getObject()->getReviewers() as $reviewer) { if ($reviewer->isUser()) { $reviewers[] = $reviewer; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 340a4e6735..3f00f4b837 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -326,9 +326,9 @@ final class DifferentialTransactionEditor // actually change the diff text. $edits = array(); - foreach ($object->getReviewerStatus() as $reviewer) { + foreach ($object->getReviewers() as $reviewer) { if ($downgrade_rejects) { - if ($reviewer->getStatus() == $new_reject) { + if ($reviewer->getReviewerStatus() == $new_reject) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => array( 'status' => $old_reject, @@ -338,7 +338,7 @@ final class DifferentialTransactionEditor } if ($downgrade_accepts) { - if ($reviewer->getStatus() == $new_accept) { + if ($reviewer->getReviewerStatus() == $new_accept) { $edits[$reviewer->getReviewerPHID()] = array( 'data' => array( 'status' => $old_accept, @@ -415,9 +415,9 @@ final class DifferentialTransactionEditor ); $edits = array(); - foreach ($object->getReviewerStatus() as $reviewer) { + foreach ($object->getReviewers() as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { - if ($reviewer->getStatus() == $status_added) { + if ($reviewer->getReviewerStatus() == $status_added) { $edits[$actor_phid] = array( 'data' => $data, ); @@ -623,7 +623,7 @@ final class DifferentialTransactionEditor pht('Failed to load revision from transaction finalization.')); } - $object->attachReviewerStatus($new_revision->getReviewerStatus()); + $object->attachReviewers($new_revision->getReviewers()); $object->attachActiveDiff($new_revision->getActiveDiff()); $object->attachRepository($new_revision->getRepository()); @@ -645,7 +645,11 @@ final class DifferentialTransactionEditor $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; + $is_sticky_accept = PhabricatorEnv::getEnvConfig( + 'differential.sticky-accept'); + $old_status = $object->getStatus(); + $active_diff = $object->getActiveDiff(); switch ($old_status) { case $status_accepted: case $status_revision: @@ -661,11 +665,17 @@ final class DifferentialTransactionEditor $has_rejecting_reviewer = false; $has_rejecting_older_reviewer = false; $has_blocking_reviewer = false; - foreach ($object->getReviewerStatus() as $reviewer) { - $reviewer_status = $reviewer->getStatus(); + foreach ($object->getReviewers() as $reviewer) { + $reviewer_status = $reviewer->getReviewerStatus(); switch ($reviewer_status) { case DifferentialReviewerStatus::STATUS_REJECTED: - $has_rejecting_reviewer = true; + $action_phid = $reviewer->getLastActionDiffPHID(); + $active_phid = $active_diff->getPHID(); + $is_current = ($action_phid == $active_phid); + + if ($is_current) { + $has_rejecting_reviewer = true; + } break; case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: $has_rejecting_older_reviewer = true; @@ -675,7 +685,13 @@ final class DifferentialTransactionEditor break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($reviewer->isUser()) { - $has_accepting_user = true; + $action_phid = $reviewer->getLastActionDiffPHID(); + $active_phid = $active_diff->getPHID(); + $is_current = ($action_phid == $active_phid); + + if ($is_sticky_accept || $is_current) { + $has_accepting_user = true; + } } break; } @@ -1032,7 +1048,7 @@ final class DifferentialTransactionEditor protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); $phids[] = $object->getAuthorPHID(); - foreach ($object->getReviewerStatus() as $reviewer) { + foreach ($object->getReviewers() as $reviewer) { $phids[] = $reviewer->getReviewerPHID(); } return $phids; @@ -1507,7 +1523,7 @@ final class DifferentialTransactionEditor // and both are needlessly complex. This logic should live in the normal // transaction application pipeline. See T10967. - $reviewers = $object->getReviewerStatus(); + $reviewers = $object->getReviewers(); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); if ($is_blocking) { @@ -1528,7 +1544,7 @@ final class DifferentialTransactionEditor // If we're applying a stronger status (usually, upgrading a reviewer // into a blocking reviewer), skip this check so we apply the change. $old_strength = DifferentialReviewerStatus::getStatusStrength( - $reviewers[$phid]->getStatus()); + $reviewers[$phid]->getReviewerStatus()); if ($old_strength <= $new_strength) { continue; } diff --git a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php index ef7ad9cbc6..3b92c2b4dd 100644 --- a/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php +++ b/src/applications/differential/engineextension/DifferentialHovercardEngineExtension.php @@ -54,8 +54,7 @@ final class DifferentialHovercardEngineExtension pht('Author'), $viewer->renderHandle($revision->getAuthorPHID())); - $reviewer_phids = $revision->getReviewerStatus(); - $reviewer_phids = mpull($reviewer_phids, 'getReviewerPHID'); + $reviewer_phids = $revision->getReviewerPHIDs(); $hovercard->addField( pht('Reviewers'), diff --git a/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php b/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php index 523697b1e7..5ce8c722e8 100644 --- a/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php +++ b/src/applications/differential/field/DifferentialReviewedByCommitMessageField.php @@ -37,10 +37,9 @@ final class DifferentialReviewedByCommitMessageField } $phids = array(); - foreach ($revision->getReviewerStatus() as $reviewer) { - switch ($reviewer->getStatus()) { + foreach ($revision->getReviewers() as $reviewer) { + switch ($reviewer->getReviewerStatus()) { case DifferentialReviewerStatus::STATUS_ACCEPTED: - case DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER: $phids[] = $reviewer->getReviewerPHID(); break; } diff --git a/src/applications/differential/field/DifferentialReviewersCommitMessageField.php b/src/applications/differential/field/DifferentialReviewersCommitMessageField.php index 0f110b6e3a..100897c28b 100644 --- a/src/applications/differential/field/DifferentialReviewersCommitMessageField.php +++ b/src/applications/differential/field/DifferentialReviewersCommitMessageField.php @@ -45,8 +45,8 @@ final class DifferentialReviewersCommitMessageField $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $results = array(); - foreach ($revision->getReviewerStatus() as $reviewer) { - if ($reviewer->getStatus() == $status_blocking) { + foreach ($revision->getReviewers() as $reviewer) { + if ($reviewer->getReviewerStatus() == $status_blocking) { $suffixes = array('!' => '!'); } else { $suffixes = array(); diff --git a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php index 6574455465..bf2b5919c8 100644 --- a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php +++ b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php @@ -37,8 +37,7 @@ abstract class DifferentialReviewersHeraldAction } } - $reviewers = $object->getReviewerStatus(); - $reviewers = mpull($reviewers, null, 'getReviewerPHID'); + $reviewers = $object->getReviewers(); if ($is_blocking) { $new_status = DifferentialReviewerStatus::STATUS_BLOCKING; diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php index 509ad37564..e20a4fd37d 100644 --- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php @@ -137,8 +137,7 @@ final class HeraldDifferentialRevisionAdapter } public function loadReviewers() { - $reviewers = $this->getObject()->getReviewerStatus(); - return mpull($reviewers, 'getReviewerPHID'); + return $this->getObject()->getReviewerPHIDs(); } diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php index 2fe7c141f9..c37974fa2e 100644 --- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php +++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php @@ -13,7 +13,7 @@ final class PhabricatorDifferentialRevisionTestDataGenerator $author = $this->loadPhabricatorUser(); $revision = DifferentialRevision::initializeNewRevision($author); - $revision->attachReviewerStatus(array()); + $revision->attachReviewers(array()); $revision->attachActiveDiff(null); // This could be a bit richer and more formal than it is. diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index dd74862b58..701463e6a7 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -974,7 +974,7 @@ final class DifferentialRevisionQuery $reviewers[$reviewer_phid] = $reviewer; } - $revision->attachReviewerStatus($reviewers); + $revision->attachReviewers($reviewers); } } @@ -993,7 +993,6 @@ final class DifferentialRevisionQuery $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; - $edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST; foreach ($reviewers as $revision_phid => $reviewer_list) { if (!$allow_self) { if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) { diff --git a/src/applications/differential/query/DifferentialRevisionResultBucket.php b/src/applications/differential/query/DifferentialRevisionResultBucket.php index 3cf7d1b7ff..762e2d97f4 100644 --- a/src/applications/differential/query/DifferentialRevisionResultBucket.php +++ b/src/applications/differential/query/DifferentialRevisionResultBucket.php @@ -56,13 +56,13 @@ abstract class DifferentialRevisionResultBucket array $phids, array $statuses) { - foreach ($revision->getReviewerStatus() as $reviewer) { + foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); if (empty($phids[$reviewer_phid])) { continue; } - $status = $reviewer->getStatus(); + $status = $reviewer->getReviewerStatus(); if (empty($statuses[$status])) { continue; } diff --git a/src/applications/differential/search/DifferentialRevisionFulltextEngine.php b/src/applications/differential/search/DifferentialRevisionFulltextEngine.php index f012c59fbe..45639edbbe 100644 --- a/src/applications/differential/search/DifferentialRevisionFulltextEngine.php +++ b/src/applications/differential/search/DifferentialRevisionFulltextEngine.php @@ -14,7 +14,7 @@ final class DifferentialRevisionFulltextEngine ->executeOne(); // TODO: This isn't very clean, but custom fields currently rely on it. - $object->attachReviewerStatus($revision->getReviewerStatus()); + $object->attachReviewers($revision->getReviewers()); $document->setDocumentTitle($revision->getTitle()); @@ -36,8 +36,9 @@ final class DifferentialRevisionFulltextEngine // owner is the author (e.g., accepted, rejected, closed). $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; if ($revision->getStatus() == $status_review) { - $reviewers = $revision->getReviewerStatus(); - $reviewers = mpull($reviewers, 'getReviewerPHID', 'getReviewerPHID'); + $reviewers = $revision->getReviewerPHIDs(); + $reviewers = array_fuse($reviewers); + if ($reviewers) { foreach ($reviewers as $phid) { $document->addRelationship( diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 5904ae2d7f..08c9707225 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -27,12 +27,6 @@ final class DifferentialReviewer ) + parent::getConfiguration(); } - public function getStatus() { - // TODO: This is an older method for compatibility with some callers - // which have not yet been cleaned up. - return $this->getReviewerStatus(); - } - public function isUser() { $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; return (phid_get_type($this->getReviewerPHID()) == $user_type); diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 61210ac647..7189c5f5b4 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -70,7 +70,7 @@ final class DifferentialRevision extends DifferentialDAO ->setAuthorPHID($actor->getPHID()) ->attachRepository(null) ->attachActiveDiff(null) - ->attachReviewerStatus(array()) + ->attachReviewers(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } @@ -332,30 +332,31 @@ final class DifferentialRevision extends DifferentialDAO ); } - public function getReviewerStatus() { + public function getReviewers() { return $this->assertAttached($this->reviewerStatus); } - public function attachReviewerStatus(array $reviewers) { + public function attachReviewers(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); + $reviewers = mpull($reviewers, null, 'getReviewerPHID'); $this->reviewerStatus = $reviewers; return $this; } public function getReviewerPHIDs() { - $reviewers = $this->getReviewerStatus(); + $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); } public function getReviewerPHIDsForEdit() { - $reviewers = $this->getReviewerStatus(); + $reviewers = $this->getReviewers(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); - if ($reviewer->getStatus() == $status_blocking) { + if ($reviewer->getReviewerStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; @@ -480,9 +481,9 @@ final class DifferentialRevision extends DifferentialDAO ->withPHIDs(array($this->getPHID())) ->needReviewers(true) ->executeOne() - ->getReviewerStatus(); + ->getReviewers(); } else { - $reviewers = $this->getReviewerStatus(); + $reviewers = $this->getReviewers(); } foreach ($reviewers as $reviewer) { diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index de38f67bc0..c393ee51c5 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -21,7 +21,8 @@ abstract class DifferentialRevisionReviewTransaction $viewer, array( DifferentialReviewerStatus::STATUS_ACCEPTED, - )); + ), + true); } protected function isViewerFullyRejected( @@ -32,7 +33,8 @@ abstract class DifferentialRevisionReviewTransaction $viewer, array( DifferentialReviewerStatus::STATUS_REJECTED, - )); + ), + true); } protected function getViewerReviewerStatus( @@ -43,12 +45,12 @@ abstract class DifferentialRevisionReviewTransaction return null; } - foreach ($revision->getReviewerStatus() as $reviewer) { + foreach ($revision->getReviewers() as $reviewer) { if ($reviewer->getReviewerPHID() != $viewer->getPHID()) { continue; } - return $reviewer->getStatus(); + return $reviewer->getReviewerStatus(); } return null; @@ -57,7 +59,8 @@ abstract class DifferentialRevisionReviewTransaction protected function isViewerReviewerStatusFullyAmong( DifferentialRevision $revision, PhabricatorUser $viewer, - array $status_list) { + array $status_list, + $require_current) { // 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 @@ -67,18 +70,26 @@ abstract class DifferentialRevisionReviewTransaction return false; } + $active_phid = $this->getActiveDiffPHID($revision); + // Otherwise, check that all reviews they have authority over are in // the desired set of states. $status_map = array_fuse($status_list); - foreach ($revision->getReviewerStatus() as $reviewer) { + foreach ($revision->getReviewers() as $reviewer) { if (!$reviewer->hasAuthority($viewer)) { continue; } - $status = $reviewer->getStatus(); + $status = $reviewer->getReviewerStatus(); if (!isset($status_map[$status])) { return false; } + + if ($require_current) { + if ($reviewer->getLastActionDiffPHID() != $active_phid) { + return false; + } + } } return true; @@ -97,7 +108,7 @@ abstract class DifferentialRevisionReviewTransaction // yourself. $with_authority = ($status != DifferentialReviewerStatus::STATUS_RESIGNED); if ($with_authority) { - foreach ($revision->getReviewerStatus() as $reviewer) { + foreach ($revision->getReviewers() as $reviewer) { if ($reviewer->hasAuthority($viewer)) { $map[$reviewer->getReviewerPHID()] = $status; } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewersTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewersTransaction.php index c4112a4516..a7122f9de4 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewersTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewersTransaction.php @@ -7,8 +7,8 @@ final class DifferentialRevisionReviewersTransaction const EDITKEY = 'reviewers'; public function generateOldValue($object) { - $reviewers = $object->getReviewerStatus(); - $reviewers = mpull($reviewers, 'getStatus', 'getReviewerPHID'); + $reviewers = $object->getReviewers(); + $reviewers = mpull($reviewers, 'getReviewerStatus', 'getReviewerPHID'); return $reviewers; } diff --git a/src/applications/differential/xaction/DifferentialRevisionTransactionType.php b/src/applications/differential/xaction/DifferentialRevisionTransactionType.php index 59b0c7510d..6c36d0aaa8 100644 --- a/src/applications/differential/xaction/DifferentialRevisionTransactionType.php +++ b/src/applications/differential/xaction/DifferentialRevisionTransactionType.php @@ -57,4 +57,13 @@ abstract class DifferentialRevisionTransactionType $xaction); } + protected function getActiveDiffPHID(DifferentialRevision $revision) { + try { + $diff = $revision->getActiveDiff(); + return $diff->getPHID(); + } catch (Exception $ex) { + return null; + } + } + } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 1192ea379b..75ae0c9c14 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -165,7 +165,7 @@ final class PhabricatorRepositoryCommitOwnersWorker $accepted_statuses = array_fuse($accepted_statuses); $found_accept = false; - foreach ($revision->getReviewerStatus() as $reviewer) { + foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); // If this reviewer isn't a package owner, just ignore them. @@ -175,7 +175,7 @@ final class PhabricatorRepositoryCommitOwnersWorker // If this reviewer accepted the revision and owns the package, we're // all clear and do not need to trigger an audit. - if (isset($accepted_statuses[$reviewer->getStatus()])) { + if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) { $found_accept = true; break; } From 1a5d92184cb6fd8d9f04b2e6ae3fe906ec24d67b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 20 Mar 2017 17:42:33 -0700 Subject: [PATCH 014/239] Try to guess a name for the 'Add to Dashboard' workflow Summary: Ref T5307. Just makes the dialog a little easier to use. Picks a name if we already have one. Test Plan: Test a builtin, custom saved, and a new advanced search (no name). Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T5307 Differential Revision: https://secure.phabricator.com/D17523 --- .../PhabricatorDashboardQueryPanelInstallController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php index ecc635966e..eeeaaf133b 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php @@ -45,6 +45,11 @@ final class PhabricatorDashboardQueryPanelInstallController return new Aphront404Response(); } + $named_query = idx($engine->loadEnabledNamedQueries(), $v_query); + if ($named_query) { + $v_name = $named_query->getQueryName(); + } + $errors = array(); if ($request->isFormPost()) { From 1182bbcae7807537247fdc19b6dc1f1c8b5cb80a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 20 Mar 2017 20:02:47 -0700 Subject: [PATCH 015/239] Remove FreeNode from "support" options Summary: Don't think it's fair to send users there anymore, we can use Conpherence better (and searchable). Test Plan: Remove copy. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17525 --- src/docs/user/support.diviner | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/docs/user/support.diviner b/src/docs/user/support.diviner index 3402c099e7..18de79a9a1 100644 --- a/src/docs/user/support.diviner +++ b/src/docs/user/support.diviner @@ -121,5 +121,3 @@ Conpherence room on this install, and you can ask questions in are not upstream support channels and you may not receive a response to questions, but someone in the community may be able to point you in the right direction. - -There is also a community IRC channel in `#phabricator` on FreeNode. From 7d4c0f002f115d01d8b77e0098b44479e87bb818 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 21 Mar 2017 09:25:05 -0700 Subject: [PATCH 016/239] Allow searching Dashboards by Editable Summary: Ref T10390. I find myself wanting to find dashboards I can edit, even if I am not the author. I think this is useful for larger installs with multiple admins. Also make disabled Dashboards more grey in UI results. Test Plan: Log in a test user, create a dashboard with I cannot edit. Log into my account, search for editable dashboards and only see mine. Set dashboard to all users, search under test account and see editable dashboards. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10390 Differential Revision: https://secure.phabricator.com/D17524 --- .../dashboard/query/PhabricatorDashboardQuery.php | 15 +++++++++++++++ .../query/PhabricatorDashboardSearchEngine.php | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/applications/dashboard/query/PhabricatorDashboardQuery.php b/src/applications/dashboard/query/PhabricatorDashboardQuery.php index c005197df2..9f5e256391 100644 --- a/src/applications/dashboard/query/PhabricatorDashboardQuery.php +++ b/src/applications/dashboard/query/PhabricatorDashboardQuery.php @@ -7,6 +7,7 @@ final class PhabricatorDashboardQuery private $phids; private $statuses; private $authorPHIDs; + private $canEdit; private $needPanels; private $needProjects; @@ -41,6 +42,11 @@ final class PhabricatorDashboardQuery return $this; } + public function withCanEdit($can_edit) { + $this->canEdit = $can_edit; + return $this; + } + public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( id(new PhabricatorDashboardNgrams()), @@ -59,6 +65,15 @@ final class PhabricatorDashboardQuery $phids = mpull($dashboards, 'getPHID'); + if ($this->canEdit) { + $dashboards = id(new PhabricatorPolicyFilter()) + ->setViewer($this->getViewer()) + ->requireCapabilities(array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->apply($dashboards); + } + if ($this->needPanels) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) diff --git a/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php index a854b066f8..a05d1c4121 100644 --- a/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php +++ b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php @@ -34,6 +34,10 @@ final class PhabricatorDashboardSearchEngine ->setKey('statuses') ->setLabel(pht('Status')) ->setOptions(PhabricatorDashboard::getStatusNameMap()), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('editable') + ->setLabel(pht('Editable')) + ->setOptions(array('editable' => null)), ); } @@ -94,6 +98,10 @@ final class PhabricatorDashboardSearchEngine $query->withNameNgrams($map['name']); } + if ($map['editable'] !== null) { + $query->withCanEdit($map['editable']); + } + return $query; } @@ -126,8 +134,10 @@ final class PhabricatorDashboardSearchEngine ->setHref($this->getApplicationURI("view/{$id}/")) ->setObject($dashboard); + $bg_color = 'bg-dark'; if ($dashboard->isArchived()) { $item->setDisabled(true); + $bg_color = 'bg-grey'; } $panels = $dashboard->getPanels(); @@ -142,7 +152,7 @@ final class PhabricatorDashboardSearchEngine $icon = id(new PHUIIconView()) ->setIcon($dashboard->getIcon()) - ->setBackground('bg-dark'); + ->setBackground($bg_color); $item->setImageIcon($icon); $item->setEpoch($dashboard->getDateModified()); From d6f7da868506e183a4c524fb5c6e83d03fd9975b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 21 Mar 2017 09:57:22 -0700 Subject: [PATCH 017/239] Add some new Dashboard icons Summary: Ref T10390. Fixes the missing "fa-dashboard" icon and adds a few more for an even 25. Test Plan: Create new dashboard, see dashboard icon, select new dashboard icon. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10390 Differential Revision: https://secure.phabricator.com/D17526 --- .../icon/PhabricatorDashboardIconSet.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/applications/dashboard/icon/PhabricatorDashboardIconSet.php b/src/applications/dashboard/icon/PhabricatorDashboardIconSet.php index 1bd12609b7..a93c361bb5 100644 --- a/src/applications/dashboard/icon/PhabricatorDashboardIconSet.php +++ b/src/applications/dashboard/icon/PhabricatorDashboardIconSet.php @@ -12,6 +12,7 @@ final class PhabricatorDashboardIconSet protected function newIcons() { $map = array( 'fa-home' => pht('Home'), + 'fa-dashboard' => pht('Dashboard'), 'fa-th-large' => pht('Blocks'), 'fa-columns' => pht('Columns'), 'fa-bookmark' => pht('Page Saver'), @@ -20,16 +21,26 @@ final class PhabricatorDashboardIconSet 'fa-bomb' => pht('Kaboom'), 'fa-pie-chart' => pht('Apple Blueberry'), 'fa-bar-chart' => pht('Serious Business'), + 'fa-briefcase' => pht('Project'), 'fa-bell' => pht('Ding Ding'), 'fa-credit-card' => pht('Plastic Debt'), 'fa-code' => pht('PHP is Life'), 'fa-sticky-note' => pht('To Self'), - 'fa-newspaper-o' => pht('Stay Woke'), + 'fa-server' => pht('Metallica'), 'fa-hashtag' => pht('Corned Beef'), - 'fa-group' => pht('Triplets'), + 'fa-anchor' => pht('Tasks'), + 'fa-calendar' => pht('Calendar'), + 'fa-compass' => pht('Wayfinding'), + + 'fa-futbol-o' => pht('Sports'), + 'fa-flag' => pht('Flag'), + 'fa-ship' => pht('Water Vessel'), + 'fa-feed' => pht('Wireless'), + 'fa-bullhorn' => pht('Announcement'), + ); $icons = array(); From 3a838ba312697626a82ed79b416fc062dbd75fa5 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 21 Mar 2017 11:06:44 -0700 Subject: [PATCH 018/239] Add Dashboards as a default pinned application Summary: Ref T10390. Dashboard usability is high enough that I think we should pin it by default for users to create custom home pages. Test Plan: Review order of applications in sandbox. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10390 Differential Revision: https://secure.phabricator.com/D17527 --- .../application/PhabricatorDashboardApplication.php | 8 ++++++++ .../project/application/PhabricatorProjectApplication.php | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index 29024cbcb8..d8e2701727 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -18,6 +18,14 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { return 'fa-dashboard'; } + public function isPinnedByDefault(PhabricatorUser $viewer) { + return true; + } + + public function getApplicationOrder() { + return 0.160; + } + public function getRoutes() { return array( '/W(?P\d+)' => 'PhabricatorDashboardPanelViewController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 95f65253a8..0e1a9f37c7 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -138,6 +138,10 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { ); } + public function getApplicationOrder() { + return 0.150; + } + public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array( array( From 5e423c5fe021cd2e97adcf71c5faaba685471b06 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 21 Mar 2017 11:24:54 -0700 Subject: [PATCH 019/239] Provide a 'no dashboards' fallback state if you can't add any Summary: Ref T10390. Catch if the user doesn't have any dashboards they can edit and give them a helpful message instead. Test Plan: Clean install, no dashboards, Click "Add to Dashboard" on ApplicationSearch results, see no dashboards message Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10390 Differential Revision: https://secure.phabricator.com/D17528 --- ...ricatorDashboardQueryPanelInstallController.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php index eeeaaf133b..77068531a9 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php @@ -149,6 +149,19 @@ final class PhabricatorDashboardQueryPanelInstallController $redirect_uri = $engine->getQueryResultsPageURI($v_query); + if (!$options) { + $notice = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->appendChild(pht('You do not have access to any dashboards. To '. + 'continue, please create a dashboard first.')); + + return $this->newDialog() + ->setTitle(pht('No Dashboards')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($notice) + ->addCancelButton($redirect_uri); + } + $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('engine', $v_engine) @@ -175,6 +188,7 @@ final class PhabricatorDashboardQueryPanelInstallController ->appendChild($form->buildLayoutView()) ->addCancelButton($redirect_uri) ->addSubmitButton(pht('Add Panel')); + } } From 3d35d6d3f901dec6a34d1cc35179ee3d34225e6d Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 05:01:58 -0700 Subject: [PATCH 020/239] Remove duplicate "Change Default Values" action in form editing workflow Summary: Fixes T12434. I accidentally copy/pasted this too much in D17442. Test Plan: Viewed a form edit page, no longer saw two copies of this action. Reviewers: chad, cspeckmim Reviewed By: chad, cspeckmim Maniphest Tasks: T12434 Differential Revision: https://secure.phabricator.com/D17530 --- .../PhabricatorEditEngineConfigurationViewController.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php index c07b2d5f3d..80939dd1c8 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php @@ -163,15 +163,6 @@ final class PhabricatorEditEngineConfigurationViewController ->setDisabled(!$can_edit)); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Change Default Values')) - ->setIcon('fa-paint-brush') - ->setHref($defaults_uri) - ->setWorkflow(!$can_edit) - ->setDisabled(!$can_edit)); - - $disable_uri = "{$base_uri}/disable/{$form_key}/"; if ($config->getIsDisabled()) { From 8913552970748beb43d46b663c971b06159d2358 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 06:38:35 -0700 Subject: [PATCH 021/239] Store "resigned" as an explicit reviewer state Summary: Fixes T11050. Today, when a user resigns, we just delete the record of them ever being a reviewer. However, this means you have no way to say "I don't care about this and don't want to see it on my dashboard" if you are a member of any project or package reviewers. Instead, store "resigned" as a distinct state from "not a reviewer", and treat it a little differently in the UI: - On the bucketing screen, discard revisions any responsible user has resigned from. - On the main `/Dxxx` page, show these users as resigned explicitly (we could just hide them, too, but I think this is good to start with). - In the query, don't treat a "resigned" state as a real "reviewer" (this change happened earlier, in D17517). - When resigning, write a "resigned" state instead of deleting the row. - When editing a list of reviewers, I'm still treating this reviewer as a reviewer and not special casing it. I think that's sufficiently clear but we could tailor this behavior later. Test Plan: - Resigned from a revision. - Saw "Resigned" in reviewers list. - Saw revision disappear from my dashboard. - Edited revision, saw user still appear as an editable reviewer. Saved revision, saw no weird side effects. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11050 Differential Revision: https://secure.phabricator.com/D17531 --- ...tialRevisionRequiredActionResultBucket.php | 29 +++++++++++++++++++ .../storage/DifferentialReviewer.php | 5 ++++ .../view/DifferentialReviewersView.php | 23 ++++++++++++++- .../DifferentialRevisionReviewTransaction.php | 14 +++------ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php b/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php index d37754227e..6d83d97a47 100644 --- a/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php +++ b/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php @@ -29,6 +29,14 @@ final class DifferentialRevisionRequiredActionResultBucket } $phids = array_fuse($phids); + // Before continuing, throw away any revisions which responsible users + // have explicitly resigned from. + + // The goal is to allow users to resign from revisions they don't want to + // review to get these revisions off their dashboard, even if there are + // other project or package reviewers which they have authority over. + $this->filterResigned($phids); + $groups = array(); $groups[] = $this->newGroup() @@ -229,4 +237,25 @@ final class DifferentialRevisionRequiredActionResultBucket return $results; } + private function filterResigned(array $phids) { + $resigned = array( + DifferentialReviewerStatus::STATUS_RESIGNED, + ); + $resigned = array_fuse($resigned); + + $objects = $this->getRevisionsNotAuthored($this->objects, $phids); + + $results = array(); + foreach ($objects as $key => $object) { + if (!$this->hasReviewersWithStatus($object, $phids, $resigned)) { + continue; + } + + $results[$key] = $object; + unset($this->objects[$key]); + } + + return $results; + } + } diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 08c9707225..836022c882 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -42,4 +42,9 @@ final class DifferentialReviewer return $this->assertAttachedKey($this->authority, $cache_fragment); } + public function isResigned() { + $status_resigned = DifferentialReviewerStatus::STATUS_RESIGNED; + return ($this->getReviewerStatus() == $status_resigned); + } + } diff --git a/src/applications/differential/view/DifferentialReviewersView.php b/src/applications/differential/view/DifferentialReviewersView.php index 54b7e35257..291f859d08 100644 --- a/src/applications/differential/view/DifferentialReviewersView.php +++ b/src/applications/differential/view/DifferentialReviewersView.php @@ -25,9 +25,23 @@ final class DifferentialReviewersView extends AphrontView { public function render() { $viewer = $this->getUser(); + $reviewers = $this->reviewers; $view = new PHUIStatusListView(); - foreach ($this->reviewers as $reviewer) { + + // Move resigned reviewers to the bottom. + $head = array(); + $tail = array(); + foreach ($reviewers as $key => $reviewer) { + if ($reviewer->isResigned()) { + $tail[$key] = $reviewer; + } else { + $head[$key] = $reviewer; + } + } + + $reviewers = $head + $tail; + foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); $handle = $this->handles[$phid]; @@ -98,6 +112,13 @@ final class DifferentialReviewersView extends AphrontView { pht('Blocking Review')); break; + case DifferentialReviewerStatus::STATUS_RESIGNED: + $item->setIcon( + 'fa-times', + 'grey', + pht('Resigned')); + break; + default: $item->setIcon( PHUIStatusItemView::ICON_QUESTION, diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index c393ee51c5..42f644e8d1 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -178,16 +178,10 @@ abstract class DifferentialRevisionReviewTransaction $reviewer->setLastActionDiffPHID($diff_phid); } - if ($status == DifferentialReviewerStatus::STATUS_RESIGNED) { - if ($reviewer->getID()) { - $reviewer->delete(); - } - } else { - try { - $reviewer->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - // At least for now, just ignore it if we lost a race. - } + try { + $reviewer->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + // At least for now, just ignore it if we lost a race. } } } From 3e7b63aa736a2a75940ee28fe3579eacc8e55c64 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 06:50:51 -0700 Subject: [PATCH 022/239] Add a key to the reviewers table Summary: Ref T10967. I'm not 100% sure we need this, but the old edge table had it and I recall an issue long ago where not having this key left us with a bad query plan. Our data doesn't really provide a way to test this key (we have many revisions and few reviewers, so the query planner always uses revision keys), and building a convincing test case would take a while (lipsum needs some improvements to add reviewers). But in the worst case this key is mostly useless and wastes a few MB of disk space, which isn't a big deal. So I can't conclusively prove that this key does anything to the dashboard query, but the migration removed it and I'm more comfortable keeping it so I'm not worried about breaking stuff. At the very least, MySQL does select this key in the query plan when I do a "Reviewers:" query explicitly so it isn't //useless//. Test Plan: Ran `bin/storage upgrade`, ran dashboard query, the query plan didn't get any worse. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17532 --- src/applications/differential/storage/DifferentialReviewer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 836022c882..a0309beddd 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -23,6 +23,9 @@ final class DifferentialReviewer 'columns' => array('revisionPHID', 'reviewerPHID'), 'unique' => true, ), + 'key_reviewer' => array( + 'columns' => array('reviewerPHID', 'revisionPHID'), + ), ), ) + parent::getConfiguration(); } From 5e16e460396e7aa3031ae21ff220412f96c59f2c Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 22 Mar 2017 09:48:10 -0700 Subject: [PATCH 023/239] Remove "Aleo" as specialized font for headers Summary: Fixes T11865. Part of a 'clean up remarkup' pass, removing Aleo helps simplify coding, is lighter on the wire, and gives a more consistent, clean look. Test Plan: run celerity, grep for 'aleo' and 'Aleo', test Phriction, tasks Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11865 Differential Revision: https://secure.phabricator.com/D17535 --- resources/celerity/map.php | 44 +- resources/celerity/packages.php | 1 - src/view/page/PhabricatorStandardPageView.php | 1 - .../application/conpherence/header-pane.css | 1 - webroot/rsrc/css/application/phame/phame.css | 3 - .../application/project/project-card-view.css | 2 - webroot/rsrc/css/font/font-aleo.css | 40 - webroot/rsrc/css/phui/phui-fontkit.css | 20 - webroot/rsrc/css/phui/phui-header-view.css | 1 - .../rsrc/css/phui/phui-two-column-view.css | 1 - webroot/rsrc/externals/font/aleo/LICENSE.txt | 203 - .../rsrc/externals/font/aleo/aleo-bold.eot | Bin 40674 -> 0 bytes .../rsrc/externals/font/aleo/aleo-bold.svg | 5078 ----------------- .../rsrc/externals/font/aleo/aleo-bold.ttf | Bin 95472 -> 0 bytes .../rsrc/externals/font/aleo/aleo-bold.woff | Bin 45488 -> 0 bytes .../rsrc/externals/font/aleo/aleo-bold.woff2 | Bin 36404 -> 0 bytes .../rsrc/externals/font/aleo/aleo-regular.eot | Bin 40168 -> 0 bytes .../rsrc/externals/font/aleo/aleo-regular.svg | 4644 --------------- .../rsrc/externals/font/aleo/aleo-regular.ttf | Bin 85808 -> 0 bytes .../externals/font/aleo/aleo-regular.woff | Bin 44704 -> 0 bytes .../externals/font/aleo/aleo-regular.woff2 | Bin 35916 -> 0 bytes 21 files changed, 14 insertions(+), 10025 deletions(-) delete mode 100644 webroot/rsrc/css/font/font-aleo.css delete mode 100644 webroot/rsrc/externals/font/aleo/LICENSE.txt delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-bold.eot delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-bold.svg delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-bold.ttf delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-bold.woff delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-bold.woff2 delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-regular.eot delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-regular.svg delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-regular.ttf delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-regular.woff delete mode 100644 webroot/rsrc/externals/font/aleo/aleo-regular.woff2 diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 2eab0ac0c3..f316e2a727 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,9 +7,9 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '32f2c040', + 'conpherence.pkg.css' => '82aca405', 'conpherence.pkg.js' => '6249a1cf', - 'core.pkg.css' => '491d7018', + 'core.pkg.css' => 'c012c648', 'core.pkg.js' => '1fa7c0c5', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '90b30783', @@ -46,7 +46,7 @@ return array( 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/durable-column.css' => '292c71f0', - 'rsrc/css/application/conpherence/header-pane.css' => 'db93ebc6', + 'rsrc/css/application/conpherence/header-pane.css' => '4082233d', 'rsrc/css/application/conpherence/menu.css' => '3d8e5c9c', 'rsrc/css/application/conpherence/message-pane.css' => 'd1fc13e1', 'rsrc/css/application/conpherence/notification.css' => '965db05b', @@ -83,7 +83,7 @@ return array( 'rsrc/css/application/paste/paste.css' => '1898e534', 'rsrc/css/application/people/people-picture-menu-item.css' => 'a06f7f34', 'rsrc/css/application/people/people-profile.css' => '4df76faf', - 'rsrc/css/application/phame/phame.css' => '53fa6236', + 'rsrc/css/application/phame/phame.css' => 'b3a0b3a3', 'rsrc/css/application/pholio/pholio-edit.css' => '07676f51', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', @@ -96,7 +96,7 @@ return array( 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/ponder-view.css' => 'fbd45f96', - 'rsrc/css/application/project/project-card-view.css' => '1be8c87b', + 'rsrc/css/application/project/project-card-view.css' => '3d3c1f91', 'rsrc/css/application/project/project-view.css' => '792c9057', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', @@ -112,7 +112,6 @@ return array( 'rsrc/css/core/syntax.css' => '769d3498', 'rsrc/css/core/z-index.css' => '5e72c4e0', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', - 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'e838e088', 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', @@ -145,11 +144,11 @@ return array( 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => 'c32e8dec', 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9', - 'rsrc/css/phui/phui-fontkit.css' => 'b78a0059', + 'rsrc/css/phui/phui-fontkit.css' => '1320ed01', 'rsrc/css/phui/phui-form-view.css' => 'cf198e10', 'rsrc/css/phui/phui-form.css' => 'b62c01d8', 'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f', - 'rsrc/css/phui/phui-header-view.css' => 'fef6a54e', + 'rsrc/css/phui/phui-header-view.css' => '9cf828ce', 'rsrc/css/phui/phui-hovercard.css' => 'ae091fc5', 'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee', 'rsrc/css/phui/phui-icon.css' => '12b387a1', @@ -169,7 +168,7 @@ return array( 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => '84d65f26', 'rsrc/css/phui/phui-timeline-view.css' => 'bf45789e', - 'rsrc/css/phui/phui-two-column-view.css' => '8a1074c7', + 'rsrc/css/phui/phui-two-column-view.css' => 'ce9fa0b7', 'rsrc/css/phui/workboards/phui-workboard-color.css' => '783cdff5', 'rsrc/css/phui/workboards/phui-workboard.css' => '3bc85455', 'rsrc/css/phui/workboards/phui-workcard.css' => 'cca5fa92', @@ -178,16 +177,6 @@ return array( 'rsrc/css/sprite-tokens.css' => '9cdfd599', 'rsrc/css/syntax/syntax-default.css' => '9923583c', 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2', - 'rsrc/externals/font/aleo/aleo-bold.eot' => 'd3d3bed7', - 'rsrc/externals/font/aleo/aleo-bold.svg' => '45899c8e', - 'rsrc/externals/font/aleo/aleo-bold.ttf' => '4b08bef0', - 'rsrc/externals/font/aleo/aleo-bold.woff' => '93b513a1', - 'rsrc/externals/font/aleo/aleo-bold.woff2' => '75fbf322', - 'rsrc/externals/font/aleo/aleo-regular.eot' => 'a4e29e2f', - 'rsrc/externals/font/aleo/aleo-regular.svg' => '42a86f7a', - 'rsrc/externals/font/aleo/aleo-regular.ttf' => '751e7479', - 'rsrc/externals/font/aleo/aleo-regular.woff' => 'c3744be9', - 'rsrc/externals/font/aleo/aleo-regular.woff2' => '851aa0ee', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '24a7064f', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '0039fe26', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'de978a43', @@ -564,7 +553,7 @@ return array( 'config-options-css' => '0ede4c9b', 'config-page-css' => 'c1d5121b', 'conpherence-durable-column-view' => '292c71f0', - 'conpherence-header-pane-css' => 'db93ebc6', + 'conpherence-header-pane-css' => '4082233d', 'conpherence-menu-css' => '3d8e5c9c', 'conpherence-message-pane-css' => 'd1fc13e1', 'conpherence-notification-css' => '965db05b', @@ -584,7 +573,6 @@ return array( 'diffusion-readme-css' => '297373eb', 'diffusion-source-css' => '750add59', 'diviner-shared-css' => '896f1d43', - 'font-aleo' => '8bdb2835', 'font-fontawesome' => 'e838e088', 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => '5c1b47c2', @@ -826,7 +814,7 @@ return array( 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '5e72c4e0', - 'phame-css' => '53fa6236', + 'phame-css' => 'b3a0b3a3', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '07676f51', 'pholio-inline-comments-css' => '8e545e49', @@ -857,11 +845,11 @@ return array( 'phui-document-view-pro-css' => 'f56738ed', 'phui-feed-story-css' => '44a9c8e9', 'phui-font-icon-base-css' => '870a7360', - 'phui-fontkit-css' => 'b78a0059', + 'phui-fontkit-css' => '1320ed01', 'phui-form-css' => 'b62c01d8', 'phui-form-view-css' => 'cf198e10', 'phui-head-thing-view-css' => 'fd311e5f', - 'phui-header-view-css' => 'fef6a54e', + 'phui-header-view-css' => '9cf828ce', 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'ae091fc5', 'phui-icon-set-selector-css' => '87db8fee', @@ -890,7 +878,7 @@ return array( 'phui-tag-view-css' => '84d65f26', 'phui-theme-css' => '9f261c6b', 'phui-timeline-view-css' => 'bf45789e', - 'phui-two-column-view-css' => '8a1074c7', + 'phui-two-column-view-css' => 'ce9fa0b7', 'phui-workboard-color-css' => '783cdff5', 'phui-workboard-view-css' => '3bc85455', 'phui-workcard-view-css' => 'cca5fa92', @@ -905,7 +893,7 @@ return array( 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => 'fbd45f96', - 'project-card-view-css' => '1be8c87b', + 'project-card-view-css' => '3d3c1f91', 'project-view-css' => '792c9057', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', @@ -1585,9 +1573,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '8bdb2835' => array( - 'phui-fontkit-css', - ), '8ce821c5' => array( 'phabricator-notification', 'javelin-stratcom', @@ -2322,7 +2307,6 @@ return array( 'phui-list-view-css', 'font-fontawesome', 'font-lato', - 'font-aleo', 'phui-font-icon-base-css', 'phui-fontkit-css', 'phui-box-css', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 4822633426..5f935c8620 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -138,7 +138,6 @@ return array( 'font-fontawesome', 'font-lato', - 'font-aleo', 'phui-font-icon-base-css', 'phui-fontkit-css', 'phui-box-css', diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 67c4dc188c..ffa82ab1b9 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -216,7 +216,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('conpherence-durable-column-view'); require_celerity_resource('font-lato'); - require_celerity_resource('font-aleo'); Javelin::initBehavior('workflow', array()); diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index f181a1adb1..11fe34e3f1 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -9,7 +9,6 @@ .conpherence-header-pane .phui-header-header { font-size: 16px; - font-family: 'Aleo', {$fontfamily}; color: #000; } diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 7b20009356..080f5ca344 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -139,7 +139,6 @@ .phame-next-post-view { margin: 0 auto; padding: 12px 0; - font-family: 'Aleo', {$fontfamily}; } .phame-next { @@ -294,7 +293,6 @@ color: #000; font-size: 28px; font-weight: bold; - font-family: 'Aleo', {$fontfamily}; padding-top: 24px; } @@ -305,7 +303,6 @@ .phame-mega-header .phame-header-subtitle { color: {$greytext}; font-size: 20px; - font-family: 'Aleo', {$fontfamily}; padding-top: 8px; } diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css index 7d1e9ce746..0d45de9485 100644 --- a/webroot/rsrc/css/application/project/project-card-view.css +++ b/webroot/rsrc/css/application/project/project-card-view.css @@ -28,7 +28,6 @@ .project-card-view .phui-header-shell .phui-header-header { font-size: 18px; - font-family: 'Aleo', {$fontfamily}; width: 290px; overflow: hidden; white-space: nowrap; @@ -71,7 +70,6 @@ .project-card-header .project-card-name { font-size: 20px; - font-family: 'Aleo', {$fontfamily}; font-weight: bold; color: #000; margin-bottom: 2px; diff --git a/webroot/rsrc/css/font/font-aleo.css b/webroot/rsrc/css/font/font-aleo.css deleted file mode 100644 index 91ef6e2dc4..0000000000 --- a/webroot/rsrc/css/font/font-aleo.css +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @provides font-aleo - * @requires phui-fontkit-css - */ - -@font-face { - font-family: 'Aleo'; - font-weight: bold; - font-style: normal; - src: url(/rsrc/externals/font/aleo/aleo-bold.eot); - src: url(/rsrc/externals/font/aleo/aleo-bold.eot?#iefix) - format('embedded-opentype'), - url(/rsrc/externals/font/aleo/aleo-bold.woff2) - format('woff2'), - url(/rsrc/externals/font/aleo/aleo-bold.woff) - format('woff'), - url(/rsrc/externals/font/aleo/aleo-bold.ttf) - format('truetype'), - url(/rsrc/externals/font/aleo/aleo-bold.svg#aleo-bold) - format('svg'); - -} - -@font-face { - font-family: 'Aleo'; - font-weight: normal; - font-style: normal; - src: url(/rsrc/externals/font/aleo/aleo-regular.eot); - src: url(/rsrc/externals/font/aleo/aleo-regular.eot?#iefix) - format('embedded-opentype'), - url(/rsrc/externals/font/aleo/aleo-regular.woff2) - format('woff2'), - url(/rsrc/externals/font/aleo/aleo-regular.woff) - format('woff'), - url(/rsrc/externals/font/aleo/aleo-regular.ttf) - format('truetype'), - url(/rsrc/externals/font/aleo/aleo-regular.svg#aleo-regular) - format('svg'); - -} diff --git a/webroot/rsrc/css/phui/phui-fontkit.css b/webroot/rsrc/css/phui/phui-fontkit.css index 7bca35d277..875ac41981 100644 --- a/webroot/rsrc/css/phui/phui-fontkit.css +++ b/webroot/rsrc/css/phui/phui-fontkit.css @@ -2,30 +2,10 @@ * @provides phui-fontkit-css */ -/* - Roboto Slab --------------------------------------------------------------- - - Used as Primary Headers in Object Boxes, Headers in Documents - -*/ - .diviner-document-section .phui-header-header { - font-family: 'Aleo', {$fontfamily}; color: #000; } -.phui-document-view .phui-header-tall .phui-header-header { - font-family: 'Aleo', {$fontfamily}; -} - -.phui-document-view .phabricator-remarkup h1.remarkup-header, -.phui-document-view .phabricator-remarkup h2.remarkup-header, -.phui-document-view .phabricator-remarkup h3.remarkup-header, -.phui-document-view .phabricator-remarkup h4.remarkup-header, -.phui-document-view .phabricator-remarkup h5.remarkup-header, -.phui-document-view .phabricator-remarkup h6.remarkup-header { - font-family: 'Aleo', {$fontfamily}; -} - .phui-document-view .phabricator-remarkup .remarkup-header { margin-bottom: 8px; } diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index b47c93fe33..14e0af986f 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -345,7 +345,6 @@ body .phui-header-shell.phui-bleed-header } .phui-profile-header.phui-header-shell .phui-header-header { - font-family: 'Aleo', {$fontfamily}; font-size: 24px; color: #000; } diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index 5418644b28..d910cf71a0 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -18,7 +18,6 @@ .phui-two-column-header .phui-header-header { font-size: 20px; - font-family: 'Aleo', {$fontfamily}; color: #000; } diff --git a/webroot/rsrc/externals/font/aleo/LICENSE.txt b/webroot/rsrc/externals/font/aleo/LICENSE.txt deleted file mode 100644 index b851fd439a..0000000000 --- a/webroot/rsrc/externals/font/aleo/LICENSE.txt +++ /dev/null @@ -1,203 +0,0 @@ -Font data copyright Google 2013 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/webroot/rsrc/externals/font/aleo/aleo-bold.eot b/webroot/rsrc/externals/font/aleo/aleo-bold.eot deleted file mode 100644 index 294fef7412387afecacbc24c8d8ac7a4ea4101b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40674 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 SRRH0%FIs>BJg5I`AN_!}((V5M diff --git a/webroot/rsrc/externals/font/aleo/aleo-bold.svg b/webroot/rsrc/externals/font/aleo/aleo-bold.svg deleted file mode 100644 index e49c14686d..0000000000 --- a/webroot/rsrc/externals/font/aleo/aleo-bold.svg +++ /dev/null @@ -1,5078 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/webroot/rsrc/externals/font/aleo/aleo-bold.ttf b/webroot/rsrc/externals/font/aleo/aleo-bold.ttf deleted file mode 100644 index b05d3e78e2e7c9cf3ec9022e816dd08e164d17c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95472 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 diff --git a/webroot/rsrc/externals/font/aleo/aleo-bold.woff b/webroot/rsrc/externals/font/aleo/aleo-bold.woff deleted file mode 100644 index a2c7a9d77b1b2172c1a8432e5aa244697cf8ca9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45488 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

^&yiX(7WAulmwa6% zeXA4HfycGEUMVyCp-v9^#f>@F?x()+lh)Dpx*LKJd!5Es8`f*!nCpiVaVv!?v{s-G zFr|B>cH|FGy9_jrmYzgg%QP74(pUkVq#@LzX{B>+sUaRZu(V_IY`GW-`Mr@abGar( zhO4C))z+D|id{d5cjW+b#Cj&%-`f4Esg&;8c8K3lc+E-wQhl_UPVERK{BEbfLsTo7 zp{{+TxqVBeCzaE+Df<3sEBmdL6=-xUpMR`i5u!GKI5Ac_T!&H~3FhO~qPlHtGM(!| z)dw`CE8@cil{dMaiEtUZorV^};OCF7x}D{<*E&Z!c?YPd47k1!`T3;alI*r}ZAUc{ zL@G}4_LRP~AKeH?DU){n>HBbf7O>3?a~1O7WWvIeXnFLNk+hxLD1_4yNz^b5eZ9=6 zp)Ex9a&6<`PB)?xee^E1RHT4qGYPe4$Ej;!pq^zXG zL&<(uDZg_V88&A-v7u9RZ>8+>s?fA;pq&l`?`|ut{1(4Qk=zukS+?rS};HG|Jr@9@CR*f6xR1U z;>k0shlU!lXf!@Fvby`GPwieE*%XaN!xKY?RzGpqV52c~=UpSi!woVWPmGKnIdsdd zhmMVGio@}nrVbps^_D~XrbiO-i}&qZ0kbR1%S+$*gQcBlj!N@s@-~Qj$FaTGLk-&? zv|HP^rJxVWLm$+FocERsrD#qjp_{N)8NUnhhF#lp=ef!dPkRX3H`TI!2pK4sjX*u0 zb}emL7-`fhJ|(HObK6KBJ7N<25_#5n3v8%PK`z$S10!vCOYTFVk;;zwlgs@hc}o&v zNJcBny!eSScIIC5R;avIb*)rljz~aQ)>p3$BiT97^v*GCISku+my{CbkSrk!eCRK;s zZq2D0A2y#N?}KYpKpndp)_6P4B*GgeHM81YqD~|UHCNJkbEX}wywX`u>ntRFu~?ia zj`vn_ITe{;YJ&xVB);;t0av?*&xuv4;^jdCa=ZEjBqypWqhprp_JyO{$EOQ=n4rFS z{`#K-w<%*}90W(Uv1Umy}L*X*W5QfSFPW|3EA5^qo1ch*Xf40UA~S6Xaxb? z=OOQl0Lvs589Mefs_kp(h&J!HR4d_ZFH4=Mih9xu!eoux@t-aff`pM1cqI~9s1`Hc zl*L48%9A$nY45UGee^%Xe6-LzGfQT}-?u`qiAl$on{|qTPu5b9IKfc(-2&2$zRdPwie#St5ZEj74_O~Zq= zYGgFou8-)5Kg#+Ruh0$vBAeL8h;z~540l&aTv{PPJH1M|cV_z7cm4y5${$F!dNLaf z*kuD_lTw)&8xL4~jchz(ce>=m(_44U-E=|^0~#20aw3~{Nb#~)nxwp=J)sEru3DSi z)W{c{?%Ls(+5<}H3-CE&7I z-3H1Dj6R3oqw%WiqxbN4sTC}NoocX9dk|5mhugIFqPo!2sL39&q?u;a9|>DQ!KqH* z6hDo@qkyJPb&bQihG|{Yf_}Gbvr3}CQzVuE3DiZ63dPE;R7}-cM=%k^Zfu=MX(x0A z-n{S2$KbV3mkFPI7qB<~!6hf%W(y~9CV}LLnab3-@aSjpY2NHU;s$=J+KtcJ}t%;i+lMG{b+F`|P@NZp(u;hgi1Ukahex!ttVbTm&o68NZ)mc!c5q)s^}4EfmNF2y?o1*v z)8`Da*lTdkSuENJMFR@a$@p438DEE$HDXzi>12G{6%g9P>RZz_M@CELA|Zet4|(sH zN^_}b^PBHBzv)ewjg&h2=FdxU=_@ADN}3I<{b7gKOsfA(Dk!T@F!aS|i3Rvv?E(%1 z(OmTpPuoU3(SC3qYB9jzYTXotD;<_Ihyx>c`nhRqM0s2lVrcDrGb$yv=|9uK~;VL_iuj z%Jc(E;s?$5y>T;3sf&O9ze{+~b5bC27AHlLvwV}5oVFXB4vGFxGGQS|&S1Gg4wxzR z7r0IYNCoQRvsrvkFU{o#-I28?vXCULMMET^ng@pzet@_1Su#1iWaBIr&bIca+xcET z&>QLlkCKKqne><`xgDe9bvT2U{@3Q-6{ZX_$-jM5 ziCU=Azr3m5hEiG9%>+xR@8Ew&lqezUd*kop*_MCzzk}WytntS)-9NKH|)&|Jvc|4wT||LM7Vnd)3mRU45@+o|W`; zR%d9t`PJoP8_{-;tdAYHq_ts&&k=BhR7X+kai@x%Hd(E*wXzocH2}T&6>-q$B`^Mj zG4bGEn}1}Wwas{hhoNju3pl|N#@KeSv-#99o82eiU!8Tw!KnFKpWWWItFhL=JT}ojDy{eXcsrl3sG_&?=y`DxgbY>3P3* zXvpg~8VqKFc39QI=QxoX)uY7yru zi-CmGFF6^`F8Q6wKr&JBk61>m;py2GhNMii^@Qv!lzPWz3k7*HIxwx(g*=5d;TjNn z|2(aq1QMK9+dowd*Is#rE_`sdwO)R0y$(0{IY|9H1vx5g#_b<$kk0$e4OwzW|QB8yOMblRjDXH$hx@=2x#B={Cj;G@iN0dNDW9;?97-qWdZv5J z(nw%Fr>4AVXF#-bMu!-1N4?peT&^gLO6BTYPZr0G@J_-Rx3f^}pBPsQ><<5MAwM|( z<#|LX6+BGxL=;P-I41_>`!q)4y9-gi1`P6!Po4zWTrGY-dkW7 z&7P1khCduhHnt2l@;wfjcQTTNk0yuahX?bj-DM%Xl!+ry{yEuv3haTH-UMe5M-4!T z^$fD!BOWp!hDF0&ZQ-q22OL$4*MMKsxnqGfNKadgZ1d-wxzz$EAlkYb!+UKG6W4qS z4==6`-*#lj>f+iv$?t@#Zv+PXT!n{5u4qV?5Ki1QSUjbq!fk;c$mEt?L9XtaL| zBOz4Q`idj`3sK2wq<~KAbxu&nwj7M{vBCagE)k2$h_ly=23l>whx97U^|s=;-d5ZW z^^fLIl(amVf#@n7h@#mmAlznWV#{=_&~3_IGm~X@+#ZdGBLnr-u_=}$ zBui+8qi4Qll()x>w1KSp@qT6~9CxCE0=IAN11aP*I{hS3425~&SIfmv&~7m?XT2_) z?cO~WBKEmM$8Njf;L)*(l*hPlM7B%6h0ijL=J(=cx)FhDZf$|LNT|@@x{<8HKxnp% z%enmErnxO0jB*S06KoRotg}NMQZM2L8b{Pa+>3TWG62$<>h3s2)2I;%+Enh2#lp5k zG9EV4-Zbjq(hoYWSGy1Mdfz&YpHUhZHMi5d(W>YRYCRY%SK0}TaMWQ>pYVEaTwXb~ zV{jr@^9TF~qt2+0Q*3sxB*%_aRPeizaPvu*y)ZY0@8m>F!g(>7vn52UB>WlvV%R0W zzr3>Zj#%9045yQ+Mneemij5N}dVJ*8e>o_p{>w-;Tb7R5BcjFjb&j=()<_@)*jCT2 zH;BI@zX+va3U#fQM2i8ss8}K%^(rWwH%$V<4ga4KXdIi2c~B!+wEr0FZEq%0h}qF$ zl;KLD442l~CSG;6MWpKNG;4GkI$c@}ailP->si;9%Iq|+hClaPjK1^d9*fm-ec8TP z<*{>*$Kk50?~LR5$rWvEau)|Iv9-y6pxw5pH0JI zkKOWBS>$=6@3Gt9rR7SdFVw4Fs{M*bVzu6$csS@$ctLwjFp+yf0CassP zZVJ}%p5FQodYtx7W8ZS{Ja9L!dn0O7*q_QqgV_jm$xltPBm8FbC{UU&g30EK&E|K& zMDsiNy~Kzp+neT6z*&tYvT^XwPMh`O_wQf*eDi(4e|{D95QBF1f@Wt8sEV*vWw~Fg zuf(+4#ZC(+XmdsQxBSX0$nsu%x%p2QUunH><1XT6>c`j_?0%$!&HzAt_SOR_lE4P_ zo{yd09gtzrOAVm8#rJkvc!1ecCuXNn+qQUEv##B5E4a#*juzWos$_Fu<69Bb;`+9c z*4qy@_Sb8(V&{zqT|Jo)M-r0B$cYZCA!rPFbFn}wY;&=kti;v0G$Kue`a&_6GaB2L z2zlH}xi-DDtI$_a3-kUOXAvDX7ZqYXsZ3>Yk8IE8a{d{|tf7$3IOJq{r>b~ea#5Y0 z+kIy1{_@(3{%HIfWxp*cS-nbOvokT1x7o=HR+452iL@9@CZlMV?Cy}P*xaJQYY12a z{%|4N*fP2)5v=B>=IV{S>QGq5D&;);BqPfcHd^4!{-`z1CX9ApAeISu-ARTI%U+-M?h*2F{59&km=_zwoDG}y z%8^}!-UB_C&0e}!u-ja;R)YF9IcqSBgfn1^1Z@Ggz;Tkv9FV;MIp7w};2g`@;ki)Q z7BGuE%L;I$?DxrD!OF8#T59gL4 zU-?bOX&u8X_uc!6GY9t1PNQvh3?iKtL*l^|iKFdB-O%In(t4EW6&g@&a16Q)kEU3C z_@(VbyCn_%kQh%3V6pbxnWNV%)`k^-I59B3u;uXC`K^^Pfw5XdpF#1)a^-419Ee#Z zzo#dy)|7ER+m}wb^3f6CrD4x7JwrShW zmA%KV-Mf3|^6ixpd>BRT9UR*>echo0pF4Tu(+7*{;?k*;pIh9%6++`|IpvULN2;9d zn;6@^RogR2EaHpQJJej0TV=+p{B&|o;gpp-U7V|&|0!!j{j z9~>PSoU6~pqhVh*U_@ts%1T8+hTz{=!TXECRfY!x^l!97wtBa(QFXy zq|YLTlwdlPaJbF9B&7;tTMy)Vd>&2-SRi`j4T0MD=t5t<;0Xy*^GAWOCy+1M6HcT% z23t`(&*Gtv0axSQ5}%98ilr zAX+5;0Y1Jsjl&~OfqOuJ?#TmOd=YmBz*pkvr3O~{eVJxO2dv&%$ZA%FMS)DLezQO zsvBeCfgi6;5qvWU!v7!p)sr0^l*1OG7CMK0;oJLwfoa!h=4n70a0B8j3Ntd4@c_oK z0Q%*I18s;UcU~EzrCk4AS7@nN_qJ{4ZaZ~i+wpDJA33zVbI11S$x0~_N}Jk5I{>9~?diCG0Nv5Y;ngo3C4w(AE&gIp8Hmhj< z<2QSx-!k%ov$|aM$1HY^HV8H)5xX9Lw_AC*)yhzP}^;GN>@kZJqsK|hb76UKV|&L+9T9$kLxl< zgz;ZeZ(}}an?3q2lP>%X)lN328|W*xm0UME)sOFjn1o8S+E<^OulM#A6h(2V#akEV z>H})Qtvt)IUO5ns_!41<+r;xGx2q9LdqU_7d2XUHnkmU{x3ioZZcI!x2GnB7?NST% z#%MU~@yceNw>aGXgb&q{xIDawzUcZRi)|ro#x9I&^s|`3g3?*6}1id zVM|c)8s;zzb+PKa6vKGQOED0X#_#RC7^8WH_`8cP@>k8TzSMji?0E@Xdr^K#dnd`G z_#*ur+s(Iv`ny;vmvpDsL=MVE{0e~vi@puAg7#|R^Yq$V zj6rzI;xQ;9!=MJ=RwoDXkBrl}`LHxQcesqS6lm$&)f_&xO$r9raC?G8aw>-+AWB3>sI z=(paWr|C^_Ie=n;77wNQ+usPR(yVJ%Evi^ysa4U zlM2cxmg@!lgAd=$ZudDoMvg&6eweBpX5hr>Nc$heY%loRa>4y{>u%> zm7#W_jIsVYCl@BKzfH#;7WJP?aXEF{JMX;r&dDv?>a}Pv81Aoc+j%YZ>YEqJ)N2>Y z#BwmyTe@ay$APK&a<9*?;f}|_3q%<6cl7#>Rsk#qHs`b#!u-0Xzhw)GW5lC@bXf72 z1;OH0!ig~auDC50d^Hk75ehvjkqE`2AvcOw@QOPW*53DX9=we4SQz{7dOX}D@=%Qi zL2IqyH=^|ot~HCwIrJT*BuZJ~D{wLGI7;wvdQlt(PG5RPy22T+cE)klYhqWq2Ej4{ z9z?q%Ph;yRme3zh*raH?Im+0WuA9xI_*(BNI!^)sG8eEASVyB zUe}189tBhN{T}432%UZbCEKm4SZ(j^tE!@E4<3GqoEPjIZy~Gy^zhl{CXxp?sDoyM z!7%@4Yv*p%Z21EIDzxQ(ERS~0Q=rS3qar$J%~9E0G9JhW^Z&lGx$gG_fLkUSgXyBp z<#P0-2gip;$A?pShs$N}NewrqMk2vrXlyDJ2t=s29rkpw)R>rT^!B9f$Ro9q+XfoNPMp5(>S(g1Yj^bq;FB~5| zjm{h&AHN(Wj+TsV{BqZRKaRrD_Qtx+HOz06MtkqAJEb`mrBeVIs@;z0`_Z9Y4RHzgi$ zfafm4oyf+qHr$nBBRSiGgo<(y0NybH=k93hkh-&@sj;CxTW6@ytB9-dsGSXD6EY|< zIq5m8`Pdn2FL9|PYD@t>dq3!;a^PuiK6Zhcafc%H=X{5F$=>&c!q0qgFHphE*WTL> zEyiIuBs!Sm?(@%;}0x57;79-r++B=jZlKP3##T+cB~&-`AT?1_N$q%#KQ~EjQ*CycnjE zB&$Y_%mMoP&m@MdxYO z-EytT7YrxQQPZv{l>K|3e1e@QiTbI#3fq4M9iUz{u0#LFo|I#z7y2No9Mj>Rtlrbn zsTlr5xUnvoP6PtsVCau|j`joBC$kQPxhrA1uRmC&jFK&Wiutc_pAHiD5Pw9xk=O44 zvONoI(f|+zzl^Htir(cg0i9U8EL|i7n`je6Xq+N1vQRtFwG|}WBD83p6!}F**+vPw z9n2{lSz)QZ{LY*B9Gs@M+qpRfs@g1l)@(i;4*k)?=g*xyK6hYtW_)aLYgb3;p71@^ zB#5c3FPo@P)e5szq=vV4I$!L^XXNn)cAR7#tcLn&bf|=lGOkC!q0t@O3GmR z#cR%<##V1aQoO;Ayg%edK3-!kA8!mNTtUaU(@4OFra4))Im{-H$+Ma*8cU_aAsIS( zhMtMJ3`{-28S|pk7jBAmP!E%ETD);}6l*?XpgCz5(VSF8I48+Bq5l;LrK8*GnqAO& z*)q_8d_Y;WyS#~9b5IBXoe@O|ZWw05ZR!%-3_i?pHoKxZ<8GJY@SB`SJMzgEcT|wy zdzX(2cvk<ue7OFlPb7u9UmQrAKh;=G+E!$&94{-f2|vXd&#Xw&l)Csj(gXp?Z7NhKAP8&Y_vn z?Pq-f(}+jwq*i;KVX~g*HOk`+bgI!bbfN8Hz%`m^$|ZwNuTx1Vg<&t^&yGfXm?6Y2 z6}d7Dm(Bi0ljw&|w`95x^lcj-8fZ@?{CrIC8D`|k|DEJWu`Zr4l7boz#FA~{i0Re6 z4x0*Bf-v>FNRLIeZCFeNzI5?GH9+9JFvgLKVjEQ;k&=Pwo&M;Gjs+|5TQbf#q|Q*qtA(kdFvhg6CL%(FPa+a1_HWt|X`KuDRLCz2m5o zVN2K1?xR>_(#GvcUti@3{Bx6X{F-0YFUvNsD;;Uh1bnGhAz=6v{c+jmccl~AM6{3N zTU;9!>vOUa$>dTIzpjXaV$gQo?S-#neqEEC6y*|;|6$3w9m{!&xRcnD@2-$EI;Dno z$TBMm_}uM>=J)I#=)-~)Zeme0Dz}3h9}QNPP28lY!)DU!^O`=F(}7M5?Ye1%B8ERI zOW+tcfS9o zwNkxlFH=LOQF6jim-|g>;VaRgHyx{QOvb~|7&SGR@ny1^WGEJPsP}q&>6FjYnyG8u zu++cUFyeD7vTTP_DLIvhn-N};#E?H6t517;K2JK^+G_P-bC;Q`#1Pz>{w#0X-bxaD z6q@=qaZ?ImHkp+uK#NeyjuvjfhQRO!!wfCeoB8n2WU}D*p2J|Iv3G1|DvhiO{GS? zWCP$4lIZgcJxn61hW9RmFVEQAicX)|u?4eIbdRQfk4Y&=k|&?}TbV`bNT@*@9DyqN zKmGq*e~WxA)tkv$JPd^^n6u?L_%kc3qi+E$4FJJqSm<=R0Dz&5ul8MvM)@Xu&P6Z? zuK-3SXuapom+&7<3zP}B)lsDeLaVt>TGufD@?qLWhXp_>i**y!UY#AJ*Dl^s#TxBK;D}Pplt;&>GK$0=% zQYlWE{-S&~NI!XQ_R&+H6A#7{x--}uY-`OV>iurk1zk?)HS$JSR};-!6Xy>uG39*X zuf78`(VRjgJAZxg;>1jUJ{pT9KQx6nc0%o$5aQU8>~1i2_l4eOA?r+~+d}@3;$cOW z6UN;X!m1$cvVt;q_e&W}sPpV!6$y17QGb>2UT!60iJ_50GmqSR?ASxe7)z^m9dYom zMV#m}(0A>IUjEZn3^biKzzh5(IMIbmYmm{1wPZBiH6vHyr4eg+X+m3i0I_|zXJX4l zEkBKPbNn=*BA~Epj!`(Van>5}77Qiql0i}m{gbO6nUxtFNW8HDIvcP+vLXqs;dK<6 z7DI?CJ&Y62R_UQj5=nuFlH>_~Ek)RB712{WbQMk5YBdq*ftu(U@7X;zI<&1;7xtUf z1tU|{@yf>aK(mE6D4u0CiTC{Is)M%+l7n$`8`8p?BuCRM{kj8c!-`W%I?(W3_+?oK zXl?EyM4k{&2w=lHD9$A~Uc7V@!}HxWBiF6L*?a&HC@0z5vuRT)VWl1$smhRQ;~J!X z^F6^-P^{u|>Nv14bre|mRfmkK8p8-))D0)>FW`FFPV6R{^7RQ3Bc9N} zD)a}`r3!MQu8wV6J9c;N&NX{nOo{c$qCP2hsaH}zm2oLRHC+l=DJ=z33GxL?aC}y* zKm{oL+m~{Hr|eq!ZI(s2+FvS+b0+PO6dGFiMgoo>rLRDsD%s__|r1NC&iDilNCj%8NL*3yOqmcstxj zC%#!UixNf$BC$PzPA;oNfeu^YXJ4whFq^lJ!`%-)Tgh;uB{cRT4%Ko=tzb5KdBsF? zw!CJN9|0n(FU>Zc6#=IHYGWAMAs4MihyFzy;{BTXu7I*n$PSQY&@@Uw*}na|Ye-yp zQbB=LV|6j)a;pJOxMmJRH=9L>6ErytSIu7_j5{c}U&1xBotPpn5smpQrceW;48Yzc zg{yT+LOFZ;!TIsA{w=xY8&IgNW;(!Fn^0x7^~(bGI(W7Drh**2h>S$k)^3qwRy?mvkqwDD6c|(%&CPVvollo@4J7Aut?f`!Xj$jNzhH9Hu``4s&-kHJVqwck`4G$ zL7&GA$l50yHorR=Y03tYK7Yu)Sp-OcmL1Vdt}g7wsZvR@sV-l#BNYp1iU>D&*#fG@ z97$Xjb0qC2T5(M+Lti2oz+9>(Oe*%!ZOf1C%J1K{9}y>Un?Rhj+ID*@gi6=1a9&y} zo(27gQ7Cj4M8duI^_!+zqF=5&69NMD3!+ATQ;7QvHF={m4nSX8k#H)X&rjtink?~o zZdrU3v%>y-E3(^O_%8;f;obU&KZd)tD9N;oy9K$u34Iv6cNY(eN;1>1%=Qrr-xvin zTiU!M(DerF2?nexK-9#{((I)*Zkf$X`=)N&>)tdW7x#A1RJpa^ij?QAQzrNbLd7Z5 zO-q^jmbK$Uw~VB%?S7e@Pou1Rc>xyj0u05zxT5^F<`<71n4O*)9~&OZpWb>J$I`zAKWJ8zjZ;c>U*uHL+zH9 z-hZ$>v6YXbYz{lRSU!$&z>87jrrA{6&FiDR?3mS@Ood!QyLG8}GUc%RGF#MT)ysN_ z*Qht?M+h6ieqCTdb5J@?AQ8ZU z!`t^y&5TD9Es--n{Lr?oMLRfl;mn?1(cvO_-OxYz%I&?~@AU-GgKXqy1z?QCf zgnHKWG+ck-*uBdM4h_BGT)kZd`ti?y_UnIbDcI+2nsPW?C_QxT_!F1E_K}(SL8>nw zOKd}S%h2#FHS7r?DW+iu^J^66AQ0zB2`y8eZ7=;sFeNf)v~ud9)ey$ z_R~=I)3;BbJbL&KAbXvWK@9czRC`04CHc<68;lBAQ=lCTs-1zpk)3gIn=1ApDO!K6jk<}EV;KsxnCkaMfB%;E36&v;so5qNxqiZ@8g$0avv%o zSrWVOL5cn5v%{OW6wTnIalkAmjn!sfyp%MS9jhgcH%)c5NoCcwF$p2VM zD&u?%=3V0ursa85skUo-s>r+sl}o{$1odN(Oa%$s>?|N=&(EGYw$Rz?_YgB+A6G-h zR-0rl6S7e?Y;16y(u@4mdKDKNwPMbdc|4XQg(5?nl`}*-zzJxSfZ(%YVIC*TST+}l zPkjVYwDBY>F(&x~x5k^cxcmDansSgdlLPvFQ~zG711zp)k_|?BZhkp$TTO!6Vs5Kq zM#RKz+Lq)*1#KH#%5a5X#ah-$5cCXlm2eRml(&ZOz{{u%iv_4KzcTws5Sf(64exYD zP=}HmwKl2|XHCmcYIz547J#wBTfq6+XV2fhb1QNfM+Pt4{^V<~jrR6L(+J-q6YuIB zeGQ0(-!fMVv1q<~ZvW{^w;jm$Clf!{^=P`WXTBz0&V-^)KFE0tGZ-;lKC;PGpUL&Z zN#4`eXu3o})m|A`pbN6uI7EHB5Ti+}zxW zx#QU-H3mK?HR=_uvYW5RUtKd))BX9+I1EpGa}`D0YSG1$>k_7*Xi>w3!74!55Sig@EFRwCBdrP{J0WSfO;l;(W zmTWd*&JO3@>M9=C=;#_v`{w5!_~>09y5qv>lSdBEJvRSXN$HLcaur@5P$YEjH{Tc> zAw~b9A_qL_L`x)Q#^Q?U3~0)qt5U?9N!LXZAwL@z%uSv6_bYl`M{70_)-_qw4PMfe zY)gBxE@J2oNuivKM9@Wh`4+a9yNN#|zPO6Tw63(r3UjtK4W(n9gKAgxPkr*i`|r5m zFPVk(!L0>Wv%#td?5lJlSSa|LZYD2yqki|c;>ovyivJ^VbjXfS&EO@3cecW{O|_a% zUmFZPtpk3Hfu`bD_NX_o>Xnimu@Q?)m$(cfE7*~O>&i+d!4o!-$33M2BMOT*;Md*t zqJe7*OQV;Mvf21Kg6Flh9nG8l_I%F<2G%&6(4If`*h3F|=u+b!z@BdeDqGd4!Lgff z!-cQtaw3#zOs871p@ghyYBZG1G&Lt-oTBOKuctNL;Mv^O?m#vhj`$s_>hMRx z>Bh!nEaZ2{vcn&WrR(cVuh)~QYekF+XpelG4bTn50$lMP1@~Lk`i{fAcOc)t?$2y!G1g#{gwmk9ZZzR?t)Wh>0|PvYkUHbq@@qI@rNOUPu+VDAnyO@ zJ&)e|=*9EL7UvJ#ws+Ug;h~PUY$oV;8LC1ofCXhGFp5Y;vThmCD@NE{1^KSB)4LM; z4$TO*c9yh*N}5>uBh%v(?OXg2nqpazcV55Z*uLX@Uq?1T)oX4;lDVzTb?xmf{Vjdx z{=PrZ)YiDAyDvXH)R#?rT|xiuJ$GJs6;71COg!#L7w5^o(+3~8cXTY(J3Ui){I0=W zjcAf2cQ!X>B2iTX|Lt^NzXV=|alI>BPiJHC{^9+{&v;CSU1I3ZZQlWRXfhzNoG{%P zz$~*1=Q=VChSw$;S|Zw)8}IJxZfg&Osl z5pMCfl;Xx2)PFjE+^z}iY4uerp4m*D-m*2RMweE zw?=(I)yv>68V}9jE*cB}#;Z@+)FsaDo#`@Xx4S9VjX1jpnj4*E&Th)$mu7yB`K2d_ zuU0Wkqa~2wc;e!6{%9MHq!%b$DB|VvU9jOi+ZuY=@Hk5^!&ooF z6T^FU?fd}sQiXkX>v{q0g<=ZFWhQP(F;>RLYnQFuo28qpSU3BLzs!rf0Vf~@ktGH3 z@C`z!mGn^pMOrp2XmJGvZG0S7Pzb4K|Mb48?ZY3WdSX!wNZ=r?YK%?MQSRA@b`Dj* zOSZF=At_(`*I(a)szAH8D54~cxJZgDPQk!f$kNxhu)dBG4-r?YW>apttOdAA@pC9N zR(45PZFrP5laQkByXW@PbF;g5)vC(AX)Byj$&|iwk$NrCw^gNn>qMxaEM79u39+ZFl>f5hG5t%-iqE|4%IsqhbT(P=0L~{I`<%ZMOcIDcYHo#N(d=no|qjr-k*YE)-D;9p7aT@Yx|I}@2 z>h)JDPbm?_Yw;xV+Kc!UlVl>Y-TN*t7}mJ}iMf=YgU^#BS}UIiJ`8-Mi--%i>UmPB zRBNiGA-n8(su*sU7?!D4S7p+jOyr)rzKq5e9kV}m9sS7ie_URFJ@HpoAge7AlA~rB zCQ`Y`T6+08Tx08qJ|dTI&H<9fJnSUm977EtsC}PSDxalqOK(p@HX2?@GfTzuWz|s7 z;`N#(Z94bG!j&8lX+vix{}`uCcJi9-By zr^ku8ypabK7{^(G;h})jP#6yE zu$Fv-K$J#+T11jws!yza+mDJ89ot9t?;AP3{diw*Ll(DEVZ=iW#9_f8x-c0EjxBJI zQMrDvBwfrJsJB+rOD&Hsz%em0CN#)KmnY7L4xikA=;fTq+ia6v%`K5|G8WGdE-pND zVc+zQ!9*$?>F?h+d+Nm4bhk6tw*My&cXzbL)4EAX0beklY-#H2Xo-ak&2UCxU_lS4 zT3F3e8qFzeJmfT|m59INu1UitDKgj4Iykg*`+r*e+}-z$@2$^vlG`ZJ#|H%A}kgyXtl(qaCfi!~NZL!tIk~vCY@ec4w_S>1B7W!sYFq+Jt1S(j{12Jtb4UkzyLmlY}E zt&26msM3p$E|v(xLoGK+7*4;UXjyb(0S!mOCls=#)_1g`BMa3o%lOE`@`vdLxaS~F z@gCyN672>$ODXcfr`Re>=AKQ9OLV_&pM)NZbIOx9c@iL%{`UO7URi;EzNWh1*8ilcc@i5F7Ti|2BFgoWZC%Ck8b(;xSY8aES%QUvAd0I1 zF~?y!@YV9V@qJic9we>dp}}n}O}8p7e3DJ2WU5#+s&7DOzk-g%S(2k!l4RjO7m#!D z!vFg9M&w8R9vwo{Jc<7~y5wtEa{NA)<1BI6l4HG8ZdxQbj)Ad1!xY72MVYoLajyS% zEY+AbV<#te?ie2I>ut+5%{I?g%GZ7~Gk4vG#_N{*Z{UgwV~v2BO*W>3W~71lYd&-q zEC;-qSW7Z&ods_J%P%z0L=E)c!IiV8F`EcFHA&Dtw5%x&x%O-VEu7`E-T>AK;$^BM zMv2qJX-g|n)FaKzaZA$WOOfP~E^c%a)_@xji$|v>hqkq~CgP!>A`<}+SZ3A5v#iQc zkPKs)hq1`@8#UJ4Ae>AgEet}rCm*$x@MtoXN(@3BC_wD%?;j|t zk-w)S4=-$n?bw~PIUKe)W{@hgOND~aH57C^R?EmSq?VH(M_T##$uoC<_{^~pODVHp za`wQ1=@*`xrb8t@DcRw)d{rIS&)Sh|8FB?3HfubGSr9-Ns7A$eRtrsC`Kom>f=DjFjATy-EF@LaCP_Ba z2)GkTcK}15F2CX$!xt#|Rq&Ir&$`DjJ&2;xV`2llAzt5MdXO-Tbe1FeS?+~Ve=Wm(@9xT;7ZjcfF`XlMOVmhL56YTIz7!@uS6BL!Q z8zwHUB0DG*C>tmkmaUZCT!rjbx}_w$a^AGQXQ-|yI`2)2&IXn|pbg6Id++IR z&H2J-956<{DcKc%>*9*Ff+g8qg|gd2oL^aXaQ<;52PH!wp!XH+ZpB+_?9RSteD_%L z79~fOydh9duxwO-&qcH|(3gSX3QLL?SVdByM|2aSL2OAGOY{9VK+CuHjsdBHe^;=u3fSE3XsvxM z1t)APugIz+Y@+#4c#i%7TuWMrK7@yxO-39TlY^jOM2UHuP(+D4UT~Jc3C3Dm0nysm zx}~ebXUY=M0-DPmFR0zcGEz{KIBB^@OMI>tDoJGul>S(M8*8ge6nu6~PW{~v3NHjw zc9F^TzWM*OQ9k_{pwXfL_meNW8*{(Qz6XOMr$y&O`%uFW#k22yDj-SVX*k)7GC}Xt zRN)S)U1|vWVovZl&7KErl*jnza5VB7cy;iPIq17*6p*CJBQ;* zUBh!5+sC({eM}G+P)AhRLQrm%o5~U$MtAgfm+XVzuzi5F5s_~I zn7kp2DttkbRp=SRSsJ!OAK9it_ca)R?kk*$;6Vw4^~9 z_*94_1szexkNT?L=8A6@E907Bh(gf}VwoU#f#MgT2hYwCEDPtqAdqt?o#d(1La|&dUS}HanYle(2KaQ;Q2zle>2fZR_uAZfr=2tgG4tFvei0$Tw{n zK(3M8UAKpIN)a^8*X*%~XG@X|(>>SE9CYP!}AI2~AcVGMo4H^#P)9s&8U+xTmWw9gDi1 z97AjYTSSaqw-zu+X&G2CNx`TTV-fT&>pi6}AlTxk?>=|iOn+Y@kw|rR??<@BnQSzP zaEp1>V?Q_h=wtu!D}SCRh&27+fS65^*nHEV-IE1H?gB6jfT`zRc3;*2ae5 zlP@Ko&+p2QCQ^;@ZH;LGkp#KfRntN1D3hyd*(#OHwG5mdFV(~KFD$QzYl$ReMbpEv zrpkJ_fdXbm{8Pp0iI-~Q_~=sM+xEJ0;hS0Cx&;-!4Q1=yrNTG*szs(4kYvOcTKFn` z7u=u5iNDOtPH6cC034LINEN|Xd!R=NSFuwBL$ExDNHK62%ZPCh+xbrF)#fn zidcxK05ace4Mvq5B@`XQG? z&eZ3-+nO8i%}F|)!>&GpPz@a~9Q&_@LyKVqp{fXmWa#B3jfDQWW#@&jQvXC}iM_-@ z;+Lps0u?u^UVKVPMVHEe$e*h$bYuG99u@KpoZff1cS|~973|!zyvk>E z?4twq9cI$$@`N3uedBxg#z-#*r=FS&X%x%9boL<){DXuGe7+}Jj!bK=zMf5Gw{AOo z{A0zU99+V4hubRQd3torq1p|*Y|f7)k`6cN5}jdBNK+YT0Cd+w7hAHpd@{PCURHaTLl*Q%6rxemM<5jSlGU$x{Xp@ ziiJsPVJZa>HZxo!wh;4qsWaE=F%6v~a1j}5gfVGUicP_d5zlM5vL3J?7JPXRzC@xO zaM`ulyaN#3UC9_vqn(Z^5(j5N70v>O9=CiNT?EtMEZB_%{F%YioGP4cVXytgl{TKW zYg}4-CHRyDoPaP`HH7ELyOA8gg-GEg@Q*AhfQ8qS=avu6>Q0R5ZWh|?(kC}Ve4fc#KwC1&T;6dKQmqk;oT<|RxXE0&Qg$WCCvG$2iK-qT?Qy`cZI{--p zVVqr}%4u2|xnIPEP6CF0N7cpLJn{l9?0=-MmepYU*BThd4b5u|6y^v7BY;0+mZ<%R z{P0bT#;Eo9M}8jqRes)T^qUypmd~}s4aM_K_ zBhX?Yian-(6d26nbL}Gs?!NO&|J#S|pV{Bq($czr=Kcph|HV5Wx~)GD@O5_V*|m7= zN@p$<3WZxcuN+(4HPPP5Tzz8qb4lYO2A@CcMxN8Rk94+ocAYr>@ypLVefbl|PIPs* z_a3}~GoA(b1>R0nG?vTMFM zo>X9P)6#l+v8_Fk9Lb+O{6`NTK0Z8{O0{<$K6w7x<(X+PGkxH|Y~h938S?wZp0c>p zjX6f&f1CPs5#R3dyx0yHN`n4KDYkFqi;+6!!(`xwcxqXf6^_%FqR6J20WhFkW5jIU zn4Y%4^78#X9c``g!7c$|A3=Rt^p7yOZ&!QC)|HTbmZ?cyIl?TF&aS+lQf~xCwrxE! z_dw|Tzv4Y8u#slJ`{De6-&X6le)dN3mYx5hGu!A5$#T%& zytUCh#03xawRR}A+bUw3B+vC9Ir{Rz)?|El0?fp{pN6yhOm`~b@jJqd8uqp{4edU- zZ|7KFdnPeecr4}pL_|{DzDT&fIT?3{?Od1?Grp!A{a6jYlpE@=39jpl#06q9zh@Ed z$t}=%Mo4)a!xfiC7-3#4BVIkGnlLdfWax zZ#&ds;H1FkKZBA2)VNNwBBjs{&K3%Gggv_9KvFNjG%s1ja=R0cR}Q+>!6^>uzi?eX(>0*|d~8D9;T(O$j4#^CtoRXGFm0hD33$q+pD;NZdzE<@a8M#$B2( z)OzNNQaNAlyXWMIKLC_xvKoKpFyg-uzDK3Z0xZj&4xL=-fXk0dWRojR2KO<(jnYp<*=1N zDwp%4mQgA<0SEDyA(vOx4Uuz|m_eL|=ujL%S0GZxsH~t(E%F}4V0`!#^hJV1j=0~7 zOBw~9EFTpRIQB$5o&-3706b>#nIM|m2jHYsNqn?WGXOat(947i@Jd5w^D6YPbD`E` zR!z9HgetQBtN+hp*&!vD!xDQC*hGe*Gy&WjWbsO%N>6JVzB>>0xuRLIYdc~fA} zemdy12d`f^>`2iMSJjZ0>}~M~D!-Lk`mYWpKtmYahCx3;DYHZHTh8;#00Ew{THj6eZCLG*Oj*Ol!~ zSdZON%j90o)3gpt{D~}h;a(OBggvHLu-k2>5sG#s{BEBptznte?|m`1*R;v9&Gfp% zTOxkbkl~u`_IZM3JJq0_b`pPMMd*#tZR8pD1i>@>de%qFx*O0axIC_NP`;_={u`{R zvw0cS;p>*2ql3YsY)jRrm#X(izn?4)ApU;7S=NDe=Pcj(D$UP zeM8HpuR=R%Cw}~m2E>;>y7Ce$SSDU{2sg0A3Pqz`Yh>*byN*#%+y+6k=aPwdY-0;- z*_m{?i=N~XEHtH%bL`7;tN;Lo_ai`sG{ieCF|Qi^RjjB(?CB@z76fhiO!{XfKGhS z$s}xsh_G@FT>Vv?X12^8w=FLrj2U{4WzO(Rf0G&o%1ThUNG->QRVXTO=q zG^S|Q%<5}Fk~M;A>i8B_iG@=ZdRR=+^%8nmDjbb|%u@mnYh}iold-Uc_Z5z1vvuh* z-q%P|%H#EVQuVE^mP}jd?~&hQzE9K<*?gJ>Ff1ks@^BejTA`p{ACLOHZl^3UG*JiY zEXe5fby}h@_g5iBe@eCB9Z{1cnZZqxMSRCtOFSI%H}EENhoaY^ww$nz8XSR}vHEnZ z21iNp&=UH6yWy`lPW}`|C{6-sr9=UI z=Z=wF*O74v6M``=hs4==(dOq9)IZ;G*X^?lqb)ou(VRpHT+nICDKnXhCK~KYM6z_7 zpnd`-pdF*{z6N4cS96_*#~px{X^+jJFuWwY#S|N9@9*4l`|Odi z7Lk@XO=f~}Anb~JqUolD-5!(h_caxM3T8l-2(R{ir9#1_FVteX0>5y#MA0&-v!bfd zaHTW(1ju#_v~4*Hzp;tuB}SJ?4<|)KM%)umXHz!KdPWk-lza}$bb#Q$kqom0d1JY{ zZ&L1mTYjHpO=bi7=&!oYvO%s^UnJ5O`83p582WnrH!=|(J;SL3N`GthlneU91?Z)c2P14e*f|WDn@NFPe^)?SVx0Kt7M{Ve2hC&k9Bp*Ravp zNo+K-U1p0$^H%W+G$)C6(iK;-UA=An$edaPrf5#8H{e+^r(Sf!nUcLZVZ6^MzRDY{ zITb5BNk^D#M3!jFx9E1b>KiaLC(zeh5|~LzuRh8u z{-<1B0v$G0$C(XXjl@{aA-nZh_vC}#aQk>VTcPt6c!@Bk?44PxNw|$ z#;v(!S>)j&34RmdIjsPHeT5fgd$T%Oc=Fn74lP3Ez&W%gz~4UxfBzlC&isfb!QWrE zp-ro+nPlkB7LsM?Q6A8Ug@k4n@$(3k&%?3-c5KIXGm%QGW-5^s*#LZ~)t||Q$~e*4 zUgc(?TR-R(Zhs%TDWTXMPCeE;`KS?UA5VtD!HDw_>wy)HEgS<^SVmKv?qKia#EytB z7LAR!hxYEA><+ng)xq-M&kGCGGitNlA@GWqLbBl1f<$?dEEK2ap1l6nv`ceniXbYA ztsprBAp(A5mlcs$9h#|3lc@fsfp~^`jt;>6U(YKg&qxPj4Jo{=ue4(?WuqEmna=(_ zdk-C$+MVyr#A4~L-e+eH?Y*t13);w`W25^!`=hZ~IN!N{^w^QneOrj^AboA0iqYWo9$NA;egIFs)1rCve<4~^$zYW6&e~F8tSoubyiTZk;@wa5>=_N z6|PpYxYcw4+hxrNK%x5rX|LVpfpBaiBW~2aLgB04ZC#xP59ac zFNP~FhGV|C173VQ5(sGqZkz0hgaaNI`q`lwz5u+V+)s%<*HB@QEUQMbyVAA%lH!1u z2#!d+|7$^HeuibZoj6JSG4TxX_r$j>8ip*|{yBmVilfOl z0%luNU;psV{{E&`mub3N8Xnrd^Pz@jx9c9y1y-{kkcLFdUKrom+tq5q z|FyRF4^B;wY-?|BZEemC?b+RvNqL^`18<5Y4+edrk>DJg)?DubU? zSIcxoUof0JOHI4Ok%qIQS?G}7m2|t&bH4u${Un^{2Z?9$vK>$`;$xYeaJrT1DY^-` zEyL*RA^~S0C-4_(K-l4~Mlg#KVBy>pSZ=XYc5R~S2XT%NMURN)`y% z{^@<=V?%?ncq*>frBVu-$9k@^x{0eiy(-NcE0&HJY=AUe&f=?;Ab-n^U9t9D9~H}7 z_O!I`*nafz*(Z)KjLfi#q&p;9p&goPdi$I1oxCkdl9~}JJAcTBXO`hW6sjw4r&FfRiR|C(dr^&18pnoIUu-$312wX2nXA)DI|xaLRt2 z#BmRfrap1)LzMg~N`^su2+^UX>xheq4!!pa^!NVK|FB19r2tVK(aXcI0%LR^G_h$|7H-+d|# z;lRgvbM86!obP<+nm@WG|9f+cU<1^ZH_Ix!x5~LurJ{=R;fiuWP32%$d5|{SD!7^o z!K^A~O4VRN6i0Pt-VU^0Nd9Jbco};gW-fo7Se;w~D@G-QD#ixgyq4O#15}N+v%}uHp zOz@UosUQADv&J55s-Io*?>29Lpz$`kb4PoLxbM7Ie7u>Auj7_tepUGEFM#r}z71;-oC6wXgv z3EX1b3wV@xHt^izW#QH1{l?G6pC`a2&?U$qI7vuBXn}Bu@EQ>lkw2mZVg_Qn#C62C zNZ3d`lWdUuB-JAIPP$EoM`nR6ldOYmlI#N6TXIZt8geh>OB6U1HYr9aUQl|ZoTL0f zr9@RlwLr~5?UuTahMdMe%{VO)tsZSQ?F8*FIypLLbba*r^i=d-=z9R+A_F&r3x-LC zhYX(>T`_hr2{ZX%YGImT+GN&Yw#Zz-+{gTt#SDvmmL`@ZmRqbstU9b-SQ}Vxu_?1% zVCQ4^$iB#d$zhvgos)sn6Bh?p7T05LE8ORJsClgLbn;^GD)aX7e&f^TYvudH?~LCk z|0MxQfii(Nf=&cC1>XtT6S^;KQTU{YRgveSRbp1eK8jlxUy~4$n3Q-bNiS(ZvPX(Q zDog6DG^4a@=?gN-GTvp@WJzW1$WF>}$k~@0kSCJ2EB{Hsy26wqrDC<>vf@7_A*CFp z$I8x?U#i$t*;M6Gtx+RUv!XVn_D$W2`mhF`hN6aLjWUfTIABxbgvNP|I~p%FaWy$L z)igb7_G;eJBGR&~wCi0001Z+QnK)ZzIPQE@hk`Za52!zy~9ThCB#n07{<4PJlrW zgpoKif;prtQXcyffJ;+s54)RTH>L2e<(N~JUy%QhV~|r0dG)2(J$3ZXyGp5l(l57<)DP(!x4%d2e@XkE`kDHVwBJ$hAN^F?-&a3B zI+peisQ)+8eph{XbSdp09e;WBwX}b%KKba+(*BG3;G=&_`>*QJ{r9B(H}%2&UrYP% z>b?6{-2TTs_2K>Bt20%oxmu}(O4W^;s!GM`ff}iY^!Y?RQjgVBY7J>bteR@35>?Vq zO0#0sRs(94Xg{E~|MZl;nWw*{l{0*sQU8c~GFtx>E2Q*2Q8|rE)R=mf)E-lNLG6m< zN$9huE@)(|x+JL{8>{EEMh;oS6&lp@1!l7pdhBD`=?Q*W{zCnZerL3c`5xPzlbXKm z>&15T$=aHA7#)Ii<6FYIp(1rheHzhNU16@)>Q+#9cy?%R2Gosir!+TGeVVCv@W=M3 zHKEoW%~@a$kC^?Rtoj(TImyC(jWi?Ro{A(x`kYbE9abGcE;CZ2$_Nh#8OCrxGiF%L z;WvP+P4zk2m3l@}XnR()&olKg{gkw;w)33&N|Le^dlb}rL(jW3zYSa?;?8&R6M6Vw zsutN@>(r&sKJdRFzUtl{{oRIsOzUXw{t|N<W*l)?8Q#ryr?beTN!Gq}dh8-&C*2 zKikw|wrFeKkS1*J7%Xb{7+ zP4IkywTvVK8aaTp88C1@qrELB9GCE(N}h7W$>8xRdJ~b%%wq{z_FKV&eQE-k5<4~m zHf)OtP|!Frns4B>InbSA@2RJsmS0D~m3X{HWGa>#2Y#PfNfPPFA>9BtC>RIMS4!1vX=&FEeAq5-(*$Ls+$>c4~Vv8mYfeY-BT-v5ssL9fO86 zv$A_J!ZRQR}j^;Z{y7HKvL)#d!aVAl8Wk+U^a|Q%UWOYU&@-%A*vLhPDdYBz^ zti-t?MbvdnH5njNm30Se2#f^^6 z4>STR_^XK>A=iCo=(YcV$gzOkO!S+GEjE3E#|^|k*XxP;{6D6djJ45hfT*b1AIZ9o zfd$Q7da}3(X|yjbZ>t*I3$MCK(XO`uM~idHM$akBcIdZHzg^_oNFDE}uZ{(WiNqa) zjgdxc?Q+NjH3#dzX-nUODp7 zbxwWk-N(PUhJQxSR~xzSFgtxMBn@Ia(qP;9|XUq{YRxV?2Z2ZlDi;HPI+EkP4F>n_% z4DJ!+y+fAHku?{XWq52X+XQY?>~x3~OK6(gJeF8(65Gd_`bO%<%=TB{`4n~P2uSJr zZUO7DFBbCGWw^P>Qg~KltK>Bt54~$UPN#BKX;JifMY*7dvso8svR}YEbo+cQQ(Qup z4F2U#W*k3R-b5_Ny5}NSBdD~^d4sFt8#!%@)D?F*0%o)rMb#?f1%Dm>tHLRD3>_{kXPWN(`xsJPpav zaZSwO9ri`)QmhxA)WTB+%cX5K5l?vn&EI~{!x_3R+xn^F+d5w~_M9*hoXe-4heS3C zv1j^cPQFFBwHahR({gp3N>$~~>Gk!UtK&;q(VffIE4DFY_4#RPpGx*Qf$f-89FzYj zCjq~KU><_f>LFk%Et7 z>}ITK_A?r=6hq-B---D(`S#OyA;^aNgy&zbHQBk6JE8fG3>fCTbxcOE4xi8GHo|f)+gFQ* zCly|UeLt+4b9>7&7d?`inw!~}c?>Za=k_L^E9tqtPsq1a7{=1(jQI&rHFpI08rq?4 z539h#Q*Ip8YxV1H&xhttj&pE$_u%eA{M*6|V-j2Gjj?Yjb<9F)cD~i9V4uXs*k~`? z8^Spq>E>Q`*74yDhOpT#@Ua;+mT)b*~Gtl*ylY_t@ki*J`H%Y zyMEiOj##R?%BgYiDM)A53@mV**p_A4A5yvFSRHsv;a7rgC!dAe&#wFK!M5eg_#5AQ zMgQ}AulO6@dquVPUi;26eS4Pa5Od9`W!Jm9A!_2n-kaojgEI51bW2ZDxuc(KJt42x zI_tHwxp$2$Hk{wht(-o6ovAiSeL?bTEwt1x@fOUzAM4?kzpq}?xGOx<^J2Vj>QY>} z!2RAi_2@HgPo5uxb+2)bdr9N2u@ld3 zo#8F!R~Yevm0`@A#+XJIDLWCiL#r%4=5hZ4lLR!0hk>TC{*i z8@u#y|8Rzt+Ll9l<`w$;9{&GSuY3Ad|9S8lcSEk$)bH~jp+d^#bZ|?bG5*Kp2|ayAwd7O! z`xNi``M*^@#l91Yr%U7%J^@uYBQ5ct0A-LTOx{DD{x7{7IQ{^5+HKEQOw&;m$MNq0 zY@vY21_2qe+-;!(ZdTnmaN?|`48<0qRl&V*MWZqCP2-A*8uyGdaqkhgi9WcuKB#Xl z_}}>CNp3#pcW-ji3zZ=-fe@eU>mw1a0-uTcA9eRB6!+<{l1k#)!S`bVKp@b1m z1d&7$O-o{kC5~3alRzR#v?iGpQb{A7HngQ3?dd>AI$^|wnG7Y z(34)|l1DxT6k??}edtR+`ZIum6fuaw3}Gn63}ZMW7|AF`Q^FX=GLG>~U?MhY_V<5_ zS;8sS^Ogoyvyu($V4LVz!4np-PW0l-D)D0(=XfRtHnNlNeB%dO*v%C#v4=_6Swkt; zaB!Kc+~PVnxXC-pxXW$su$OYavX%$j<31I9;1kQ4OeIs8N)=8v;bI!q)KH5DH`AHH zduCEk9kZCjY>u&+dCX-#3;4)qj*Gtp@RMH>D9w4ub6!Z0v|u0mC0Igu$tzAssDw$l zMDUt7Z08YAc`T6&mO@sw+%-kF(NfG!|$vLyNgaWr1d%S*gzREXo{xuB+VTbWG9Pl!Z!% zrmo0l_qZK8r$(_xtwxDPeG`U~QkUCiw>z9}L;b(D@g4Ku`IIK5S(%~CR9cjsm08Mc zWsWje)1>Y-nG9u><(?Wxsjap`+cH}8Q7dQGe|QrXn|N;6&d+Bm^dJ8B?bvLFq^|^H%9~W0R|=p zP9TrVX*UA{g9C)e%;2(7g|Q6myK#5-Y$*?21XW#j^vOCkdjCs02(L2 i!NjArgYkds2A1B9OhBEiDJ}rKwkDSV0aiF4SO5Sa#0=vA diff --git a/webroot/rsrc/externals/font/aleo/aleo-bold.woff2 b/webroot/rsrc/externals/font/aleo/aleo-bold.woff2 deleted file mode 100644 index 9f914701131049b0298eba7c71ce021b1a1b324b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36404 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 YMMTi8MH7l*J3~DtI=eI>$G5YDNF1U-L;wH) diff --git a/webroot/rsrc/externals/font/aleo/aleo-regular.svg b/webroot/rsrc/externals/font/aleo/aleo-regular.svg deleted file mode 100644 index ec074310d0..0000000000 --- a/webroot/rsrc/externals/font/aleo/aleo-regular.svg +++ /dev/null @@ -1,4644 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/webroot/rsrc/externals/font/aleo/aleo-regular.ttf b/webroot/rsrc/externals/font/aleo/aleo-regular.ttf deleted file mode 100644 index dd20f0c10fd460794c49a77f67c07781eea4186b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85808 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 diff --git a/webroot/rsrc/externals/font/aleo/aleo-regular.woff2 b/webroot/rsrc/externals/font/aleo/aleo-regular.woff2 deleted file mode 100644 index 31f38c898f858c211b7177f6b4ac26e33ed7d346..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35916 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 Date: Wed, 22 Mar 2017 09:53:30 -0700 Subject: [PATCH 024/239] Don't require mentioned objects to have all required fields when editing comments Summary: Fixes T12439. This pathway was just missing a `setContinueOnMissingFields(...)` to skip enforcement of required fields. Test Plan: - Added a required custom field. - Mentioned any task without a field value in a comment. - Edited that comment. - Saved changes. - Before fix: fatal in log. - After fix: clean edit. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12439 Differential Revision: https://secure.phabricator.com/D17536 --- .../editor/PhabricatorApplicationTransactionCommentEditor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php index c50e5d091a..f9db0e238e 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php @@ -92,6 +92,7 @@ final class PhabricatorApplicationTransactionCommentEditor $editor ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) ->applyTransactions($object, $support_xactions); } } From e1ee8ba428e4d000090f364533087f013f1ea6bc Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 09:46:45 -0700 Subject: [PATCH 025/239] Fix a bad getStatus() call which is fataling during Herald rule evaluation Summary: Hit this while `arc diff`'ing something which is triggering 2+ rules which add reviewers, I think. Test Plan: Dug this out of a production stack trace; will push and `arc diff` again. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D17534 --- .../differential/herald/DifferentialReviewersHeraldAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php index bf2b5919c8..9537ad13d3 100644 --- a/src/applications/differential/herald/DifferentialReviewersHeraldAction.php +++ b/src/applications/differential/herald/DifferentialReviewersHeraldAction.php @@ -57,7 +57,7 @@ abstract class DifferentialReviewersHeraldAction // If we're applying a stronger status (usually, upgrading a reviewer // into a blocking reviewer), skip this check so we apply the change. $old_strength = DifferentialReviewerStatus::getStatusStrength( - $reviewers[$phid]->getStatus()); + $reviewers[$phid]->getReviewerStatus()); if ($old_strength <= $new_strength) { continue; } From fab37aa4e35a68d0da22d2a4a80247fcd8b4f3f8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 09:28:36 -0700 Subject: [PATCH 026/239] When accepting revisions, allow users to accept on behalf of a subset of reviewers Summary: Ref T12271. Currenty, when you "Accept" a revision, you always accept it for all reviewers you have authority over. There are some situations where communication can be more clear if users can accept as only themselves, or for only some packages, etc. T12271 discusses some of these use cases in more depth. Instead of making "Accept" a blanket action, default it to doing what it does now but let the user uncheck reviewers. In cases where project/package reviewers aren't in use, this doesn't change anything. For now, "reject" still acts the old way (reject everything). We could make that use checkboxes too, but I'm not sure there's as much of a use case for it, and I generally want users who are blocking stuff to have more direct accountability in a product sense. Test Plan: - Accepted normally. - Accepted a subset. - Tried to accept none. - Tried to accept bogus reviewers. - Accepted with myself not a reviewer - Accepted with only one reviewer (just got normal "this will be accepted" text). {F4251255} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12271 Differential Revision: https://secure.phabricator.com/D17533 --- resources/celerity/map.php | 18 ++-- src/__phutil_library_map__.php | 2 + .../storage/DifferentialReviewer.php | 31 ++++++ .../DifferentialRevisionAcceptTransaction.php | 101 +++++++++++++++++- .../DifferentialRevisionActionTransaction.php | 33 ++++++ .../DifferentialRevisionReviewTransaction.php | 26 +++++ ...catorEditEngineCheckboxesCommentAction.php | 36 +++++++ .../editfield/PhabricatorApplyEditField.php | 29 ++++- .../PhabricatorUSEnglishTranslation.php | 2 + webroot/rsrc/css/phui/phui-form-view.css | 13 +++ webroot/rsrc/js/phuix/PHUIXFormControl.js | 86 +++++++++++++++ 11 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 src/applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f316e2a727..4ca4c327f9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '82aca405', 'conpherence.pkg.js' => '6249a1cf', - 'core.pkg.css' => 'c012c648', + 'core.pkg.css' => 'dc689e29', 'core.pkg.js' => '1fa7c0c5', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '90b30783', @@ -145,7 +145,7 @@ return array( 'rsrc/css/phui/phui-document.css' => 'c32e8dec', 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9', 'rsrc/css/phui/phui-fontkit.css' => '1320ed01', - 'rsrc/css/phui/phui-form-view.css' => 'cf198e10', + 'rsrc/css/phui/phui-form-view.css' => '6175808d', 'rsrc/css/phui/phui-form.css' => 'b62c01d8', 'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f', 'rsrc/css/phui/phui-header-view.css' => '9cf828ce', @@ -530,7 +530,7 @@ return array( 'rsrc/js/phuix/PHUIXActionView.js' => 'b3465b9b', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '7c492cd2', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '8018ee50', - 'rsrc/js/phuix/PHUIXFormControl.js' => 'bbece68d', + 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( @@ -847,7 +847,7 @@ return array( 'phui-font-icon-base-css' => '870a7360', 'phui-fontkit-css' => '1320ed01', 'phui-form-css' => 'b62c01d8', - 'phui-form-view-css' => 'cf198e10', + 'phui-form-view-css' => '6175808d', 'phui-head-thing-view-css' => 'fd311e5f', 'phui-header-view-css' => '9cf828ce', 'phui-hovercard' => '1bd28176', @@ -887,7 +887,7 @@ return array( 'phuix-action-view' => 'b3465b9b', 'phuix-autocomplete' => '7c492cd2', 'phuix-dropdown-menu' => '8018ee50', - 'phuix-form-control-view' => 'bbece68d', + 'phuix-form-control-view' => '83e03671', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', @@ -1518,6 +1518,10 @@ return array( 'javelin-behavior', 'javelin-scrollbar', ), + '83e03671' => array( + 'javelin-install', + 'javelin-dom', + ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', @@ -1902,10 +1906,6 @@ return array( 'javelin-vector', 'javelin-install', ), - 'bbece68d' => array( - 'javelin-install', - 'javelin-dom', - ), 'bcaccd64' => array( 'javelin-behavior', 'javelin-behavior-device', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 66dfda8a65..56150ceb2f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2602,6 +2602,7 @@ phutil_register_library_map(array( 'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php', 'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php', 'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php', + 'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php', 'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php', 'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php', 'PhabricatorEditEngineCommentActionGroup' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentActionGroup.php', @@ -7686,6 +7687,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod', + 'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineCommentAction' => 'Phobject', 'PhabricatorEditEngineCommentActionGroup' => 'Phobject', diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index a0309beddd..eec05398b6 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -50,4 +50,35 @@ final class DifferentialReviewer return ($this->getReviewerStatus() == $status_resigned); } + public function isAccepted($diff_phid) { + $status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED; + + if ($this->getReviewerStatus() != $status_accepted) { + return false; + } + + if (!$diff_phid) { + return true; + } + + $action_phid = $this->getLastActionDiffPHID(); + + if (!$action_phid) { + return true; + } + + if ($action_phid == $diff_phid) { + return true; + } + + $sticky_key = 'differential.sticky-accept'; + $is_sticky = PhabricatorEnv::getEnvConfig($sticky_key); + + if ($is_sticky) { + return true; + } + + return false; + } + } diff --git a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php index 3a64a44767..4f6bedd698 100644 --- a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php @@ -48,6 +48,72 @@ final class DifferentialRevisionAcceptTransaction return pht('Accept a revision.'); } + protected function getActionOptions( + PhabricatorUser $viewer, + DifferentialRevision $revision) { + + $reviewers = $revision->getReviewers(); + + $options = array(); + $value = array(); + + // Put the viewer's user reviewer first, if it exists, so that "Accept as + // yourself" is always at the top. + $head = array(); + $tail = array(); + foreach ($reviewers as $key => $reviewer) { + if ($reviewer->isUser()) { + $head[$key] = $reviewer; + } else { + $tail[$key] = $reviewer; + } + } + $reviewers = $head + $tail; + + $diff_phid = $this->getActiveDiffPHID($revision); + $reviewer_phids = array(); + + // If the viewer isn't a reviewer, add them to the list of options first. + // This happens when you navigate to some revision you aren't involved in: + // you can accept and become a reviewer. + + $viewer_phid = $viewer->getPHID(); + if ($viewer_phid) { + if (!isset($reviewers[$viewer_phid])) { + $reviewer_phids[$viewer_phid] = $viewer_phid; + } + } + + foreach ($reviewers as $reviewer) { + if (!$reviewer->hasAuthority($viewer)) { + // If the viewer doesn't have authority to act on behalf of a reviewer, + // don't include that reviewer as an option. + continue; + } + + if ($reviewer->isAccepted($diff_phid)) { + // If a reviewer is already in a full "accepted" state, don't + // include that reviewer as an option. + continue; + } + + $reviewer_phid = $reviewer->getReviewerPHID(); + $reviewer_phids[$reviewer_phid] = $reviewer_phid; + } + + $handles = $viewer->loadHandles($reviewer_phids); + + foreach ($reviewer_phids as $reviewer_phid) { + $options[$reviewer_phid] = pht( + 'Accept as %s', + $viewer->renderHandle($reviewer_phid)); + + $value[] = $reviewer_phid; + } + + return array($options, $value); + } + public function generateOldValue($object) { $actor = $this->getActor(); return $this->isViewerFullyAccepted($object, $actor); @@ -87,10 +153,39 @@ final class DifferentialRevisionAcceptTransaction } } + protected function validateOptionValue($object, $actor, array $value) { + if (!$value) { + throw new Exception( + pht( + 'When accepting a revision, you must accept on behalf of at '. + 'least one reviewer.')); + } + + list($options) = $this->getActionOptions($actor, $object); + foreach ($value as $phid) { + if (!isset($options[$phid])) { + throw new Exception( + pht( + 'Reviewer "%s" is not a valid reviewer which you have authority '. + 'to accept on behalf of.', + $phid)); + } + } + } + public function getTitle() { - return pht( - '%s accepted this revision.', - $this->renderAuthor()); + $new = $this->getNewValue(); + if (is_array($new) && $new) { + return pht( + '%s accepted this revision as %s reviewer(s): %s.', + $this->renderAuthor(), + phutil_count($new), + $this->renderHandleList($new)); + } else { + return pht( + '%s accepted this revision.', + $this->renderAuthor()); + } } public function getTitleForFeed() { diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index 82198083f8..f232398533 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -19,6 +19,10 @@ abstract class DifferentialRevisionActionTransaction abstract protected function validateAction($object, PhabricatorUser $viewer); abstract protected function getRevisionActionLabel(); + protected function validateOptionValue($object, $actor, array $value) { + return null; + } + public function getCommandKeyword() { return null; } @@ -70,6 +74,15 @@ abstract class DifferentialRevisionActionTransaction return ($viewer->getPHID() === $revision->getAuthorPHID()); } + protected function getActionOptions( + PhabricatorUser $viewer, + DifferentialRevision $revision) { + return array( + array(), + null, + ); + } + public function newEditField( DifferentialRevision $revision, PhabricatorUser $viewer) { @@ -107,6 +120,12 @@ abstract class DifferentialRevisionActionTransaction // It's not clear that these combinations are actually useful, so just // keep things simple for now. $field->setActionConflictKey('revision.action'); + + list($options, $value) = $this->getActionOptions($viewer, $revision); + if (count($options) > 1) { + $field->setOptions($options); + $field->setValue($value); + } } } @@ -129,6 +148,20 @@ abstract class DifferentialRevisionActionTransaction $errors[] = $this->newInvalidError( $action_exception->getMessage(), $xaction); + continue; + } + + $new = $xaction->getNewValue(); + if (!is_array($new)) { + continue; + } + + try { + $this->validateOptionValue($object, $actor, $new); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + $ex->getMessage(), + $xaction); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index 42f644e8d1..a0fe0ea3d0 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -7,6 +7,26 @@ abstract class DifferentialRevisionReviewTransaction return DifferentialRevisionEditEngine::ACTIONGROUP_REVIEW; } + public function generateNewValue($object, $value) { + if (!is_array($value)) { + return true; + } + + // If the list of options is the same as the default list, just treat this + // as a "take the default action" transaction. + $viewer = $this->getActor(); + list($options, $default) = $this->getActionOptions($viewer, $object); + + sort($default); + sort($value); + + if ($default === $value) { + return true; + } + + return $value; + } + protected function isViewerAnyReviewer( DifferentialRevision $revision, PhabricatorUser $viewer) { @@ -118,6 +138,12 @@ abstract class DifferentialRevisionReviewTransaction // In all cases, you affect yourself. $map[$viewer->getPHID()] = $status; + // If the user has submitted a specific list of reviewers to act as (by + // unchecking some checkboxes under "Accept"), only affect those reviewers. + if (is_array($value)) { + $map = array_select_keys($map, $value); + } + // Convert reviewer statuses into edge data. foreach ($map as $reviewer_phid => $reviewer_status) { $map[$reviewer_phid] = array( diff --git a/src/applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php b/src/applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php new file mode 100644 index 0000000000..a149c5c7b2 --- /dev/null +++ b/src/applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php @@ -0,0 +1,36 @@ +options = $options; + return $this; + } + + public function getOptions() { + return $this->options; + } + + public function getPHUIXControlType() { + return 'checkboxes'; + } + + public function getPHUIXControlSpecification() { + $options = $this->getOptions(); + + $labels = array(); + foreach ($options as $key => $option) { + $labels[$key] = hsprintf('%s', $option); + } + + return array( + 'value' => $this->getValue(), + 'keys' => array_keys($options), + 'labels' => $labels, + ); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorApplyEditField.php b/src/applications/transactions/editfield/PhabricatorApplyEditField.php index a292a65021..d349767f94 100644 --- a/src/applications/transactions/editfield/PhabricatorApplyEditField.php +++ b/src/applications/transactions/editfield/PhabricatorApplyEditField.php @@ -5,6 +5,7 @@ final class PhabricatorApplyEditField private $actionDescription; private $actionConflictKey; + private $options; protected function newControl() { return null; @@ -28,8 +29,21 @@ final class PhabricatorApplyEditField return $this->actionConflictKey; } + public function setOptions(array $options) { + $this->options = $options; + return $this; + } + + public function getOptions() { + return $this->options; + } + protected function newHTTPParameterType() { - return new AphrontBoolHTTPParameterType(); + if ($this->getOptions()) { + return new AphrontPHIDListHTTPParameterType(); + } else { + return new AphrontBoolHTTPParameterType(); + } } protected function newConduitParameterType() { @@ -43,9 +57,16 @@ final class PhabricatorApplyEditField } protected function newCommentAction() { - return id(new PhabricatorEditEngineStaticCommentAction()) - ->setDescription($this->getActionDescription()) - ->setConflictKey($this->getActionConflictKey()); + $options = $this->getOptions(); + if ($options) { + return id(new PhabricatorEditEngineCheckboxesCommentAction()) + ->setConflictKey($this->getActionConflictKey()) + ->setOptions($options); + } else { + return id(new PhabricatorEditEngineStaticCommentAction()) + ->setConflictKey($this->getActionConflictKey()) + ->setDescription($this->getActionDescription()); + } } } diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index 428f3955cf..ee1b466dcb 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1608,6 +1608,8 @@ final class PhabricatorUSEnglishTranslation ), ), + '%s accepted this revision as %s reviewer(s): %s.' => + '%s accepted this revision as: %3$s.', ); } diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index 9dce0a4476..da1a524abb 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -548,3 +548,16 @@ properly, and submit values. */ padding: 4px; color: {$bluetext}; } + +.phuix-form-checkbox-action { + padding: 4px; + color: {$bluetext}; +} + +.phuix-form-checkbox-action input[type=checkbox] { + margin: 4px 0; +} + +.phuix-form-checkbox-label { + margin-left: 4px; +} diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js index ce2ba8bf7c..5640b95ae8 100644 --- a/webroot/rsrc/js/phuix/PHUIXFormControl.js +++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js @@ -50,6 +50,9 @@ JX.install('PHUIXFormControl', { case 'static': input = this._newStatic(spec); break; + case 'checkboxes': + input = this._newCheckboxes(spec); + break; default: // TODO: Default or better error? JX.$E('Bad Input Type'); @@ -194,6 +197,89 @@ JX.install('PHUIXFormControl', { }; }, + _newCheckboxes: function(spec) { + var checkboxes = []; + var checkbox_list = []; + for (var ii = 0; ii < spec.keys.length; ii++) { + var key = spec.keys[ii]; + var checkbox_id = 'checkbox-' + Math.floor(Math.random() * 1000000); + + var checkbox = JX.$N( + 'input', + { + type: 'checkbox', + value: key, + id: checkbox_id + }); + + checkboxes.push(checkbox); + + var label = JX.$N( + 'label', + { + className: 'phuix-form-checkbox-label', + htmlFor: checkbox_id + }, + JX.$H(spec.labels[key] || '')); + + var display = JX.$N( + 'div', + { + className: 'phuix-form-checkbox-item' + }, + [checkbox, label]); + + checkbox_list.push(display); + } + + var node = JX.$N( + 'div', + { + className: 'phuix-form-checkbox-action' + }, + checkbox_list); + + var get_value = function() { + var list = []; + for (var ii = 0; ii < checkboxes.length; ii++) { + if (checkboxes[ii].checked) { + list.push(checkboxes[ii].value); + } + } + return list; + }; + + var set_value = function(value) { + value = value || []; + + if (!value.length) { + value = []; + } + + var map = {}; + var ii; + for (ii = 0; ii < value.length; ii++) { + map[value[ii]] = true; + } + + for (ii = 0; ii < checkboxes.length; ii++) { + if (map.hasOwnProperty(checkboxes[ii].value)) { + checkboxes[ii].checked = 'checked'; + } else { + checkboxes[ii].checked = false; + } + } + }; + + set_value(spec.value); + + return { + node: node, + get: get_value, + set: set_value + }; + }, + _newPoints: function(spec) { var attrs = { type: 'text', From aa91dc992e248e6d470c0d0a546081422dd2641b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 11:42:52 -0700 Subject: [PATCH 027/239] Record which user accepted on behalf of packages/owners reviewers Summary: Ref T12271. Don't do anything with this yet, but store who accepted/rejected/whatever on behalf of reviewers. In the future, we could use this to render stuff like "Blessed Committers (accepted by epriestley)" or whatever. I don't know that this is necessarily super useful, but it's easy to track, seems likely to be useful, and would be a gigantic pain to backfill later if we decide we want it. Test Plan: Accepted/rejected a revision, saw reviewers update appropriately. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12271 Differential Revision: https://secure.phabricator.com/D17537 --- resources/sql/autopatches/20170322.reviewers.04.actor.sql | 2 ++ .../differential/storage/DifferentialReviewer.php | 2 ++ .../xaction/DifferentialRevisionReviewTransaction.php | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 resources/sql/autopatches/20170322.reviewers.04.actor.sql diff --git a/resources/sql/autopatches/20170322.reviewers.04.actor.sql b/resources/sql/autopatches/20170322.reviewers.04.actor.sql new file mode 100644 index 0000000000..27b46848a7 --- /dev/null +++ b/resources/sql/autopatches/20170322.reviewers.04.actor.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_reviewer + ADD lastActorPHID VARBINARY(64); diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index eec05398b6..d43f533b5c 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -8,6 +8,7 @@ final class DifferentialReviewer protected $reviewerStatus; protected $lastActionDiffPHID; protected $lastCommentDiffPHID; + protected $lastActorPHID; private $authority = array(); @@ -17,6 +18,7 @@ final class DifferentialReviewer 'reviewerStatus' => 'text64', 'lastActionDiffPHID' => 'phid?', 'lastCommentDiffPHID' => 'phid?', + 'lastActorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_revision' => array( diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index a0fe0ea3d0..86aba03e25 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -198,12 +198,17 @@ abstract class DifferentialRevisionReviewTransaction ->setReviewerPHID($dst_phid); } + $old_status = $reviewer->getReviewerStatus(); $reviewer->setReviewerStatus($status); if ($diff_phid) { $reviewer->setLastActionDiffPHID($diff_phid); } + if ($old_status !== $status) { + $reviewer->setLastActorPHID($this->getActingAsPHID()); + } + try { $reviewer->save(); } catch (AphrontDuplicateKeyQueryException $ex) { From 1953ab98be2bd7a71fbdf504654637d9f02c168f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 Mar 2017 05:41:11 -0700 Subject: [PATCH 028/239] Don't show the "Override Lock" prompt when creating objects Summary: Fixes T12369. When you create objects they may technically be locked: either because the default state is legitimately locked, or because the default policies prevent you from viewing so we sort of technically end in a locked state. Regardless, don't prompt during creation, since this prompt isn't useful even if the lock detection is completely legitimate. Test Plan: - In {nav Applications > Maniphest > Configure}, set "Default View Policy" to "No One". - Tried to create a task. - Before patch: prompted to override lock. - After patch: no override prompt. Reviewers: chad Reviewed By: chad Subscribers: d.maznekov Maniphest Tasks: T12369 Differential Revision: https://secure.phabricator.com/D17541 --- .../transactions/editengine/PhabricatorEditEngine.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 4ad6e39be8..1b891cddee 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -996,8 +996,12 @@ abstract class PhabricatorEditEngine $config = $this->getEditEngineConfiguration() ->attachEngine($this); + // NOTE: Don't prompt users to override locks when creating objects, + // even if the default settings would create a locked object. + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); if (!$can_interact && + !$this->getIsCreate() && !$request->getBool('editEngine') && !$request->getBool('overrideLock')) { From 9326b4d131cebfdb2c3177c174e20ecff508ea79 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 Mar 2017 10:00:05 -0700 Subject: [PATCH 029/239] Fix some range issues and 32-bit issues with avatar generation Summary: Ref T12444. A few issues: - `x % (y - z)` doesn't generate values in the full range: the largest value is never generated. Instead, use `x % (1 + y - z)`. - `digestToRange(1, count)` never generates 0. After fixing the first bug, it could generate `count`. The range of the arrays is `0..(count-1)`, inclusive. Generate the correct range instead. - `unpack('L', ...)` can unpack a negative number on a 32-bit system. Use `& 0x7FFFFFFF` to mask off the sign bit so the result is always a positive integer. - FileFinder might return arbitrary keys, but we rely on sequential keys (0, 1, 2, ...) Test Plan: - Used `bin/people profileimage ... --force` to regenerate images. - Added some debugging to verify that the math seemed to be working. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12444 Differential Revision: https://secure.phabricator.com/D17543 --- .../builtin/PhabricatorFilesComposeAvatarBuiltinFile.php | 9 +++++---- src/infrastructure/util/PhabricatorHash.php | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php index 7eca716e34..4e461ec3f5 100644 --- a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php +++ b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php @@ -119,6 +119,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile foreach ($list as $file) { $map['alphanumeric/'.$file] = $root.$file; } + return $map; } @@ -138,11 +139,11 @@ final class PhabricatorFilesComposeAvatarBuiltinFile $border_seed = $username.'_border'; $pack_key = - PhabricatorHash::digestToRange($pack_seed, 1, $pack_count); + PhabricatorHash::digestToRange($pack_seed, 0, $pack_count - 1); $color_key = - PhabricatorHash::digestToRange($color_seed, 1, $color_count); + PhabricatorHash::digestToRange($color_seed, 0, $color_count - 1); $border_key = - PhabricatorHash::digestToRange($border_seed, 1, $border_count); + PhabricatorHash::digestToRange($border_seed, 0, $border_count - 1); $pack = $pack_map[$pack_key]; $icon = 'alphanumeric/'.$pack.'/'.$file.'.png'; @@ -188,7 +189,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile ->withFollowSymlinks(false) ->find(); - return $map; + return array_values($map); } public static function getBorderMap() { diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index 7b5780460b..05b5fa719d 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -88,9 +88,10 @@ final class PhabricatorHash extends Phobject { } $hash = sha1($string, $raw_output = true); - $value = head(unpack('L', $hash)); + // Make sure this ends up positive, even on 32-bit machines. + $value = head(unpack('L', $hash)) & 0x7FFFFFFF; - return $min + ($value % ($max - $min)); + return $min + ($value % (1 + $max - $min)); } From 9099485a712500c08fc4e00cfb49b5430c2e865d Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Mar 2017 18:58:08 -0700 Subject: [PATCH 030/239] Allow the PullLocal daemon to hibernate, and wake it when repositories need an update Summary: Ref T12298. This allows the PullLocal daemon to hibernate like the Trigger daemon, but automatically wakes it back up when it needs to do something. Test Plan: - Ran `bin/phd debug pulllocal --trace`. - Saw the daemon hibernate after doing a checkup on repositories. - Saw periodic queries to look for new update messages. - After clicking "Update Now" in the web UI to schedule an update, saw the daemon wake up immediately. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12298 Differential Revision: https://secure.phabricator.com/D17540 --- src/__phutil_library_map__.php | 2 + .../PhabricatorRepositoryPullLocalDaemon.php | 13 ++++++- ...ricatorRepositoryPullLocalDaemonModule.php | 38 +++++++++++++++++++ .../PhabricatorDaemonOverseerModule.php | 29 +++----------- 4 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 56150ceb2f..f137d1b221 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3692,6 +3692,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryPullEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPullEventPHIDType.php', 'PhabricatorRepositoryPullEventQuery' => 'applications/repository/query/PhabricatorRepositoryPullEventQuery.php', 'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php', + 'PhabricatorRepositoryPullLocalDaemonModule' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php', 'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php', 'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php', 'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php', @@ -8987,6 +8988,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryPullEventPHIDType' => 'PhabricatorPHIDType', 'PhabricatorRepositoryPullEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon', + 'PhabricatorRepositoryPullLocalDaemonModule' => 'PhutilDaemonOverseerModule', 'PhabricatorRepositoryPushEvent' => array( 'PhabricatorRepositoryDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php index fe19849163..4d4b961765 100644 --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -228,7 +228,10 @@ final class PhabricatorRepositoryPullLocalDaemon continue; } - $this->waitForUpdates($min_sleep, $retry_after); + $should_hibernate = $this->waitForUpdates($min_sleep, $retry_after); + if ($should_hibernate) { + break; + } } } @@ -492,6 +495,10 @@ final class PhabricatorRepositoryPullLocalDaemon while (($sleep_until - time()) > 0) { $sleep_duration = ($sleep_until - time()); + if ($this->shouldHibernate($sleep_duration)) { + return true; + } + $this->log( pht( 'Sleeping for %s more second(s)...', @@ -501,7 +508,7 @@ final class PhabricatorRepositoryPullLocalDaemon if ($this->shouldExit()) { $this->log(pht('Awakened from sleep by graceful shutdown!')); - return; + return false; } if ($this->loadRepositoryUpdateMessages()) { @@ -509,6 +516,8 @@ final class PhabricatorRepositoryPullLocalDaemon break; } } + + return false; } } diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php new file mode 100644 index 0000000000..45dc49d9af --- /dev/null +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php @@ -0,0 +1,38 @@ +getPoolDaemonClass(); + if ($class != 'PhabricatorRepositoryPullLocalDaemon') { + return false; + } + + if ($this->shouldThrottle($class, 1)) { + return false; + } + + $table = new PhabricatorRepositoryStatusMessage(); + $table_name = $table->getTableName(); + $conn = $table->establishConnection('r'); + + $row = queryfx_one( + $conn, + 'SELECT id FROM %T WHERE statusType = %s + AND id > %d ORDER BY id DESC LIMIT 1', + $table_name, + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, + $this->cursor); + + if (!$row) { + return false; + } + + $this->cursor = (int)$row['id']; + return true; + } + +} diff --git a/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php b/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php index aa238a4bf0..608151c348 100644 --- a/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php +++ b/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php @@ -10,18 +10,9 @@ final class PhabricatorDaemonOverseerModule extends PhutilDaemonOverseerModule { private $configVersion; - private $timestamp; - - public function __construct() { - $this->timestamp = PhabricatorTime::getNow(); - } public function shouldReloadDaemons() { - $now = PhabricatorTime::getNow(); - $ago = ($now - $this->timestamp); - - // Don't check more than once every 10 seconds. - if ($ago < 10) { + if ($this->shouldThrottle('reload', 10)) { return false; } @@ -47,25 +38,17 @@ final class PhabricatorDaemonOverseerModule } /** - * Update the configuration version and timestamp. + * Check and update the configuration version. * * @return bool True if the daemons should restart, otherwise false. */ private function updateConfigVersion() { - $config_version = $this->loadConfigVersion(); - $this->timestamp = PhabricatorTime::getNow(); + $old_version = $this->configVersion; + $new_version = $this->loadConfigVersion(); - if (!$this->configVersion) { - $this->configVersion = $config_version; - return false; - } + $this->configVersion = $new_version; - if ($this->configVersion != $config_version) { - $this->configVersion = $config_version; - return true; - } - - return false; + return ($old_version != $new_version); } } From ffab52f17e19cabfa67d1f181e008996dbf3dd5b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 23 Mar 2017 12:04:07 -0700 Subject: [PATCH 031/239] Restrict Differential buckets to just ApplicationSearch views Summary: Ref T9363, If we're in a dashboard panel, only show buckets with data, or a fallback if nothing exists. Test Plan: Test 'active revisions' panel in a dashboard and in Differential. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T9363 Differential Revision: https://secure.phabricator.com/D17544 --- .../query/DifferentialRevisionSearchEngine.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index a925878bda..4248119cb9 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -162,10 +162,13 @@ final class DifferentialRevisionSearchEngine $groups = $bucket->newResultGroups($query, $revisions); foreach ($groups as $group) { - $views[] = id(clone $template) - ->setHeader($group->getName()) - ->setNoDataString($group->getNoDataString()) - ->setRevisions($group->getObjects()); + // Don't show groups in Dashboard Panels + if ($group->getObjects() || !$this->isPanelContext()) { + $views[] = id(clone $template) + ->setHeader($group->getName()) + ->setNoDataString($group->getNoDataString()) + ->setRevisions($group->getObjects()); + } } } catch (Exception $ex) { $this->addError($ex->getMessage()); @@ -176,6 +179,12 @@ final class DifferentialRevisionSearchEngine ->setHandles(array()); } + if (!$views) { + $views[] = id(new DifferentialRevisionListView()) + ->setUser($viewer) + ->setNoDataString(pht('No revisions found.')); + } + $phids = array_mergev(mpull($views, 'getRequiredHandlePHIDs')); if ($phids) { $handles = id(new PhabricatorHandleQuery()) From 4f2bca58fceaff6efd0cd5d8d614949e4fbc864a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 23 Mar 2017 12:22:29 -0700 Subject: [PATCH 032/239] Fix typo in diviner user guide / diffusion Summary: Fixes T12445. Reads better. Test Plan: Read it a few more times. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T12445 Differential Revision: https://secure.phabricator.com/D17546 --- src/docs/user/userguide/diffusion_existing.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/userguide/diffusion_existing.diviner b/src/docs/user/userguide/diffusion_existing.diviner index 56226a84a1..b5b1fbdf76 100644 --- a/src/docs/user/userguide/diffusion_existing.diviner +++ b/src/docs/user/userguide/diffusion_existing.diviner @@ -48,7 +48,7 @@ Once the import completes, disable the **Observe** URI to automatically convert it into a hosted repository. **Push to Empty Repository**: Create an activate an empty repository, then push -all of your changes to empty the repository. +all of your changes to the empty repository. In Git and Mercurial, you can do this with `git push` or `hg push`. From 2707681b48a299df1e468900930e9b545ebf6a1a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 23 Mar 2017 12:46:11 -0700 Subject: [PATCH 033/239] Restrict Audit buckets to just ApplicationSearch views Summary: Fixes T9363. This drops empty buckets from dashboard panel context. Still see full results in Audit. Test Plan: Create an "Active Audits" panel, add to Dashboard. See no commits found. Check Audit, see all buckets. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T9363 Differential Revision: https://secure.phabricator.com/D17545 --- .../query/PhabricatorCommitSearchEngine.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php index 52cfe1f94f..6811427c93 100644 --- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php +++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php @@ -178,10 +178,13 @@ final class PhabricatorCommitSearchEngine $groups = $bucket->newResultGroups($query, $commits); foreach ($groups as $group) { - $views[] = id(clone $template) - ->setHeader($group->getName()) - ->setNoDataString($group->getNoDataString()) - ->setCommits($group->getObjects()); + // Don't show groups in Dashboard Panels + if ($group->getObjects() || !$this->isPanelContext()) { + $views[] = id(clone $template) + ->setHeader($group->getName()) + ->setNoDataString($group->getNoDataString()) + ->setCommits($group->getObjects()); + } } } catch (Exception $ex) { $this->addError($ex->getMessage()); @@ -189,7 +192,13 @@ final class PhabricatorCommitSearchEngine } else { $views[] = id(clone $template) ->setCommits($commits) - ->setNoDataString(pht('No matching commits.')); + ->setNoDataString(pht('No commits found.')); + } + + if (!$views) { + $views[] = id(new PhabricatorAuditListView()) + ->setViewer($viewer) + ->setNoDataString(pht('No commits found.')); } if (count($views) == 1) { From f13637627d65efd68d9158d9a37f4b10b4f080a5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 08:17:08 -0700 Subject: [PATCH 034/239] Improve daemon "waiting" message, config reload behavior Summary: Ref T12298. Two minor daemon improvements: - Make the "waiting" message reflect hibernation. - Don't trigger a reload right after launching. Test Plan: - Read "waiting" message. - Ran "bin/phd start", didn't see an immediate SIGHUP in the log. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12298 Differential Revision: https://secure.phabricator.com/D17550 --- .../controller/PhabricatorDaemonLogViewController.php | 10 ++++------ .../overseer/PhabricatorDaemonOverseerModule.php | 6 ++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index 004ce3a84e..f4c7ba60c9 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -125,12 +125,10 @@ final class PhabricatorDaemonLogViewController case PhabricatorDaemonLog::STATUS_WAIT: $details = pht( 'This daemon is running normally and reported a status update '. - 'recently (within %s). However, it encountered an error while '. - 'doing work and is waiting a little while (%s) to resume '. - 'processing. After encountering an error, daemons wait before '. - 'resuming work to avoid overloading services.', - phutil_format_relative_time($unknown_time), - phutil_format_relative_time($wait_time)); + 'recently (within %s). The process is currently waiting to '. + 'restart, either because it is hibernating or because it '. + 'encountered an error.', + phutil_format_relative_time($unknown_time)); break; case PhabricatorDaemonLog::STATUS_EXITING: $details = pht('This daemon is shutting down gracefully.'); diff --git a/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php b/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php index 608151c348..be5ebec79e 100644 --- a/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php +++ b/src/infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php @@ -48,6 +48,12 @@ final class PhabricatorDaemonOverseerModule $this->configVersion = $new_version; + // Don't trigger a reload if we're loading the config for the very + // first time. + if ($old_version === null) { + return false; + } + return ($old_version != $new_version); } From 0ffde484e5e72e14cf97a20e9634466b44ad12fd Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 09:09:30 -0700 Subject: [PATCH 035/239] Give Daemons a mobile menu Summary: Fixes T12422. Test Plan: {F4269080} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12422 Differential Revision: https://secure.phabricator.com/D17554 --- .../daemon/controller/PhabricatorDaemonController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/daemon/controller/PhabricatorDaemonController.php b/src/applications/daemon/controller/PhabricatorDaemonController.php index a2734907fd..05c850d399 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonController.php @@ -7,6 +7,10 @@ abstract class PhabricatorDaemonController return true; } + public function buildApplicationMenu() { + return $this->buildSideNavView(true)->getMenu(); + } + protected function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); From daeb94561f148c41f131d7e535976b30e892f183 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 09:13:37 -0700 Subject: [PATCH 036/239] When destroying Calendar events, destroy invitees and notifications Summary: Fixes T12395. Test Plan: Ran `bin/remove destroy E... --trace`, saw invitee and notification destruction. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12395 Differential Revision: https://secure.phabricator.com/D17555 --- .../storage/PhabricatorCalendarEvent.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 5b64fdea53..b2e81b0e8b 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1343,7 +1343,21 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO PhabricatorDestructionEngine $engine) { $this->openTransaction(); - $this->delete(); + $invitees = id(new PhabricatorCalendarEventInvitee())->loadAllWhere( + 'eventPHID = %s', + $this->getPHID()); + foreach ($invitees as $invitee) { + $invitee->delete(); + } + + $notifications = id(new PhabricatorCalendarNotification())->loadAllWhere( + 'eventPHID = %s', + $this->getPHID()); + foreach ($notifications as $notification) { + $notification->delete(); + } + + $this->delete(); $this->saveTransaction(); } From 24b6c7d7186afd37652b1ffd2d9ba0d70d792a0c Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 13:02:10 -0700 Subject: [PATCH 037/239] Allow users to resign if they have authority over any reviewer Summary: Ref T11050. The old rule was "you can only resign if you're a reviewer". With the new behavior of "resign", the rule should be "you can resign if you're a reviewer, or you have authority over any reviewer". Make it so. Also fixes T12446. I don't know how to reproduce that but I'm pretty sure this'll fix it? Test Plan: - Could not resign from a revision with no authority/reviewer. - Resigned from a revision with myself as a reviewer. - Resigned from a revision with a package I owned as a reviewer. - Could not resign from a revision I had already resigned from. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12446, T11050 Differential Revision: https://secure.phabricator.com/D17558 --- .../DifferentialRevisionResignTransaction.php | 17 +++++++++++++---- .../DifferentialRevisionReviewTransaction.php | 14 ++++++++++++++ .../DifferentialRevisionTransactionType.php | 3 +++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php index 5b75d7753f..c7805ad35f 100644 --- a/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionResignTransaction.php @@ -44,7 +44,9 @@ final class DifferentialRevisionResignTransaction public function generateOldValue($object) { $actor = $this->getActor(); - return !$this->isViewerAnyReviewer($object, $actor); + $resigned = DifferentialReviewerStatus::STATUS_RESIGNED; + + return ($this->getViewerReviewerStatus($object, $actor) == $resigned); } public function applyExternalEffects($object, $value) { @@ -61,12 +63,19 @@ final class DifferentialRevisionResignTransaction 'been closed. You can only resign from open revisions.')); } - if (!$this->isViewerAnyReviewer($object, $viewer)) { + $resigned = DifferentialReviewerStatus::STATUS_RESIGNED; + if ($this->getViewerReviewerStatus($object, $viewer) == $resigned) { + throw new Exception( + pht( + 'You can not resign from this revision because you have already '. + 'resigned.')); + } + + if (!$this->isViewerAnyAuthority($object, $viewer)) { throw new Exception( pht( 'You can not resign from this revision because you are not a '. - 'reviewer. You can only resign from revisions where you are a '. - 'reviewer.')); + 'reviewer, and do not have authority over any reviewer.')); } } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index 86aba03e25..3db289470a 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -33,6 +33,20 @@ abstract class DifferentialRevisionReviewTransaction return ($this->getViewerReviewerStatus($revision, $viewer) !== null); } + protected function isViewerAnyAuthority( + DifferentialRevision $revision, + PhabricatorUser $viewer) { + + $reviewers = $revision->getReviewers(); + foreach ($revision->getReviewers() as $reviewer) { + if ($reviewer->hasAuthority($viewer)) { + return true; + } + } + + return false; + } + protected function isViewerFullyAccepted( DifferentialRevision $revision, PhabricatorUser $viewer) { diff --git a/src/applications/differential/xaction/DifferentialRevisionTransactionType.php b/src/applications/differential/xaction/DifferentialRevisionTransactionType.php index 6c36d0aaa8..79435bee78 100644 --- a/src/applications/differential/xaction/DifferentialRevisionTransactionType.php +++ b/src/applications/differential/xaction/DifferentialRevisionTransactionType.php @@ -60,6 +60,9 @@ abstract class DifferentialRevisionTransactionType protected function getActiveDiffPHID(DifferentialRevision $revision) { try { $diff = $revision->getActiveDiff(); + if (!$diff) { + return null; + } return $diff->getPHID(); } catch (Exception $ex) { return null; From 3cdabb9588b03cb0360fdaa0d78d0675f086720a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 12:36:02 -0700 Subject: [PATCH 038/239] Provide a hint that submitting a Conduit call shows you how to encode particular parameters Summary: Ref T12447. Test Plan: {F4270003} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12447 Differential Revision: https://secure.phabricator.com/D17557 --- .../conduit/controller/PhabricatorConduitController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index 8588bc07f9..97e1ad0294 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -56,6 +56,12 @@ abstract class PhabricatorConduitController extends PhabricatorController { $panel_link), ); + if ($params === null) { + $messages[] = pht( + 'If you submit parameters, these examples will update to show '. + 'exactly how to encode the parameters you submit.'); + } + $info_view = id(new PHUIInfoView()) ->setErrors($messages) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); From 8b553d2f183b83483deb453c3fa6dd7575a7eeef Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 13:25:22 -0700 Subject: [PATCH 039/239] Allow taskmaster daemons to hibernate Summary: Ref T12298. Like PullLocal daemons, this allows the last daemon in the pool to hibernate if there's no work to be done, and awakens the pool when work arrives. Test Plan: - Ran `bin/phd debug task --trace`. - Saw the pool hibernate and look for tasks. - Commented on an object. - Saw the pool wake up and process the queue. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12298 Differential Revision: https://secure.phabricator.com/D17559 --- src/__phutil_library_map__.php | 2 ++ .../workers/PhabricatorTaskmasterDaemon.php | 5 +++ .../PhabricatorTaskmasterDaemonModule.php | 33 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f137d1b221..30f6020dd2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3989,6 +3989,7 @@ phutil_register_library_map(array( 'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php', 'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php', 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', + 'PhabricatorTaskmasterDaemonModule' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php', 'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php', 'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', 'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php', @@ -9318,6 +9319,7 @@ phutil_register_library_map(array( 'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', + 'PhabricatorTaskmasterDaemonModule' => 'PhutilDaemonOverseerModule', 'PhabricatorTestApplication' => 'PhabricatorApplication', 'PhabricatorTestCase' => 'PhutilTestCase', 'PhabricatorTestController' => 'PhabricatorController', diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php index 49e283c946..6cbbd8698e 100644 --- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php @@ -43,6 +43,11 @@ final class PhabricatorTaskmasterDaemon extends PhabricatorDaemon { $sleep = 0; } else { + + if ($this->shouldHibernate(60)) { + break; + } + // When there's no work, sleep for one second. The pool will // autoscale down if we're continuously idle for an extended period // of time. diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php new file mode 100644 index 0000000000..ddd0e082bb --- /dev/null +++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php @@ -0,0 +1,33 @@ +getPoolDaemonClass(); + + if ($class != 'PhabricatorTaskmasterDaemon') { + return false; + } + + if ($this->shouldThrottle($class, 1)) { + return false; + } + + $table = new PhabricatorWorkerActiveTask(); + $conn = $table->establishConnection('r'); + + $row = queryfx_one( + $conn, + 'SELECT id FROM %T WHERE leaseOwner IS NULL + OR leaseExpires <= %d LIMIT 1', + $table->getTableName(), + PhabricatorTime::getNow()); + if (!$row) { + return false; + } + + return true; + } + +} From 6f80a04699f86b7e75af6ab179e9d9e9cbcb6f02 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 13:59:49 -0700 Subject: [PATCH 040/239] Paginate the profile badges view Summary: Ref T12270. Adds a pager, plus a few little cleanups from copy/paste and accumulated cruft. Test Plan: - Paginated a user with 180 badges. - Viewed a user with 0 badges. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12270 Differential Revision: https://secure.phabricator.com/D17561 --- ...abricatorPeopleProfileBadgesController.php | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php index e96ed4a89e..f3e95eeb66 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php @@ -10,13 +10,7 @@ final class PhabricatorPeopleProfileBadgesController $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($id)) - ->needProfile(true) ->needProfileImage(true) - ->needAvailability(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - )) ->executeOne(); if (!$user) { return new Aphront404Response(); @@ -50,6 +44,7 @@ final class PhabricatorPeopleProfileBadgesController PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) + ->setLimit(1) ->execute(); $button = id(new PHUIButtonView()) @@ -59,7 +54,7 @@ final class PhabricatorPeopleProfileBadgesController ->setWorkflow(true) ->setHref('/badges/award/'.$user->getID().'/'); - if (count($badges)) { + if ($badges) { $header->addActionLink($button); } @@ -80,47 +75,43 @@ final class PhabricatorPeopleProfileBadgesController private function buildBadgesView(PhabricatorUser $user) { $viewer = $this->getViewer(); + $request = $this->getRequest(); - $awards = id(new PhabricatorBadgesAwardQuery()) + $pager = id(new AphrontCursorPagerView()) + ->readFromRequest($request); + + $query = id(new PhabricatorBadgesAwardQuery()) ->setViewer($viewer) ->withRecipientPHIDs(array($user->getPHID())) - ->withBadgeStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) - ->execute(); - $awards = mpull($awards, null, 'getBadgePHID'); + ->withBadgeStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)); - $badges = array(); - foreach ($awards as $award) { - $badge = $award->getBadge(); - $badges[$award->getBadgePHID()] = $badge; - } + $awards = $query->executeWithCursorPager($pager); - if (count($badges)) { + if ($awards) { $flex = new PHUIBadgeBoxView(); + foreach ($awards as $award) { + $badge = $award->getBadge(); - foreach ($badges as $badge) { - if ($badge) { - $awarder_info = array(); + $awarder_info = array(); - $award = idx($awards, $badge->getPHID(), null); - $awarder_phid = $award->getAwarderPHID(); - $awarder_handle = $viewer->renderHandle($awarder_phid); - $awarded_date = phabricator_date($award->getDateCreated(), $viewer); + $awarder_phid = $award->getAwarderPHID(); + $awarder_handle = $viewer->renderHandle($awarder_phid); + $awarded_date = phabricator_date($award->getDateCreated(), $viewer); - $awarder_info = pht( - 'Awarded by %s', - $awarder_handle->render()); + $awarder_info = pht( + 'Awarded by %s', + $awarder_handle->render()); - $item = id(new PHUIBadgeView()) - ->setIcon($badge->getIcon()) - ->setHeader($badge->getName()) - ->setSubhead($badge->getFlavor()) - ->setQuality($badge->getQuality()) - ->setHref($badge->getViewURI()) - ->addByLine($awarder_info) - ->addByLine($awarded_date); + $item = id(new PHUIBadgeView()) + ->setIcon($badge->getIcon()) + ->setHeader($badge->getName()) + ->setSubhead($badge->getFlavor()) + ->setQuality($badge->getQuality()) + ->setHref($badge->getViewURI()) + ->addByLine($awarder_info) + ->addByLine($awarded_date); - $flex->addItem($item); - } + $flex->addItem($item); } } else { $flex = id(new PHUIInfoView()) @@ -128,6 +119,9 @@ final class PhabricatorPeopleProfileBadgesController ->appendChild(pht('User has not been awarded any badges.')); } - return $flex; + return array( + $flex, + $pager, + ); } } From 080bf064c4a3597c00aa06b1263f940d484f654b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 14:02:18 -0700 Subject: [PATCH 041/239] Remove obsolete Badges edge types Summary: Ref T12270. These no longer have any callsites. Test Plan: Used `grep` to search for each edge class constant, found no hits. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12270 Differential Revision: https://secure.phabricator.com/D17562 --- src/__phutil_library_map__.php | 4 - .../PhabricatorBadgeHasRecipientEdgeType.php | 103 ------------------ .../PhabricatorRecipientHasBadgeEdgeType.php | 103 ------------------ 3 files changed, 210 deletions(-) delete mode 100644 src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php delete mode 100644 src/applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 30f6020dd2..270fb84194 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2012,7 +2012,6 @@ phutil_register_library_map(array( 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', 'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php', - 'PhabricatorBadgeHasRecipientEdgeType' => 'applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php', 'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php', 'PhabricatorBadgesArchiveController' => 'applications/badges/controller/PhabricatorBadgesArchiveController.php', 'PhabricatorBadgesAward' => 'applications/badges/storage/PhabricatorBadgesAward.php', @@ -3623,7 +3622,6 @@ phutil_register_library_map(array( 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', - 'PhabricatorRecipientHasBadgeEdgeType' => 'applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php', 'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php', @@ -6998,7 +6996,6 @@ phutil_register_library_map(array( 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAutoEventListener' => 'PhabricatorEventListener', - 'PhabricatorBadgeHasRecipientEdgeType' => 'PhabricatorEdgeType', 'PhabricatorBadgesApplication' => 'PhabricatorApplication', 'PhabricatorBadgesArchiveController' => 'PhabricatorBadgesController', 'PhabricatorBadgesAward' => array( @@ -8878,7 +8875,6 @@ phutil_register_library_map(array( ), 'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', - 'PhabricatorRecipientHasBadgeEdgeType' => 'PhabricatorEdgeType', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', 'PhabricatorRegistrationProfile' => 'Phobject', diff --git a/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php b/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php deleted file mode 100644 index 9b6db5a63b..0000000000 --- a/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php +++ /dev/null @@ -1,103 +0,0 @@ - Date: Fri, 24 Mar 2017 21:15:42 +0000 Subject: [PATCH 042/239] Funbeta Badges Summary: Ships Badges. I can write up some basic docs too if needed. Test Plan: /applications/ Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T12270 Differential Revision: https://secure.phabricator.com/D17360 --- .../badges/application/PhabricatorBadgesApplication.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/applications/badges/application/PhabricatorBadgesApplication.php b/src/applications/badges/application/PhabricatorBadgesApplication.php index 6eab0c55f8..4df412a6fc 100644 --- a/src/applications/badges/application/PhabricatorBadgesApplication.php +++ b/src/applications/badges/application/PhabricatorBadgesApplication.php @@ -26,10 +26,6 @@ final class PhabricatorBadgesApplication extends PhabricatorApplication { return self::GROUP_UTILITIES; } - public function isPrototype() { - return true; - } - public function getRoutes() { return array( '/badges/' => array( From b4effdf26c3e7d5de0d010cf14626c5d8d404e04 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 24 Mar 2017 16:58:48 -0700 Subject: [PATCH 043/239] Fix a rendering fatal for unknown edge constants If we try to render an edge transaction which uses unknown edge constants, it turns out we fatal. Degrade instead. This happened when viewing very old badges. Auditors: chad --- .../storage/PhabricatorApplicationTransaction.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index d2250b2b5d..5e36e942ce 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -932,7 +932,15 @@ abstract class PhabricatorApplicationTransaction $type = $this->getMetadata('edge:type'); $type = head($type); - $type_obj = PhabricatorEdgeType::getByConstant($type); + try { + $type_obj = PhabricatorEdgeType::getByConstant($type); + } catch (Exception $ex) { + // Recover somewhat gracefully from edge transactions which + // we don't have the classes for. + return pht( + '%s edited an edge.', + $this->renderHandleLink($author_phid)); + } if ($add && $rem) { return $type_obj->getTransactionEditString( From 2cda280cde20cfa23f110f09f45197cb8166fe31 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 25 Mar 2017 04:14:32 -0700 Subject: [PATCH 044/239] Make the default Trigger hibernation 3 minutes instead of 5 seconds The `min()` vs `max()` fix in D17560 meant that the Trigger daemon only hibernates for 5 seconds, so we do a full GC sweep every 5 seconds. This ends up eating a fair amount of CPU for no real benefit. The GC cursors should move to persistent storage, but just bump this default up in the meantime. Auditors: chad --- src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php index 02ac55a160..9561f3d18a 100644 --- a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php @@ -275,7 +275,7 @@ final class PhabricatorTriggerDaemon * @return int Number of seconds to sleep for. */ private function getSleepDuration() { - $sleep = 5; + $sleep = phutil_units('3 minutes in seconds'); $next_triggers = id(new PhabricatorWorkerTriggerQuery()) ->setViewer($this->getViewer()) From a41d158490c0cd0a0454653473c39f7ad2b5954f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 25 Mar 2017 05:01:32 -0700 Subject: [PATCH 045/239] Only hibernate the Taskmaster after 15 seconds of inactivity Under some workloads, the taskmaster may hibernate and launch more rapidly than it should. Require 15 seconds of inactivity before hibernating. Also hibernate for longer. Auditors: chad --- .../daemon/workers/PhabricatorTaskmasterDaemon.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php index 6cbbd8698e..57a69843a4 100644 --- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php @@ -44,8 +44,11 @@ final class PhabricatorTaskmasterDaemon extends PhabricatorDaemon { $sleep = 0; } else { - if ($this->shouldHibernate(60)) { - break; + if ($this->getIdleDuration() > 15) { + $hibernate_duration = phutil_units('3 minutes in seconds'); + if ($this->shouldHibernate($hibernate_duration)) { + break; + } } // When there's no work, sleep for one second. The pool will From e41c25de5050d69b720424dadbe3d8680362ceaf Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Sun, 26 Mar 2017 08:16:47 +0000 Subject: [PATCH 046/239] Support multiple fulltext search clusters with 'cluster.search' config Summary: The goal is to make fulltext search back-ends more extensible, configurable and robust. When this is finished it will be possible to have multiple search storage back-ends and potentially multiple instances of each. Individual instances can be configured with roles such as 'read', 'write' which control which hosts will receive writes to the index and which hosts will respond to queries. These two roles make it possible to have any combination of: * read-only * write-only * read-write * disabled This 'roles' mechanism is extensible to add new roles should that be needed in the future. In addition to supporting multiple elasticsearch and mysql search instances, this refactors the connection health monitoring infrastructure from PhabricatorDatabaseHealthRecord and utilizes the same system for monitoring the health of elasticsearch nodes. This will allow Wikimedia's phabricator to be redundant across data centers (mysql already is, elasticsearch should be as well). The real-world use-case I have in mind here is writing to two indexes (two elasticsearch clusters in different data centers) but reading from only one. Then toggling the 'read' property when we want to migrate to the other data center (and when we migrate from elasticsearch 2.x to 5.x) Hopefully this is useful in the upstream as well. Remaining TODO: * test cases * documentation Test Plan: (WARNING) This will most likely require the elasticsearch index to be deleted and re-created due to schema changes. Tested with elasticsearch versions 2.4 and 5.2 using the following config: ```lang=json "cluster.search": [ { "type": "elasticsearch", "hosts": [ { "host": "localhost", "roles": { "read": true, "write": true } } ], "port": 9200, "protocol": "http", "path": "/phabricator", "version": 5 }, { "type": "mysql", "roles": { "write": true } } ] Also deployed the same changes to Wikimedia's production Phabricator instance without any issues whatsoever. ``` Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Tags: #elasticsearch, #clusters, #wikimedia Differential Revision: https://secure.phabricator.com/D17384 --- .../20161130.search.02.rebuild.php | 12 +- src/__phutil_library_map__.php | 33 +- .../PhabricatorConfigApplication.php | 1 + .../PhabricatorElasticSearchSetupCheck.php | 113 ++--- .../PhabricatorExtraConfigSetupCheck.php | 8 + .../check/PhabricatorMySQLSetupCheck.php | 9 +- ...abricatorConfigClusterSearchController.php | 129 ++++++ .../PhabricatorConfigController.php | 3 + .../PhabricatorClusterConfigOptions.php | 26 ++ .../maniphest/query/ManiphestTaskQuery.php | 8 +- .../PhabricatorProjectFulltextEngine.php | 10 +- .../config/PhabricatorSearchConfigOptions.php | 35 -- .../PhabricatorSearchDocumentFieldType.php | 1 + .../PhabricatorSearchEngineTestCase.php | 4 +- ...habricatorElasticFulltextStorageEngine.php | 413 +++++++++++------- .../PhabricatorElasticSearchQueryBuilder.php | 78 ++++ .../PhabricatorFulltextStorageEngine.php | 98 ++--- .../PhabricatorMySQLFulltextStorageEngine.php | 13 +- .../index/PhabricatorFulltextEngine.php | 3 +- ...habricatorSearchManagementInitWorkflow.php | 50 ++- .../query/PhabricatorSearchDocumentQuery.php | 5 +- src/docs/user/cluster/cluster_search.diviner | 76 ++++ ...PhabricatorClusterServiceHealthRecord.php} | 22 +- .../cluster/PhabricatorDatabaseRef.php | 12 +- ...icatorClusterDatabasesConfigOptionType.php | 0 ...abricatorClusterSearchConfigOptionType.php | 79 ++++ .../PhabricatorClusterException.php | 0 .../PhabricatorClusterExceptionHandler.php | 0 ...ricatorClusterImpossibleWriteException.php | 0 ...abricatorClusterImproperWriteException.php | 0 ...abricatorClusterNoHostForRoleException.php | 10 + .../PhabricatorClusterStrandedException.php | 0 .../search/PhabricatorElasticSearchHost.php | 82 ++++ .../search/PhabricatorMySQLSearchHost.php | 34 ++ .../cluster/search/PhabricatorSearchHost.php | 163 +++++++ .../search/PhabricatorSearchService.php | 259 +++++++++++ 36 files changed, 1411 insertions(+), 378 deletions(-) create mode 100644 src/applications/config/controller/PhabricatorConfigClusterSearchController.php delete mode 100644 src/applications/search/config/PhabricatorSearchConfigOptions.php create mode 100644 src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php create mode 100644 src/docs/user/cluster/cluster_search.diviner rename src/infrastructure/cluster/{PhabricatorDatabaseHealthRecord.php => PhabricatorClusterServiceHealthRecord.php} (89%) rename src/infrastructure/cluster/{ => config}/PhabricatorClusterDatabasesConfigOptionType.php (100%) create mode 100644 src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php rename src/infrastructure/cluster/{ => exception}/PhabricatorClusterException.php (100%) rename src/infrastructure/cluster/{ => exception}/PhabricatorClusterExceptionHandler.php (100%) rename src/infrastructure/cluster/{ => exception}/PhabricatorClusterImpossibleWriteException.php (100%) rename src/infrastructure/cluster/{ => exception}/PhabricatorClusterImproperWriteException.php (100%) create mode 100644 src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php rename src/infrastructure/cluster/{ => exception}/PhabricatorClusterStrandedException.php (100%) create mode 100644 src/infrastructure/cluster/search/PhabricatorElasticSearchHost.php create mode 100644 src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php create mode 100644 src/infrastructure/cluster/search/PhabricatorSearchHost.php create mode 100644 src/infrastructure/cluster/search/PhabricatorSearchService.php diff --git a/resources/sql/autopatches/20161130.search.02.rebuild.php b/resources/sql/autopatches/20161130.search.02.rebuild.php index a5a9755839..d179c44c30 100644 --- a/resources/sql/autopatches/20161130.search.02.rebuild.php +++ b/resources/sql/autopatches/20161130.search.02.rebuild.php @@ -1,7 +1,15 @@ getEngine(); + if ($engine instanceof PhabricatorMySQLFulltextStorageEngine) { + $use_mysql = true; + } +} if ($use_mysql) { $field = new PhabricatorSearchDocumentField(); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 270fb84194..7c233cb93a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2259,12 +2259,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 +2313,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 +2547,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,6 +2654,8 @@ phutil_register_library_map(array( 'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php', 'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php', 'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.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', @@ -3073,6 +3078,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', @@ -3762,7 +3768,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', @@ -3785,6 +3790,7 @@ phutil_register_library_map(array( '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 +3810,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', @@ -7303,6 +7310,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 +7364,7 @@ phutil_register_library_map(array( 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController', + 'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController', 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', @@ -7624,7 +7635,6 @@ phutil_register_library_map(array( 'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController', 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', - 'PhabricatorDatabaseHealthRecord' => 'Phobject', 'PhabricatorDatabaseRef' => 'Phobject', 'PhabricatorDatabaseRefParser' => 'Phobject', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', @@ -7738,6 +7748,7 @@ phutil_register_library_map(array( 'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting', 'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', + 'PhabricatorElasticSearchHost' => 'PhabricatorSearchHost', 'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailContentSource' => 'PhabricatorContentSource', @@ -8208,6 +8219,7 @@ phutil_register_library_map(array( 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', + 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', 'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorNamedQuery' => array( 'PhabricatorSearchDAO', @@ -9074,7 +9086,6 @@ phutil_register_library_map(array( 'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', - 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSearchConstraintException' => 'Exception', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField', @@ -9097,6 +9108,7 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorSearchField' => 'Phobject', + 'PhabricatorSearchHost' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', @@ -9116,6 +9128,7 @@ phutil_register_library_map(array( 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', + 'PhabricatorSearchService' => 'Phobject', 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchTextField' => 'PhabricatorSearchField', diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php index 6b2704b0b4..510cb6f76d 100644 --- a/src/applications/config/application/PhabricatorConfigApplication.php +++ b/src/applications/config/application/PhabricatorConfigApplication.php @@ -69,6 +69,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication { 'databases/' => 'PhabricatorConfigClusterDatabasesController', 'notifications/' => 'PhabricatorConfigClusterNotificationsController', 'repositories/' => 'PhabricatorConfigClusterRepositoriesController', + 'search/' => 'PhabricatorConfigClusterSearchController', ), ), ); diff --git a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php index f137f2527f..d8864b5740 100644 --- a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php +++ b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php @@ -7,71 +7,74 @@ final class PhabricatorElasticSearchSetupCheck extends PhabricatorSetupCheck { } protected function executeChecks() { - if (!$this->shouldUseElasticSearchEngine()) { - return; - } + $services = PhabricatorSearchService::getAllServices(); - $engine = new PhabricatorElasticFulltextStorageEngine(); - - $index_exists = null; - $index_sane = null; - try { - $index_exists = $engine->indexExists(); - if ($index_exists) { - $index_sane = $engine->indexIsSane(); + foreach ($services as $service) { + try { + $host = $service->getAnyHostForRole('read'); + } catch (PhabricatorClusterNoHostForRoleException $e) { + // ignore the error + continue; } - } 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())); + 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('search.elastic.host'); - return; - } + $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.'); + 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.'); + $message = pht( + 'You likely enabled cluster.search 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.'); + $this + ->newIssue('elastic.missing-index') + ->setName(pht('Elasticsearch index Not Found')) + ->setSummary($summary) + ->setMessage($message) + ->addRelatedPhabricatorConfig('cluster.search'); + } 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.'); + $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); + $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); - } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index f607610684..84fd5bedf2 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -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; diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php index 152af61bf4..a9f6a77cb7 100644 --- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php +++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php @@ -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; } } diff --git a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php new file mode 100644 index 0000000000..4d3ce407ab --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php @@ -0,0 +1,129 @@ +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; + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index 5ad0ecbbf8..2abf2b3b31 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -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.'/', diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php index bcf498c32b..c3636c31e0 100644 --- a/src/applications/config/option/PhabricatorClusterConfigOptions.php +++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php @@ -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(<<newOption('cluster.addresses', 'list', 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, + ), + ), + )), ); } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index d95fd8c2cf..0f55ce8bdd 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -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 + // 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', 100000); + $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); diff --git a/src/applications/project/search/PhabricatorProjectFulltextEngine.php b/src/applications/project/search/PhabricatorProjectFulltextEngine.php index f0940286e5..14314c3436 100644 --- a/src/applications/project/search/PhabricatorProjectFulltextEngine.php +++ b/src/applications/project/search/PhabricatorProjectFulltextEngine.php @@ -10,7 +10,15 @@ final class PhabricatorProjectFulltextEngine $project = $object; $project->updateDatasourceTokens(); - $document->setDocumentTitle($project->getName()); + $document->setDocumentTitle($project->getDisplayName()); + $document->addField(PhabricatorSearchDocumentFieldType::FIELD_KEYWORDS, + $project->getPrimarySlug()); + try { + $slugs = $project->getSlugs(); + foreach ($slugs as $slug) {} + } catch (PhabricatorDataNotAttachedException $e) { + // ignore + } $document->addRelationship( $project->isArchived() diff --git a/src/applications/search/config/PhabricatorSearchConfigOptions.php b/src/applications/search/config/PhabricatorSearchConfigOptions.php deleted file mode 100644 index 2f2cc4f902..0000000000 --- a/src/applications/search/config/PhabricatorSearchConfigOptions.php +++ /dev/null @@ -1,35 +0,0 @@ -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')), - ); - } - -} diff --git a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php index 10dbf0ca65..12c90f8469 100644 --- a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php +++ b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php @@ -5,5 +5,6 @@ final class PhabricatorSearchDocumentFieldType extends Phobject { const FIELD_TITLE = 'titl'; const FIELD_BODY = 'body'; const FIELD_COMMENT = 'cmnt'; + const FIELD_KEYWORDS = 'kwrd'; } diff --git a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php index b535d4f5cf..f5dbd9ef9c 100644 --- a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php +++ b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php @@ -3,8 +3,8 @@ final class PhabricatorSearchEngineTestCase extends PhabricatorTestCase { public function testLoadAllEngines() { - PhabricatorFulltextStorageEngine::loadAllEngines(); - $this->assertTrue(true); + $services = PhabricatorSearchService::getAllServices(); + $this->assertTrue(!empty($services)); } } diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index ee067b942d..bc32da5ef4 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -1,37 +1,52 @@ 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; + /** + * @return PhabricatorElasticSearchHost + */ + public function getHostForRead() { + return $this->getService()->getAnyHostForRole('read'); + } + + /** + * @return PhabricatorElasticSearchHost + */ + public function getHostForWrite() { + return $this->getService()->getAnyHostForRole('write'); } public function setTimeout($timeout) { @@ -39,21 +54,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 +76,47 @@ final class PhabricatorElasticFulltextStorageEngine ->withPHIDs(array($phid)) ->executeOne(); + $timestamp_key = $this->getTimestampField(); + // URL is not used internally but it can be useful externally. $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 +129,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 +150,51 @@ 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( + '_all', + ), + 'default_operator' => 'OR', ), - ); + )); - $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( + PhabricatorSearchDocumentFieldType::FIELD_TITLE.'^4', + PhabricatorSearchDocumentFieldType::FIELD_BODY.'^3', + PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'^1.2', + ), + 'analyzer' => 'english_exact', + 'default_operator' => 'and', ), - ); + )); + } $exclude = $query->getParameter('exclude'); if ($exclude) { - $filter[] = array( + $q->addFilterClause(array( 'not' => array( 'ids' => array( 'values' => array($exclude), ), ), - ); + )); } $relationship_map = array( @@ -176,75 +219,59 @@ final class PhabricatorElasticFulltextStorageEngine $include_closed = !empty($statuses[$rel_closed]); if ($include_open && !$include_closed) { - $relationship_map[$rel_open] = true; + $q->addExistsClause($rel_open); } else if (!$include_open && $include_closed) { - $relationship_map[$rel_closed] = true; + $q->addExistsClause($rel_closed); } if ($query->getParameter('withUnowned')) { - $relationship_map[$rel_unowned] = true; + $q->addExistsClause($rel_unowned); } $rel_owner = PhabricatorSearchRelationship::RELATIONSHIP_OWNER; if ($query->getParameter('withAnyOwner')) { - $relationship_map[$rel_owner] = true; + $q->addExistsClause($rel_owner); } else { $owner_phids = $query->getParameter('ownerPHIDs', array()); - $relationship_map[$rel_owner] = $owner_phids; - } - - foreach ($relationship_map as $field => $param) { - if (is_array($param) && $param) { - $should = array(); - foreach ($param as $val) { - $should[] = array( - 'match' => array( - "relationship.{$field}.phid" => array( - 'query' => $val, - 'type' => 'phrase', - ), - ), - ); - } - // We couldn't solve it by minimum_number_should_match because it can - // match multiple owners without matching author. - $spec[] = array('bool' => array('should' => $should)); - } else if ($param) { - $filter[] = array( - 'exists' => array( - 'field' => "relationship.{$field}.phid", - ), - ); + if (count($owner_phids)) { + $q->addTermsClause($rel_owner, $owner_phids); } } - if ($spec) { - $spec = array('query' => array('bool' => array('must' => $spec))); - if ($title_spec) { - $spec['query']['bool']['should'] = $title_spec; + foreach ($relationship_map as $field => $phids) { + if (is_array($phids) && !empty($phids)) { + $q->addTermsClause($field, $phids); } } - if ($filter) { - $filter = array('filter' => array('and' => $filter)); - if (!$spec) { - $spec = array('query' => array('match_all' => new stdClass())); - } - $spec = array( - 'query' => array( - 'filtered' => $spec + $filter, - ), - ); + if (!$q->getClauseCount('must')) { + $q->addMustClause(array('match_all' => array('boost' => 1 ))); } + $spec = array( + '_source' => false, + 'query' => array( + 'bool' => $q->toArray(), + ), + ); + + if (!$query->getParameter('query')) { $spec['sort'] = array( array('dateCreated' => 'desc'), ); } - $spec['from'] = (int)$query->getParameter('offset', 0); - $spec['size'] = (int)$query->getParameter('limit', 25); + $offset = (int)$query->getParameter('offset', 0); + $limit = (int)$query->getParameter('limit', 101); + if ($offset + $limit > 10000) { + throw new Exception(pht( + 'Query offset is too large. offset+limit=%s (max=%s)', + $offset + $limit, + 10000)); + } + $spec['from'] = $offset; + $spec['size'] = $limit; return $spec; } @@ -261,30 +288,36 @@ final class PhabricatorElasticFulltextStorageEngine // some bigger index). Use '/$types/_search' instead. $uri = '/'.implode(',', $types).'/_search'; - try { - $response = $this->executeRequest($uri, $this->buildSpec($query)); - } catch (HTTPFutureHTTPResponseStatus $ex) { - // elasticsearch probably uses Lucene query syntax: - // http://lucene.apache.org/core/3_6_1/queryparsersyntax.html - // Try literal search if operator search fails. - if (!strlen($query->getParameter('query'))) { - throw $ex; - } - $query = clone $query; - $query->setParameter( - 'query', - addcslashes( - $query->getParameter('query'), '+-&|!(){}[]^"~*?:\\')); - $response = $this->executeRequest($uri, $this->buildSpec($query)); - } + $spec = $this->buildSpec($query); + $exceptions = array(); - $phids = ipull($response['hits']['hits'], '_id'); - return $phids; + foreach ($this->service->getAllHostsForRole('read') as $host) { + try { + $response = $this->executeRequest($host, $uri, $spec); + $phids = ipull($response['hits']['hits'], '_id'); + return $phids; + } catch (Exception $e) { + $exceptions[] = $e; + } + } + throw new PhutilAggregateException('All 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; @@ -299,53 +332,85 @@ final class PhabricatorElasticFulltextStorageEngine 'index' => array( 'auto_expand_replicas' => '0-2', 'analysis' => array( - 'filter' => array( - 'trigrams_filter' => array( - 'min_gram' => 3, - 'type' => 'ngram', - 'max_gram' => 3, - ), - ), 'analyzer' => array( - 'custom_trigrams' => array( - 'type' => 'custom', - 'filter' => array( - 'lowercase', - 'kstem', - 'trigrams_filter', - ), + 'english_exact' => array( 'tokenizer' => 'standard', + 'filter' => array('lowercase'), ), ), ), ), ); - $types = array_keys( + $fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType'); + $relationships = $this->getTypeConstants('PhabricatorSearchRelationship'); + + $doc_types = array_keys( PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes()); - foreach ($types as $type) { - // Use the custom trigram analyzer for the corpus of text - $data['mappings'][$type]['properties']['field']['properties']['corpus'] = - array('type' => 'string', 'analyzer' => 'custom_trigrams'); + + $text_type = $this->getTextFieldType(); + + foreach ($doc_types as $type) { + $properties = array(); + foreach ($fields as $field) { + // Use the custom analyzer for the corpus of text + $properties[$field] = array( + 'type' => $text_type, + 'analyzer' => 'english_exact', + 'search_analyzer' => 'english', + 'search_quote_analyzer' => 'english_exact', + ); + } + + if ($this->version < 5) { + foreach ($relationships as $rel) { + $properties[$rel] = array( + 'type' => 'string', + 'index' => 'not_analyzed', + 'include_in_all' => false, + ); + $properties[$rel.'_ts'] = array( + 'type' => 'date', + 'include_in_all' => false, + ); + } + } else { + foreach ($relationships as $rel) { + $properties[$rel] = array( + 'type' => 'keyword', + 'include_in_all' => false, + 'doc_values' => false, + ); + $properties[$rel.'_ts'] = array( + 'type' => 'date', + 'include_in_all' => false, + ); + } + } // Ensure we have dateCreated since the default query requires it - $data['mappings'][$type]['properties']['dateCreated']['type'] = 'string'; - } + $properties['dateCreated']['type'] = 'date'; + $properties['lastModified']['type'] = 'date'; + $data['mappings'][$type]['properties'] = $properties; + } return $data; } - public function indexIsSane() { - if (!$this->indexExists()) { + public function indexIsSane(PhabricatorElasticSearchHost $host = null) { + if (!$host) { + $host = $this->getHostForRead(); + } + if (!$this->indexExists($host)) { return false; } - - $cur_mapping = $this->executeRequest('/_mapping/', array()); - $cur_settings = $this->executeRequest('/_settings/', array()); + $cur_mapping = $this->executeRequest($host, '/_mapping/', array()); + $cur_settings = $this->executeRequest($host, '/_settings/', array()); $actual = array_merge($cur_settings[$this->index], $cur_mapping[$this->index]); - return $this->check($actual, $this->getIndexConfiguration()); + $res = $this->check($actual, $this->getIndexConfiguration()); + return $res; } /** @@ -355,7 +420,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 +434,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,19 +468,44 @@ 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/'; + $host = $this->getHostForRead(); + $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 = json_encode($data); $future = new HTTPSFuture($uri, $data); if ($method != 'GET') { $future->setMethod($method); @@ -423,19 +513,30 @@ final class PhabricatorElasticFulltextStorageEngine if ($this->getTimeout()) { $future->setTimeout($this->getTimeout()); } - list($body) = $future->resolvex(); + try { + list($body) = $future->resolvex(); + } catch (HTTPFutureResponseStatus $ex) { + if ($ex->isTimeout() || (int)$ex->getStatusCode() > 499) { + $host->didHealthCheck(false); + } + throw $ex; + } if ($method != 'GET') { return null; } try { - return phutil_json_decode($body); + $data = phutil_json_decode($body); + $host->didHealthCheck(true); + return $data; } catch (PhutilJSONParserException $ex) { + $host->didHealthCheck(false); throw new PhutilProxyException( pht('ElasticSearch server returned invalid JSON!'), $ex); } + } } diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php b/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php new file mode 100644 index 0000000000..659660d813 --- /dev/null +++ b/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php @@ -0,0 +1,78 @@ +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; + } + +} diff --git a/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php index beae237168..5e919258bd 100644 --- a/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php @@ -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()); - } - } diff --git a/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php index c30c74139e..72c49576f0 100644 --- a/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php @@ -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; + } + } diff --git a/src/applications/search/index/PhabricatorFulltextEngine.php b/src/applications/search/index/PhabricatorFulltextEngine.php index 64cbe4ebb5..9f20917b3f 100644 --- a/src/applications/search/index/PhabricatorFulltextEngine.php +++ b/src/applications/search/index/PhabricatorFulltextEngine.php @@ -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) { diff --git a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php index 4c35b61dd5..1b5da49e66 100644 --- a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php @@ -13,27 +13,41 @@ final class PhabricatorSearchManagementInitWorkflow public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); - $engine = PhabricatorFulltextStorageEngine::loadEngine(); - $work_done = false; - if (!$engine->indexExists()) { - $console->writeOut( - '%s', - pht('Index does not exist, creating...')); - $engine->initIndex(); + foreach (PhabricatorSearchService::getAllServices() as $service) { $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; + pht('Initializing search service "%s"', $service->getDisplayName())); + + try { + $host = $service->getAnyHostForRole('write'); + } catch (PhabricatorClusterNoHostForRoleException $e) { + // If there are no writable hosts for a given cluster, skip it + $console->writeOut("%s\n", $e->getMessage()); + continue; + } + + $engine = $host->getEngine(); + + 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) { diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php index 002c3364af..d4700904c9 100644 --- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php +++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php @@ -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() { diff --git a/src/docs/user/cluster/cluster_search.diviner b/src/docs/user/cluster/cluster_search.diviner new file mode 100644 index 0000000000..662abecbc3 --- /dev/null +++ b/src/docs/user/cluster/cluster_search.diviner @@ -0,0 +1,76 @@ +@title Cluster: Search +@group cluster + +Overview +======== + +You can configure phabricator to connect to one or more fulltext search clusters +running either Elasticsearch or MySQL. By default and without further +configuration, Phabricator will use MySQL for fulltext search. This will be +adequate for the vast majority of users. Installs with a very large number of +objects or specialized search needs can consider enabling Elasticsearch for +better scalability and potentially better search results. + +Configuring Search Services +=========================== + +To configure an Elasticsearch service, use the `cluster.search` configuration +option. A typical Elasticsearch configuration will probably look similar to +the following example: + +```lang=json +{ + "cluster.search": [ + { + "type": "elasticsearch", + "hosts": [ + { + "host": "127.0.0.1", + "roles": { "write": true, "read": true } + } + ], + "port": 9200, + "protocol": "http", + "path": "/phabricator", + "version": 5 + }, + ], +} +``` + +Supported Options +----------------- +| Key | Type |Comments| +|`type` | String |Engine type. Currently, 'elasticsearch' or 'mysql'| +|`protocol`| String |Either 'http' or 'https'| +|`port`| Int |The TCP port that Elasticsearch is bound to| +|`path`| String |The path portion of the url for phabricator's index.| +|`version`| Int |The version of Elasticsearch server. Supports either 2 or 5.| +|`hosts`| List |A list of one or more Elasticsearch host names / addresses.| + +Host Configuration +------------------ +Each search service must have one or more hosts associated with it. Each host +entry consists of a `host` key, a dictionary of roles and can optionally +override any of the options that are valid at the service level (see above). + +Currently supported roles are `read` and `write`. These can be individually +enabled or disabled on a per-host basis. A typical setup might include two +elasticsearch clusters in two separate datacenters. You can configure one +cluster for reads and both for writes. When one cluster is down for maintenance +you can simply swap the read role over to the backup cluster and then proceed +with maintenance without any service interruption. + +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. + +NOTE: This page runs its diagnostics //from the web server that is serving the +request//. If you are recovering from a disaster, the view this page shows +may be partial or misleading, and two requests served by different servers may +see different views of the cluster. diff --git a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php b/src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php similarity index 89% rename from src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php rename to src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php index 580b3f1b27..252c116653 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php +++ b/src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php @@ -1,20 +1,19 @@ 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); } diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index 337d139df8..5a32ef7c11 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -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; } diff --git a/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php similarity index 100% rename from src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php rename to src/infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php diff --git a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php new file mode 100644 index 0000000000..4a5f7ea6c5 --- /dev/null +++ b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php @@ -0,0 +1,79 @@ + $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>', + 'roles' => 'optional map', + '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 include: %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', + '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())); + } + } + } + } + } +} diff --git a/src/infrastructure/cluster/PhabricatorClusterException.php b/src/infrastructure/cluster/exception/PhabricatorClusterException.php similarity index 100% rename from src/infrastructure/cluster/PhabricatorClusterException.php rename to src/infrastructure/cluster/exception/PhabricatorClusterException.php diff --git a/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php b/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php similarity index 100% rename from src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php rename to src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php diff --git a/src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php b/src/infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php similarity index 100% rename from src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php rename to src/infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php diff --git a/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php b/src/infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php similarity index 100% rename from src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php rename to src/infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php diff --git a/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php b/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php new file mode 100644 index 0000000000..f7e9cb5550 --- /dev/null +++ b/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php @@ -0,0 +1,10 @@ +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; + } + +} diff --git a/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php new file mode 100644 index 0000000000..ced23cd542 --- /dev/null +++ b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php @@ -0,0 +1,34 @@ +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 getConnectionStatus() { + PhabricatorDatabaseRef::queryAll(); + $ref = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication('search'); + $status = $ref->getConnectionStatus(); + return $status; + } + +} diff --git a/src/infrastructure/cluster/search/PhabricatorSearchHost.php b/src/infrastructure/cluster/search/PhabricatorSearchHost.php new file mode 100644 index 0000000000..834e786789 --- /dev/null +++ b/src/infrastructure/cluster/search/PhabricatorSearchHost.php @@ -0,0 +1,163 @@ +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(); + + public static function reindexAbstractDocument( + PhabricatorSearchAbstractDocument $doc) { + + $services = self::getAllServices(); + $indexed = 0; + foreach (self::getWritableHostForEachService() as $host) { + $host->getEngine()->reindexAbstractDocument($doc); + $indexed++; + } + if ($indexed == 0) { + throw new PhabricatorClusterNoHostForRoleException('write'); + } + } + + public static function executeSearch(PhabricatorSavedQuery $query) { + $services = self::getAllServices(); + foreach ($services as $service) { + $hosts = $service->getAllHostsForRole('read'); + // try all hosts until one succeeds + foreach ($hosts as $host) { + $last_exception = null; + try { + $res = $host->getEngine()->executeSearch($query); + // return immediately if we get results without an exception + $host->didHealthCheck(true); + return $res; + } catch (Exception $ex) { + // try each server in turn, only throw if none succeed + $last_exception = $ex; + $host->didHealthCheck(false); + } + } + } + if ($last_exception) { + throw $last_exception; + } + return $res; + } + +} diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php new file mode 100644 index 0000000000..20c0664456 --- /dev/null +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -0,0 +1,259 @@ +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 function setDisabled($disabled) { + $this->disabled = $disabled; + return $this; + } + + public function getDisabled() { + return $this->disabled; + } + + 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 $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) { + if ($val === false && isset($this->roles[$role])) { + unset($this->roles[$role]); + } else { + $this->roles[$role] = $val; + } + } + return $this; + } + + public function getRoles() { + return $this->roles; + } + + 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) { + $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) { + $engine = $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 + * @throws PhabricatorClusterNoHostForRoleException + */ + public static function reindexAbstractDocument( + PhabricatorSearchAbstractDocument $doc) { + $indexed = 0; + foreach (self::getAllServices() as $service) { + $service->getEngine()->reindexAbstractDocument($doc); + $indexed++; + } + if ($indexed == 0) { + throw new PhabricatorClusterNoHostForRoleException('write'); + } + } + + /** + * Execute a full-text query and return a list of PHIDs of matching objects. + * @return string[] + * @throws PhutilAggregateException + */ + public static function executeSearch(PhabricatorSavedQuery $query) { + $services = self::getAllServices(); + $exceptions = array(); + foreach ($services as $service) { + $engine = $service->getEngine(); + // try all hosts until one succeeds + try { + $res = $engine->executeSearch($query); + // return immediately if we get results without an exception + return $res; + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + throw new PhutilAggregateException('All search engines failed:', + $exceptions); + } + +} From 76404c5fdb6a51f7b04ca3afd1b81239be2d88e7 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 27 Mar 2017 09:19:12 -0700 Subject: [PATCH 047/239] Cleaner fullscreen / preview states for Remarkup bar Summary: General CSS and usability touchup of the Remarkup bar states for fullscreen and preview. Larger fonts, more spacing, some hint of the underlying page. Disable buttons that can't be used in preview mode. Test Plan: Formal test coming with mobile, browsers. This is a kick the tires upload. {F4283448} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17563 --- resources/celerity/map.php | 37 +-- .../control/PhabricatorRemarkupControl.php | 9 - webroot/rsrc/css/core/remarkup.css | 217 ++++++++++++------ webroot/rsrc/css/phui/phui-comment-form.css | 2 +- .../behavior-phabricator-remarkup-assist.js | 5 + 5 files changed, 177 insertions(+), 93 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4ca4c327f9..4d9406ac5f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -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' => 'acd257dc', + 'core.pkg.js' => '021685f1', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '90b30783', 'differential.pkg.js' => 'ddfeb49b', @@ -108,7 +108,7 @@ 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/remarkup.css' => '17c0fb37', 'rsrc/css/core/syntax.css' => '769d3498', 'rsrc/css/core/z-index.css' => '5e72c4e0', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', @@ -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', @@ -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', @@ -793,7 +793,7 @@ return array( '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', @@ -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', @@ -1705,6 +1695,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', diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 35a1bcc8df..52fb44617d 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -172,11 +172,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 +195,6 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { } if ($mode_actions) { - $actions[] = array( - 'spacer' => true, - 'align' => 'right', - ); $actions += $mode_actions; } diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 03479398b3..a986409227 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -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; +} diff --git a/webroot/rsrc/css/phui/phui-comment-form.css b/webroot/rsrc/css/phui/phui-comment-form.css index 3db0a82924..a49d033c3e 100644 --- a/webroot/rsrc/css/phui/phui-comment-form.css +++ b/webroot/rsrc/css/phui/phui-comment-form.css @@ -62,7 +62,7 @@ body.device .phui-box.phui-object-box.phui-comment-form-view { border-color: {$lightblueborder}; border-top: 1px solid {$thinblueborder}; padding: 8px; - height: 10em; + height: 12em; background-color: rgba({$alphablue},.02); } diff --git a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js index 6e89a18d88..f4cbc4f4fa 100644 --- a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js +++ b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js @@ -8,6 +8,7 @@ * javelin-workflow * javelin-vector * phuix-autocomplete + * javelin-mask */ JX.behavior('phabricator-remarkup-assist', function(config) { @@ -39,6 +40,7 @@ JX.behavior('phabricator-remarkup-assist', function(config) { if (edit_mode == 'fa-arrows-alt') { JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', false); JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', false); + JX.Mask.hide('jx-light-mask'); } area.style.height = ''; @@ -59,6 +61,7 @@ JX.behavior('phabricator-remarkup-assist', function(config) { if (mode == 'fa-arrows-alt') { JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', true); JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', true); + JX.Mask.show('jx-light-mask'); // If we're in preview mode, expand the preview to full-size. if (preview) { @@ -275,6 +278,7 @@ JX.behavior('phabricator-remarkup-assist', function(config) { area.parentNode.insertBefore(preview, area); JX.DOM.alterClass(button, 'preview-active', true); + JX.DOM.alterClass(root, 'remarkup-preview-active', true); resize_preview(); JX.DOM.hide(area); @@ -286,6 +290,7 @@ JX.behavior('phabricator-remarkup-assist', function(config) { preview = null; JX.DOM.alterClass(button, 'preview-active', false); + JX.DOM.alterClass(root, 'remarkup-preview-active', false); } break; case 'fa-thumb-tack': From 9e2ab4f80e8c87b2cdb12b988d500ba0a5cc920b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 27 Mar 2017 10:23:08 -0700 Subject: [PATCH 048/239] Scope syntax css rules to direct descendants only in diffs Summary: Fixes T11641. We're overbroad here (and this may need more scoping?) but this seems to resolve the immediate issue. Test Plan: Upload a few diffs and ask disabled accounts to comment on them inline. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11641 Differential Revision: https://secure.phabricator.com/D17565 --- resources/celerity/map.php | 12 ++++++------ webroot/rsrc/css/core/syntax.css | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4d9406ac5f..8a4146b0a3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '82aca405', 'conpherence.pkg.js' => '6249a1cf', - 'core.pkg.css' => 'acd257dc', + 'core.pkg.css' => '87c434ee', 'core.pkg.js' => '021685f1', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '90b30783', @@ -109,7 +109,7 @@ return array( 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '9f4cb463', 'rsrc/css/core/remarkup.css' => '17c0fb37', - 'rsrc/css/core/syntax.css' => '769d3498', + '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', @@ -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', @@ -1436,9 +1436,6 @@ return array( 'phabricator-shaped-request', 'conpherence-thread-manager', ), - '769d3498' => array( - 'syntax-default-css', - ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2000,6 +1997,9 @@ return array( 'phabricator-title', 'phabricator-favicon', ), + 'cae95e89' => array( + 'syntax-default-css', + ), 'ccf1cbf8' => array( 'javelin-install', 'javelin-dom', diff --git a/webroot/rsrc/css/core/syntax.css b/webroot/rsrc/css/core/syntax.css index 5de3b5d4a4..a0b84ea2b3 100644 --- a/webroot/rsrc/css/core/syntax.css +++ b/webroot/rsrc/css/core/syntax.css @@ -11,7 +11,7 @@ margin-right: 1px; } -.remarkup-code td span { +.remarkup-code td > span { display: inline; word-break: break-all; } From 9e2f263bb49c3fdc433fbec63ca849ff9e1fa2b6 Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Tue, 28 Mar 2017 07:58:22 +0000 Subject: [PATCH 049/239] Add repositories to fulltext search index. Summary: This implements a simplistic `PhabricatorRepositoryFulltextEngine` Currently only the repository name, description, timestamps and status are indexed. Note: I had to change the `search index` workflow to disambiguate PhabricatorRepository from PhabricatorRepositoryCommit Test Plan: * ran `./bin/search index --type PhabricatorRepository --force` * searched for some repositories. Saw reasonable results matching on either title or description. * Edited a repository in the web ui * Added unique key words to the repo description. * I was then able to find that repo by searching for the new keywords. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin Tags: #search, #diffusion Differential Revision: https://secure.phabricator.com/D17300 --- src/__phutil_library_map__.php | 3 +++ .../PhabricatorRepositoryFulltextEngine.php | 27 +++++++++++++++++++ .../storage/PhabricatorRepository.php | 10 ++++++- ...abricatorSearchManagementIndexWorkflow.php | 6 +++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/applications/repository/search/PhabricatorRepositoryFulltextEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7c233cb93a..5d9e3de268 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3660,6 +3660,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', @@ -8910,6 +8911,7 @@ phutil_register_library_map(array( 'PhabricatorProjectInterface', 'PhabricatorSpacesInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorFulltextInterface', ), 'PhabricatorRepositoryAuditRequest' => array( 'PhabricatorRepositoryDAO', @@ -8951,6 +8953,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorRepositoryEngine' => 'Phobject', + 'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryGitLFSRef' => array( diff --git a/src/applications/repository/search/PhabricatorRepositoryFulltextEngine.php b/src/applications/repository/search/PhabricatorRepositoryFulltextEngine.php new file mode 100644 index 0000000000..f666af552f --- /dev/null +++ b/src/applications/repository/search/PhabricatorRepositoryFulltextEngine.php @@ -0,0 +1,27 @@ +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()); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 1a42eb8bd2..fd7413c392 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -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(); + } + } diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index 36996861a9..7838808f48 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -158,9 +158,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; + } } From 7d3956bec17f5cbd274b7c497cc596be78f0f6ed Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 09:58:47 -0700 Subject: [PATCH 050/239] Correct spelling of "Dasbhoard" Summary: Before the speling pollice lock us in prisun. Test Plan: Used a dicationairey. Reviewers: chad, jmeador Reviewed By: jmeador Differential Revision: https://secure.phabricator.com/D17570 --- .../controller/PhabricatorApplicationSearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index dd3508e6bb..b0cab13e07 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -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}/"); } From aea46e55dac035e33080016c190c6c0765a84fa1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 05:23:42 -0700 Subject: [PATCH 051/239] Fix an issue where "Request Review" of a fully-accepted revision would transition to "Accepted" Summary: Ref T10967. This is explained in more detail in T10967#217125 When an author does "Request Review" on an accepted revision, void (in the sense of "cancel out", like a bank check) any "accepted" reviewers on the current diff. Test Plan: - Create a revision with author A and reviewer B. - Accept as B. - "Request Review" as A. - (With sticky accepts enabled.) - Before patch: revision swithced back to "accepted". - After patch: the earlier review is "voided" by te "Request Review", and the revision switches to "Review Requested". Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17566 --- .../20170328.reviewers.01.void.sql | 2 + src/__phutil_library_map__.php | 2 + .../editor/DifferentialTransactionEditor.php | 13 ++-- .../storage/DifferentialReviewer.php | 9 +++ .../DifferentialRevisionReviewTransaction.php | 41 +++++++----- .../DifferentialRevisionVoidTransaction.php | 63 +++++++++++++++++++ 6 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 resources/sql/autopatches/20170328.reviewers.01.void.sql create mode 100644 src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php diff --git a/resources/sql/autopatches/20170328.reviewers.01.void.sql b/resources/sql/autopatches/20170328.reviewers.01.void.sql new file mode 100644 index 0000000000..b46cb9351d --- /dev/null +++ b/resources/sql/autopatches/20170328.reviewers.01.void.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_reviewer + ADD voidedPHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5d9e3de268..0921fa21e9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', @@ -5357,6 +5358,7 @@ phutil_register_library_map(array( 'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', + 'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialStoredCustomField' => 'DifferentialCustomField', diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 3f00f4b837..9b837876fb 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -357,6 +357,14 @@ final class DifferentialTransactionEditor } } + if ($downgrade_accepts) { + $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: @@ -685,11 +693,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; } } diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index d43f533b5c..6912e2af1e 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -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( @@ -59,6 +61,13 @@ 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 (!$diff_phid) { return true; } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index 3db289470a..c0971a5a2b 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -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,33 @@ abstract class DifferentialRevisionReviewTransaction $active_phid = $this->getActiveDiffPHID($revision); + $status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED; + $is_accepted = ($require_status == $status_accepted); + // Otherwise, check that all reviews they have authority over are in // the desired set of states. - $status_map = array_fuse($status_list); foreach ($revision->getReviewers() as $reviewer) { if (!$reviewer->hasAuthority($viewer)) { continue; } $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; } } + + // 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; @@ -223,6 +225,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) { diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php new file mode 100644 index 0000000000..e40cb8af62 --- /dev/null +++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php @@ -0,0 +1,63 @@ +getTableName(); + $conn = $table->establishConnection('w'); + + $rows = queryfx_all( + $conn, + 'SELECT reviewerPHID FROM %T + WHERE revisionPHID = %s + AND voidedPHID IS NULL + AND reviewerStatus = %s', + $table_name, + $object->getPHID(), + DifferentialReviewerStatus::STATUS_ACCEPTED); + + 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 = %s', + $table_name, + $this->getActingAsPHID(), + $object->getPHID(), + DifferentialReviewerStatus::STATUS_ACCEPTED); + } + + public function shouldHide() { + // This is an internal transaction, so don't show it in feeds or + // transaction logs. + return true; + } + +} From 415ad784844e5fd05239dcd34b186c7561fc8860 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 06:07:08 -0700 Subject: [PATCH 052/239] Remove old code for "Request Review" action from Differential Summary: Ref T10967. This moves all remaining "request review" pathways (just `differential.createcomment`) to the new code, and removes the old action. Test Plan: Requested review on a revision, `grep`'d for the action constant. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17567 --- ...ferentialCreateCommentConduitAPIMethod.php | 2 + .../editor/DifferentialTransactionEditor.php | 53 ------------------- .../storage/DifferentialTransaction.php | 2 - 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php index ee70644537..52736d6f3b 100644 --- a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php @@ -60,6 +60,8 @@ final class DifferentialCreateCommentConduitAPIMethod 'accept' => DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE, 'reject' => DifferentialRevisionRejectTransaction::TRANSACTIONTYPE, 'resign' => DifferentialRevisionResignTransaction::TRANSACTIONTYPE, + 'request_review' => + DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE, ); $action = $request->getValue('action'); diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 9b837876fb..e35257ff9b 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -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; } } @@ -952,41 +934,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 diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 868a24ebb0..c6c55e0cdd 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -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 '. From ddc02ce420a56d964eb3f38770d03a1bf4d35dac Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 06:22:19 -0700 Subject: [PATCH 053/239] When voiding "Accept" reviews, also void "Reject" reviews Summary: Ref T10967. This change is similar to D17566, but for rejects. Test Plan: - Create a revision as A, with reviewer B. - Reject as B. - Request review as A. - Before patch: stuck in "rejected". - After patch: transitions back to "needs review". Reviewers: chad Reviewed By: chad Maniphest Tasks: T10967 Differential Revision: https://secure.phabricator.com/D17568 --- .../editor/DifferentialTransactionEditor.php | 7 +--- .../storage/DifferentialReviewer.php | 41 +++++++++++++++---- .../DifferentialRevisionReviewTransaction.php | 9 ++++ .../DifferentialRevisionVoidTransaction.php | 15 +++++-- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index e35257ff9b..97ad60fc12 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -339,7 +339,7 @@ final class DifferentialTransactionEditor } } - if ($downgrade_accepts) { + if ($downgrade_accepts || $downgrade_rejects) { $void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE; $results[] = id(new DifferentialTransaction()) ->setTransactionType($void_type) @@ -659,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; diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index 6912e2af1e..b202baddf2 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -54,6 +54,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; @@ -68,6 +87,21 @@ final class DifferentialReviewer 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; } @@ -82,13 +116,6 @@ final class DifferentialReviewer return true; } - $sticky_key = 'differential.sticky-accept'; - $is_sticky = PhabricatorEnv::getEnvConfig($sticky_key); - - if ($is_sticky) { - return true; - } - return false; } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index c0971a5a2b..4e3e7b29e6 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -100,7 +100,10 @@ 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. @@ -121,6 +124,12 @@ abstract class DifferentialRevisionReviewTransaction } } + 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) { diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php index e40cb8af62..ae684d94fe 100644 --- a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php @@ -25,10 +25,10 @@ final class DifferentialRevisionVoidTransaction 'SELECT reviewerPHID FROM %T WHERE revisionPHID = %s AND voidedPHID IS NULL - AND reviewerStatus = %s', + AND reviewerStatus IN (%Ls)', $table_name, $object->getPHID(), - DifferentialReviewerStatus::STATUS_ACCEPTED); + $this->getVoidableStatuses()); return ipull($rows, 'reviewerPHID'); } @@ -47,11 +47,11 @@ final class DifferentialRevisionVoidTransaction 'UPDATE %T SET voidedPHID = %s WHERE revisionPHID = %s AND voidedPHID IS NULL - AND reviewerStatus = %s', + AND reviewerStatus IN (%Ls)', $table_name, $this->getActingAsPHID(), $object->getPHID(), - DifferentialReviewerStatus::STATUS_ACCEPTED); + $this->getVoidableStatuses()); } public function shouldHide() { @@ -60,4 +60,11 @@ final class DifferentialRevisionVoidTransaction return true; } + private function getVoidableStatuses() { + return array( + DifferentialReviewerStatus::STATUS_ACCEPTED, + DifferentialReviewerStatus::STATUS_REJECTED, + ); + } + } From 2fbc9a52da9bf2132df6b22bd6df370c67802ec9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 09:30:58 -0700 Subject: [PATCH 054/239] Allow users to "Force accept" package reviews if they own a more general package Summary: Ref T12272. If you own a package which owns "/", this allows you to force-accept package reviews for packages which own sub-paths, like "/src/adventure/". The default UI looks something like this: ``` [X] Accept as epriestley [X] Accept as Root Package [ ] Force accept as Adventure Package ``` By default, force-accepts are not selected. (I may do some UI cleanup and/or annotate "because you own X" in the future and/or mark these accepts specially in some way, particularly if this proves confusing along whatever dimension.) Test Plan: {F4314747} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12272 Differential Revision: https://secure.phabricator.com/D17569 --- .../controller/DifferentialController.php | 6 +- .../editor/DifferentialTransactionEditor.php | 2 - .../storage/DifferentialChangeset.php | 17 ++ .../storage/DifferentialReviewer.php | 5 + .../storage/DifferentialRevision.php | 238 ++++++++++++++++++ .../DifferentialRevisionAcceptTransaction.php | 38 ++- .../DifferentialRevisionActionTransaction.php | 7 +- .../DifferentialRevisionReviewTransaction.php | 26 +- .../query/PhabricatorOwnersPackageQuery.php | 12 +- 9 files changed, 332 insertions(+), 19 deletions(-) diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php index 1991580116..2258c4fdcb 100644 --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -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); } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 97ad60fc12..bf7faa2700 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1857,6 +1857,4 @@ final class DifferentialTransactionEditor $acting_phid); } - - } diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php index 448cdb6343..be83c5e73b 100644 --- a/src/applications/differential/storage/DifferentialChangeset.php +++ b/src/applications/differential/storage/DifferentialChangeset.php @@ -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(); diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index b202baddf2..3aa9bf6362 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -39,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; diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 7189c5f5b4..72204256e5 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -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 )----------------------------------------- */ diff --git a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php index 4f6bedd698..7d1f1a92ef 100644 --- a/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionAcceptTransaction.php @@ -84,11 +84,18 @@ final class DifferentialRevisionAcceptTransaction } } + $default_unchecked = array(); foreach ($reviewers as $reviewer) { + $reviewer_phid = $reviewer->getReviewerPHID(); + if (!$reviewer->hasAuthority($viewer)) { // If the viewer doesn't have authority to act on behalf of a reviewer, - // don't include that reviewer as an option. - continue; + // we check if they can accept by force. + if ($revision->canReviewerForceAccept($viewer, $reviewer)) { + $default_unchecked[$reviewer_phid] = true; + } else { + continue; + } } if ($reviewer->isAccepted($diff_phid)) { @@ -97,20 +104,37 @@ final class DifferentialRevisionAcceptTransaction continue; } - $reviewer_phid = $reviewer->getReviewerPHID(); $reviewer_phids[$reviewer_phid] = $reviewer_phid; } $handles = $viewer->loadHandles($reviewer_phids); + $head = array(); + $tail = array(); foreach ($reviewer_phids as $reviewer_phid) { - $options[$reviewer_phid] = pht( - 'Accept as %s', - $viewer->renderHandle($reviewer_phid)); + $is_force = isset($default_unchecked[$reviewer_phid]); - $value[] = $reviewer_phid; + if ($is_force) { + $tail[] = $reviewer_phid; + + $options[$reviewer_phid] = pht( + 'Force accept as %s', + $viewer->renderHandle($reviewer_phid)); + } else { + $head[] = $reviewer_phid; + $value[] = $reviewer_phid; + + $options[$reviewer_phid] = pht( + 'Accept as %s', + $viewer->renderHandle($reviewer_phid)); + } } + // Reorder reviewers so "force accept" reviewers come at the end. + $options = + array_select_keys($options, $head) + + array_select_keys($options, $tail); + return array($options, $value); } diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index f232398533..8e1c437c53 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -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); } diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index 4e3e7b29e6..019c4c036f 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -109,7 +109,17 @@ abstract class DifferentialRevisionReviewTransaction // the desired set of states. foreach ($revision->getReviewers() as $reviewer) { if (!$reviewer->hasAuthority($viewer)) { - continue; + $can_force = false; + + if ($is_accepted) { + if ($revision->canReviewerForceAccept($viewer, $reviewer)) { + $can_force = true; + } + } + + if (!$can_force) { + continue; + } } $status = $reviewer->getReviewerStatus(); @@ -152,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; } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index 8194dd7a6a..a1c10cd5e9 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -348,7 +348,10 @@ final class PhabricatorOwnersPackageQuery * * @return list 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, ); } From 699228c73b74e2a3ea2e8355ed822c9314fb9f88 Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Tue, 28 Mar 2017 20:19:38 +0000 Subject: [PATCH 055/239] Address some New Search Configuration Errata Summary: [ ] Write an "Upgrading: ..." guidance task with narrow instructions for installs that are upgrading. [ ] Do we need to add an indexing activity (T11932) for installs with ElasticSearch? [ ] We should more clearly detail exactly which versions of ElasticSearch are supported (for example, is ElasticSearch <2 no longer supported)? From T9893 it seems like we may //only// have supported ElasticSearch <2 before, so are the two regions of support totally nonoverlapping and all ElasticSearch users will need to upgrade? [ ] Documentation should provide stronger guidance toward MySQL and away from Elastic for the vast majority of installs, because we've historically seen users choosing Elastic when they aren't actually trying to solve any specific problem. [ ] When users search for fulltext results in Maniphest and hit too many documents, the current behavior is approximately silent failure (see T12443). D17384 has also lowered the ceiling for ElasticSearch, although previous changes lowered it for MySQL search. We should not fail silently, and ideally should build toward T12003. [ ] D17384 added a new "keywords" field, but MySQL does not search it (I think?). The behavior should be as consistent across MySQL and Elastic as we can make it. Likely cleaner is giving "Project" objects a body, with "slugs" and "description" separated by newlines? [ ] `PhabricatorSearchEngineTestCase` is now pointless and only detects local misconfigurations. [ ] It would be nice to build a practical test suite instead, where we put specific documents into the index and then search for them. The upstream test could run against MySQL, and some `bin/search test` could run against a configured engine like ElasticSearch. This would make it easier to make sure that behavior was as uniform as possible across engine implementations. [ ] Does every assigned task now match "user" in ElasticSearch? [x] `PhabricatorElasticFulltextStorageEngine` has a `json_encode()` which should be `phutil_json_encode()`. [ ] `PhabricatorSearchService` throws an untranslated exception. [ ] When a search cluster is down, we probably don't degrade with much grace (unhandled exception)? [ ] I haven't run bin/search init, but bin/search index doesn't warn me that I may want to. This might be worth adding. The UI does warn me. [ ] bin/search init warns me that the index is "incorrect". It might be more clear to distinguish between "missing" and "incorrect", since it's more comforting to users to see "everything is as we expect, doing normal first-time setup now" than "something is wrong, fixing it". [ ] CLI message "Initializing search service "ElasticSearch"" does not end with a period, which is inconsistent with other UI messages. [ ] It might be nice to let bin/search commands like init and index select a specific service (or even service + host) to act on, as bin/storage --ref ... now does. You can generally get the result you want by fiddling with config. [ ] When a service isn't writable, bin/search init reports "Search cluster has no hosts for role "write".". This is accurate but does not provide guidance: it might be more useful to the user to explain "This service is not writable, so we're skipping index check for it.". [x] Even with write off for MySQL, bin/search index --type task --trace still updates MySQL, I think? I may be misreading the trace output. But this behavior doesn't make sense if it is the actual behavior, and it seems like reindexAbstractDocument() uses "all services", not "writable services", and the MySQL engine doesn't make sure it's writable before indexing. [x] Searching or user fails to find task Grant users tokens when a mention is created, suggesting that stemming is not working. [x] Searching for users finds that task, but fails to find a task containing "per user per month" in a comment, also suggesting that stemming is not working. [x] Searching for maniphest fails to find task maniphest.query elephant, suggesting that tokenization in ElasticSearch is not as good as the MySQL tokenization for these words (see D17330). [x] The "index incorrect" warning UI uses inconsistent title case. [x] The "index incorrect" warning UI could format the command to be run more cleanly (with addCommand(), I think). refs T12450 Test Plan: * Stared blankly at the code. * Disabled 'write' role on mysql fulltext service. * Edited a task, ran search indexer, verified that the mysql index wasn't being updated. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17564 --- .../PhabricatorElasticSearchSetupCheck.php | 16 +++--- ...habricatorElasticFulltextStorageEngine.php | 55 +++++++++++++++++-- .../search/PhabricatorMySQLSearchHost.php | 9 +++ .../cluster/search/PhabricatorSearchHost.php | 40 -------------- .../search/PhabricatorSearchService.php | 25 +++++---- 5 files changed, 80 insertions(+), 65 deletions(-) diff --git a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php index d8864b5740..dab886b92a 100644 --- a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php +++ b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php @@ -49,26 +49,28 @@ final class PhabricatorElasticSearchSetupCheck extends PhabricatorSetupCheck { $message = pht( 'You likely enabled cluster.search without creating the '. - 'index. Run `./bin/search init` to correct the index.'); + 'index. Use the following command to create a new index.'); $this ->newIssue('elastic.missing-index') - ->setName(pht('Elasticsearch index Not Found')) + ->setName(pht('Elasticsearch Index Not Found')) + ->addCommand('./bin/search init') ->setSummary($summary) - ->setMessage($message) - ->addRelatedPhabricatorConfig('cluster.search'); + ->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. Run '. - '`./bin/search init` to correct the index.'); + 'or Elasticsearch created the index automatically. '. + 'Use the following command to rebuild the index.'); $this ->newIssue('elastic.broken-index') - ->setName(pht('Elasticsearch index Incorrect')) + ->setName(pht('Elasticsearch Index Schema Mismatch')) + ->addCommand('./bin/search init') ->setSummary($summary) ->setMessage($message); } diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index bc32da5ef4..6d2cd9adc5 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -161,9 +161,11 @@ class PhabricatorElasticFulltextStorageEngine 'simple_query_string' => array( 'query' => $query_string, 'fields' => array( - '_all', + PhabricatorSearchDocumentFieldType::FIELD_TITLE.'.*', + PhabricatorSearchDocumentFieldType::FIELD_BODY.'.*', + PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'.*', ), - 'default_operator' => 'OR', + 'default_operator' => 'AND', ), )); @@ -175,6 +177,7 @@ class PhabricatorElasticFulltextStorageEngine 'simple_query_string' => array( 'query' => $query_string, 'fields' => array( + '*.raw', PhabricatorSearchDocumentFieldType::FIELD_TITLE.'^4', PhabricatorSearchDocumentFieldType::FIELD_BODY.'^3', PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'^1.2', @@ -332,11 +335,38 @@ class PhabricatorElasticFulltextStorageEngine 'index' => array( 'auto_expand_replicas' => '0-2', 'analysis' => array( + 'filter' => array( + '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( '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', + ), + ), ), ), ), @@ -356,9 +386,22 @@ class PhabricatorElasticFulltextStorageEngine // Use the custom analyzer for the corpus of text $properties[$field] = array( 'type' => $text_type, - 'analyzer' => 'english_exact', - 'search_analyzer' => 'english', - 'search_quote_analyzer' => 'english_exact', + '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', + ), + ), ); } @@ -505,7 +548,7 @@ class PhabricatorElasticFulltextStorageEngine array $data, $method = 'GET') { $uri = $host->getURI($path); - $data = json_encode($data); + $data = phutil_json_encode($data); $future = new HTTPSFuture($uri, $data); if ($method != 'GET') { $future->setMethod($method); diff --git a/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php index ced23cd542..742b5713d3 100644 --- a/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php +++ b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php @@ -24,6 +24,15 @@ final class PhabricatorMySQLSearchHost 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'); diff --git a/src/infrastructure/cluster/search/PhabricatorSearchHost.php b/src/infrastructure/cluster/search/PhabricatorSearchHost.php index 834e786789..93c3c4d938 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchHost.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchHost.php @@ -13,7 +13,6 @@ abstract class PhabricatorSearchHost protected $disabled; protected $host; protected $port; - protected $hostRefs = array(); const STATUS_OKAY = 'okay'; const STATUS_FAIL = 'fail'; @@ -121,43 +120,4 @@ abstract class PhabricatorSearchHost abstract public function getConnectionStatus(); - public static function reindexAbstractDocument( - PhabricatorSearchAbstractDocument $doc) { - - $services = self::getAllServices(); - $indexed = 0; - foreach (self::getWritableHostForEachService() as $host) { - $host->getEngine()->reindexAbstractDocument($doc); - $indexed++; - } - if ($indexed == 0) { - throw new PhabricatorClusterNoHostForRoleException('write'); - } - } - - public static function executeSearch(PhabricatorSavedQuery $query) { - $services = self::getAllServices(); - foreach ($services as $service) { - $hosts = $service->getAllHostsForRole('read'); - // try all hosts until one succeeds - foreach ($hosts as $host) { - $last_exception = null; - try { - $res = $host->getEngine()->executeSearch($query); - // return immediately if we get results without an exception - $host->didHealthCheck(true); - return $res; - } catch (Exception $ex) { - // try each server in turn, only throw if none succeed - $last_exception = $ex; - $host->didHealthCheck(false); - } - } - } - if ($last_exception) { - throw $last_exception; - } - return $res; - } - } diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index 20c0664456..0a563207b1 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -46,6 +46,7 @@ class PhabricatorSearchService public function setConfig($config) { $this->config = $config; + $this->setRoles(idx($config, 'roles', array())); if (!isset($config['hosts'])) { $config['hosts'] = array( @@ -67,15 +68,6 @@ class PhabricatorSearchService return $this->config; } - public function setDisabled($disabled) { - $this->disabled = $disabled; - return $this; - } - - public function getDisabled() { - return $this->disabled; - } - public static function getConnectionStatusMap() { return array( self::STATUS_OKAY => array( @@ -100,7 +92,7 @@ class PhabricatorSearchService } public function hasRole($role) { - return isset($this->roles[$role]) && $this->roles[$role] === true; + return isset($this->roles[$role]) && $this->roles[$role] !== false; } public function setRoles(array $roles) { @@ -160,6 +152,12 @@ class PhabricatorSearchService * @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)) { @@ -225,8 +223,11 @@ class PhabricatorSearchService PhabricatorSearchAbstractDocument $doc) { $indexed = 0; foreach (self::getAllServices() as $service) { - $service->getEngine()->reindexAbstractDocument($doc); - $indexed++; + $hosts = $service->getAllHostsForRole('write'); + if (count($hosts)) { + $service->getEngine()->reindexAbstractDocument($doc); + $indexed++; + } } if ($indexed == 0) { throw new PhabricatorClusterNoHostForRoleException('write'); From e7c76d92d546b2b331a3aa1b10fb182b4320f4f9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 12:47:58 -0700 Subject: [PATCH 056/239] Make `bin/search init` messaging a little more consistent Summary: Ref T12450. This mostly just smooths out the text a little to improve consistency. Also: - Use `isWritable()`. - Make the "skipping because not writable" message more clear and tailored. - Try not to use the word "index" too much to avoid confusion with `bin/search index` -- instead, talk about "initialize a service". Test Plan: Ran `bin/search init` with a couple of different (writable / not writable) configs, saw slightly clearer messaging. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17572 --- ...habricatorSearchManagementInitWorkflow.php | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php index 1b5da49e66..c984b2ddbf 100644 --- a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php @@ -6,60 +6,65 @@ 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(); $work_done = false; foreach (PhabricatorSearchService::getAllServices() as $service) { - $console->writeOut( + echo tsprintf( "%s\n", - pht('Initializing search service "%s"', $service->getDisplayName())); + pht( + 'Initializing search service "%s".', + $service->getDisplayName())); - try { - $host = $service->getAnyHostForRole('write'); - } catch (PhabricatorClusterNoHostForRoleException $e) { - // If there are no writable hosts for a given cluster, skip it - $console->writeOut("%s\n", $e->getMessage()); + if (!$service->isWritable()) { + echo tsprintf( + "%s\n", + pht( + 'Skipping service "%s" because it is not writable.', + $service->getDisplayName())); continue; } - $engine = $host->getEngine(); + $engine = $service->getEngine(); if (!$engine->indexExists()) { - $console->writeOut( - '%s', - pht('Index does not exist, creating...')); - $engine->initIndex(); - $console->writeOut( + echo tsprintf( "%s\n", - pht('done.')); + pht('Service index does not exist, creating...')); + + $engine->initIndex(); $work_done = true; } else if (!$engine->indexIsSane()) { - $console->writeOut( - '%s', - pht('Index exists but is incorrect, fixing...')); - $engine->initIndex(); - $console->writeOut( + echo tsprintf( "%s\n", - pht('done.')); + 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) { - $console->writeOut( + if (!$work_done) { + echo tsprintf( "%s\n", - pht( - 'Index maintenance complete. Run `%s` to reindex documents', - './bin/search index')); - } else { - $console->writeOut( - "%s\n", - pht('Nothing to do.')); + pht('No services need initialization.')); + return 0; } + + echo tsprintf( + "%s\n", + pht('Service initialization complete.')); } } From c22693ff2915ac03c34d5e22ebaaa4fe2355b8b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 12:56:15 -0700 Subject: [PATCH 057/239] Remove PhabricatorSearchEngineTestCase Summary: Ref T12450. This is now pointless and just asserts that `cluster.search` has a default value. We might restore a fancier version of this eventually, but get rid of this for now. Test Plan: Scruitinized the test case. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17573 --- src/__phutil_library_map__.php | 2 -- .../__tests__/PhabricatorSearchEngineTestCase.php | 10 ---------- 2 files changed, 12 deletions(-) delete mode 100644 src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0921fa21e9..f9d013f497 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3790,7 +3790,6 @@ 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', @@ -9111,7 +9110,6 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineAttachment' => 'Phobject', 'PhabricatorSearchEngineExtension' => 'Phobject', 'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule', - 'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorSearchField' => 'Phobject', 'PhabricatorSearchHost' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', diff --git a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php deleted file mode 100644 index f5dbd9ef9c..0000000000 --- a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php +++ /dev/null @@ -1,10 +0,0 @@ -assertTrue(!empty($services)); - } - -} From c40be811ea9b2419335fce7fe16096a16e7d6b04 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 12:47:56 -0700 Subject: [PATCH 058/239] Fix isReadable() and isWritable() in SearchService Summary: Ref T12450. Minor cleanup: - setRoles() has no callers. - getRoles() has no callers (these two methods are leftovers from an earlier iteration of the change). - The `hasRole()` logic doesn't work since nothing calls `setRole()`. - `hasRole()` has only `isreadable/iswritable` as callers. - The `isReadable()/isWritable()` logic doesn't work since `hasRole()` doesn't work. Instead, just check if there are any readable/writable hosts. `Host` already inherits its config from `Service` so this gets the same answer without any fuss. Also add some read/write constants to make grepping this stuff a little easier. Test Plan: - Grepped for all removed symbols, saw only newer-generation calls in `Host`. - See next diff for use of `isWritable()`. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17571 --- .../search/PhabricatorSearchService.php | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index 0a563207b1..b4f6b76aec 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -16,6 +16,9 @@ class PhabricatorSearchService 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(); @@ -84,30 +87,11 @@ class PhabricatorSearchService } public function isWritable() { - return $this->hasRole('write'); + return (bool)$this->getAllHostsForRole(self::ROLE_WRITE); } public function isReadable() { - return $this->hasRole('read'); - } - - public function hasRole($role) { - return isset($this->roles[$role]) && $this->roles[$role] !== false; - } - - public function setRoles(array $roles) { - foreach ($roles as $role => $val) { - if ($val === false && isset($this->roles[$role])) { - unset($this->roles[$role]); - } else { - $this->roles[$role] = $val; - } - } - return $this; - } - - public function getRoles() { - return $this->roles; + return (bool)$this->getAllHostsForRole(self::ROLE_READ); } public function getPort() { From 8879118b696f51ea8797e52b1a686bfc8ee1d7f8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 13:59:59 -0700 Subject: [PATCH 059/239] Fix a mid-air collision around SearchService roles My D17571 didn't interact nicely with D17564, which added callsites for one of the methods I removed. Auditors: 20after4 --- src/infrastructure/cluster/search/PhabricatorSearchService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index b4f6b76aec..2085f1d887 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -49,7 +49,6 @@ class PhabricatorSearchService public function setConfig($config) { $this->config = $config; - $this->setRoles(idx($config, 'roles', array())); if (!isset($config['hosts'])) { $config['hosts'] = array( From 5f939dcce0f850cbadeea25bd4ab4eae1f8e6417 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 13:09:27 -0700 Subject: [PATCH 060/239] Re-run config validation from `bin/search` Summary: Ref T12450. Normally, we validate config when: - You restart the webserver. - You edit it with `bin/config set ...`. - You edit it with the web UI. However, you can also change config by editing `local.json`, `some_env.conf.php`, a `SiteConfig` class, etc. In these cases, you may miss config warnings. Explicitly re-run search config checks from `bin/search`, similar to the additional database checks we run from `bin/storage`, to try to produce a better error message if the user has made a configuration error. Test Plan: ``` $ ./bin/search init Usage Exception: Setting "cluster.search" is misconfigured: Invalid search engine type: elastic. Valid types are: elasticsearch, mysql. ``` Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17574 --- ...abricatorSearchManagementIndexWorkflow.php | 2 ++ ...habricatorSearchManagementInitWorkflow.php | 1 + .../PhabricatorSearchManagementWorkflow.php | 24 ++++++++++++++++++- ...abricatorClusterSearchConfigOptionType.php | 7 +++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index 7838808f48..a324a20637 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -45,6 +45,8 @@ final class PhabricatorSearchManagementIndexWorkflow } public function execute(PhutilArgumentParser $args) { + $this->validateClusterSearchConfig(); + $console = PhutilConsole::getConsole(); $is_all = $args->getArg('all'); diff --git a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php index c984b2ddbf..8728b72ee5 100644 --- a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php @@ -11,6 +11,7 @@ final class PhabricatorSearchManagementInitWorkflow } public function execute(PhutilArgumentParser $args) { + $this->validateClusterSearchConfig(); $work_done = false; foreach (PhabricatorSearchService::getAllServices() as $service) { diff --git a/src/applications/search/management/PhabricatorSearchManagementWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementWorkflow.php index 23333665e3..86d8c104cb 100644 --- a/src/applications/search/management/PhabricatorSearchManagementWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementWorkflow.php @@ -1,4 +1,26 @@ getMessage())); + } + } + +} diff --git a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php index 4a5f7ea6c5..3c099a0548 100644 --- a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php +++ b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php @@ -4,6 +4,10 @@ final class PhabricatorClusterSearchConfigOptionType extends PhabricatorConfigJSONOptionType { public function validateOption(PhabricatorConfigOption $option, $value) { + self::validateClusterSearchConfigValue($value); + } + + public static function validateValue($value) { if (!is_array($value)) { throw new Exception( pht( @@ -46,7 +50,8 @@ final class PhabricatorClusterSearchConfigOptionType if (!array_key_exists($spec['type'], $engines)) { throw new Exception( - pht('Invalid search engine type: %s. Valid types include: %s', + pht( + 'Invalid search engine type: %s. Valid types are: %s.', $spec['type'], implode(', ', array_keys($engines)))); } From 88798354e8c91554b15dd38b63732dfdda672a78 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 15:10:59 -0700 Subject: [PATCH 061/239] Soften a possible cluster search setup fatal Summary: Ref T12450. The way that config repair and setup issues interact is kind of complicated, and if `cluster.search` is invalid we may end up using `cluster.search` before we repair it. I poked at things for a bit but wasn't confident I could get it to consistently repair before we use it without doing a big messy change. The only thing that really matters is whether "type" is valid or not, so just put a slightly softer/more-tailored check in for that. Test Plan: - With `"type": "elastic"`, loaded setup issues. - Before patch: hard fatal. - After patch: softer fatal with more useful messaging. {F4321048} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17576 --- .../PhabricatorClusterSearchConfigOptionType.php | 2 +- .../cluster/search/PhabricatorSearchService.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php index 3c099a0548..90ead23e6d 100644 --- a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php +++ b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php @@ -4,7 +4,7 @@ final class PhabricatorClusterSearchConfigOptionType extends PhabricatorConfigJSONOptionType { public function validateOption(PhabricatorConfigOption $option, $value) { - self::validateClusterSearchConfigValue($value); + self::validateValue($value); } public static function validateValue($value) { diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index 2085f1d887..ed34f5cdf6 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -186,6 +186,18 @@ class PhabricatorSearchService $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 = $engines[$config['type']]; $cluster = new self($engine); $cluster->setConfig($config); From add103810989ede8424f081b529159284669b50a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Mar 2017 15:33:32 -0700 Subject: [PATCH 062/239] Don't summon the emoji autocompleter for ":3" Summary: Fixes T12460. Also ":)", ":(", ":/", and oldschool ":-)" variants. Not included are variants with actual letters (`:D`, `:O`, `:P`) and obscure variants (`:^)`, `:*)`). Test Plan: Typed `:3` (no emoji summoned). Typed `:dog3` (emoji summoned). Typed `@3` (user autocomplete summoned). Reviewers: chad Reviewed By: chad Maniphest Tasks: T12460 Differential Revision: https://secure.phabricator.com/D17577 --- resources/celerity/map.php | 16 ++++++++-------- .../form/control/PhabricatorRemarkupControl.php | 9 +++++++++ webroot/rsrc/js/phuix/PHUIXAutocomplete.js | 12 ++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8a4146b0a3..f83c2b0f09 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -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' => '7910aacb', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '8018ee50', 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', @@ -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' => '7910aacb', 'phuix-dropdown-menu' => '8018ee50', 'phuix-form-control-view' => '83e03671', 'phuix-icon-view' => 'bff6884b', @@ -1456,6 +1456,12 @@ return array( 'multirow-row-manager', 'javelin-json', ), + '7910aacb' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-icon-view', + 'phabricator-prefab', + ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', @@ -1464,12 +1470,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', diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 52fb44617d..75054b9575 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -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', + ')', + '(', + '-', + '/', + ), ), ), )); diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js index b7116c557b..e99dcc34f2 100644 --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -343,6 +343,10 @@ JX.install('PHUIXAutocomplete', { return [' ', ':', ',', '.', '!', '?']; }, + _getIgnoreList: function() { + return this._map[this._active].ignore || []; + }, + _isTerminatedString: function(string) { var terminators = this._getTerminators(); for (var ii = 0; ii < terminators.length; ii++) { @@ -517,6 +521,14 @@ JX.install('PHUIXAutocomplete', { } } + var ignore = this._getIgnoreList(); + for (ii = 0; ii < ignore.length; ii++) { + if (trim.indexOf(ignore[ii]) === 0) { + this._deactivate(); + return; + } + } + // If the input is terminated by a space or another word-terminating // punctuation mark, we're going to deactivate if the results can not // be refined by addding more words. From 654f0f6043f810ca4c9deef10f97178dc5b811b9 Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Tue, 28 Mar 2017 23:17:35 +0000 Subject: [PATCH 063/239] Make messages translatable and more sensible. Summary: These exception messages & comments didn't quite match reality. Fixed and added pht() around a couple of them. Test Plan: I didn't test this :P Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17578 --- ...habricatorElasticFulltextStorageEngine.php | 3 +- src/docs/user/cluster/cluster.diviner | 28 +++++++++++++++++++ .../search/PhabricatorSearchService.php | 13 ++++----- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index 6d2cd9adc5..aa966caddc 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -303,7 +303,8 @@ class PhabricatorElasticFulltextStorageEngine $exceptions[] = $e; } } - throw new PhutilAggregateException('All search hosts failed:', $exceptions); + throw new PhutilAggregateException(pht('All Fulltext Search hosts failed:'), + $exceptions); } public function indexExists(PhabricatorElasticSearchHost $host = null) { diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 7704428e9c..15eed86eb4 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -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** | Moderate | **High** | Minimal Risk | Moderate See below for a walkthrough of these services in greater detail. @@ -237,6 +238,33 @@ hosts is unlikely to have much impact on scalability. For details, see @{article:Cluster: Notifications}. +Cluster: Fulltext Search +======================== + +At a certain scale, you may begin to bump up against the limitations of MySQL's +built-in fulltext search capabilities. We have seen this with very large +installations with several million objects in the database and very many +simultaneous requests. At this point you may consider adding Elasticsearch +hosts to your cluster to reduce the load on your MySQL hosts. + +Elasticsearch has the ability to spread the load across multiple hosts and can +handle very large indexes by sharding. + +Search does not involve any risk of data lost because it's always possible to +rebuild the search index from the original database objects. This process can +be very time consuming, however, especially when the database grows very large. + +With multiple Elasticsearch hosts, you can survive the loss of a single host +with minimal disruption as Phabricator will detect the problem and direct +queries to one of the remaining hosts. + +Phabricator supports writing to multiple indexing servers. This Simplifies +Elasticsearch upgrades and makes it possible to recover more quickly from +problems with the search index. + +For details, see @{article:Cluster: Search}. + + Overlaying Services =================== diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index ed34f5cdf6..59ef7c408d 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -235,21 +235,20 @@ class PhabricatorSearchService * @throws PhutilAggregateException */ public static function executeSearch(PhabricatorSavedQuery $query) { - $services = self::getAllServices(); $exceptions = array(); - foreach ($services as $service) { - $engine = $service->getEngine(); - // try all hosts until one succeeds + // 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 without an exception + // return immediately if we get results return $res; } catch (Exception $ex) { $exceptions[] = $ex; } } - throw new PhutilAggregateException('All search engines failed:', - $exceptions); + $msg = pht('All of the configured Fulltext Search services failed.'); + throw new PhutilAggregateException($msg, $exceptions); } } From 67a1c40476474ad741a26e067f437f1e31392ded Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Thu, 30 Mar 2017 18:07:47 +0000 Subject: [PATCH 064/239] Set content-type to application/json Summary: Elasticsearch really wants a raw json body and it fails to accept the request as of es version 5.3 Test Plan: Tested with elasticsearch 5.2 and 5.3. Before this change 5.2 worked but 5.3 failed with `HTTP/406 "Content-Type header [application/x-www-form-urlencoded] is not supported"` [1] After this change, both worked. [1] https://phabricator.wikimedia.org/P5158 Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17580 --- .../fulltextstorage/PhabricatorElasticFulltextStorageEngine.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index aa966caddc..2e456ec408 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -551,6 +551,8 @@ class PhabricatorElasticFulltextStorageEngine $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); } From cb1d90465447c8a7cc5bc42aa2566c12692fec25 Mon Sep 17 00:00:00 2001 From: Mukunda Modell Date: Thu, 30 Mar 2017 18:08:05 +0000 Subject: [PATCH 065/239] Make sure writes go to the right cluster Summary: Two little issues 1. there was an extra call to getHostForWrite, 2. The engine instance was shared between multiple service definitions so it was overwriting the list of writable hosts from one service with hosts from another. Test Plan: tested in wikimedia production with multiple services defined like this: ```language=json [ { "hosts": [ { "host": "search.svc.codfw.wmnet", "protocol": "https", "roles": { "read": true, "write": true }, "version": 5 } ], "path": "/phabricator", "port": 9243, "type": "elasticsearch" }, { "hosts": [ { "host": "search.svc.eqiad.wmnet", "protocol": "https", "roles": { "read": true, "write": true }, "version": 5 } ], "path": "/phabricator", "port": 9243, "type": "elasticsearch" } ] ``` Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D17581 --- .../fulltextstorage/PhabricatorElasticFulltextStorageEngine.php | 1 - src/infrastructure/cluster/search/PhabricatorSearchService.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index 2e456ec408..45011ba982 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -528,7 +528,6 @@ class PhabricatorElasticFulltextStorageEngine $host = $this->getHostForRead(); } $uri = '/_stats/'; - $host = $this->getHostForRead(); $res = $this->executeRequest($host, $uri, array()); $stats = $res['indices'][$this->index]; diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index 59ef7c408d..10cf78d94b 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -198,7 +198,7 @@ class PhabricatorSearchService implode(', ', array_keys($engines)))); } - $engine = $engines[$config['type']]; + $engine = clone($engines[$config['type']]); $cluster = new self($engine); $cluster->setConfig($config); $engine->setService($cluster); From 130ebd2c421a0bd0ae0f84ab60acc00ff0436850 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Mar 2017 08:32:41 -0700 Subject: [PATCH 066/239] Immediately deactivate remarkup autocomplete if there's no query Summary: Fixes T12479. If you end a line with a character like ":" in a context which can trigger autocomplete (e.g., `.:`), then try to make a newline, we swallow the keystroke. Instead, allow the keystroke through if the user hasn't typed anything else yet. Test Plan: - Autocompleted emoji and users normally. - In an empty textarea, typed `.:`, got a newline instead of a swallowed keystroke. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12479 Differential Revision: https://secure.phabricator.com/D17583 --- resources/celerity/map.php | 16 ++++++------ webroot/rsrc/js/phuix/PHUIXAutocomplete.js | 29 ++++++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f83c2b0f09..6bee132238 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -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' => '7910aacb', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'd5b2abf3', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '8018ee50', 'rsrc/js/phuix/PHUIXFormControl.js' => '83e03671', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', @@ -885,7 +885,7 @@ return array( 'phui-workpanel-view-css' => 'a3a63478', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => 'b3465b9b', - 'phuix-autocomplete' => '7910aacb', + 'phuix-autocomplete' => 'd5b2abf3', 'phuix-dropdown-menu' => '8018ee50', 'phuix-form-control-view' => '83e03671', 'phuix-icon-view' => 'bff6884b', @@ -1456,12 +1456,6 @@ return array( 'multirow-row-manager', 'javelin-json', ), - '7910aacb' => array( - 'javelin-install', - 'javelin-dom', - 'phuix-icon-view', - 'phabricator-prefab', - ), '7927a7d3' => array( 'javelin-behavior', 'javelin-quicksand', @@ -2053,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', diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js index e99dcc34f2..a03c2adf70 100644 --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -433,6 +433,16 @@ JX.install('PHUIXAutocomplete', { } } + // Deactivate if the user moves the cursor to the left of the assist + // range. For example, they might press the "left" arrow to move the + // cursor to the left, or click in the textarea prior to the active + // range. + var range = JX.TextAreaUtils.getSelectionRange(area); + if (range.start < this._cursorHead) { + this._deactivate(); + return; + } + if (special == 'tab' || special == 'return') { var r = e.getRawEvent(); if (r.shiftKey && special == 'tab') { @@ -443,6 +453,15 @@ JX.install('PHUIXAutocomplete', { return; } + // If the user hasn't typed any text yet after typing the character + // which can summon the autocomplete, deactivate and let the keystroke + // through. For example, We hit this when a line ends with an + // autocomplete character and the user is trying to type a newline. + if (range.start == this._cursorHead) { + this._deactivate(); + return; + } + // If we autocomplete, we're done. Otherwise, just eat the event. This // happens if you type too fast and try to tab complete before results // load. @@ -454,16 +473,6 @@ JX.install('PHUIXAutocomplete', { return; } - // Deactivate if the user moves the cursor to the left of the assist - // range. For example, they might press the "left" arrow to move the - // cursor to the left, or click in the textarea prior to the active - // range. - var range = JX.TextAreaUtils.getSelectionRange(area); - if (range.start < this._cursorHead) { - this._deactivate(); - return; - } - // Deactivate if the user moves the cursor to the right of the assist // range. For example, they might click later in the document. If the user // is pressing the "right" arrow key, they are not allowed to move the From 86673486c07aa1abdfe3557a89da67044b766157 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 30 Mar 2017 12:11:20 -0700 Subject: [PATCH 067/239] Move Phortune Contollers into folders Summary: Move individual controller files into cooresponding folders. Makes it easier to locate sections and expand without clutter. Also made "chargelist" part of account since it's tied to having an account specifically. Test Plan: Vist charges, merchants, subscription, accounts, and other pages. No errors from file move. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17587 --- src/__phutil_library_map__.php | 58 +++++++++---------- .../PhabricatorPhortuneApplication.php | 2 +- .../PhortuneAccountChargeListController.php} | 2 +- .../PhortuneAccountEditController.php | 0 .../PhortuneAccountListController.php | 0 .../PhortuneAccountViewController.php | 0 .../PhortuneCartAcceptController.php | 0 .../PhortuneCartCancelController.php | 0 .../PhortuneCartCheckoutController.php | 0 .../{ => cart}/PhortuneCartController.php | 0 .../{ => cart}/PhortuneCartListController.php | 0 .../PhortuneCartUpdateController.php | 0 .../{ => cart}/PhortuneCartViewController.php | 0 .../PhortuneMerchantController.php | 0 .../PhortuneMerchantEditController.php | 0 ...hortuneMerchantInvoiceCreateController.php | 0 .../PhortuneMerchantListController.php | 0 .../PhortuneMerchantPictureController.php | 0 .../PhortuneMerchantViewController.php | 0 .../PhortunePaymentMethodCreateController.php | 0 ...PhortunePaymentMethodDisableController.php | 0 .../PhortunePaymentMethodEditController.php | 0 .../PhortuneProductListController.php | 0 .../PhortuneProductViewController.php | 0 .../PhortuneProviderActionController.php | 0 .../PhortuneProviderDisableController.php | 0 .../PhortuneProviderEditController.php | 0 .../PhortuneSubscriptionEditController.php | 0 .../PhortuneSubscriptionListController.php | 0 .../PhortuneSubscriptionViewController.php | 0 30 files changed, 31 insertions(+), 31 deletions(-) rename src/applications/phortune/controller/{PhortuneChargeListController.php => account/PhortuneAccountChargeListController.php} (97%) rename src/applications/phortune/controller/{ => account}/PhortuneAccountEditController.php (100%) rename src/applications/phortune/controller/{ => account}/PhortuneAccountListController.php (100%) rename src/applications/phortune/controller/{ => account}/PhortuneAccountViewController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartAcceptController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartCancelController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartCheckoutController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartListController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartUpdateController.php (100%) rename src/applications/phortune/controller/{ => cart}/PhortuneCartViewController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantEditController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantInvoiceCreateController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantListController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantPictureController.php (100%) rename src/applications/phortune/controller/{ => merchant}/PhortuneMerchantViewController.php (100%) rename src/applications/phortune/controller/{ => payment}/PhortunePaymentMethodCreateController.php (100%) rename src/applications/phortune/controller/{ => payment}/PhortunePaymentMethodDisableController.php (100%) rename src/applications/phortune/controller/{ => payment}/PhortunePaymentMethodEditController.php (100%) rename src/applications/phortune/controller/{ => product}/PhortuneProductListController.php (100%) rename src/applications/phortune/controller/{ => product}/PhortuneProductViewController.php (100%) rename src/applications/phortune/controller/{ => provider}/PhortuneProviderActionController.php (100%) rename src/applications/phortune/controller/{ => provider}/PhortuneProviderDisableController.php (100%) rename src/applications/phortune/controller/{ => provider}/PhortuneProviderEditController.php (100%) rename src/applications/phortune/controller/{ => subscription}/PhortuneSubscriptionEditController.php (100%) rename src/applications/phortune/controller/{ => subscription}/PhortuneSubscriptionListController.php (100%) rename src/applications/phortune/controller/{ => subscription}/PhortuneSubscriptionViewController.php (100%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f9d013f497..563b15e389 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4311,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', @@ -4358,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', @@ -4391,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', @@ -4405,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', @@ -9742,6 +9742,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), + 'PhortuneAccountChargeListController' => 'PhortuneController', 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', @@ -9777,7 +9778,6 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), - 'PhortuneChargeListController' => 'PhortuneController', 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index c87914b79f..15c312773b 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -52,7 +52,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { => 'PhortuneCartListController', ), 'charge/(?:query/(?P[^/]+)/)?' - => 'PhortuneChargeListController', + => 'PhortuneAccountChargeListController', ), 'card/(?P\d+)/' => array( 'edit/' => 'PhortunePaymentMethodEditController', diff --git a/src/applications/phortune/controller/PhortuneChargeListController.php b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php similarity index 97% rename from src/applications/phortune/controller/PhortuneChargeListController.php rename to src/applications/phortune/controller/account/PhortuneAccountChargeListController.php index b8edb92507..ed3f901675 100644 --- a/src/applications/phortune/controller/PhortuneChargeListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php @@ -1,6 +1,6 @@ Date: Thu, 30 Mar 2017 11:17:28 -0700 Subject: [PATCH 068/239] Update PhortuneLanding page UI Summary: Minor, uses 'user-circle' for account, and merchant logo for merchants in lists. Test Plan: View the landing page, see updated logos and icons. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D17586 --- .../controller/account/PhortuneAccountListController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/applications/phortune/controller/account/PhortuneAccountListController.php b/src/applications/phortune/controller/account/PhortuneAccountListController.php index d95fd709d5..177f84d75d 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountListController.php @@ -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); } From 7ab4e7dbced90f74dfa5a18b04def800a01bf986 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 30 Mar 2017 14:59:40 -0700 Subject: [PATCH 069/239] Allow Owner Packages to be in a Dashboard Panel Summary: Ref T12324. Add back Owners. Test Plan: read carefully Reviewers: epriestley, eadler Reviewed By: eadler Subscribers: Korvin Maniphest Tasks: T12324 Differential Revision: https://secure.phabricator.com/D17588 --- .../owners/query/PhabricatorOwnersPackageSearchEngine.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php index d6001419b4..728c3f42a8 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php @@ -15,10 +15,6 @@ final class PhabricatorOwnersPackageSearchEngine return new PhabricatorOwnersPackageQuery(); } - public function canUseInPanelContext() { - return false; - } - protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchDatasourceField()) From 4d29d8e2b7712f1ca462913c794edf789a605d6a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 30 Mar 2017 22:15:45 -0700 Subject: [PATCH 070/239] Fix filetree drag nav CSS Summary: Fixes T11630. Not sure what the max-width fixes, but I don't see anything off on various mobile, desktop. Test Plan: Enable filetree in differential, drag navigation all over, see normal width calculations. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11630 Differential Revision: https://secure.phabricator.com/D17591 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/aphront/phabricator-nav-view.css | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6bee132238..f45c16ec69 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '82aca405', 'conpherence.pkg.js' => '6249a1cf', - 'core.pkg.css' => '87c434ee', + 'core.pkg.css' => '1bf8fa70', 'core.pkg.js' => '021685f1', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '90b30783', @@ -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', @@ -786,7 +786,7 @@ 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', diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index a9c32f2e00..e8081a55e6 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -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; From 1c5503cb292f1b25ed96956fd98ad774be4d466e Mon Sep 17 00:00:00 2001 From: Daniel Stone Date: Sun, 2 Apr 2017 15:26:26 +0000 Subject: [PATCH 071/239] Custom fields: Render 'required' for tokenizer fields Summary: When building a tokenizer-based edit control for a custom field (e.g. a datasource type), preserve a field validation error whilst building edit controls. Test Plan: - Create custom datasource field, set it to required - Observe that 'Required' does not appear next to control - Apply patch - Observe 'Required' appears next to control Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D17592 --- .../standard/PhabricatorStandardCustomFieldTokenizer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index d0e4e8d6ee..d2b063ffde 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -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'); From 515cb98819ae0cafeaf664c25e9d150007d5c423 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 08:46:37 -0700 Subject: [PATCH 072/239] When running unit tests, ignore any custom task fields Summary: If you have `maniphest.custom-field-definitions` set to include "required" fields, a bunch of tests which create tasks can fail. To avoid this, reset this config while running tests. This mechanism should probably be more general (e.g., reset all config by default, only whitelist some config) but just fix this for now since it's a one-liner and doesn't make eventual cleanup any harder. Test Plan: Ran `arc unit`, hitting tests that create tasks. Reviewers: chad, 20after4 Reviewed By: chad Differential Revision: https://secure.phabricator.com/D17595 --- src/infrastructure/testing/PhabricatorTestCase.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php index c9790cd1e4..c4881ab846 100644 --- a/src/infrastructure/testing/PhabricatorTestCase.php +++ b/src/infrastructure/testing/PhabricatorTestCase.php @@ -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() { From 64234535e3c96e7c60ce86815b25b592e7ab8a41 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 08:43:02 -0700 Subject: [PATCH 073/239] Remove FIELD_KEYWORDS, index project slugs as body content Summary: D17384 added a "keywords" field but only partially implemented it. - Remove this field. - Index project slugs as part of the document body instead. Test Plan: - Ran `bin/search index PHID-PROJ-... --force`. - Found project by searching for a unique slug. Reviewers: chad, 20after4 Reviewed By: chad Differential Revision: https://secure.phabricator.com/D17596 --- .../PhabricatorProjectFulltextEngine.php | 25 +++++++++++++------ .../PhabricatorSearchDocumentFieldType.php | 1 - 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/applications/project/search/PhabricatorProjectFulltextEngine.php b/src/applications/project/search/PhabricatorProjectFulltextEngine.php index 14314c3436..ecec952990 100644 --- a/src/applications/project/search/PhabricatorProjectFulltextEngine.php +++ b/src/applications/project/search/PhabricatorProjectFulltextEngine.php @@ -8,17 +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->getDisplayName()); - $document->addField(PhabricatorSearchDocumentFieldType::FIELD_KEYWORDS, - $project->getPrimarySlug()); - try { - $slugs = $project->getSlugs(); - foreach ($slugs as $slug) {} - } catch (PhabricatorDataNotAttachedException $e) { - // ignore + $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() diff --git a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php index 12c90f8469..10dbf0ca65 100644 --- a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php +++ b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php @@ -5,6 +5,5 @@ final class PhabricatorSearchDocumentFieldType extends Phobject { const FIELD_TITLE = 'titl'; const FIELD_BODY = 'body'; const FIELD_COMMENT = 'cmnt'; - const FIELD_KEYWORDS = 'kwrd'; } From 287e708c4d3ecdec3af77f5c409d0aa9f118ef94 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 12:55:38 -0700 Subject: [PATCH 074/239] Adjust and wordsmith Search documentation Summary: Ref T12450. General adjustments: - Try to make "Cluster: Search" more about "stuff in common + types" instead of pretty much all being Elastic-specific, so we can add Solr or whatever later. - Provide guidance about rebuilding indexes after making a change. - Simplify the basic examples, then provide a more advanced example at the ed. - Really try to avoid suggesting anyone configure Elasticsearch ever for any reason. Test Plan: Read documents, previewed in remarkup. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17602 --- src/docs/user/cluster/cluster.diviner | 26 +-- src/docs/user/cluster/cluster_search.diviner | 234 +++++++++++++++---- 2 files changed, 191 insertions(+), 69 deletions(-) diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 15eed86eb4..30ad53efb8 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -47,7 +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** | Moderate | **High** | Minimal Risk | Moderate +| **Fulltext Search** | Minimal | Low | No Risk | Low See below for a walkthrough of these services in greater detail. @@ -241,26 +241,14 @@ For details, see @{article:Cluster: Notifications}. Cluster: Fulltext Search ======================== -At a certain scale, you may begin to bump up against the limitations of MySQL's -built-in fulltext search capabilities. We have seen this with very large -installations with several million objects in the database and very many -simultaneous requests. At this point you may consider adding Elasticsearch -hosts to your cluster to reduce the load on your MySQL hosts. +Configuring search services is relatively simple and has no pre-requisites. -Elasticsearch has the ability to spread the load across multiple hosts and can -handle very large indexes by sharding. +By default, Phabricator uses MySQL as a fulltext search engine, so deploying +multiple database hosts will effectively also deploy multiple fulltext search +hosts. -Search does not involve any risk of data lost because it's always possible to -rebuild the search index from the original database objects. This process can -be very time consuming, however, especially when the database grows very large. - -With multiple Elasticsearch hosts, you can survive the loss of a single host -with minimal disruption as Phabricator will detect the problem and direct -queries to one of the remaining hosts. - -Phabricator supports writing to multiple indexing servers. This Simplifies -Elasticsearch upgrades and makes it possible to recover more quickly from -problems with the search index. +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}. diff --git a/src/docs/user/cluster/cluster_search.diviner b/src/docs/user/cluster/cluster_search.diviner index 662abecbc3..c658f50db4 100644 --- a/src/docs/user/cluster/cluster_search.diviner +++ b/src/docs/user/cluster/cluster_search.diviner @@ -4,73 +4,207 @@ Overview ======== -You can configure phabricator to connect to one or more fulltext search clusters -running either Elasticsearch or MySQL. By default and without further -configuration, Phabricator will use MySQL for fulltext search. This will be -adequate for the vast majority of users. Installs with a very large number of -objects or specialized search needs can consider enabling Elasticsearch for -better scalability and potentially better search results. +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 an Elasticsearch service, use the `cluster.search` configuration -option. A typical Elasticsearch configuration will probably look similar to -the following example: +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 { - "cluster.search": [ - { - "type": "elasticsearch", - "hosts": [ - { - "host": "127.0.0.1", - "roles": { "write": true, "read": true } - } - ], - "port": 9200, - "protocol": "http", - "path": "/phabricator", - "version": 5 - }, - ], + "type": "mysql" } ``` -Supported Options ------------------ -| Key | Type |Comments| -|`type` | String |Engine type. Currently, 'elasticsearch' or 'mysql'| -|`protocol`| String |Either 'http' or 'https'| -|`port`| Int |The TCP port that Elasticsearch is bound to| -|`path`| String |The path portion of the url for phabricator's index.| -|`version`| Int |The version of Elasticsearch server. Supports either 2 or 5.| -|`hosts`| List |A list of one or more Elasticsearch host names / addresses.| -Host Configuration ------------------- -Each search service must have one or more hosts associated with it. Each host -entry consists of a `host` key, a dictionary of roles and can optionally -override any of the options that are valid at the service level (see above). +Service Type: Elasticsearch +====================== -Currently supported roles are `read` and `write`. These can be individually -enabled or disabled on a per-host basis. A typical setup might include two -elasticsearch clusters in two separate datacenters. You can configure one -cluster for reads and both for writes. When one cluster is down for maintenance -you can simply swap the read role over to the backup cluster and then proceed -with maintenance without any service interruption. +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. +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. -NOTE: This page runs its diagnostics //from the web server that is serving the -request//. If you are recovering from a disaster, the view this page shows -may be partial or misleading, and two requests served by different servers may -see different views of the cluster. + +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" + } +] +``` From 6d81675032661359fa340ec6a74bee0e02eed652 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 09:59:49 -0700 Subject: [PATCH 075/239] Remove "url" from Elasticsearch index Summary: Ref T12450. This was added a very very long time ago (D2298). I don't want to put this in the upstream index anymore because I don't want to encourage third parties to develop software which reads the index directly. Reading the index directly is a big skeleton key which bypasses policy checks. This was added before much of the policy model existed, when that wasn't as much of a concern. On a tecnhnical note, this also doesn't update when `phabricator.base-uri` changes. This can be written as a search index extension if an install relies on it for some bizarre reason, although none should and I'm unaware of any actual use cases in the wild for it, even at Facebook. Test Plan: Indexed some random stuff into ElasticSearch. Reviewers: chad, 20after4 Reviewed By: chad Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17600 --- .../fulltextstorage/PhabricatorElasticFulltextStorageEngine.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index 45011ba982..51e849f91a 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -78,10 +78,8 @@ class PhabricatorElasticFulltextStorageEngine $timestamp_key = $this->getTimestampField(); - // URL is not used internally but it can be useful externally. $spec = array( 'title' => $doc->getDocumentTitle(), - 'url' => PhabricatorEnv::getProductionURI($handle->getURI()), 'dateCreated' => $doc->getDocumentCreated(), $timestamp_key => $doc->getDocumentModified(), ); From bd939782001ed1b4cb6ed2b812bad58c75309a13 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 09:17:29 -0700 Subject: [PATCH 076/239] Count and report skipped documents from "bin/search index" Summary: Ref T12450. There's currently a bad behavior where inserting a document into one search service marks it as up to date everywhere. This isn't nearly as obvious as it should be because `bin/search index` doesn't make it terribly clear when a document was skipped because the index version was already up to date. When running `bin/seach index` without `--force` or `--background`, keep track of updated vs not-updated documents and print out some guidance. In other configurations, try to provide more help too. Test Plan: {F4452134} Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17597 --- ...abricatorSearchManagementIndexWorkflow.php | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index a324a20637..d7667b86b6 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -87,8 +87,9 @@ final class PhabricatorSearchManagementIndexWorkflow } if (!$is_background) { - $console->writeOut( - "%s\n", + echo tsprintf( + "** %s ** %s\n", + pht('NOTE'), pht( 'Run this workflow with "%s" to queue tasks for the daemon workers.', '--background')); @@ -109,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); @@ -127,6 +151,45 @@ final class PhabricatorSearchManagementIndexWorkflow pht('Failed to rebuild search index for any documents.')); } + if ($track_skips) { + if ($count_updated) { + echo tsprintf( + "** %s ** %s\n", + pht('DONE'), + pht( + 'Updated search indexes for %s document(s).', + new PhutilNumber($count_updated))); + } + + if ($count_skipped) { + echo tsprintf( + "** %s ** %s\n", + pht('SKIP'), + pht( + 'Skipped %s documents(s) which have not updated since they were '. + 'last indexed.', + new PhutilNumber($count_skipped))); + echo tsprintf( + "** %s ** %s\n", + pht('NOTE'), + pht( + 'Use "--force" to force the index to update these documents.')); + } + } else if ($is_background) { + echo tsprintf( + "** %s ** %s\n", + pht('DONE'), + pht( + 'Queued %s document(s) for background indexing.', + new PhutilNumber(count($phids)))); + } else { + echo tsprintf( + "** %s ** %s\n", + pht('DONE'), + pht( + 'Forced search index updates for %s document(s).', + new PhutilNumber(count($phids)))); + } } private function loadPHIDsByNames(array $names) { @@ -206,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); + } } From 0f144d29e92059b787cfb3816a0d3988d4b1feb3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 09:27:19 -0700 Subject: [PATCH 077/239] When "cluster.search" changes, don't trust the old index versions Summary: Ref T12450. We track a "document version" for updating search indexes, so that if a document is rapidly updated many times in a row we can skip most of the work. However, this version doesn't consider "cluster.search" configuration, so if you add a new service (like a new ElasticSearch host) we still think that every document is up-to-date. When you run `bin/search index` to populate the index (without `--force`), we just do nothing. This isn't necessarily very obvious. D17597 makes it more clear, by printing "everything was skipped and nothing happened" at the end. Here, fix the issue by considering the content of "cluster.search" when computing fulltext document versions: if you change `cluster.search`, we throw away the version index and reindex everything. This is slightly more work than we need to do, but changes to "cluster.search" are rare and this is much easier than trying to individually track which versions of which documents are in which services, which probably isn't very useful anyway. Test Plan: - Ran `bin/search index --type project`, saw everything get skipped. - Changed `cluster.search`. - Ran `search index` again, saw everything get updated. - Ran a third time without changing `cluster.search`, everything was properly skipped. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17598 --- ...habricatorFulltextIndexEngineExtension.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php index 0767849abe..ab4da88420 100644 --- a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php +++ b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php @@ -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); + } + } From 304d19f92a7bea08573045d6951cefa4b14e7086 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 09:53:26 -0700 Subject: [PATCH 078/239] After a fulltext write to a particular service fails, keep trying writes to other services Summary: Ref T12450. Currently, if a write fails, we stop and don't try to write to other index services. There's no technical reason not to keep trying writes, it makes some testing easier, and it would improve behavior in a scenario where engines are configured as "primary" and "backup" and the primary service is having some issues. Also, make "no writable services are configured" acceptable, rather than an error. This state is probably goofy but if we want to detect it I think it should probably be a config-validation issue, not a write-time check. I also think it's not totally unreasonable to want to just turn off all writes for a while (maybe to reduce load while you're doing a background update). Test Plan: - Configured a bad ElasticSearch engine and a good MySQL engine. - Ran `bin/search index ... --force`. - Saw MySQL get updated even though ElasticSearch failed. Reviewers: chad, 20after4 Reviewed By: 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17599 --- .../search/PhabricatorSearchService.php | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php index 10cf78d94b..a9ceb0e7e5 100644 --- a/src/infrastructure/cluster/search/PhabricatorSearchService.php +++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php @@ -212,20 +212,30 @@ class PhabricatorSearchService /** * (re)index the document: attempt to pass the document to all writable * fulltext search hosts - * @throws PhabricatorClusterNoHostForRoleException */ public static function reindexAbstractDocument( - PhabricatorSearchAbstractDocument $doc) { - $indexed = 0; + PhabricatorSearchAbstractDocument $document) { + + $exceptions = array(); foreach (self::getAllServices() as $service) { - $hosts = $service->getAllHostsForRole('write'); - if (count($hosts)) { - $service->getEngine()->reindexAbstractDocument($doc); - $indexed++; + if (!$service->isWritable()) { + continue; + } + + $engine = $service->getEngine(); + try { + $engine->reindexAbstractDocument($document); + } catch (Exception $ex) { + $exceptions[] = $ex; } } - if ($indexed == 0) { - throw new PhabricatorClusterNoHostForRoleException('write'); + + if ($exceptions) { + throw new PhutilAggregateException( + pht( + 'Writes to search services failed while reindexing document "%s".', + $document->getPHID()), + $exceptions); } } From a9e2732a5cb365b7ec30fe7fbe9e9305fbbad2d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 12:03:28 -0700 Subject: [PATCH 079/239] Spell "Elasticsearch" correctly, not "ElasticSearch" Summary: Ref T12450. These are like 95% my fault, but Elastic appears to spell the name "Elasticsearch" consistently in their branding. Test Plan: `grep ElasticSearch` Reviewers: chad, 20after4 Maniphest Tasks: T12450 Differential Revision: https://secure.phabricator.com/D17601 --- src/__phutil_library_map__.php | 10 +++++----- ...atorElasticsearchSetupCheck.php.lowercase} | 4 ++-- .../check/PhabricatorMySQLSetupCheck.php | 6 +++--- .../maniphest/query/ManiphestTaskQuery.php | 2 +- ...habricatorElasticFulltextStorageEngine.php | 20 +++++++------------ ...orElasticsearchQueryBuilder.php.lowercase} | 2 +- ...habricatorElasticsearchHost.php.lowercase} | 4 ++-- 7 files changed, 21 insertions(+), 27 deletions(-) rename src/applications/config/check/{PhabricatorElasticSearchSetupCheck.php => PhabricatorElasticsearchSetupCheck.php.lowercase} (95%) rename src/applications/search/fulltextstorage/{PhabricatorElasticSearchQueryBuilder.php => PhabricatorElasticsearchQueryBuilder.php.lowercase} (97%) rename src/infrastructure/cluster/search/{PhabricatorElasticSearchHost.php => PhabricatorElasticsearchHost.php.lowercase} (96%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 563b15e389..9ccc495e7a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2655,9 +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', - 'PhabricatorElasticSearchHost' => 'infrastructure/cluster/search/PhabricatorElasticSearchHost.php', - 'PhabricatorElasticSearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.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', @@ -7750,8 +7750,8 @@ phutil_register_library_map(array( 'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting', 'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine', - 'PhabricatorElasticSearchHost' => 'PhabricatorSearchHost', - 'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost', + 'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailContentSource' => 'PhabricatorContentSource', 'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel', diff --git a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php b/src/applications/config/check/PhabricatorElasticsearchSetupCheck.php.lowercase similarity index 95% rename from src/applications/config/check/PhabricatorElasticSearchSetupCheck.php rename to src/applications/config/check/PhabricatorElasticsearchSetupCheck.php.lowercase index dab886b92a..157db5e141 100644 --- a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php +++ b/src/applications/config/check/PhabricatorElasticsearchSetupCheck.php.lowercase @@ -1,6 +1,6 @@ setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 10,000 will raise errors in - // ElasticSearch, and billions of results won't fit in memory anyway. + // Elasticsearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 10000); $fulltext_query->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST)); diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php index 51e849f91a..8c75c17e36 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php @@ -32,19 +32,13 @@ class PhabricatorElasticFulltextStorageEngine } public function getHostType() { - return new PhabricatorElasticSearchHost($this); + return new PhabricatorElasticsearchHost($this); } - /** - * @return PhabricatorElasticSearchHost - */ public function getHostForRead() { return $this->getService()->getAnyHostForRole('read'); } - /** - * @return PhabricatorElasticSearchHost - */ public function getHostForWrite() { return $this->getService()->getAnyHostForRole('write'); } @@ -148,7 +142,7 @@ class PhabricatorElasticFulltextStorageEngine } private function buildSpec(PhabricatorSavedQuery $query) { - $q = new PhabricatorElasticSearchQueryBuilder('bool'); + $q = new PhabricatorElasticsearchQueryBuilder('bool'); $query_string = $query->getParameter('query'); if (strlen($query_string)) { $fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType'); @@ -305,7 +299,7 @@ class PhabricatorElasticFulltextStorageEngine $exceptions); } - public function indexExists(PhabricatorElasticSearchHost $host = null) { + public function indexExists(PhabricatorElasticsearchHost $host = null) { if (!$host) { $host = $this->getHostForRead(); } @@ -439,7 +433,7 @@ class PhabricatorElasticFulltextStorageEngine return $data; } - public function indexIsSane(PhabricatorElasticSearchHost $host = null) { + public function indexIsSane(PhabricatorElasticsearchHost $host = null) { if (!$host) { $host = $this->getHostForRead(); } @@ -518,7 +512,7 @@ class PhabricatorElasticFulltextStorageEngine $this->executeRequest($host, '/', $data, 'PUT'); } - public function getIndexStats(PhabricatorElasticSearchHost $host = null) { + public function getIndexStats(PhabricatorElasticsearchHost $host = null) { if ($this->version < 2) { return false; } @@ -542,7 +536,7 @@ class PhabricatorElasticFulltextStorageEngine ); } - private function executeRequest(PhabricatorElasticSearchHost $host, $path, + private function executeRequest(PhabricatorElasticsearchHost $host, $path, array $data, $method = 'GET') { $uri = $host->getURI($path); @@ -576,7 +570,7 @@ class PhabricatorElasticFulltextStorageEngine } catch (PhutilJSONParserException $ex) { $host->didHealthCheck(false); throw new PhutilProxyException( - pht('ElasticSearch server returned invalid JSON!'), + pht('Elasticsearch server returned invalid JSON!'), $ex); } diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php b/src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php.lowercase similarity index 97% rename from src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php rename to src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php.lowercase index 659660d813..0f835eb726 100644 --- a/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php +++ b/src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php.lowercase @@ -1,6 +1,6 @@ Date: Sun, 2 Apr 2017 14:59:36 -0700 Subject: [PATCH 080/239] Rename "ElasticSearch" filenames to "Elasticsearch" (2/2) Sometimes git does some odd magic on case-insensitive filesystems, try to trick it. Auditors: chad --- ...Check.php.lowercase => PhabricatorElasticsearchSetupCheck.php} | 0 ...der.php.lowercase => PhabricatorElasticsearchQueryBuilder.php} | 0 ...csearchHost.php.lowercase => PhabricatorElasticsearchHost.php} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/applications/config/check/{PhabricatorElasticsearchSetupCheck.php.lowercase => PhabricatorElasticsearchSetupCheck.php} (100%) rename src/applications/search/fulltextstorage/{PhabricatorElasticsearchQueryBuilder.php.lowercase => PhabricatorElasticsearchQueryBuilder.php} (100%) rename src/infrastructure/cluster/search/{PhabricatorElasticsearchHost.php.lowercase => PhabricatorElasticsearchHost.php} (100%) diff --git a/src/applications/config/check/PhabricatorElasticsearchSetupCheck.php.lowercase b/src/applications/config/check/PhabricatorElasticsearchSetupCheck.php similarity index 100% rename from src/applications/config/check/PhabricatorElasticsearchSetupCheck.php.lowercase rename to src/applications/config/check/PhabricatorElasticsearchSetupCheck.php diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php.lowercase b/src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php similarity index 100% rename from src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php.lowercase rename to src/applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php diff --git a/src/infrastructure/cluster/search/PhabricatorElasticsearchHost.php.lowercase b/src/infrastructure/cluster/search/PhabricatorElasticsearchHost.php similarity index 100% rename from src/infrastructure/cluster/search/PhabricatorElasticsearchHost.php.lowercase rename to src/infrastructure/cluster/search/PhabricatorElasticsearchHost.php From 009aff1a23d876689e4b85c1acfe87da1594eb8a Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 17:12:18 -0700 Subject: [PATCH 081/239] Return task descriptions from "maniphest.search" Summary: Fixes T12461. This returns the field as a dictionary with a `"raw"` value, so we could eventually do this if we want without breaking the API: ``` { "type": "remarkup", "raw": "**raw**", "html": "raw", "text": "raw" } ``` Test Plan: Called `maniphest.search`, reviewed output. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12461 Differential Revision: https://secure.phabricator.com/D17603 --- src/applications/maniphest/storage/ManiphestTask.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index f03bc277ac..ebcdef691d 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -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, From 163e1ec4426eb40e15ad46f831a731f13b275918 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 2 Apr 2017 17:27:20 -0700 Subject: [PATCH 082/239] Expose the commit/task/revision relationship edges to "edge.search" Summary: Fixes T12480. Test Plan: {F4465908} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12480 Differential Revision: https://secure.phabricator.com/D17604 --- .../edge/DifferentialRevisionHasCommitEdgeType.php | 13 +++++++++++++ .../edge/DifferentialRevisionHasTaskEdgeType.php | 12 ++++++++++++ .../edge/DiffusionCommitHasRevisionEdgeType.php | 13 +++++++++++++ .../edge/DiffusionCommitHasTaskEdgeType.php | 12 ++++++++++++ .../edge/ManiphestTaskHasCommitEdgeType.php | 12 ++++++++++++ .../edge/ManiphestTaskHasRevisionEdgeType.php | 12 ++++++++++++ 6 files changed, 74 insertions(+) diff --git a/src/applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php b/src/applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php index 8fe95bb844..819794cbd6 100644 --- a/src/applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php +++ b/src/applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php @@ -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, diff --git a/src/applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php b/src/applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php index de9a9cad57..c4cf84c5fe 100644 --- a/src/applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php +++ b/src/applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php @@ -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, diff --git a/src/applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php index c0b51dd086..ce7a899bda 100644 --- a/src/applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php @@ -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.'); + } + } diff --git a/src/applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php index ad769eff72..497b242650 100644 --- a/src/applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php @@ -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, diff --git a/src/applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php b/src/applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php index 4e3505163d..55515a3464 100644 --- a/src/applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php +++ b/src/applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php @@ -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, diff --git a/src/applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php b/src/applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php index 6bd1e6c94e..7e35f01627 100644 --- a/src/applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php +++ b/src/applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php @@ -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, From 9ebb5f8cda56a5ff057500265a5afc5a3e4f0a3b Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 3 Apr 2017 08:35:29 -0700 Subject: [PATCH 083/239] Don't downgrade accepts on update (fix "sticky accept") Summary: Fixes T12496. Sticky accept was accidentally impacted by the "void" changes in D17566. Instead, don't always downgrade all accepts/rejects: on update, we only want to downgrade accepts. Test Plan: - With sticky accept off, updated an accepted revision: new state is "needs review". - With sticky accept on, updated an accepted revision: new state is "accepted" (sticky accept working correctly). - Did "reject" + "request review" to make sure that still works, worked fine. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12496 Differential Revision: https://secure.phabricator.com/D17605 --- .../editor/DifferentialTransactionEditor.php | 14 ++++++++++++-- .../DifferentialRevisionVoidTransaction.php | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index bf7faa2700..f24bc7abda 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -339,12 +339,22 @@ final class DifferentialTransactionEditor } } - if ($downgrade_accepts || $downgrade_rejects) { + $downgrade = array(); + if ($downgrade_accepts) { + $downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; + } + + if ($downgrade_accepts) { + $downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; + } + + if ($downgrade) { $void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE; + $results[] = id(new DifferentialTransaction()) ->setTransactionType($void_type) ->setIgnoreOnNoEffect(true) - ->setNewValue(true); + ->setNewValue($downgrade); } $is_commandeer = false; diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php index ae684d94fe..5073a9c464 100644 --- a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php @@ -28,7 +28,7 @@ final class DifferentialRevisionVoidTransaction AND reviewerStatus IN (%Ls)', $table_name, $object->getPHID(), - $this->getVoidableStatuses()); + $value); return ipull($rows, 'reviewerPHID'); } @@ -47,11 +47,11 @@ final class DifferentialRevisionVoidTransaction 'UPDATE %T SET voidedPHID = %s WHERE revisionPHID = %s AND voidedPHID IS NULL - AND reviewerStatus IN (%Ls)', + AND reviewerPHID IN (%Ls)', $table_name, $this->getActingAsPHID(), $object->getPHID(), - $this->getVoidableStatuses()); + $value); } public function shouldHide() { From 5e447112184e48a57509a57efeea7c5c067fa754 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 4 Apr 2017 05:53:39 -0700 Subject: [PATCH 084/239] Provide a missing feed transaction string for space creation Summary: Fixes T12502. This transaction probably should not be getting picked for feed rendering, but it currently does get selected in some cases. This should probably be revisited eventually (e.g., when Maniphest moves to ModularTransactions) but just fix the brokenness for now. Test Plan: - Created a task in a space. - Viewed feed. - Saw the story render with readable text. {F4555747} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12502 Differential Revision: https://secure.phabricator.com/D17609 --- .../PhabricatorApplicationTransaction.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 5e36e942ce..bf3c616df9 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1149,12 +1149,20 @@ abstract class PhabricatorApplicationTransaction $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SPACE: - return pht( - '%s shifted %s from the %s space to the %s space.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid), - $this->renderHandleLink($old), - $this->renderHandleLink($new)); + if ($this->getIsCreateTransaction()) { + return pht( + '%s created %s in the %s space.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $this->renderHandleLink($new)); + } else { + return pht( + '%s shifted %s from the %s space to the %s space.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); From 8500f78e451c61079ba9ca4e4f27a087e7ab1403 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 4 Apr 2017 07:43:20 -0700 Subject: [PATCH 085/239] Move Files to ModularTransactions Summary: Ref T11357. A lot of file creation doesn't go through transactions, so we only actually have one real transaction type: editing a file name. Test Plan: Created and edited files. {F4559287} Reviewers: chad Reviewed By: chad Maniphest Tasks: T11357 Differential Revision: https://secure.phabricator.com/D17610 --- src/__phutil_library_map__.php | 6 +- .../PhabricatorBadgesBadgeNameTransaction.php | 2 +- .../PhabricatorFileEditController.php | 4 +- .../files/editor/PhabricatorFileEditor.php | 67 ------------------ .../storage/PhabricatorFileTransaction.php | 69 +------------------ .../__tests__/PhabricatorFileTestCase.php | 2 +- .../PhabricatorFileNameTransaction.php | 55 +++++++++++++++ .../PhabricatorFileTransactionType.php | 4 ++ 8 files changed, 71 insertions(+), 138 deletions(-) create mode 100644 src/applications/files/xaction/PhabricatorFileNameTransaction.php create mode 100644 src/applications/files/xaction/PhabricatorFileTransactionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9ccc495e7a..14d84a9ae7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2765,6 +2765,7 @@ phutil_register_library_map(array( 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', + 'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php', 'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php', 'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php', 'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php', @@ -2783,6 +2784,7 @@ phutil_register_library_map(array( 'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php', 'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php', 'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php', + 'PhabricatorFileTransactionType' => 'applications/files/xaction/PhabricatorFileTransactionType.php', 'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php', 'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', 'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php', @@ -7890,6 +7892,7 @@ phutil_register_library_map(array( 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 'PhabricatorFileLinkView' => 'AphrontTagView', 'PhabricatorFileListController' => 'PhabricatorFileController', + 'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat', @@ -7905,9 +7908,10 @@ phutil_register_library_map(array( 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform', - 'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorFileTransaction' => 'PhabricatorModularTransaction', 'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorFileTransform' => 'Phobject', 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileTransformListController' => 'PhabricatorFileController', diff --git a/src/applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php b/src/applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php index 1df7d89a70..3a609fedb2 100644 --- a/src/applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php +++ b/src/applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php @@ -43,7 +43,7 @@ final class PhabricatorBadgesBadgeNameTransaction $new_value = $xaction->getNewValue(); $new_length = strlen($new_value); if ($new_length > $max_length) { - $errors[] = $this->newRequiredError( + $errors[] = $this->newInvalidError( pht('The name can be no longer than %s characters.', new PhutilNumber($max_length))); } diff --git a/src/applications/files/controller/PhabricatorFileEditController.php b/src/applications/files/controller/PhabricatorFileEditController.php index e1b34afd73..a1e7a16d80 100644 --- a/src/applications/files/controller/PhabricatorFileEditController.php +++ b/src/applications/files/controller/PhabricatorFileEditController.php @@ -31,7 +31,7 @@ final class PhabricatorFileEditController extends PhabricatorFileController { $file_name = $request->getStr('name'); $errors = array(); - $type_name = PhabricatorFileTransaction::TYPE_NAME; + $type_name = PhabricatorFileNameTransaction::TRANSACTIONTYPE; $xactions = array(); @@ -40,7 +40,7 @@ final class PhabricatorFileEditController extends PhabricatorFileController { ->setNewValue($can_view); $xactions[] = id(new PhabricatorFileTransaction()) - ->setTransactionType(PhabricatorFileTransaction::TYPE_NAME) + ->setTransactionType($type_name) ->setNewValue($file_name); $editor = id(new PhabricatorFileEditor()) diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php index 9cd81a4c1e..28b781fb37 100644 --- a/src/applications/files/editor/PhabricatorFileEditor.php +++ b/src/applications/files/editor/PhabricatorFileEditor.php @@ -17,46 +17,9 @@ final class PhabricatorFileEditor $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; - $types[] = PhabricatorFileTransaction::TYPE_NAME; - return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorFileTransaction::TYPE_NAME: - return $object->getName(); - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorFileTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorFileTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - break; - } - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) {} - protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { @@ -111,34 +74,4 @@ final class PhabricatorFileEditor return false; } - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case PhabricatorFileTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('File name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; - } - - } diff --git a/src/applications/files/storage/PhabricatorFileTransaction.php b/src/applications/files/storage/PhabricatorFileTransaction.php index 663dde28c7..3c72be6fd7 100644 --- a/src/applications/files/storage/PhabricatorFileTransaction.php +++ b/src/applications/files/storage/PhabricatorFileTransaction.php @@ -1,9 +1,7 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - return pht( - '%s updated the name for this file from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - break; - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'PhabricatorFileTransactionType'; } - public function getTitleForFeed() { - $author_phid = $this->getAuthorPHID(); - $object_phid = $this->getObjectPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case self::TYPE_NAME: - return pht( - '%s updated the name of %s from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid), - $old, - $new); - break; - } - - return parent::getTitleForFeed(); - } - - public function getIcon() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - return 'fa-pencil'; - } - - return parent::getIcon(); - } - - - public function getColor() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - return PhabricatorTransactions::COLOR_BLUE; - } - - return parent::getColor(); - } } diff --git a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php index 8aa22c6578..f9090c9c83 100644 --- a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php +++ b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php @@ -32,7 +32,7 @@ final class PhabricatorFileTestCase extends PhabricatorTestCase { // First, change the name: this should not scramble the secret. $xactions = array(); $xactions[] = id(new PhabricatorFileTransaction()) - ->setTransactionType(PhabricatorFileTransaction::TYPE_NAME) + ->setTransactionType(PhabricatorFileNameTransaction::TRANSACTIONTYPE) ->setNewValue('test.dat2'); $engine = id(new PhabricatorFileEditor()) diff --git a/src/applications/files/xaction/PhabricatorFileNameTransaction.php b/src/applications/files/xaction/PhabricatorFileNameTransaction.php new file mode 100644 index 0000000000..7eb9396b66 --- /dev/null +++ b/src/applications/files/xaction/PhabricatorFileNameTransaction.php @@ -0,0 +1,55 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s updated the name for this file from "%s" to "%s".', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function getTitleForFeed() { + return pht( + '%s updated the name of %s from "%s" to "%s".', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError(pht('Files must have a name.')); + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'File names must not be longer than %s character(s).', + new PhutilNumber($max_length))); + } + } + + return $errors; + } + +} diff --git a/src/applications/files/xaction/PhabricatorFileTransactionType.php b/src/applications/files/xaction/PhabricatorFileTransactionType.php new file mode 100644 index 0000000000..cc708ac541 --- /dev/null +++ b/src/applications/files/xaction/PhabricatorFileTransactionType.php @@ -0,0 +1,4 @@ + Date: Tue, 4 Apr 2017 08:02:37 -0700 Subject: [PATCH 086/239] Move Files editing and commenting to EditEngine Summary: Ref T11357. This moves editing and commenting (but not creation) to EditEngine. Since only the name is really editable, this is pretty straightforward. Test Plan: Renamed files; commented on files. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11357 Differential Revision: https://secure.phabricator.com/D17611 --- src/__phutil_library_map__.php | 4 +- .../PhabricatorFilesApplication.php | 3 +- .../PhabricatorFileCommentController.php | 62 ---------- .../PhabricatorFileEditController.php | 112 +----------------- .../PhabricatorFileInfoController.php | 21 ++-- .../editor/PhabricatorFileEditEngine.php | 79 ++++++++++++ .../files/storage/PhabricatorFile.php | 3 + 7 files changed, 98 insertions(+), 186 deletions(-) delete mode 100644 src/applications/files/controller/PhabricatorFileCommentController.php create mode 100644 src/applications/files/editor/PhabricatorFileEditEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 14d84a9ae7..99f9810052 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2743,7 +2743,6 @@ phutil_register_library_map(array( 'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php', 'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php', 'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php', - 'PhabricatorFileCommentController' => 'applications/files/controller/PhabricatorFileCommentController.php', 'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php', 'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php', 'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php', @@ -2751,6 +2750,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', + 'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php', 'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php', 'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php', 'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php', @@ -7860,7 +7860,6 @@ phutil_register_library_map(array( 'Iterator', ), 'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'PhabricatorFileCommentController' => 'PhabricatorFileController', 'PhabricatorFileComposeController' => 'PhabricatorFileController', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', @@ -7868,6 +7867,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', + 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', 'PhabricatorFileEditField' => 'PhabricatorEditField', 'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorFileExternalRequest' => array( diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index 5733439fcf..1010818c19 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -78,7 +78,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { 'comment/(?P[1-9]\d*)/' => 'PhabricatorFileCommentController', 'thread/(?P[^/]+)/' => 'PhabricatorFileLightboxController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorFileDeleteController', - 'edit/(?P[1-9]\d*)/' => 'PhabricatorFileEditController', + $this->getEditRoutePattern('edit/') + => 'PhabricatorFileEditController', 'info/(?P[^/]+)/' => 'PhabricatorFileInfoController', 'imageproxy/' => 'PhabricatorFileImageProxyController', 'transforms/(?P[1-9]\d*)/' => diff --git a/src/applications/files/controller/PhabricatorFileCommentController.php b/src/applications/files/controller/PhabricatorFileCommentController.php deleted file mode 100644 index 11833ea8d8..0000000000 --- a/src/applications/files/controller/PhabricatorFileCommentController.php +++ /dev/null @@ -1,62 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - if (!$request->isFormPost()) { - return new Aphront400Response(); - } - - $file = id(new PhabricatorFileQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$file) { - return new Aphront404Response(); - } - - $is_preview = $request->isPreviewRequest(); - $draft = PhabricatorDraft::buildFromRequest($request); - - $view_uri = $file->getInfoURI(); - - $xactions = array(); - $xactions[] = id(new PhabricatorFileTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) - ->attachComment( - id(new PhabricatorFileTransactionComment()) - ->setContent($request->getStr('comment'))); - - $editor = id(new PhabricatorFileEditor()) - ->setActor($viewer) - ->setContinueOnNoEffect($request->isContinueRequest()) - ->setContentSourceFromRequest($request) - ->setIsPreview($is_preview); - - try { - $xactions = $editor->applyTransactions($file, $xactions); - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { - return id(new PhabricatorApplicationTransactionNoEffectResponse()) - ->setCancelURI($view_uri) - ->setException($ex); - } - - if ($draft) { - $draft->replaceOrDelete(); - } - - if ($request->isAjax() && $is_preview) { - return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($viewer) - ->setTransactions($xactions) - ->setIsPreview($is_preview); - } else { - return id(new AphrontRedirectResponse()) - ->setURI($view_uri); - } - } - -} diff --git a/src/applications/files/controller/PhabricatorFileEditController.php b/src/applications/files/controller/PhabricatorFileEditController.php index a1e7a16d80..ea07d72722 100644 --- a/src/applications/files/controller/PhabricatorFileEditController.php +++ b/src/applications/files/controller/PhabricatorFileEditController.php @@ -1,114 +1,12 @@ getViewer(); - $id = $request->getURIData('id'); - - $file = id(new PhabricatorFileQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$file) { - return new Aphront404Response(); - } - - $title = pht('Edit File: %s', $file->getName()); - $file_name = $file->getName(); - $header_icon = 'fa-pencil'; - $view_uri = '/'.$file->getMonogram(); - $error_name = true; - $validation_exception = null; - - if ($request->isFormPost()) { - $can_view = $request->getStr('canView'); - $file_name = $request->getStr('name'); - $errors = array(); - - $type_name = PhabricatorFileNameTransaction::TRANSACTIONTYPE; - - $xactions = array(); - - $xactions[] = id(new PhabricatorFileTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($can_view); - - $xactions[] = id(new PhabricatorFileTransaction()) - ->setTransactionType($type_name) - ->setNewValue($file_name); - - $editor = id(new PhabricatorFileEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - - try { - $editor->applyTransactions($file, $xactions); - return id(new AphrontRedirectResponse())->setURI($view_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - $error_name = $ex->getShortMessage($type_name); - - $file->setViewPolicy($can_view); - } - } - - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($file) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('name') - ->setValue($file_name) - ->setLabel(pht('Name')) - ->setError($error_name)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicyObject($file) - ->setPolicies($policies) - ->setName('canView')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($view_uri) - ->setValue(pht('Save Changes'))); - - $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($file->getMonogram(), $view_uri) - ->addTextCrumb(pht('Edit')) - ->setBorder(true); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setValidationException($validation_exception) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($form); - - $header = id(new PHUIHeaderView()) - ->setHeader($title) - ->setHeaderIcon($header_icon); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter($box); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - + return id(new PhabricatorFileEditEngine()) + ->setController($this) + ->buildResponse(); } } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 4ca3bbb72e..8e92423fbc 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -94,25 +94,18 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $file, new PhabricatorFileTransactionQuery()); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + $comment_view = id(new PhabricatorFileEditEngine()) + ->setViewer($viewer) + ->buildEditEngineCommentView($file); - $add_comment_header = $is_serious - ? pht('Add Comment') - : pht('Question File Integrity'); + $monogram = $file->getMonogram(); - $draft = PhabricatorDraft::newFromUserAndKey($viewer, $file->getPHID()); - - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($viewer) - ->setObjectPHID($file->getPHID()) - ->setDraft($draft) - ->setHeaderText($add_comment_header) - ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/')) - ->setSubmitButtonName(pht('Add Comment')); + $timeline->setQuoteRef($monogram); + $comment_view->setTransactionTimeline($timeline); return array( $timeline, - $add_comment_form, + $comment_view, ); } diff --git a/src/applications/files/editor/PhabricatorFileEditEngine.php b/src/applications/files/editor/PhabricatorFileEditEngine.php new file mode 100644 index 0000000000..04c4753bd5 --- /dev/null +++ b/src/applications/files/editor/PhabricatorFileEditEngine.php @@ -0,0 +1,79 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return $object->getMonogram(); + } + + protected function getObjectCreateShortText() { + return pht('Create File'); + } + + protected function getObjectName() { + return pht('File'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setTransactionType(PhabricatorFileNameTransaction::TRANSACTIONTYPE) + ->setDescription(pht('The name of the file.')) + ->setConduitDescription(pht('Rename the file.')) + ->setConduitTypeDescription(pht('New file name.')) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index d301abceca..da7ad3f729 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -774,6 +774,9 @@ final class PhabricatorFile extends PhabricatorFileDAO return $format->newReadIterator($raw_iterator); } + public function getURI() { + return $this->getInfoURI(); + } public function getViewURI() { if (!$this->getPHID()) { From 2369fa38e1879134571f02b93961b98e19236bd9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 4 Apr 2017 08:48:05 -0700 Subject: [PATCH 087/239] Provide a modern ("v3") API for querying files ("file.search") Summary: Ref T11357. Implements a modern `file.search` for files, and freezes `file.info`. Test Plan: Ran `file.search` from the Conduit console. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11357 Differential Revision: https://secure.phabricator.com/D17612 --- src/__phutil_library_map__.php | 3 ++ .../conduit/FileInfoConduitAPIMethod.php | 10 ++++++ .../PhabricatorFileSearchConduitAPIMethod.php | 18 ++++++++++ .../files/storage/PhabricatorFile.php | 36 ++++++++++++++++++- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 99f9810052..f082c2fe22 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2770,6 +2770,7 @@ phutil_register_library_map(array( 'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php', 'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php', 'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php', + 'PhabricatorFileSearchConduitAPIMethod' => 'applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php', 'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php', 'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php', 'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', @@ -7847,6 +7848,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorConduitResultInterface', ), 'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileBundleLoader' => 'Phobject', @@ -7897,6 +7899,7 @@ phutil_register_library_map(array( 'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', diff --git a/src/applications/files/conduit/FileInfoConduitAPIMethod.php b/src/applications/files/conduit/FileInfoConduitAPIMethod.php index 5f1bb6e936..f1c8f5941a 100644 --- a/src/applications/files/conduit/FileInfoConduitAPIMethod.php +++ b/src/applications/files/conduit/FileInfoConduitAPIMethod.php @@ -10,6 +10,16 @@ final class FileInfoConduitAPIMethod extends FileConduitAPIMethod { return pht('Get information about a file.'); } + public function getMethodStatus() { + return self::METHOD_STATUS_FROZEN; + } + + public function getMethodStatusDescription() { + return pht( + 'This method is frozen and will eventually be deprecated. New code '. + 'should use "file.search" instead.'); + } + protected function defineParamTypes() { return array( 'phid' => 'optional phid', diff --git a/src/applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php b/src/applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php new file mode 100644 index 0000000000..0b6920d559 --- /dev/null +++ b/src/applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +saveTransaction(); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the file.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('dataURI') + ->setType('string') + ->setDescription(pht('Download URI for the file data.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('size') + ->setType('int') + ->setDescription(pht('File size, in bytes.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getName(), + 'dataURI' => $this->getCDNURI(), + 'size' => (int)$this->getByteSize(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } From 2896da384cb7366fbcf41f8488ecea281393dfba Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 4 Apr 2017 09:06:00 -0700 Subject: [PATCH 088/239] Only require POST to fetch file data if the viewer is logged in Summary: Ref T11357. In D17611, I added `file.search`, which includes a `"dataURI"`. Partly, this is building toward resolving T8348. However, in some cases you can't GET this URI because of a security measure: - You have not configured `security.alternate-file-domain`. - The file isn't web-viewable. - (The request isn't an LFS request.) The goal of this security mechanism is just to protect against session hijacking, so it's also safe to disable it if the viewer didn't present any credentials (since that means there's nothing to hijack). Add that exception, and reorganize the code a little bit. Test Plan: - From the browser (with a session), tried to GET a binary data file. Got redirected. - Got a download with POST. - From the CLI (without a session), tried to GET a binary data file. Go a download. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11357 Differential Revision: https://secure.phabricator.com/D17613 --- .../PhabricatorFileDataController.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index 5c2fc7bbda..43b5aa419f 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -84,18 +84,28 @@ final class PhabricatorFileDataController extends PhabricatorFileController { if ($is_viewable && !$force_download) { $response->setMimeType($file->getViewableMimeType()); } else { - if (!$request->isHTTPPost() && !$is_alternate_domain && !$is_lfs) { - // NOTE: Require POST to download files from the primary domain. We'd - // rather go full-bore and do a real CSRF check, but can't currently - // authenticate users on the file domain. This should blunt any - // attacks based on iframes, script tags, applet tags, etc., at least. - // Send the user to the "info" page if they're using some other method. + $is_public = !$viewer->isLoggedIn(); + $is_post = $request->isHTTPPost(); + // NOTE: Require POST to download files from the primary domain if the + // request includes credentials. The "Download File" links we generate + // in the web UI are forms which use POST to satisfy this requirement. + + // The intent is to make attacks based on tags like "